Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
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
215 changes: 215 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,215 @@
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;
};

describe('installTsAdInit', () => {
beforeEach(() => {
vi.resetModules();
delete (window as TestWindow).__ts_ad_slots;
delete (window as TestWindow).__ts_bids;
delete (window as TestWindow).__tsAdInit;
// 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('atf'),
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',
gam_unit_path: '/123/atf',
div_id: 'atf',
formats: [[300, 250]],
targeting: { pos: 'atf' },
},
];
(window as TestWindow).__ts_bids = {
atf: {
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('atf'),
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',
gam_unit_path: '/123/atf',
div_id: 'atf',
formats: [[300, 250]],
targeting: {},
},
];
(window as TestWindow).__ts_bids = {
atf: {
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('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('atf'),
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',
gam_unit_path: '/123/atf',
div_id: 'atf',
formats: [[300, 250]],
targeting: {},
},
];
(window as TestWindow).__ts_bids = {
atf: {
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('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 = [];
(window as TestWindow).__ts_bids = {};

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

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