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
47 changes: 40 additions & 7 deletions crates/trusted-server-js/lib/src/integrations/prebid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,35 @@ function recordUserIdModuleDiagnostics(): PrebidUserIdDiagnostics {
/** Resolved endpoint — set by installPrebidNpm, read by the adapter. */
let auctionEndpoint = '/auction';

// Prebid normalizes each bid into its own internal object during `addBidResponse`,
// which can drop unknown top-level fields — so the custom `trustedServerRenderer`
// descriptor set in `interpretResponse` may be gone by the time the `bidResponse`
// listener runs (observed in production: the field is absent as early as `bidAccepted`).
// To make registration independent of custom-field survival, the descriptor is also
// stashed here keyed by `requestId` (a first-class field Prebid preserves) at
// `interpretResponse` time, and the `bidResponse` listener falls back to it. Bounded.
const MAX_PENDING_APS_RENDERERS = 256;
const pendingApsRenderersByRequestId = new Map<string, unknown>();

function stashPendingApsRenderer(requestId: unknown, renderer: unknown): void {
if (typeof requestId !== 'string' || requestId.length === 0) return;
if (
!pendingApsRenderersByRequestId.has(requestId) &&
pendingApsRenderersByRequestId.size >= MAX_PENDING_APS_RENDERERS
) {
const oldest = pendingApsRenderersByRequestId.keys().next().value;
if (oldest !== undefined) pendingApsRenderersByRequestId.delete(oldest);
}
pendingApsRenderersByRequestId.set(requestId, renderer);
}

function takePendingApsRenderer(requestId: unknown): unknown {
if (typeof requestId !== 'string') return undefined;
const renderer = pendingApsRenderersByRequestId.get(requestId);
if (renderer !== undefined) pendingApsRenderersByRequestId.delete(requestId);
return renderer;
}

/**
* Convert parsed {@link AuctionBid}s into Prebid bid response objects,
* linking each bid back to the original BidRequest via `requestId`.
Expand Down Expand Up @@ -220,9 +249,12 @@ export function auctionBidsToPrebidBids(auctionBids: AuctionBid[], bidRequests:
}

const origReq = requestsByCode.get(bid.impid);
const requestId = origReq?.bidId ?? bid.impid;
// Stash by requestId so registration survives Prebid stripping the custom field.
if (renderer) stashPendingApsRenderer(requestId, renderer);
return [
{
requestId: origReq?.bidId ?? bid.impid,
requestId,
cpm: bid.price,
width: bid.width,
height: bid.height,
Expand Down Expand Up @@ -524,12 +556,13 @@ function installApsBidResponseRegistry(): void {

pbjs.onEvent('bidResponse', (rawBid) => {
const bid = rawBid as unknown as Record<string, unknown>;
const renderer = bid[APS_RENDERER_FIELD];
if (
bid['adapterCode'] !== ADAPTER_CODE ||
bid['bidderCode'] !== APS_BIDDER_CODE ||
renderer === undefined
) {
if (bid['adapterCode'] !== ADAPTER_CODE || bid['bidderCode'] !== APS_BIDDER_CODE) {
return;
}
// Prefer the custom field; fall back to the requestId stash when Prebid has stripped
// it during bid normalization (the field is often gone before this listener runs).
const renderer = bid[APS_RENDERER_FIELD] ?? takePendingApsRenderer(bid['requestId']);
if (renderer === undefined) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,49 @@ describe('prebid/installPrebidNpm', () => {
);
});

it('registers APS renderer via requestId when Prebid strips the custom field', () => {
installPrebidNpm();

const bidResponseListener = mockOnEvent.mock.calls.find(
([eventName]) => eventName === 'bidResponse'
)?.[1] as ((bid: Record<string, unknown>) => void) | undefined;
expect(bidResponseListener).toBeTypeOf('function');

const renderer = apsRenderer();
// interpretResponse output: stashes the descriptor keyed by requestId (survives
// Prebid's bid normalization, which strips the custom trustedServerRenderer field).
auctionBidsToPrebidBids(
[
{
impid: 'div-aps',
renderer,
price: 1.0,
width: 300,
height: 250,
seat: 'aps',
creativeId: 'cr-aps',
adomain: [],
},
],
[{ adUnitCode: 'div-aps', bidId: 'req-strip' }]
);

// Prebid delivered the bid with the custom field REMOVED — only requestId survives.
bidResponseListener!({
adapterCode: 'trustedServer',
bidderCode: 'aps',
adId: 'stripped-field-ad-id',
adUnitCode: 'div-aps',
ttl: 300,
requestId: 'req-strip',
});

const entry = (window as any).tsjs.apsPrebidRenderers['stripped-field-ad-id'];
expect(entry).toEqual(
expect.objectContaining({ adUnitCode: 'div-aps', renderer, markWinner: expect.any(Function) })
);
});

it('does not register malformed or non-trusted APS renderer capabilities', () => {
const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => {});
installPrebidNpm();
Expand Down
Loading