Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
5e18041
Document server-side ad template implementation plan
prk-Jr May 2, 2026
ba516e2
Add glob dependency for creative opportunity matching
prk-Jr May 2, 2026
d410c2e
Add Prebid price bucket granularity
prk-Jr May 2, 2026
bd1331a
Add bid ad IDs for GAM targeting
prk-Jr May 2, 2026
a907c25
Add creative opportunity config matching
prk-Jr May 2, 2026
10fe804
Wire creative opportunities into settings and build validation
prk-Jr May 3, 2026
7af30f0
Add request-scoped bid cache
prk-Jr May 3, 2026
c4160b0
Inject server-side ad globals at head open
prk-Jr May 3, 2026
21a2864
Add publisher helpers for ad template responses
prk-Jr May 3, 2026
87e7f7a
Add GPT bid fetch bootstrap
prk-Jr May 3, 2026
90f050b
Add ts-bids endpoint
prk-Jr May 3, 2026
3854141
Start ad template auctions without blocking HTML streaming
prk-Jr May 4, 2026
6a76686
Fire Prebid nurls from the edge
prk-Jr May 4, 2026
01cbbe0
Avoid nested executor while polling ad auctions
prk-Jr May 4, 2026
533bb36
Address review findings for ad templates
prk-Jr May 4, 2026
5d474dc
Update doc status
prk-Jr May 4, 2026
24350b1
Remove no-op globals_injected param, drop thread::sleep from WASM dra…
prk-Jr May 4, 2026
e501d16
Fix fmt in build.rs, expand creative-opportunities example page patterns
prk-Jr May 4, 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
193 changes: 192 additions & 1 deletion crates/js/lib/src/integrations/gpt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,20 @@ interface GoogleTagSlot {
getAdUnitPath(): string;
getSlotElementId(): string;
setTargeting(key: string, value: string | string[]): GoogleTagSlot;
getTargeting(key: string): string[];
addService(service: GoogleTagPubAdsService): GoogleTagSlot;
}

interface GoogleTagPubAdsService {
setTargeting(key: string, value: string | string[]): GoogleTagPubAdsService;
getTargeting(key: string): string[];
enableSingleRequest(): void;
disableInitialLoad(): void;
addEventListener(
eventName: 'slotRenderEnded',
callback: (event: SlotRenderEndedEvent) => void
): void;
refresh(slots?: GoogleTagSlot[]): void;
}

interface GoogleTag {
Expand All @@ -54,6 +62,38 @@ interface GoogleTag {

type GptWindow = Window & {
googletag?: Partial<GoogleTag>;
__ts_ad_slots?: TsAdSlot[];
__ts_request_id?: string;
__tsAdInit?: () => boolean;
__tsAdInitInstalled?: boolean;
};

type TsAdSlot = {
id?: string;
gam_unit_path?: string;
div_id?: string;
formats?: Array<number | number[]>;
targeting?: Record<string, string | string[]>;
};

type TsBidTargeting = {
hb_pb?: string;
hb_bidder?: string;
hb_adid?: string;
burl?: string;
};

type TsBidMap = Record<string, TsBidTargeting | undefined>;

type DefinedTsSlot = {
descriptor: TsAdSlot;
slot: GoogleTagSlot;
};

type SlotRenderEndedEvent = {
slot?: {
getTargeting?: (key: string) => string[];
};
};

// ------------------------------------------------------------------
Expand Down Expand Up @@ -137,6 +177,155 @@ function patchCommandQueue(tag: Partial<GoogleTag>): void {
log.debug('GPT shim: command queue patched', { pendingCommands: queue.length });
}

function readTsAdSlots(win: GptWindow): TsAdSlot[] {
return Array.isArray(win.__ts_ad_slots) ? win.__ts_ad_slots : [];
}

function fetchTsBids(win: GptWindow): Promise<TsBidMap> {
const rid = win.__ts_request_id;
if (!rid || typeof fetch !== 'function') {
return Promise.resolve({});
}

return fetch(`/ts-bids?rid=${encodeURIComponent(rid)}`, { credentials: 'omit' })
.then((response) => response.json() as Promise<TsBidMap>)
.catch(() => ({}));
}

function applyStaticTargeting(slot: GoogleTagSlot, targeting: TsAdSlot['targeting']): void {
for (const [key, value] of Object.entries(targeting ?? {})) {
slot.setTargeting(key, value);
}
}

function applyBidTargeting(slot: GoogleTagSlot, bid: TsBidTargeting): void {
for (const key of ['hb_pb', 'hb_bidder', 'hb_adid'] as const) {
const value = bid[key];
if (value != null) {
slot.setTargeting(key, String(value));
}
}
}

function installBurlListener(
pubads: GoogleTagPubAdsService,
bidsByAdId: Map<string, TsBidTargeting>
): void {
if (typeof pubads.addEventListener !== 'function') {
return;
}

pubads.addEventListener('slotRenderEnded', (event) => {
const hbAdIds = event.slot?.getTargeting?.('hb_adid') ?? [];
const hbAdId = hbAdIds[0];
const bid = hbAdId ? bidsByAdId.get(hbAdId) : undefined;

if (
!bid?.burl ||
typeof navigator === 'undefined' ||
typeof navigator.sendBeacon !== 'function'
) {
return;
}

navigator.sendBeacon(bid.burl);
bidsByAdId.delete(hbAdId);
});
}

function runTsAdInit(win: GptWindow): void {
const tag = win.googletag as GoogleTag | undefined;
const bidsPromise = fetchTsBids(win);
const slots = readTsAdSlots(win);
const definedSlots: DefinedTsSlot[] = [];
const bidsByAdId = new Map<string, TsBidTargeting>();

if (
!tag ||
typeof tag.defineSlot !== 'function' ||
typeof tag.pubads !== 'function' ||
typeof tag.enableServices !== 'function'
) {
return;
}

const pubads = tag.pubads();
installBurlListener(pubads, bidsByAdId);

for (const descriptor of slots) {
if (!descriptor.gam_unit_path || !descriptor.div_id || !descriptor.id) {
continue;
}

const slot = tag.defineSlot(
descriptor.gam_unit_path,
descriptor.formats ?? [],
descriptor.div_id
);
if (!slot) {
continue;
}

if (typeof slot.addService === 'function') {
slot.addService(pubads);
}
applyStaticTargeting(slot, descriptor.targeting);
definedSlots.push({ descriptor, slot });
}

if (typeof pubads.disableInitialLoad === 'function') {
pubads.disableInitialLoad();
}
tag.enableServices();

for (const { descriptor } of definedSlots) {
if (typeof tag.display === 'function') {
tag.display(descriptor.div_id as string);
}
}

bidsPromise.then((bids) => {
for (const { descriptor, slot } of definedSlots) {
const bid = bids[descriptor.id as string];
if (!bid) {
continue;
}

applyBidTargeting(slot, bid);
if (bid.hb_adid) {
bidsByAdId.set(String(bid.hb_adid), bid);
}
}

if (typeof pubads.refresh === 'function') {
pubads.refresh(definedSlots.map(({ slot }) => slot));
}
});
}

/**
* Install the Trusted Server ad bootstrap for GPT slots.
*
* The bootstrap reads `window.__ts_ad_slots` and `window.__ts_request_id`,
* defines GPT slots immediately, then applies server-side bid targeting from
* `/ts-bids` before refreshing the slots.
*/
export function installTsAdInit(): boolean {
if (typeof window === 'undefined') {
return false;
}

const win = window as GptWindow;
if (win.__tsAdInitInstalled) {
return true;
}

win.__tsAdInitInstalled = true;
const tag = ensureGoogleTagStub(win);
tag.cmd!.push(() => runTsAdInit(win));
return true;
}

/**
* Install the GPT integration shim.
*
Expand All @@ -156,6 +345,7 @@ export function installGptShim(): boolean {

const tag = ensureGoogleTagStub(win);
patchCommandQueue(tag);
installTsAdInit();

log.info('GPT shim installed');
return true;
Expand All @@ -170,9 +360,10 @@ export function installGptShim(): boolean {
// regardless of script order, the module also checks for a pre-set enable flag
// immediately after registering the function.
if (typeof window !== 'undefined') {
const win = window as Record<string, unknown>;
const win = window as unknown as Record<string, unknown>;

win.__tsjs_installGptShim = installGptShim;
win.__tsAdInit = installTsAdInit;

if (win.__tsjs_gpt_enabled === true) {
installGptShim();
Expand Down
Loading
Loading