@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.
@@ -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,2 @@
1
+ /** True if the current user agent looks like a known bot/crawler. */
2
+ export declare function isBot(userAgent?: string): boolean;
@@ -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 = [], timeoutBids = [] } = event;
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, { auctionEnd: event, createdAt, missed, auctionEndTimeoutId, bidWonEvents: [] });
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 = [], timeoutBids = [] } = auctionEndEvent;
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 ((_c = (_b = (_a = bidReceived === null || bidReceived === void 0 ? void 0 : 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) {
431
- bid.splitTestAssignment = bidReceived.ortb2Imp.ext.optable.splitTestAssignment;
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: ((_a = this.prebidInstance) === null || _a === void 0 ? void 0 : _a.version) || "unknown",
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: ((_b = window.optable) === null || _b === void 0 ? void 0 : _b.pageAuctionsCount) || 1,
463
- originSlug: ((_d = (_c = this.optableInstance) === null || _c === void 0 ? void 0 : _c.dcn) === null || _d === void 0 ? void 0 : _d.site) || ((_e = window.optable) === null || _e === void 0 ? void 0 : _e.site) || "unknown",
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 ((_f = window.optable) === null || _f === void 0 ? void 0 : _f.customAnalytics) {
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) || ((_d = ortb2.user) === null || _d === void 0 ? void 0 : _d.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: ((_d = (_c = this.optableInstance) === null || _c === void 0 ? void 0 : _c.dcn) === null || _d === void 0 ? void 0 : _d.site) || ((_e = window.optable) === null || _e === void 0 ? void 0 : _e.site) || "unknown",
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`);
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "v0.54.1"
2
+ "version": "v0.55.1"
3
3
  }
@@ -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, _d;
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
- const bidder = (_d = reqBidsConfigObj.ortb2Fragments.bidder[route]) !== null && _d !== void 0 ? _d : {};
253
- skippedEids += merge(config, bidder, ortb2);
254
- // eslint-disable-next-line no-param-reassign
255
- reqBidsConfigObj.ortb2Fragments.bidder[route] = bidder;
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, _h;
269
- if (sessionStorage.optableForceGlobalRouting || options.forceGlobalRouting) {
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: (_b = (_a = sessionStorage.optableDebug) !== null && _a !== void 0 ? _a : options.enableLogging) !== null && _b !== void 0 ? _b : false,
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: (_c = options.eidSources) !== null && _c !== void 0 ? _c : Object.assign({}, defaultEIDSources),
280
- skipMerge: sessionStorage.optableForceSkipMerge
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: (_d = options.optableCacheTargeting) !== null && _d !== void 0 ? _d : "OPTABLE_RESOLVED",
286
- matcherFilter: (_e = options.matcherFilter) !== null && _e !== void 0 ? _e : [],
287
- matcherExclude: (_f = options.matcherExclude) !== null && _f !== void 0 ? _f : [],
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: (_g = options.instance) !== null && _g !== void 0 ? _g : "instance",
295
- waitForTargeting: (_h = options.waitForTargeting) !== null && _h !== void 0 ? _h : false,
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,2 @@
1
+ import type { ABTestConfig } from "../config";
2
+ export declare function determineABTest(abTests?: ABTestConfig[]): ABTestConfig | null;
@@ -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, ABTestConfig, MatcherOverride } from "../config";
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, determineABTest };
46
+ export { Targeting, TargetingFromCache, TargetingClearCache, PrebidORTB2, TargetingKeyValues };
42
47
  export default Targeting;
43
- export type { TargetingResponse, TargetingRequest, ABTestConfig, MatcherOverride };
48
+ export type { TargetingResponse, TargetingRequest, MatcherOverride };