Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export const WITHOUT_IIQ = "B";
export const DEFAULT_PERCENTAGE = 95;
export const CLIENT_HINTS_KEY = "_iiq_ch";
export const EMPTY = "EMPTY";
export const GVLID = "1323";
export const VERSION = 0.37;
export const GVLID = 1323;
export const VERSION = 0.38;
export const PREBID = "pbjs";
export const HOURS_24 = 86400000;
export const HOURS_72 = HOURS_24 * 3;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function isCHSupported(nav) {
export function isCHSupported(nav?) {
const n = nav ?? (typeof navigator !== 'undefined' ? navigator : undefined);
return typeof n?.userAgentData?.getHighEntropyValues === 'function';
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @param {number} [key=42] The XOR key (0–255) to use for encryption.
* @returns {string} The encrypted text as a dot-separated string.
*/
export function encryptData(plainText, key = 42) {
export function encryptData(plainText: string, key: number = 42): string {
let out = '';
for (let i = 0; i < plainText.length; i++) {
out += (plainText.charCodeAt(i) ^ key) + '.';
Expand All @@ -21,11 +21,11 @@ export function encryptData(plainText, key = 42) {
* @param {number} [key=42] The XOR key (0–255) used for encryption.
* @returns {string} The decrypted plaintext.
*/
export function decryptData(encryptedText, key = 42) {
export function decryptData(encryptedText: string, key: number = 42): string {
const parts = encryptedText.split('.');
let out = '';
for (let i = 0; i < parts.length; i++) {
out += String.fromCharCode(parts[i] ^ key);
out += String.fromCharCode(Number(parts[i]) ^ key);
}
return out;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@ import {
WITHOUT_IIQ,
DEFAULT_PERCENTAGE,
AB_CONFIG_SOURCE,
} from "../intentIqConstants/intentIqConstants.js";
} from '../intentIqConstants/intentIqConstants.js';

/**
* A/B testing configuration source — controls how the test group is assigned.
* - `'percentage'` — random assignment based on `abPercentage`
* - `'group'` — fixed group supplied via the `group` param
* - `'IIQServer'` — server-driven assignment (default)
* - `'disabled'` — A/B testing disabled; always use IIQ
*/
export type IntentIqABConfigSource = 'percentage' | 'group' | 'IIQServer' | 'disabled';

type ABGroup = typeof WITH_IIQ | typeof WITHOUT_IIQ;

interface ABTestingConfig {
ABTestingConfigurationSource?: string;
abPercentage?: number;
group?: string;
}

/**
* Fix percentage if provided some incorrect data
* clampPct(150) => 100
* clampPct(-5) => 0
* clampPct('abc') => DEFAULT_PERCENTAGE
*/
function clampPct(val) {
function clampPct(val: unknown): number {
const n = Number(val);
if (!Number.isFinite(n)) return DEFAULT_PERCENTAGE; // fallback = 95
return Math.max(0, Math.min(100, n));
Expand All @@ -24,16 +41,18 @@ function clampPct(val) {
* @param {number} pct The percentage threshold (0–100).
* @returns {string} Returns WITH_IIQ for Group A or WITHOUT_IIQ for Group B.
*/
function pickABByPercentage(pct) {
function pickABByPercentage(pct?: number): ABGroup {
const percentageToUse =
typeof pct === "number" ? pct : DEFAULT_PERCENTAGE;
typeof pct === 'number' ? pct : DEFAULT_PERCENTAGE;
const percentage = clampPct(percentageToUse);
const roll = Math.floor(Math.random() * 100) + 1;
return roll <= percentage ? WITH_IIQ : WITHOUT_IIQ; // A : B
}

function configurationSourceGroupInitialization(group) {
return typeof group === 'string' && group.toUpperCase() === WITHOUT_IIQ ? WITHOUT_IIQ : WITH_IIQ;
function configurationSourceGroupInitialization(group?: string): ABGroup {
return typeof group === 'string' && group.toUpperCase() === WITHOUT_IIQ
? WITHOUT_IIQ
: WITH_IIQ;
}

/**
Expand All @@ -47,23 +66,27 @@ function configurationSourceGroupInitialization(group) {
* @param {number} [abPercentage] A/B percentage provided by partner.
* @returns {string} The determined group: WITH_IIQ (A) or WITHOUT_IIQ (B).
*/

function IIQServerConfigurationSource(tc, abPercentage) {
if (typeof tc === "number" && Number.isFinite(tc)) {
function IIQServerConfigurationSource(tc?: number, abPercentage?: number): ABGroup {
if (typeof tc === 'number' && Number.isFinite(tc)) {
return tc === 41 ? WITHOUT_IIQ : WITH_IIQ;
}

return pickABByPercentage(abPercentage);
}

export function defineABTestingGroup(configObject, tc) {
export function defineABTestingGroup(
configObject: ABTestingConfig,
tc?: number
): ABGroup {
switch (configObject.ABTestingConfigurationSource) {
case AB_CONFIG_SOURCE.GROUP:
return configurationSourceGroupInitialization(
configObject.group
);

case AB_CONFIG_SOURCE.PERCENTAGE:
return pickABByPercentage(configObject.abPercentage);

default: {
if (!configObject.ABTestingConfigurationSource) {
configObject.ABTestingConfigurationSource = AB_CONFIG_SOURCE.IIQ_SERVER;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { logError } from '../../src/utils.js';

type BrowserName =
| 'chrome'
| 'edge'
| 'firefox'
| 'ie'
| 'opera'
| 'safari'
| 'unknown';

/**
* Detects the browser using either userAgent or userAgentData
* @return {string} The name of the detected browser or 'unknown' if unable to detect
*/
export function detectBrowser() {
export function detectBrowser(): BrowserName {
try {
if (navigator.userAgent) {
if (navigator?.userAgent) {
return detectBrowserFromUserAgent(navigator.userAgent);
} else if (navigator.userAgentData) {
return detectBrowserFromUserAgentData(navigator.userAgentData);
} else if ((navigator as any)?.userAgentData) {
return detectBrowserFromUserAgentData((navigator as any)?.userAgentData);

Check warning on line 21 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

20-21 lines are not covered with tests

Check warning on line 21 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

20-21 lines are not covered with tests
}
} catch (error) {
logError('Error detecting browser:', error);

Check warning on line 24 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

24 line is not covered with tests

Check warning on line 24 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

24 line is not covered with tests
}
return 'unknown';

Check warning on line 26 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

26 line is not covered with tests

Check warning on line 26 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

26 line is not covered with tests
}

/**
Expand All @@ -22,8 +31,8 @@
* @param {string} userAgent - The user agent string from the browser
* @return {string} The name of the detected browser or 'unknown' if unable to detect
*/
export function detectBrowserFromUserAgent(userAgent) {
const browserRegexPatterns = {
export function detectBrowserFromUserAgent(userAgent: string): BrowserName {
const browserRegexPatterns: Record<string, RegExp> = {
opera: /Opera|OPR/,
edge: /Edg/,
chrome: /Chrome|CriOS/,
Expand All @@ -34,12 +43,12 @@

// Check for Edge first
if (browserRegexPatterns.edge.test(userAgent)) {
return 'edge';

Check warning on line 46 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

46 line is not covered with tests

Check warning on line 46 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

46 line is not covered with tests
}

// Check for Opera next
if (browserRegexPatterns.opera.test(userAgent)) {
return 'opera';

Check warning on line 51 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

51 line is not covered with tests

Check warning on line 51 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

51 line is not covered with tests
}

// Check for Chrome first to avoid confusion with Safari
Expand All @@ -48,18 +57,21 @@
}

// Now we can safely check for Safari
if (browserRegexPatterns.safari.test(userAgent) && !browserRegexPatterns.chrome.test(userAgent)) {
if (
browserRegexPatterns.safari.test(userAgent) &&
!browserRegexPatterns.chrome.test(userAgent)
) {
return 'safari';
}

// Check other browsers
for (const browser in browserRegexPatterns) {
if (browserRegexPatterns[browser].test(userAgent)) {
return browser;
return browser as BrowserName;
}
}

return 'unknown';

Check warning on line 74 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

74 line is not covered with tests

Check warning on line 74 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

74 line is not covered with tests
}

/**
Expand All @@ -67,14 +79,18 @@
* @param {Object} userAgentData - The user agent data object from the browser
* @return {string} The name of the detected browser or 'unknown' if unable to detect
*/
export function detectBrowserFromUserAgentData(userAgentData) {
export function detectBrowserFromUserAgentData(
userAgentData
): BrowserName {
const brandNames = userAgentData.brands.map(brand => brand.brand);

if (brandNames.includes('Microsoft Edge')) {
return 'edge';
} else if (brandNames.includes('Opera')) {
return 'opera';

Check warning on line 90 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

90 line is not covered with tests

Check warning on line 90 in libraries/intentIqUtils/detectBrowserUtils.ts

View workflow job for this annotation

GitHub Actions / Coverage

90 line is not covered with tests
} else if (brandNames.some(brand => brand === 'Chromium' || brand === 'Google Chrome')) {
} else if (
brandNames.some(brand => brand === 'Chromium' || brand === 'Google Chrome')
) {
return 'chrome';
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
import { logError } from '../../src/utils.js';
import { getSlotTargetingMap } from '../../src/utils/gptTargeting.js';

export function gamPredictionReport (gamObjectReference, sendData) {
export function gamPredictionReport(
gamObjectReference: any,
sendData: (data: Record<string, any>) => void
): void {
try {
if (!gamObjectReference || !sendData) {
logError('Failed to get gamPredictionReport, required data is missed');
return;

Check warning on line 12 in libraries/intentIqUtils/gamPredictionReport.ts

View workflow job for this annotation

GitHub Actions / Coverage

11-12 lines are not covered with tests

Check warning on line 12 in libraries/intentIqUtils/gamPredictionReport.ts

View workflow job for this annotation

GitHub Actions / Coverage

11-12 lines are not covered with tests
}
const getSlotTargeting = (slot) => {

const getSlotTargeting = (slot: any): Record<string, string[]> => {
try {
return getSlotTargetingMap(slot);
} catch (e) {
Expand All @@ -17,11 +21,11 @@
}
};

const extractWinData = (gamEvent) => {
const extractWinData = (gamEvent: any): Record<string, any> | undefined => {
const slot = gamEvent.slot;
const targeting = getSlotTargeting(slot);

const dataToSend = {
const dataToSend: Record<string, any> = {
placementId: slot.getSlotElementId && slot.getSlotElementId(),
adUnitPath: slot.getAdUnitPath && slot.getAdUnitPath(),
bidderCode: targeting.hb_bidder ? targeting.hb_bidder[0] : null,
Expand All @@ -30,70 +34,88 @@

if (dataToSend.placementId) {
// TODO check auto subscription to prebid events
const bidWonEvents = getEvents().filter((ev) => ev.eventType === 'bidWon');
const bidWonEvents = getEvents().filter((ev: any) => ev.eventType === 'bidWon');
if (bidWonEvents.length) {
for (let i = bidWonEvents.length - 1; i >= 0; i--) {
const element = bidWonEvents[i];
if (

Check warning on line 41 in libraries/intentIqUtils/gamPredictionReport.ts

View workflow job for this annotation

GitHub Actions / Coverage

39-41 lines are not covered with tests

Check warning on line 41 in libraries/intentIqUtils/gamPredictionReport.ts

View workflow job for this annotation

GitHub Actions / Coverage

39-41 lines are not covered with tests
dataToSend.placementId === element.id &&
targeting.hb_adid &&
targeting.hb_adid[0] === element.args.adId
targeting.hb_adid &&
targeting.hb_adid[0] === (element.args as any).adId
) {
return; // don't send report if there was bidWon event earlier
return;

Check warning on line 46 in libraries/intentIqUtils/gamPredictionReport.ts

View workflow job for this annotation

GitHub Actions / Coverage

46 line is not covered with tests

Check warning on line 46 in libraries/intentIqUtils/gamPredictionReport.ts

View workflow job for this annotation

GitHub Actions / Coverage

46 line is not covered with tests
}
}
}
const endEvents = getEvents().filter((ev) => ev.eventType === 'auctionEnd');

const endEvents = getEvents().filter((ev: any) => ev.eventType === 'auctionEnd');

if (endEvents.length) {
for (let i = endEvents.length - 1; i >= 0; i--) {
// starting from the last event
const element = endEvents[i];
if (element.args?.adUnitCodes?.includes(dataToSend.placementId)) {
const defineRelevantData = (bid) => {
dataToSend.cpm = bid.cpm + 0.01; // add one cent to the cpm

if ((element.args as any)?.adUnitCodes?.includes(dataToSend.placementId)) {
const defineRelevantData = (bid: any): void => {
dataToSend.cpm = bid.cpm + 0.01;
dataToSend.currency = bid.currency;
dataToSend.originalCpm = bid.originalCpm;
dataToSend.originalCurrency = bid.originalCurrency;
dataToSend.status = bid.status;
dataToSend.prebidAuctionId = element.args?.auctionId;
if (!dataToSend.bidderCode) dataToSend.bidderCode = 'GAM';
dataToSend.prebidAuctionId = (element.args as any)?.auctionId;

if (!dataToSend.bidderCode) {
dataToSend.bidderCode = 'GAM';
}
};

if (dataToSend.bidderCode) {
const relevantBid = element.args?.bidsReceived.find(
(item) =>
const relevantBid = (element.args as any)?.bidsReceived.find(
(item: any) =>
item.bidder === dataToSend.bidderCode &&
item.adUnitCode === dataToSend.placementId
item.adUnitCode === dataToSend.placementId
);

if (relevantBid) {
defineRelevantData(relevantBid);
break;
}
} else {
let highestBid = 0;
element.args?.bidsReceived.forEach((bid) => {
if (bid.adUnitCode === dataToSend.placementId && bid.cpm > highestBid) {

(element.args as any)?.bidsReceived.forEach((bid: any) => {
if (
bid.adUnitCode === dataToSend.placementId &&
bid.cpm > highestBid
) {
highestBid = bid.cpm;
defineRelevantData(bid);
}
});

break;
}
}
}
}
}

return dataToSend;
};

gamObjectReference.cmd.push(() => {
gamObjectReference.pubads().addEventListener('slotRenderEnded', (event) => {
if (event.isEmpty) return;
const data = extractWinData(event);
if (data) {
sendData(data);
gamObjectReference.pubads().addEventListener(
'slotRenderEnded',
(event: any) => {
if (event.isEmpty) return;

const data = extractWinData(event);
if (data) {
sendData(data);
}
}
});
);
});
} catch (error) {
logError('Failed to subscribe to GAM: ' + error);
}
};
}
Loading
Loading