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
3 changes: 2 additions & 1 deletion lib/core/prebid/rtd.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ On each auction, `handleRtd`:

1. Returns null while `isControlGroup()` is true — no EIDs reach bids.
2. Serves from the cache immediately when it holds EIDs.
3. Otherwise, with `waitForTargeting` on and a Prebid `auctionDelay` configured, waits for the `optable-targeting:change` event (sent whenever the SDK writes the targeting cache) up to the auction delay, then serves whatever the cache holds. A cache entry without EIDs does not skip the wait: targeting may still be in flight.
3. Otherwise, with `waitForTargeting` on and a Prebid `auctionDelay` configured, waits for the `optable-targeting:change` event (sent whenever the SDK writes the targeting cache) up to the auction delay, then serves whatever the cache holds. A cache entry without EIDs does not skip the wait: targeting may still be in flight. The delay is read from `window[prebidGlobal]`, so a page running several Prebid instances has to name the one whose `realTimeData` config carries it, or no wait happens and the auction is served unenriched.
4. Routes each EID to the bidders configured for its source, falling back to `global` for unknown sources or bidders absent from the auction, and merges per the source's strategy.

## Options

| Option | Default | Description |
| ----------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `waitForTargeting` | `false` | Wait (bounded by Prebid's `auctionDelay`) for a targeting response when the cache has no EIDs. |
| `prebidGlobal` | `"pbjs"` | Name of the Prebid global whose `realTimeData.auctionDelay` bounds that wait. |
| `isControlGroup` | `() => false` | Split-test gate, evaluated per auction; while true, `handleRtd` serves no EIDs. |
| `eidSources` | built-in routing | Per-source bidder routes and merge strategy. Pass `{}` to route everything to `global`. |
| `mergeStrategy` | append | Global merge strategy; `appendMergeStrategy`, `prependMergeStrategy`, `replaceMergeStrategy` and `appendNewMergeStrategy` are exported. |
Expand Down
60 changes: 58 additions & 2 deletions lib/core/prebid/rtd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { ReqBidsConfigObj } from "./rtd";

const EIDS = [{ source: "uidapi.com", uids: [{ id: "uid2-token" }] }];

const w = window as unknown as { pbjs?: { getConfig: () => unknown } };
type PrebidStub = { getConfig: () => unknown };
const w = window as unknown as { pbjs?: PrebidStub; asc?: PrebidStub };

function bidsConfig(): ReqBidsConfigObj {
return { ortb2Fragments: { global: {}, bidder: {} } };
Expand All @@ -18,10 +19,15 @@ function seedCache(eids: unknown[]) {
localStorage.setItem("OPTABLE_RESOLVED", JSON.stringify({ ortb2: { user: { eids } } }));
}

function withAuctionDelay(ms: number): PrebidStub {
return { getConfig: () => ({ realTimeData: { auctionDelay: ms } }) };
}

beforeEach(() => {
localStorage.clear();
sessionStorage.clear();
w.pbjs = { getConfig: () => ({ realTimeData: { auctionDelay: 500 } }) };
w.pbjs = withAuctionDelay(500);
delete w.asc;
});

describe("buildRTD - waitForTargeting", () => {
Expand Down Expand Up @@ -75,6 +81,56 @@ describe("buildRTD - waitForTargeting", () => {
});
});

describe("buildRTD - prebidGlobal", () => {
it("takes the auction delay from the named global", async () => {
delete w.pbjs;
w.asc = withAuctionDelay(500);
const req = bidsConfig();

const pending = buildRTD({ waitForTargeting: true, prebidGlobal: "asc" }).handleRtd(req);
seedCache(EIDS);
window.dispatchEvent(new CustomEvent(targetingEventName));
await pending;

expect(globalEids(req)).toHaveLength(1);
});

// The bug this option exists for: the auction runs on another instance, the
// delay lookup misses, and the wait never happens.
it("does not wait when only another instance declares the delay", async () => {
delete w.pbjs;
w.asc = withAuctionDelay(500);
const req = bidsConfig();

await buildRTD({ waitForTargeting: true }).handleRtd(req);

expect(globalEids(req)).toHaveLength(0);
});

it("still defaults to pbjs", async () => {
w.asc = withAuctionDelay(500);
const req = bidsConfig();

const pending = buildRTD({ waitForTargeting: true }).handleRtd(req);
seedCache(EIDS);
window.dispatchEvent(new CustomEvent(targetingEventName));
await pending;

expect(globalEids(req)).toHaveLength(1);
});

it("warns when the wait is asked for but no delay bounds it", async () => {
delete w.pbjs;
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
const req = bidsConfig();

await buildRTD({ waitForTargeting: true, enableLogging: true }).handleRtd(req);

expect(warn.mock.calls.flat().join(" ")).toContain("no realTimeData.auctionDelay");
warn.mockRestore();
});
});

describe("buildRTD - isControlGroup", () => {
it("serves no EIDs while the gate returns true", async () => {
seedCache(EIDS);
Expand Down
16 changes: 15 additions & 1 deletion lib/core/prebid/rtd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ interface RTDConfig {
targetingFromCache: (config?: RTDConfig) => TargetingData | null;
handleRtd: (reqBidsConfigObj: ReqBidsConfigObj, optableExtraData?: any, mergeFn?: any) => Promise<void | null>;
instance: string;
prebidGlobal: string;
waitForTargeting: boolean;
isControlGroup: () => boolean;
}
Expand All @@ -83,6 +84,9 @@ interface RTDOptions {
forceGlobalRouting?: boolean;
mergeStrategy?: MergeStrategy;
instance?: string;
// Name of the publisher's Prebid global, the one whose realTimeData
// auctionDelay bounds the waitForTargeting wait.
prebidGlobal?: string;
waitForTargeting?: boolean;
// Split-test gate: while it returns true, handleRtd serves no EIDs. Wired
// to the wrapper's assignment (for example setupAB's result).
Expand Down Expand Up @@ -168,7 +172,16 @@ async function readTargetingData(config: RTDConfig): Promise<TargetingData> {
const cacheHasEids = (cachedData?.ortb2?.user?.eids?.length ?? 0) > 0;

// Get auction delay from pbjs config
const delay = (window as any)?.pbjs?.getConfig?.()?.realTimeData?.auctionDelay;
const delay = (window as any)?.[config.prebidGlobal]?.getConfig?.()?.realTimeData?.auctionDelay;

// An unbounded wait would hold the auction indefinitely, so a missing delay
// still serves. Say so: it reads as waitForTargeting silently doing nothing.
if (config.waitForTargeting && !cacheHasEids && !delay) {
config.log(
"warn",
`waitForTargeting is set but window.${config.prebidGlobal} reports no realTimeData.auctionDelay; serving this auction unenriched`
);
}

// Return immediately when waitForTargeting is off, the cache already has
// EIDs to serve, or no auction delay bounds a wait. A cache entry without
Expand Down Expand Up @@ -389,6 +402,7 @@ function buildRTD(options: RTDOptions = {}): RTDConfig {
appendNewMergeStrategy,
targetingFromCache,
instance: options.instance ?? "instance",
prebidGlobal: options.prebidGlobal ?? "pbjs",
waitForTargeting: options.waitForTargeting ?? false,
isControlGroup: options.isControlGroup ?? (() => false),
async handleRtd(reqBidsConfigObj: ReqBidsConfigObj, optableExtraData?: any, mergeFn?: any): Promise<void | null> {
Expand Down