Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
2b326e7
Add server-side ad templates design spec
jevansnyc Apr 15, 2026
33e07af
Merge branch 'main' into server-side-ad-templates-spec
prk-Jr Apr 29, 2026
9182cef
Update server-side ad templates spec and add implementation plan
prk-Jr Apr 30, 2026
46e60fe
Update server-side ad templates spec and add implementation plan
prk-Jr Apr 30, 2026
5ca9db1
Merge branch 'main' into server-side-ad-templates-spec
prk-Jr Apr 30, 2026
7598054
Update rust edition from 2021 to 2024
prk-Jr Apr 30, 2026
cf0f908
Rework spec for non-blocking page rendering with /ts-bids fetch
jevansnyc Apr 30, 2026
664b20e
fmt docs and update plan as per spec
prk-Jr May 1, 2026
87b58b2
Rework ad templates spec for body injection and add identity model
jevansnyc May 5, 2026
0faa198
Update the server side ad template implementation plan
prk-Jr May 5, 2026
47be0a9
Add glob workspace dependency for URL pattern matching
prk-Jr May 5, 2026
91ab687
Add Prebid price granularity bucketing (dense default, auto = dense)
prk-Jr May 5, 2026
bf03be7
Add MediaType::banner(), Bid::ad_id, and suppress_nurl config to Preb…
prk-Jr May 5, 2026
1be9a10
Add creative_opportunities config types, URL glob matching, APS param…
prk-Jr May 5, 2026
ba705a6
Wire CreativeOpportunitiesConfig into Settings; add creative-opportun…
prk-Jr May 5, 2026
d587576
Validate creative-opportunities.toml slot IDs at build time using inl…
prk-Jr May 5, 2026
f8aff38
Inject __ts_ad_slots at head-open and __ts_bids before </body> via sh…
prk-Jr May 5, 2026
8b9500c
Convert handle_publisher_request to async; body-inject __ts_bids; eli…
prk-Jr May 5, 2026
9cdbb36
Emit __tsAdInit with synchronous window.__ts_bids read; nurl+burl fro…
prk-Jr May 5, 2026
6b624e3
Fix bid map shape and ad slots property names; resolve clippy errors
prk-Jr May 6, 2026
c212ec5
Wire slots_file and orchestrator into adapter; parse creative-opportu…
prk-Jr May 6, 2026
fb4cf0d
Add synchronous __tsAdInit (reads window.__ts_bids inline); nurl+burl…
prk-Jr May 6, 2026
ed6d0d8
Fix cmd type regression and null guard in installTsAdInit
prk-Jr May 6, 2026
95aad76
Add end-to-end publisher helper tests for body-injection architecture
prk-Jr May 6, 2026
b047add
Enable server-side auction with APS provider and adserver_mock mediator
prk-Jr May 6, 2026
6a5df10
Fix adserver_mock test for numeric price; fix GPT JS formatting
prk-Jr May 6, 2026
e6c18ad
Replace explicit any in GPT integration with typed interfaces
prk-Jr May 6, 2026
74bbc25
Update creative-opportunities config to real autoblog.com GAM values
prk-Jr May 6, 2026
51aba8f
Update auction timeout and APS slot ID bug
prk-Jr May 6, 2026
3d51fe4
Call __tsAdInit after injecting __ts_bids into page
prk-Jr May 6, 2026
4cf6d98
Fix format error
prk-Jr May 6, 2026
e06af4b
Add PBS inline bidder params via creative-opportunities.toml
prk-Jr May 6, 2026
5cbf05f
Fix clippy errors
prk-Jr May 6, 2026
60011f0
Fix test assertion
prk-Jr May 6, 2026
cf5091f
Fix double __ts_bids injection
prk-Jr May 6, 2026
eccfd45
Fix max-age cookie issue -> no-store
prk-Jr May 6, 2026
5bb12d0
Add /__ts/page-bids endpoint for pushState/replaceState
prk-Jr May 7, 2026
982fa3e
Fix format ts
prk-Jr May 7, 2026
77d3c4a
__tsDivToSlotId now replaced per navigation (not merged) — stale div_…
prk-Jr May 7, 2026
38c8bf1
Update timeout for mocktioneer
prk-Jr May 7, 2026
e32bfa5
Revert with updated tiomeout
prk-Jr May 7, 2026
b1e74c9
Wip: Align with the spec
prk-Jr May 9, 2026
a2d08e7
Fix clippy explicit-auto-deref in stream_publisher_body_async call
prk-Jr May 10, 2026
b03af6b
Fix Cache-Control headers applied only when slots matched — apply to …
prk-Jr May 10, 2026
349dcdc
cargo fmt
prk-Jr May 10, 2026
78885f9
Fix auction consent gate blocking non-GDPR regions — only require TCF…
prk-Jr May 10, 2026
3783e68
Fix SSP requests using placeholder headers — pass real request to dis…
prk-Jr May 10, 2026
0c94652
Fix async auction collect abandoning SSP bids when origin is slow
prk-Jr May 11, 2026
f172f44
Cargo fmt
prk-Jr May 11, 2026
14cd493
Fix mediator always skipped when origin body exceeds SSP auction budget
prk-Jr May 11, 2026
913deb4
Merge main into server-side-ad-templates-impl
prk-Jr May 11, 2026
6210ebb
Cargo fmt
prk-Jr May 11, 2026
3cdf995
Adding debug info for auction
prk-Jr May 11, 2026
e35c593
Fix auction bids missing on Next.js buffered HTML path
prk-Jr May 11, 2026
3dac776
Add path label and auction time to debug HTML comment
prk-Jr May 11, 2026
65c0ad3
Fix XSS in script injection and cap mediator at A_deadline
prk-Jr May 11, 2026
0f67a8d
Added footer slot id
prk-Jr May 13, 2026
a7e8751
Fix page-bids auction context, protect Cache-Control from operator ov…
prk-Jr May 14, 2026
4011363
Restore nurl/burl/ad_id through adserver_mock mediation path
prk-Jr May 14, 2026
8516caa
Populate device.user_agent in auction request
prk-Jr May 14, 2026
9e0ec5b
Fix __tsAdInit fallback: look up bids by slot id not div id
prk-Jr May 14, 2026
d27a329
Format lint using cargo fmt
prk-Jr May 14, 2026
790c123
Fix clippy doc-markdown lint in adserver_mock
prk-Jr May 14, 2026
299f6ba
Remove inline PBS bidder params from creative-opportunities.toml
prk-Jr May 15, 2026
03d39f2
Clarify and test APS floor price enforcement in mediation path
prk-Jr May 15, 2026
f09eb34
Document and test /auction API contract for non-Prebid.js callers
prk-Jr May 15, 2026
a03c70a
Verify and document graceful degradation when no slots match URL
prk-Jr May 15, 2026
4218333
Format publisher.rs with cargo fmt
prk-Jr May 15, 2026
0762999
Document scroll/refresh handoff contract between TS and slim-Prebid
prk-Jr May 15, 2026
e45f458
feat: add support for reading Extended User IDs from the ts-eids cook…
prk-Jr May 15, 2026
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
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ fastly = "0.11.12"
fern = "0.7.1"
flate2 = "1.1"
futures = "0.3"
glob = "0.3"
hex = "0.4.3"
hmac = "0.12.1"
http = "1.4.0"
Expand Down
339 changes: 339 additions & 0 deletions crates/js/lib/src/integrations/gpt/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';

interface SlotRenderEvent {
isEmpty: boolean;
slot: {
getSlotElementId(): string;
getTargeting(key: string): string[];
};
}

type TestWindow = Window & {
googletag?: unknown;
__ts_ad_slots?: unknown;
__ts_bids?: unknown;
__tsAdInit?: () => void;
__tsPrevGptSlots?: unknown;
__tsServicesEnabled?: boolean;
__tsSpaHookInstalled?: boolean;
__tsDivToSlotId?: Record<string, string>;
};

describe('installTsAdInit', () => {
beforeEach(() => {
vi.resetModules();
delete (window as TestWindow).__ts_ad_slots;
delete (window as TestWindow).__ts_bids;
delete (window as TestWindow).__tsAdInit;
delete (window as TestWindow).__tsPrevGptSlots;
delete (window as TestWindow).__tsSpaHookInstalled;
delete (window as TestWindow).__tsDivToSlotId;
(window as TestWindow).__tsServicesEnabled = false;
// jsdom does not implement navigator.sendBeacon; polyfill it for tests
if (!('sendBeacon' in navigator)) {
Object.defineProperty(navigator, 'sendBeacon', {
value: vi.fn().mockReturnValue(true),
writable: true,
configurable: true,
});
}
});

it('reads window.__ts_bids synchronously and applies bid targeting before refresh', async () => {
const mockSlot = {
addService: vi.fn().mockReturnThis(),
setTargeting: vi.fn().mockReturnThis(),
getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'),
getTargeting: vi.fn().mockReturnValue(['abc']),
};
const mockPubads = {
enableSingleRequest: vi.fn(),
addEventListener: vi.fn(),
refresh: vi.fn(),
};
(window as TestWindow).googletag = {
cmd: { push: vi.fn((fn: () => void) => fn()) },
defineSlot: vi.fn().mockReturnValue(mockSlot),
pubads: vi.fn().mockReturnValue(mockPubads),
enableServices: vi.fn(),
};
(window as TestWindow).__ts_ad_slots = [
{
id: 'atf_sidebar_ad',
gam_unit_path: '/123/atf',
div_id: 'div-atf-sidebar',
formats: [[300, 250]],
targeting: { pos: 'atf' },
},
];
(window as TestWindow).__ts_bids = {
atf_sidebar_ad: {
hb_pb: '1.00',
hb_bidder: 'kargo',
hb_adid: 'abc',
nurl: 'https://ssp/win',
burl: 'https://ssp/bill',
},
};

const fetchSpy = vi.spyOn(global, 'fetch');

const { installTsAdInit } = await import('./index');
installTsAdInit();
(window as TestWindow).__tsAdInit!();

expect(fetchSpy).not.toHaveBeenCalled();
expect(mockSlot.setTargeting).toHaveBeenCalledWith('hb_pb', '1.00');
expect(mockSlot.setTargeting).toHaveBeenCalledWith('hb_bidder', 'kargo');
expect(mockSlot.setTargeting).toHaveBeenCalledWith('ts_initial', '1');
expect(mockPubads.refresh).toHaveBeenCalled();

fetchSpy.mockRestore();
});

it('fires both nurl and burl via sendBeacon on slotRenderEnded when our bid won', async () => {
const beaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true);
let capturedListener: ((e: SlotRenderEvent) => void) | undefined;

const mockSlot = {
addService: vi.fn().mockReturnThis(),
setTargeting: vi.fn().mockReturnThis(),
getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'),
getTargeting: vi.fn().mockReturnValue(['abc']),
};
const mockPubads = {
enableSingleRequest: vi.fn(),
refresh: vi.fn(),
addEventListener: vi.fn((event: string, fn: (e: SlotRenderEvent) => void) => {
if (event === 'slotRenderEnded') capturedListener = fn;
}),
};
(window as TestWindow).googletag = {
cmd: { push: vi.fn((fn: () => void) => fn()) },
defineSlot: vi.fn().mockReturnValue(mockSlot),
pubads: vi.fn().mockReturnValue(mockPubads),
enableServices: vi.fn(),
};
(window as TestWindow).__ts_ad_slots = [
{
id: 'atf_sidebar_ad',
gam_unit_path: '/123/atf',
div_id: 'div-atf-sidebar',
formats: [[300, 250]],
targeting: {},
},
];
(window as TestWindow).__ts_bids = {
atf_sidebar_ad: {
hb_pb: '1.00',
hb_bidder: 'kargo',
hb_adid: 'abc',
nurl: 'https://ssp/win',
burl: 'https://ssp/bill',
},
};

const { installTsAdInit } = await import('./index');
installTsAdInit();
(window as TestWindow).__tsAdInit!();

expect(capturedListener).toBeDefined();
capturedListener!({ isEmpty: false, slot: mockSlot });

expect(beaconSpy).toHaveBeenCalledWith('https://ssp/win');
expect(beaconSpy).toHaveBeenCalledWith('https://ssp/bill');
beaconSpy.mockRestore();
});

it('fires beacons for APS bid (no hb_adid) when ad renders in our slot', async () => {
const beaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true);
let capturedListener: ((e: SlotRenderEvent) => void) | undefined;

const mockSlot = {
addService: vi.fn().mockReturnThis(),
setTargeting: vi.fn().mockReturnThis(),
getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'),
getTargeting: vi.fn().mockReturnValue([]),
};
const mockPubads = {
enableSingleRequest: vi.fn(),
refresh: vi.fn(),
addEventListener: vi.fn((event: string, fn: (e: SlotRenderEvent) => void) => {
if (event === 'slotRenderEnded') capturedListener = fn;
}),
};
(window as TestWindow).googletag = {
cmd: { push: vi.fn((fn: () => void) => fn()) },
defineSlot: vi.fn().mockReturnValue(mockSlot),
pubads: vi.fn().mockReturnValue(mockPubads),
enableServices: vi.fn(),
};
(window as TestWindow).__ts_ad_slots = [
{
id: 'atf_sidebar_ad',
gam_unit_path: '/123/atf',
div_id: 'div-atf-sidebar',
formats: [[300, 250]],
targeting: {},
},
];
(window as TestWindow).__ts_bids = {
atf_sidebar_ad: {
hb_pb: '1.50',
hb_bidder: 'aps',
nurl: 'https://aps/win',
burl: 'https://aps/bill',
},
};

const { installTsAdInit } = await import('./index');
installTsAdInit();
(window as TestWindow).__tsAdInit!();

expect(capturedListener).toBeDefined();
capturedListener!({ isEmpty: false, slot: mockSlot });

expect(beaconSpy).toHaveBeenCalledWith('https://aps/win');
expect(beaconSpy).toHaveBeenCalledWith('https://aps/bill');

beaconSpy.mockClear();
capturedListener!({ isEmpty: true, slot: mockSlot });
expect(beaconSpy).not.toHaveBeenCalled();

beaconSpy.mockRestore();
});

it('does not fire nurl/burl when bid did not win GAM line item', async () => {
const beaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true);
let capturedListener: ((e: SlotRenderEvent) => void) | undefined;

const mockSlotNoMatch = {
addService: vi.fn().mockReturnThis(),
setTargeting: vi.fn().mockReturnThis(),
getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'),
getTargeting: vi.fn().mockReturnValue(['OTHER_BID_ID']),
};
const mockPubads = {
enableSingleRequest: vi.fn(),
refresh: vi.fn(),
addEventListener: vi.fn((event: string, fn: (e: SlotRenderEvent) => void) => {
if (event === 'slotRenderEnded') capturedListener = fn;
}),
};
(window as TestWindow).googletag = {
cmd: { push: vi.fn((fn: () => void) => fn()) },
defineSlot: vi.fn().mockReturnValue(mockSlotNoMatch),
pubads: vi.fn().mockReturnValue(mockPubads),
enableServices: vi.fn(),
};
(window as TestWindow).__ts_ad_slots = [
{
id: 'atf_sidebar_ad',
gam_unit_path: '/123/atf',
div_id: 'div-atf-sidebar',
formats: [[300, 250]],
targeting: {},
},
];
(window as TestWindow).__ts_bids = {
atf_sidebar_ad: {
hb_pb: '1.00',
hb_bidder: 'kargo',
hb_adid: 'abc',
nurl: 'https://ssp/win',
burl: 'https://ssp/bill',
},
};

const { installTsAdInit } = await import('./index');
installTsAdInit();
(window as TestWindow).__tsAdInit!();
capturedListener!({ isEmpty: false, slot: mockSlotNoMatch });

expect(beaconSpy).not.toHaveBeenCalled();
beaconSpy.mockRestore();
});

it('does not fire beacons for slotRenderEnded on slots not owned by TS', async () => {
const beaconSpy = vi.spyOn(navigator, 'sendBeacon').mockReturnValue(true);
let capturedListener: ((e: SlotRenderEvent) => void) | undefined;

const mockSlot = {
addService: vi.fn().mockReturnThis(),
setTargeting: vi.fn().mockReturnThis(),
getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'),
getTargeting: vi.fn().mockReturnValue(['abc']),
};
const arenaSlot = {
getSlotElementId: () => 'arena-owned-div',
getTargeting: () => [],
};
const mockPubads = {
enableSingleRequest: vi.fn(),
refresh: vi.fn(),
addEventListener: vi.fn((event: string, fn: (e: SlotRenderEvent) => void) => {
if (event === 'slotRenderEnded') capturedListener = fn;
}),
};
(window as TestWindow).googletag = {
cmd: { push: vi.fn((fn: () => void) => fn()) },
defineSlot: vi.fn().mockReturnValue(mockSlot),
pubads: vi.fn().mockReturnValue(mockPubads),
enableServices: vi.fn(),
};
(window as TestWindow).__ts_ad_slots = [
{
id: 'atf_sidebar_ad',
gam_unit_path: '/123/atf',
div_id: 'div-atf-sidebar',
formats: [[300, 250]],
targeting: {},
},
];
(window as TestWindow).__ts_bids = {
atf_sidebar_ad: { hb_pb: '1.00', hb_bidder: 'kargo', hb_adid: 'abc' },
};

const { installTsAdInit } = await import('./index');
installTsAdInit();
(window as TestWindow).__tsAdInit!();

capturedListener!({ isEmpty: false, slot: arenaSlot });

expect(beaconSpy).not.toHaveBeenCalled();
beaconSpy.mockRestore();
});

it('calls refresh even when __ts_bids is empty (graceful fallback)', async () => {
const mockPubads = {
enableSingleRequest: vi.fn(),
addEventListener: vi.fn(),
refresh: vi.fn(),
};
(window as TestWindow).googletag = {
cmd: { push: vi.fn((fn: () => void) => fn()) },
defineSlot: vi.fn().mockReturnValue({
addService: vi.fn().mockReturnThis(),
setTargeting: vi.fn().mockReturnThis(),
}),
pubads: vi.fn().mockReturnValue(mockPubads),
enableServices: vi.fn(),
};
(window as TestWindow).__ts_ad_slots = [
{
id: 'atf_sidebar_ad',
gam_unit_path: '/123/atf',
div_id: 'div-atf-sidebar',
formats: [[300, 250]],
targeting: {},
},
];
(window as TestWindow).__ts_bids = {};

const { installTsAdInit } = await import('./index');
installTsAdInit();
(window as TestWindow).__tsAdInit!();

expect(mockPubads.refresh).toHaveBeenCalled();
});
});
Loading
Loading