@optable/web-sdk 0.54.1 → 0.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/browser/dist/sdk.js +1 -1
- package/lib/dist/addons/abTestAssignment.d.ts +22 -0
- package/lib/dist/addons/abTestAssignment.js +117 -0
- package/lib/dist/addons/botDetection.d.ts +2 -0
- package/lib/dist/addons/botDetection.js +29 -0
- package/lib/dist/addons/geotargeting.d.ts +8 -0
- package/lib/dist/addons/geotargeting.js +54 -0
- package/lib/dist/addons/prebid/analytics.d.ts +2 -0
- package/lib/dist/addons/prebid/analytics.js +86 -11
- package/lib/dist/addons/prototypes/analytics.js +3 -3
- package/lib/dist/build.json +1 -1
- package/lib/dist/core/flags.d.ts +6 -0
- package/lib/dist/core/flags.js +49 -0
- package/lib/dist/core/prebid/rtd.js +21 -17
- package/lib/dist/edge/abTest.d.ts +2 -0
- package/lib/dist/edge/abTest.js +19 -0
- package/lib/dist/edge/targeting.d.ts +9 -4
- package/lib/dist/edge/targeting.js +21 -23
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { determineABTest } from "../edge/abTest";
|
|
2
|
+
import { getFlags } from "../core/flags";
|
|
3
|
+
const DEFAULT_STORAGE_KEY = "OPTABLE_SPLIT_TEST";
|
|
4
|
+
function fillTrafficPercentages(variants) {
|
|
5
|
+
const allocated = variants.reduce((sum, v) => { var _a; return sum + ((_a = v.trafficPercentage) !== null && _a !== void 0 ? _a : 0); }, 0);
|
|
6
|
+
const unassigned = variants.filter((v) => v.trafficPercentage === undefined);
|
|
7
|
+
const each = unassigned.length > 0 ? (100 - allocated) / unassigned.length : 0;
|
|
8
|
+
return variants.map((v) => {
|
|
9
|
+
var _a;
|
|
10
|
+
return ({
|
|
11
|
+
id: v.id,
|
|
12
|
+
trafficPercentage: (_a = v.trafficPercentage) !== null && _a !== void 0 ? _a : each,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function setupAB(config) {
|
|
17
|
+
var _a, _b, _c;
|
|
18
|
+
const { variants, storageKey = DEFAULT_STORAGE_KEY, controlId = "none", treatmentId = "all", sdk, pbjs } = config;
|
|
19
|
+
// Process the provided variant config so that every variant has an explicit traffic percentage.
|
|
20
|
+
// Variants without one share the remaining percentage equally.
|
|
21
|
+
const filled = fillTrafficPercentages(variants);
|
|
22
|
+
let selected = null;
|
|
23
|
+
// Priority 1 — QA/debug override via URL param or sessionStorage flag.
|
|
24
|
+
// ?optableControlGroup=1 forces the control variant; =0 forces treatment.
|
|
25
|
+
// This lets QA verify both branches without clearing localStorage.
|
|
26
|
+
const controlGroupFlag = getFlags().optableControlGroup;
|
|
27
|
+
if (controlGroupFlag === "1") {
|
|
28
|
+
selected = (_a = filled.find((v) => v.id === controlId)) !== null && _a !== void 0 ? _a : { id: controlId, trafficPercentage: 0 };
|
|
29
|
+
}
|
|
30
|
+
else if (controlGroupFlag === "0") {
|
|
31
|
+
selected = (_b = filled.find((v) => v.id === treatmentId)) !== null && _b !== void 0 ? _b : { id: treatmentId, trafficPercentage: 0 };
|
|
32
|
+
}
|
|
33
|
+
// Priority 2 — sticky assignment from a previous visit.
|
|
34
|
+
// Once a user is assigned a variant it must not change across page loads or
|
|
35
|
+
// sessions, otherwise the same user could appear in both groups. We validate
|
|
36
|
+
// the cached id against the current variant list so a stale cache from an
|
|
37
|
+
// old experiment config is silently discarded.
|
|
38
|
+
if (!selected) {
|
|
39
|
+
try {
|
|
40
|
+
const cached = localStorage.getItem(storageKey);
|
|
41
|
+
if (cached) {
|
|
42
|
+
const parsed = JSON.parse(cached);
|
|
43
|
+
if ((parsed === null || parsed === void 0 ? void 0 : parsed.id) && filled.some((v) => v.id === parsed.id)) {
|
|
44
|
+
selected = parsed;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (_d) {
|
|
49
|
+
// localStorage unavailable or invalid JSON
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Priority 3 — first visit: randomly assign based on traffic weights.
|
|
53
|
+
// determineABTest returns null when the random bucket falls outside all
|
|
54
|
+
// defined ranges (i.e. weights sum to less than 100). filled[0] is the
|
|
55
|
+
// fallback so selected is always non-null after this point.
|
|
56
|
+
if (!selected) {
|
|
57
|
+
selected = (_c = determineABTest(filled)) !== null && _c !== void 0 ? _c : filled[0];
|
|
58
|
+
}
|
|
59
|
+
// Persist the assignment so subsequent visits return the same variant.
|
|
60
|
+
try {
|
|
61
|
+
localStorage.setItem(storageKey, JSON.stringify(selected));
|
|
62
|
+
}
|
|
63
|
+
catch (_e) {
|
|
64
|
+
// localStorage unavailable
|
|
65
|
+
}
|
|
66
|
+
const isControl = selected.id !== treatmentId;
|
|
67
|
+
const assignment = selected.id;
|
|
68
|
+
// Control group: clear cached targeting data so RTD, PPID and TargetingFromCache
|
|
69
|
+
// serve nothing for this user. Without this, a user moved into the control group
|
|
70
|
+
// would still receive Optable targeting from a previous session's cache.
|
|
71
|
+
if (isControl) {
|
|
72
|
+
try {
|
|
73
|
+
localStorage.removeItem("OPTABLE_RESOLVED");
|
|
74
|
+
if (sdk) {
|
|
75
|
+
sdk.targetingClearCache();
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
Object.keys(localStorage)
|
|
79
|
+
.filter((k) => k.startsWith("OPTABLE_TARGETING_"))
|
|
80
|
+
.forEach((k) => localStorage.removeItem(k));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (_f) {
|
|
84
|
+
// localStorage unavailable
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function applyToAuctionEvent(event) {
|
|
88
|
+
(event.bidderRequests || []).forEach((br) => {
|
|
89
|
+
(br.bids || []).forEach((b) => {
|
|
90
|
+
var _a, _b, _c;
|
|
91
|
+
if ((_c = (_b = (_a = b.ortb2Imp) === null || _a === void 0 ? void 0 : _a.ext) === null || _b === void 0 ? void 0 : _b.optable) === null || _c === void 0 ? void 0 : _c.splitTestAssignment)
|
|
92
|
+
return;
|
|
93
|
+
b.ortb2Imp = b.ortb2Imp || {};
|
|
94
|
+
b.ortb2Imp.ext = b.ortb2Imp.ext || {};
|
|
95
|
+
b.ortb2Imp.ext.optable = b.ortb2Imp.ext.optable || {};
|
|
96
|
+
b.ortb2Imp.ext.optable.splitTestAssignment = assignment;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function setHooks(pbjsInstance) {
|
|
101
|
+
pbjsInstance.getEvents().forEach((event) => {
|
|
102
|
+
if (event.eventType === "auctionEnd") {
|
|
103
|
+
applyToAuctionEvent(event.args);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
pbjsInstance.onEvent("auctionEnd", applyToAuctionEvent);
|
|
107
|
+
}
|
|
108
|
+
if (pbjs) {
|
|
109
|
+
setHooks(pbjs);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
variant: selected,
|
|
113
|
+
isControl,
|
|
114
|
+
splitTestAssignment: assignment,
|
|
115
|
+
setHooks,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const BOT_PATTERN = new RegExp([
|
|
2
|
+
"bot",
|
|
3
|
+
"crawler",
|
|
4
|
+
"spider",
|
|
5
|
+
"scraper",
|
|
6
|
+
"headless",
|
|
7
|
+
"phantomjs",
|
|
8
|
+
"selenium",
|
|
9
|
+
"webdriver",
|
|
10
|
+
"curl",
|
|
11
|
+
"wget",
|
|
12
|
+
"python",
|
|
13
|
+
"java",
|
|
14
|
+
"perl",
|
|
15
|
+
"ruby",
|
|
16
|
+
"go-http-client",
|
|
17
|
+
"okhttp",
|
|
18
|
+
"axios",
|
|
19
|
+
"fetch",
|
|
20
|
+
"postman",
|
|
21
|
+
"insomnia",
|
|
22
|
+
"googleother",
|
|
23
|
+
"google-extended",
|
|
24
|
+
"google-inspectiontool",
|
|
25
|
+
].join("|"), "i");
|
|
26
|
+
/** True if the current user agent looks like a known bot/crawler. */
|
|
27
|
+
export function isBot(userAgent = navigator.userAgent) {
|
|
28
|
+
return BOT_PATTERN.test(userAgent || "");
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type GeoMapEntry = [string, string] | [string, string, string];
|
|
2
|
+
export type GeoMap = Record<string, GeoMapEntry>;
|
|
3
|
+
export interface GeoConfig {
|
|
4
|
+
host: string;
|
|
5
|
+
node: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare const DEFAULT_GEO_MAP: GeoMap;
|
|
8
|
+
export declare function getGeoConfig(nodeName: string, geo: string | undefined, geoMap?: GeoMap): GeoConfig | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The geotargeting addon maps a visitor's geo (country code) to the Optable
|
|
3
|
+
* host and node that should serve them, so that a single SDK bundle can route
|
|
4
|
+
* traffic to region-specific DCNs.
|
|
5
|
+
*
|
|
6
|
+
* A GeoMap entry is a tuple of host fragments keyed by country code:
|
|
7
|
+
* [0] — host suffix for the standard (non-auth) node
|
|
8
|
+
* [1] — host suffix for the auth node
|
|
9
|
+
* [2] — optional regional edge host shared by multiple nodes
|
|
10
|
+
*
|
|
11
|
+
* When a regional edge host ([2]) is present, it is used as the host and the
|
|
12
|
+
* node name is derived from the tenant name plus the suffix with ".cloud"
|
|
13
|
+
* removed (e.g. "acme" + "-ca-auth" → "acme-ca-auth"). Otherwise the tenant
|
|
14
|
+
* runs on a dedicated cloud host built as `${name}${suffix}.optable.co` and
|
|
15
|
+
* the node is undefined (the host's default node is used).
|
|
16
|
+
*
|
|
17
|
+
* DEFAULT_GEO_MAP reflects one specific provisioning shape: regional edge
|
|
18
|
+
* nodes in US/CA and dedicated per-tenant cloud hosts in AU and the EU. The
|
|
19
|
+
* dedicated hosts only exist for tenants provisioned that way — tenants with
|
|
20
|
+
* a different topology must pass their own GeoMap.
|
|
21
|
+
*/
|
|
22
|
+
const EU_ENTRY = [".cloud.eu", "-auth.cloud.eu"];
|
|
23
|
+
export const DEFAULT_GEO_MAP = {
|
|
24
|
+
AU: [".cloud.au", "-auth.cloud.au"],
|
|
25
|
+
CA: ["-ca.cloud", "-ca-auth.cloud", "ca.edge.optable.co"],
|
|
26
|
+
GB: EU_ENTRY,
|
|
27
|
+
UK: EU_ENTRY,
|
|
28
|
+
US: [".cloud", "-auth.cloud", "na.edge.optable.co"],
|
|
29
|
+
};
|
|
30
|
+
/*
|
|
31
|
+
* getGeoConfig() resolves the host and node for a node name in a given geo.
|
|
32
|
+
*
|
|
33
|
+
* nodeName is the tenant name, optionally with an "-auth" suffix selecting the
|
|
34
|
+
* auth variant of the node (e.g. "acme" or "acme-auth").
|
|
35
|
+
*
|
|
36
|
+
* Returns null when the geo is missing or not present in the map, in which
|
|
37
|
+
* case the caller should skip region-specific initialization.
|
|
38
|
+
*/
|
|
39
|
+
export function getGeoConfig(nodeName, geo, geoMap = DEFAULT_GEO_MAP) {
|
|
40
|
+
const entry = geo === undefined ? undefined : geoMap[geo];
|
|
41
|
+
// Array.isArray also rejects inherited Object.prototype members picked up
|
|
42
|
+
// when an unexpected geo like "constructor" is looked up in the map
|
|
43
|
+
if (!Array.isArray(entry)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const auth = /-auth$/i.test(nodeName);
|
|
47
|
+
const name = auth ? nodeName.replace(/-auth$/i, "") : nodeName;
|
|
48
|
+
const suffix = auth ? entry[1] : entry[0];
|
|
49
|
+
const edgeHost = entry[2];
|
|
50
|
+
if (edgeHost != null) {
|
|
51
|
+
return { host: edgeHost, node: name + suffix.replace(/\.cloud/, "") };
|
|
52
|
+
}
|
|
53
|
+
return { host: `${name}${suffix}.optable.co`, node: undefined };
|
|
54
|
+
}
|
|
@@ -22,6 +22,7 @@ declare class OptablePrebidAnalytics {
|
|
|
22
22
|
private readonly maxAuctionDataSize;
|
|
23
23
|
private auctions;
|
|
24
24
|
private missedAuctionIds;
|
|
25
|
+
private pendingTimeoutBids;
|
|
25
26
|
private prebidInstance;
|
|
26
27
|
/**
|
|
27
28
|
* Create a new OptablePrebidAnalytics instance.
|
|
@@ -29,6 +30,7 @@ declare class OptablePrebidAnalytics {
|
|
|
29
30
|
* @param config - Optional configuration for sampling, debug and analytics behavior.
|
|
30
31
|
*/
|
|
31
32
|
constructor(optableInstance: OptableSDK, config?: OptablePrebidAnalyticsConfig);
|
|
33
|
+
private handleVisibilityChange;
|
|
32
34
|
/**
|
|
33
35
|
* Log messages to the console when debugging is enabled.
|
|
34
36
|
* @param args - Values to log.
|
|
@@ -7,6 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { buildRequest } from "../../core/network";
|
|
10
11
|
import * as Bowser from "bowser";
|
|
11
12
|
const STATUS = {
|
|
12
13
|
REQUESTED: "REQUESTED",
|
|
@@ -30,6 +31,33 @@ class OptablePrebidAnalytics {
|
|
|
30
31
|
this.maxAuctionDataSize = 50;
|
|
31
32
|
this.auctions = new Map();
|
|
32
33
|
this.missedAuctionIds = new Set();
|
|
34
|
+
this.pendingTimeoutBids = new Map();
|
|
35
|
+
this.handleVisibilityChange = () => {
|
|
36
|
+
if (document.visibilityState !== "hidden")
|
|
37
|
+
return;
|
|
38
|
+
if (!this.optableInstance.dcn)
|
|
39
|
+
return;
|
|
40
|
+
const witnessUrl = buildRequest("/witness", this.optableInstance.dcn).url;
|
|
41
|
+
this.auctions.forEach((auction, auctionId) => {
|
|
42
|
+
if (!auction.auctionEndTimeoutId)
|
|
43
|
+
return;
|
|
44
|
+
if (!auction.sampled)
|
|
45
|
+
return;
|
|
46
|
+
clearTimeout(auction.auctionEndTimeoutId);
|
|
47
|
+
this.auctions.delete(auctionId);
|
|
48
|
+
this.toWitness(auction.auctionEnd, auction.bidWonEvents, auction.missed).then((payload) => {
|
|
49
|
+
payload["auctionEndAt"] = auction.createdAt.toISOString();
|
|
50
|
+
payload["bidWonAt"] =
|
|
51
|
+
auction.bidWonEvents.length > 0
|
|
52
|
+
? new Date(Math.min(...auction.bidWonEvents.map((e) => e._receivedAt.getTime()))).toISOString()
|
|
53
|
+
: null;
|
|
54
|
+
payload["optableLoaded"] = !auction.missed;
|
|
55
|
+
navigator.sendBeacon(witnessUrl, new Blob([JSON.stringify({ event: "optable.prebid.auction", properties: payload })], {
|
|
56
|
+
type: "application/json",
|
|
57
|
+
}));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
};
|
|
33
61
|
if (!optableInstance || typeof optableInstance.witness !== "function") {
|
|
34
62
|
throw new Error("OptablePrebidAnalytics requires a valid optable instance with witness() method");
|
|
35
63
|
}
|
|
@@ -47,6 +75,7 @@ class OptablePrebidAnalytics {
|
|
|
47
75
|
this.isInitialized = true;
|
|
48
76
|
// Store auction data
|
|
49
77
|
this.maxAuctionDataSize = 50;
|
|
78
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
50
79
|
this.log("OptablePrebidAnalytics initialized");
|
|
51
80
|
}
|
|
52
81
|
/**
|
|
@@ -124,6 +153,13 @@ class OptablePrebidAnalytics {
|
|
|
124
153
|
if (event.eventType === "auctionInit") {
|
|
125
154
|
this.missedAuctionIds.add(event.args.auctionId);
|
|
126
155
|
}
|
|
156
|
+
else if (event.eventType === "bidTimeout") {
|
|
157
|
+
event.args.forEach((bid) => {
|
|
158
|
+
const existing = this.pendingTimeoutBids.get(bid.auctionId) || [];
|
|
159
|
+
existing.push(bid);
|
|
160
|
+
this.pendingTimeoutBids.set(bid.auctionId, existing);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
127
163
|
else if (event.eventType === "auctionEnd") {
|
|
128
164
|
this.missedAuctionIds.delete(event.args.auctionId);
|
|
129
165
|
this.log(`auction ${event.args.auctionId} missed (completed before hook)`);
|
|
@@ -135,6 +171,14 @@ class OptablePrebidAnalytics {
|
|
|
135
171
|
}
|
|
136
172
|
});
|
|
137
173
|
this.log("Hooking into Prebid.js events");
|
|
174
|
+
pbjs.onEvent("bidTimeout", (timedOutBids) => {
|
|
175
|
+
this.log("bidTimeout event received", timedOutBids);
|
|
176
|
+
timedOutBids.forEach((bid) => {
|
|
177
|
+
const existing = this.pendingTimeoutBids.get(bid.auctionId) || [];
|
|
178
|
+
existing.push(bid);
|
|
179
|
+
this.pendingTimeoutBids.set(bid.auctionId, existing);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
138
182
|
pbjs.onEvent("auctionEnd", (event) => {
|
|
139
183
|
this.log("auctionEnd event received");
|
|
140
184
|
const missed = this.missedAuctionIds.has(event.auctionId);
|
|
@@ -180,7 +224,9 @@ class OptablePrebidAnalytics {
|
|
|
180
224
|
*/
|
|
181
225
|
trackAuctionEnd(event_1) {
|
|
182
226
|
return __awaiter(this, arguments, void 0, function* (event, missed = false) {
|
|
183
|
-
const { auctionId, timeout, bidderRequests = [], bidsReceived = [], noBids = []
|
|
227
|
+
const { auctionId, timeout, bidderRequests = [], bidsReceived = [], noBids = [] } = event;
|
|
228
|
+
const timeoutBids = this.pendingTimeoutBids.get(auctionId) || [];
|
|
229
|
+
const sampled = !!this.config.analytics && this.shouldSample();
|
|
184
230
|
this.log(`Processing auction ${auctionId} with ${bidderRequests.length} bidder requests`);
|
|
185
231
|
window.optable = window.optable || {};
|
|
186
232
|
window.optable.pageAuctionsCount = (Number(window.optable.pageAuctionsCount) || 0) + 1;
|
|
@@ -307,6 +353,10 @@ class OptablePrebidAnalytics {
|
|
|
307
353
|
const storedAuction = this.auctions.get(auctionId);
|
|
308
354
|
if (!storedAuction)
|
|
309
355
|
return;
|
|
356
|
+
if (!storedAuction.sampled) {
|
|
357
|
+
this.auctions.delete(auctionId);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
310
360
|
const effectiveMissed = storedAuction.missed;
|
|
311
361
|
const payload = yield this.toWitness(event, storedAuction.bidWonEvents, effectiveMissed);
|
|
312
362
|
payload["auctionEndAt"] = createdAt.toISOString();
|
|
@@ -319,7 +369,16 @@ class OptablePrebidAnalytics {
|
|
|
319
369
|
this.auctions.delete(auctionId);
|
|
320
370
|
}), this.config.bidWinTimeout);
|
|
321
371
|
// Store the auction data
|
|
322
|
-
this.auctions.set(auctionId, {
|
|
372
|
+
this.auctions.set(auctionId, {
|
|
373
|
+
auctionEnd: event,
|
|
374
|
+
createdAt,
|
|
375
|
+
missed,
|
|
376
|
+
auctionEndTimeoutId,
|
|
377
|
+
bidWonEvents: [],
|
|
378
|
+
timeoutBids,
|
|
379
|
+
sampled,
|
|
380
|
+
});
|
|
381
|
+
this.pendingTimeoutBids.delete(auctionId);
|
|
323
382
|
// Clean up old auctions
|
|
324
383
|
this.cleanupOldAuctions();
|
|
325
384
|
});
|
|
@@ -377,8 +436,9 @@ class OptablePrebidAnalytics {
|
|
|
377
436
|
*/
|
|
378
437
|
toWitness(auctionEndEvent_1, bidWonEvents_1) {
|
|
379
438
|
return __awaiter(this, arguments, void 0, function* (auctionEndEvent, bidWonEvents, missed = false) {
|
|
380
|
-
var _a, _b, _c, _d, _e, _f;
|
|
381
|
-
const { auctionId, bidderRequests = [], bidsReceived = [], noBids = []
|
|
439
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
440
|
+
const { auctionId, bidderRequests = [], bidsReceived = [], noBids = [] } = auctionEndEvent;
|
|
441
|
+
const timeoutBids = ((_a = this.auctions.get(auctionId)) === null || _a === void 0 ? void 0 : _a.timeoutBids) || [];
|
|
382
442
|
const oMatchersSet = new Set();
|
|
383
443
|
const oSourcesSet = new Set();
|
|
384
444
|
let adUnitCode = "unknown";
|
|
@@ -421,16 +481,31 @@ class OptablePrebidAnalytics {
|
|
|
421
481
|
}),
|
|
422
482
|
};
|
|
423
483
|
});
|
|
424
|
-
// Merge splitTestAssignment from bidsReceived into the requests
|
|
425
484
|
const bidsReceivedMap = new Map(bidsReceived.map((b) => [b.requestId, b]));
|
|
485
|
+
const noBidRequestIds = new Set(noBids.map((nb) => nb.bidderRequestId));
|
|
486
|
+
const timedOutRequestIds = new Set(timeoutBids.map((tb) => tb.bidderRequestId));
|
|
426
487
|
requests.forEach((request) => {
|
|
427
488
|
request.bids.forEach((bid) => {
|
|
428
489
|
var _a, _b, _c;
|
|
429
490
|
const bidReceived = bidsReceivedMap.get(bid.bidId);
|
|
430
|
-
if (
|
|
431
|
-
bid.
|
|
491
|
+
if (bidReceived) {
|
|
492
|
+
bid.status = STATUS.RECEIVED;
|
|
493
|
+
bid.cpm = bidReceived.cpm;
|
|
494
|
+
bid.size = `${bidReceived.width}x${bidReceived.height}`;
|
|
495
|
+
bid.currency = bidReceived.currency;
|
|
496
|
+
if ((_c = (_b = (_a = bidReceived.ortb2Imp) === null || _a === void 0 ? void 0 : _a.ext) === null || _b === void 0 ? void 0 : _b.optable) === null || _c === void 0 ? void 0 : _c.splitTestAssignment) {
|
|
497
|
+
bid.splitTestAssignment = bidReceived.ortb2Imp.ext.optable.splitTestAssignment;
|
|
498
|
+
}
|
|
499
|
+
if (request.status === STATUS.REQUESTED)
|
|
500
|
+
request.status = STATUS.RECEIVED;
|
|
432
501
|
}
|
|
433
502
|
});
|
|
503
|
+
if (noBidRequestIds.has(request.bidderRequestId) && request.status === STATUS.REQUESTED) {
|
|
504
|
+
request.status = STATUS.NO_BID;
|
|
505
|
+
}
|
|
506
|
+
if (timedOutRequestIds.has(request.bidderRequestId)) {
|
|
507
|
+
request.status = STATUS.TIMEOUT;
|
|
508
|
+
}
|
|
434
509
|
});
|
|
435
510
|
const witnessData = {
|
|
436
511
|
bidderRequests: requests.map((br) => {
|
|
@@ -457,14 +532,14 @@ class OptablePrebidAnalytics {
|
|
|
457
532
|
optableWrapperVersion: SDK_WRAPPER_VERSION || "unknown",
|
|
458
533
|
userAgent: Bowser.parse(window.navigator.userAgent),
|
|
459
534
|
device,
|
|
460
|
-
prebidjsVersion: ((
|
|
535
|
+
prebidjsVersion: ((_b = this.prebidInstance) === null || _b === void 0 ? void 0 : _b.version) || "unknown",
|
|
461
536
|
sessionDepth: (sessionStorage === null || sessionStorage === void 0 ? void 0 : sessionStorage.optableSessionDepth) || 1,
|
|
462
|
-
pageAuctionsCount: ((
|
|
463
|
-
originSlug: ((
|
|
537
|
+
pageAuctionsCount: ((_c = window.optable) === null || _c === void 0 ? void 0 : _c.pageAuctionsCount) || 1,
|
|
538
|
+
originSlug: ((_e = (_d = this.optableInstance) === null || _d === void 0 ? void 0 : _d.dcn) === null || _e === void 0 ? void 0 : _e.site) || ((_f = window.optable) === null || _f === void 0 ? void 0 : _f.site) || "unknown",
|
|
464
539
|
};
|
|
465
540
|
// Log summary with bid counts
|
|
466
541
|
this.log(`Auction ${auctionId} processed: ${bidderRequests.length} requests, ${totalBids} total bids, ${bidsReceived.length} received, ${noBids.length} no-bids, ${timeoutBids.length} timeouts`);
|
|
467
|
-
if ((
|
|
542
|
+
if ((_g = window.optable) === null || _g === void 0 ? void 0 : _g.customAnalytics) {
|
|
468
543
|
yield window.optable.customAnalytics().then((response) => {
|
|
469
544
|
this.log(`Adding custom data to payload ${JSON.stringify(response)}`);
|
|
470
545
|
Object.assign(witnessData, response);
|
|
@@ -110,10 +110,10 @@ class OptablePrebidAnalytics {
|
|
|
110
110
|
auctionId,
|
|
111
111
|
timeout,
|
|
112
112
|
bidderRequests: bidderRequests.map((br) => {
|
|
113
|
-
var _a, _b, _c, _d;
|
|
113
|
+
var _a, _b, _c, _d, _e, _f;
|
|
114
114
|
const { bidderCode, bidderRequestId, ortb2, bids = [] } = br;
|
|
115
115
|
const domain = (_a = ortb2 === null || ortb2 === void 0 ? void 0 : ortb2.site) === null || _a === void 0 ? void 0 : _a.domain;
|
|
116
|
-
const eids = ((_c = (_b = ortb2 === null || ortb2 === void 0 ? void 0 : ortb2.user) === null || _b === void 0 ? void 0 : _b.ext) === null || _c === void 0 ? void 0 : _c.eids)
|
|
116
|
+
const eids = [...((_d = (_c = (_b = ortb2 === null || ortb2 === void 0 ? void 0 : ortb2.user) === null || _b === void 0 ? void 0 : _b.ext) === null || _c === void 0 ? void 0 : _c.eids) !== null && _d !== void 0 ? _d : []), ...((_f = (_e = ortb2 === null || ortb2 === void 0 ? void 0 : ortb2.user) === null || _e === void 0 ? void 0 : _e.eids) !== null && _f !== void 0 ? _f : [])];
|
|
117
117
|
// Optable EIDs
|
|
118
118
|
const optableEIDS = eids.filter((e) => e.inserter === "optable.co");
|
|
119
119
|
const optableMatchers = [...new Set(optableEIDS.map((e) => e.matcher).filter(Boolean))];
|
|
@@ -274,7 +274,7 @@ class OptablePrebidAnalytics {
|
|
|
274
274
|
prebidjsVersion: ((_a = this.prebidInstance) === null || _a === void 0 ? void 0 : _a.version) || "unknown",
|
|
275
275
|
sessionDepth: (sessionStorage === null || sessionStorage === void 0 ? void 0 : sessionStorage.optableSessionDepthIndex) || 1,
|
|
276
276
|
pageAuctionsCount: ((_b = window.optable) === null || _b === void 0 ? void 0 : _b.pageAuctionIndex) || 1,
|
|
277
|
-
originSlug: ((
|
|
277
|
+
originSlug: ((_c = window.optable) === null || _c === void 0 ? void 0 : _c.site) || ((_e = (_d = this.optableInstance) === null || _d === void 0 ? void 0 : _d.dcn) === null || _e === void 0 ? void 0 : _e.site) || "unknown", // optable.site first since dcn is analytics always
|
|
278
278
|
};
|
|
279
279
|
// Log summary with bid counts
|
|
280
280
|
this.log(`Auction ${auctionId} processed: ${bidderRequests.length} requests, ${totalBids} total bids, ${bidsReceived.length} received, ${noBids.length} no-bids, ${timeoutBids.length} timeouts`);
|
package/lib/dist/build.json
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare const FLAG_KEYS: readonly ["optableDebug", "optableDisableConsent", "optableResolve1P", "optableResolve3P", "optableEnableAnalytics", "optableControlGroup", "optableForceTargeting", "optableForceGlobalRouting", "optableForceSkipMerge"];
|
|
2
|
+
export type FlagKey = (typeof FLAG_KEYS)[number];
|
|
3
|
+
export type Flags = Partial<Record<FlagKey, string>>;
|
|
4
|
+
export declare function getFlags(): Flags;
|
|
5
|
+
export declare function resetFlags(): void;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const FLAG_KEYS = [
|
|
2
|
+
"optableDebug",
|
|
3
|
+
"optableDisableConsent",
|
|
4
|
+
"optableResolve1P",
|
|
5
|
+
"optableResolve3P",
|
|
6
|
+
"optableEnableAnalytics",
|
|
7
|
+
"optableControlGroup",
|
|
8
|
+
"optableForceTargeting",
|
|
9
|
+
"optableForceGlobalRouting",
|
|
10
|
+
"optableForceSkipMerge",
|
|
11
|
+
];
|
|
12
|
+
function parseFlags() {
|
|
13
|
+
const flags = {};
|
|
14
|
+
try {
|
|
15
|
+
const params = new URLSearchParams(window.location.search);
|
|
16
|
+
for (const key of FLAG_KEYS) {
|
|
17
|
+
if (params.has(key)) {
|
|
18
|
+
flags[key] = params.get(key) || "1";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (_a) {
|
|
23
|
+
// URL params unavailable
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
for (const key of FLAG_KEYS) {
|
|
27
|
+
if (!(key in flags)) {
|
|
28
|
+
const val = sessionStorage.getItem(key);
|
|
29
|
+
if (val !== null) {
|
|
30
|
+
flags[key] = val;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (_b) {
|
|
36
|
+
// sessionStorage unavailable
|
|
37
|
+
}
|
|
38
|
+
return flags;
|
|
39
|
+
}
|
|
40
|
+
let _flags = null;
|
|
41
|
+
export function getFlags() {
|
|
42
|
+
if (!_flags) {
|
|
43
|
+
_flags = parseFlags();
|
|
44
|
+
}
|
|
45
|
+
return _flags;
|
|
46
|
+
}
|
|
47
|
+
export function resetFlags() {
|
|
48
|
+
_flags = null;
|
|
49
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// RTD (Real-Time Data) module for Prebid.js integration
|
|
2
1
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
2
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
3
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -19,6 +18,8 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
19
18
|
}
|
|
20
19
|
return t;
|
|
21
20
|
};
|
|
21
|
+
// RTD (Real-Time Data) module for Prebid.js integration
|
|
22
|
+
import { getFlags } from "../flags";
|
|
22
23
|
// Merge strategies for EIDs
|
|
23
24
|
function appendMergeStrategy(existingEids, newEids) {
|
|
24
25
|
return [...existingEids, ...newEids];
|
|
@@ -241,18 +242,20 @@ function handleRtd(config, reqBidsConfigObj, targetingData, _optableExtraData, _
|
|
|
241
242
|
let globalEidsCount = 0;
|
|
242
243
|
let bidderEidsCount = 0;
|
|
243
244
|
Object.entries(eidsPerRoute).forEach(([route, ortb2]) => {
|
|
244
|
-
var _a, _b, _c
|
|
245
|
+
var _a, _b, _c;
|
|
245
246
|
const count = ((_c = (_b = (_a = ortb2.user) === null || _a === void 0 ? void 0 : _a.ext) === null || _b === void 0 ? void 0 : _b.eids) === null || _c === void 0 ? void 0 : _c.length) || 0;
|
|
246
247
|
if (route === "global") {
|
|
247
248
|
globalEidsCount += count;
|
|
248
249
|
skippedEids += merge(config, reqBidsConfigObj.ortb2Fragments.global, ortb2);
|
|
249
250
|
}
|
|
250
|
-
else {
|
|
251
|
+
else if (route in reqBidsConfigObj.ortb2Fragments.bidder) {
|
|
251
252
|
bidderEidsCount += count;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
skippedEids += merge(config, reqBidsConfigObj.ortb2Fragments.bidder[route], ortb2);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
config.log("info", `Bidder "${route}" not in auction, routing ${count} EID(s) to global`);
|
|
257
|
+
globalEidsCount += count;
|
|
258
|
+
skippedEids += merge(config, reqBidsConfigObj.ortb2Fragments.global, ortb2);
|
|
256
259
|
}
|
|
257
260
|
});
|
|
258
261
|
config.log("info", `Processed ${processedEids} EIDs, skipped ${skippedEids} EIDs`);
|
|
@@ -265,34 +268,35 @@ function liveIntentUID2(ortb2) {
|
|
|
265
268
|
return (((_c = (_b = (_a = ortb2.user) === null || _a === void 0 ? void 0 : _a.ext) === null || _b === void 0 ? void 0 : _b.eids) === null || _c === void 0 ? void 0 : _c.some((eid) => eid.source === "uidapi.com" && eid.uids.some((uid) => { var _a; return ((_a = uid.ext) === null || _a === void 0 ? void 0 : _a.provider) === "liveintent.com"; }))) || false);
|
|
266
269
|
}
|
|
267
270
|
function buildRTD(options = {}) {
|
|
268
|
-
var _a, _b, _c, _d, _e, _f, _g
|
|
269
|
-
|
|
271
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
272
|
+
const flags = getFlags();
|
|
273
|
+
if (flags.optableForceGlobalRouting || options.forceGlobalRouting) {
|
|
270
274
|
forceGlobalRouting();
|
|
271
275
|
}
|
|
272
276
|
return {
|
|
273
|
-
enableLogging:
|
|
277
|
+
enableLogging: !!flags.optableDebug || ((_a = options.enableLogging) !== null && _a !== void 0 ? _a : false),
|
|
274
278
|
log(level, message, ...args) {
|
|
275
279
|
if (this.enableLogging) {
|
|
276
280
|
log(level, message, ...args);
|
|
277
281
|
}
|
|
278
282
|
},
|
|
279
|
-
eidSources: (
|
|
280
|
-
skipMerge:
|
|
283
|
+
eidSources: (_b = options.eidSources) !== null && _b !== void 0 ? _b : Object.assign({}, defaultEIDSources),
|
|
284
|
+
skipMerge: flags.optableForceSkipMerge
|
|
281
285
|
? () => true
|
|
282
286
|
: options.skipMerge !== undefined
|
|
283
287
|
? options.skipMerge
|
|
284
288
|
: () => false,
|
|
285
|
-
optableCacheTargeting: (
|
|
286
|
-
matcherFilter: (
|
|
287
|
-
matcherExclude: (
|
|
289
|
+
optableCacheTargeting: (_c = options.optableCacheTargeting) !== null && _c !== void 0 ? _c : "OPTABLE_RESOLVED",
|
|
290
|
+
matcherFilter: (_d = options.matcherFilter) !== null && _d !== void 0 ? _d : [],
|
|
291
|
+
matcherExclude: (_e = options.matcherExclude) !== null && _e !== void 0 ? _e : [],
|
|
288
292
|
mergeStrategy: options.mergeStrategy,
|
|
289
293
|
appendMergeStrategy,
|
|
290
294
|
prependMergeStrategy,
|
|
291
295
|
replaceMergeStrategy,
|
|
292
296
|
appendNewMergeStrategy,
|
|
293
297
|
targetingFromCache,
|
|
294
|
-
instance: (
|
|
295
|
-
waitForTargeting: (
|
|
298
|
+
instance: (_f = options.instance) !== null && _f !== void 0 ? _f : "instance",
|
|
299
|
+
waitForTargeting: (_g = options.waitForTargeting) !== null && _g !== void 0 ? _g : false,
|
|
296
300
|
handleRtd(reqBidsConfigObj, optableExtraData, mergeFn) {
|
|
297
301
|
return __awaiter(this, void 0, void 0, function* () {
|
|
298
302
|
var _a;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function determineABTest(abTests) {
|
|
2
|
+
if (!abTests || abTests.length === 0) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const totalTrafficPercentage = abTests.reduce((sum, test) => sum + test.trafficPercentage, 0);
|
|
6
|
+
if (totalTrafficPercentage > 100) {
|
|
7
|
+
console.error(`AB Test Config Error: Traffic Percentage Sum Exceeds 100%`);
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const bucket = Math.floor(Math.random() * 100);
|
|
11
|
+
let cumulative = 0;
|
|
12
|
+
for (const test of abTests) {
|
|
13
|
+
cumulative += test.trafficPercentage;
|
|
14
|
+
if (bucket < cumulative) {
|
|
15
|
+
return test;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolvedConfig,
|
|
1
|
+
import type { ResolvedConfig, MatcherOverride } from "../config";
|
|
2
2
|
import * as ortb2 from "iab-openrtb/v26";
|
|
3
3
|
type Identifier = {
|
|
4
4
|
id: string;
|
|
@@ -28,16 +28,21 @@ type TargetingResponse = {
|
|
|
28
28
|
ab_test_id?: string;
|
|
29
29
|
split_test_assignment?: string;
|
|
30
30
|
};
|
|
31
|
-
declare function determineABTest(abTests?: ABTestConfig[]): ABTestConfig | null;
|
|
32
31
|
declare function Targeting(config: ResolvedConfig, req: TargetingRequest): Promise<TargetingResponse>;
|
|
33
32
|
declare function TargetingFromCache(config: ResolvedConfig): TargetingResponse | null;
|
|
34
33
|
declare function TargetingClearCache(config: ResolvedConfig): void;
|
|
34
|
+
/**
|
|
35
|
+
* Skip targeting for bots by marking targeting as already done,
|
|
36
|
+
* so RTD short-circuits and returns null. No-op for real users.
|
|
37
|
+
* Returns whether the request was identified as a bot.
|
|
38
|
+
*/
|
|
39
|
+
export declare function SkipTargetingForBots(): boolean;
|
|
35
40
|
type PrebidORTB2 = {
|
|
36
41
|
user: ortb2.User;
|
|
37
42
|
};
|
|
38
43
|
declare function PrebidORTB2(tdata: TargetingResponse | null): PrebidORTB2;
|
|
39
44
|
type TargetingKeyValues = Record<string, string[]>;
|
|
40
45
|
declare function TargetingKeyValues(tdata: TargetingResponse | null): TargetingKeyValues;
|
|
41
|
-
export { Targeting, TargetingFromCache, TargetingClearCache, PrebidORTB2, TargetingKeyValues
|
|
46
|
+
export { Targeting, TargetingFromCache, TargetingClearCache, PrebidORTB2, TargetingKeyValues };
|
|
42
47
|
export default Targeting;
|
|
43
|
-
export type { TargetingResponse, TargetingRequest,
|
|
48
|
+
export type { TargetingResponse, TargetingRequest, MatcherOverride };
|