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
19 changes: 14 additions & 5 deletions crates/trusted-server-core/src/integrations/gpt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1247,9 +1247,10 @@ mod tests {

#[test]
fn head_inserts_bootstrap_refreshes_ts_slots_when_initial_load_disabled() {
// Mirrors the bundle: when the publisher calls disableInitialLoad(),
// display() only registers a TS-defined slot, so the bootstrap must also
// refresh those slots or they render blank.
// Mirrors the bundle: when the publisher disables initial load through
// setConfig() or the legacy disableInitialLoad() method, display() only
// registers a TS-defined slot, so the bootstrap must also refresh those
// slots or they render blank.
let config = test_config();
let integration = GptIntegration::new(config);
let doc_state = IntegrationDocumentState::default();
Expand All @@ -1261,8 +1262,16 @@ mod tests {
};
let combined = integration.head_inserts(&ctx).join("");
assert!(
combined.contains("disableInitialLoad"),
"bootstrap should wrap disableInitialLoad() to detect the disabled state"
combined.contains("gpt.setConfig"),
Comment thread
ChristianPavilonis marked this conversation as resolved.
"bootstrap should wrap googletag.setConfig() to detect the disabled state"
);
assert!(
combined.contains("gpt.getConfig"),
"bootstrap should read GPT's modern initial-load configuration"
);
assert!(
combined.contains("pubads.disableInitialLoad"),
"bootstrap should wrap legacy disableInitialLoad() calls"
);
assert!(
combined.contains("gptInitialLoadDisabled"),
Expand Down
57 changes: 47 additions & 10 deletions crates/trusted-server-core/src/integrations/gpt_bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,61 @@
var ts = (window.tsjs = window.tsjs || {});
if (ts.adInit) return;

// Track whether the publisher disabled GPT initial load. GPT exposes no
// getter for this, so wrap pubads().disableInitialLoad() to record it. With
// initial load disabled, display() only registers a slot and the ad request
// must come from a later refresh(); adInit() reads this to refresh its own
// freshly defined slots so they are not left blank. Pushed onto the command
// queue so it runs before the publisher's own disableInitialLoad() call.
// Track whether the publisher disabled GPT initial load. Read the modern
// googletag.getConfig() value when available, and wrap googletag.setConfig()
// and the legacy pubads().disableInitialLoad() method as fallbacks because
// getConfig() may not report the legacy API's state. With initial load
// disabled, display() only registers a slot and the ad request must come from
// a later refresh(); adInit() reads this to refresh its own freshly defined
// slots so they are not left blank. Pushed onto the command queue so it runs
// before the publisher's own GPT configuration.
function syncInitialLoadDisabled(gpt) {
if (typeof gpt.getConfig !== "function") return false;
var config = gpt.getConfig("disableInitialLoad");
if (!config || typeof config.disableInitialLoad === "undefined") {
return false;
}
ts.gptInitialLoadDisabled = config.disableInitialLoad === true;
return true;
}

(window.googletag = window.googletag || { cmd: [] }).cmd.push(function () {
var pubads = googletag.pubads && googletag.pubads();
var gpt = window.googletag;
syncInitialLoadDisabled(gpt);
if (
typeof gpt.setConfig === "function" &&
!gpt.__tsInitialLoadConfigHooked
) {
var originalSetConfig = gpt.setConfig.bind(gpt);
gpt.setConfig = function (config) {
var result = originalSetConfig.apply(gpt, arguments);
if (
!syncInitialLoadDisabled(gpt) &&
config &&
"disableInitialLoad" in config
) {
ts.gptInitialLoadDisabled = config.disableInitialLoad === true;
}
return result;
};
gpt.__tsInitialLoadConfigHooked = true;
}

var pubads = gpt.pubads && gpt.pubads();
if (
!pubads ||
typeof pubads.disableInitialLoad !== "function" ||
pubads.__tsInitialLoadHooked
) {
return;
}
var original = pubads.disableInitialLoad.bind(pubads);
var originalDisableInitialLoad = pubads.disableInitialLoad.bind(pubads);
pubads.disableInitialLoad = function () {
ts.gptInitialLoadDisabled = true;
return original();
var result = originalDisableInitialLoad.apply(pubads, arguments);
if (!syncInitialLoadDisabled(gpt)) {
ts.gptInitialLoadDisabled = true;
}
return result;
};
pubads.__tsInitialLoadHooked = true;
});
Expand Down Expand Up @@ -150,6 +186,7 @@
// unless the publisher disabled initial load, in which case display() only
// registers them and refresh() must request the ad — otherwise they render
// blank. Only add them in that case to avoid double-requesting.
syncInitialLoadDisabled(window.googletag);
var slotsNeedingRefresh = ts.gptInitialLoadDisabled
? slotsToRefresh.concat(newSlots)
: slotsToRefresh;
Expand Down
12 changes: 7 additions & 5 deletions crates/trusted-server-js/lib/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,13 @@ export interface TsjsApi {
*/
adInitRefreshInProgress?: boolean;
/**
* True once the publisher has called `googletag.pubads().disableInitialLoad()`.
* GPT exposes no getter for this state, so it is tracked by wrapping the
* setter. When set, `display()` only registers a slot and the ad request must
* come from a `refresh()`; adInit() uses this to refresh its own freshly
* defined slots so they are not left blank.
* True once the publisher has disabled GPT initial load through
* `googletag.setConfig()` or `googletag.pubads().disableInitialLoad()`.
* GPT's getter may not report state set through the legacy API, so TS tracks
* both configuration APIs.
* When set, `display()` only registers a slot and the ad request must come
* from a `refresh()`; adInit() uses this to refresh its own freshly defined
* slots so they are not left blank.
*/
gptInitialLoadDisabled?: boolean;
/** Guards SPA pushState hook installation. */
Expand Down
71 changes: 56 additions & 15 deletions crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ interface GoogleTagPubAdsService {
disableInitialLoad?(): void;
}

interface GoogleTagConfig extends Record<string, unknown> {
Comment thread
ChristianPavilonis marked this conversation as resolved.
disableInitialLoad?: boolean | null;
}

interface GoogleTag {
cmd: Array<() => void>;
pubads(): GoogleTagPubAdsService;
Expand All @@ -120,6 +124,8 @@ interface GoogleTag {
destroySlots(slots?: GoogleTagSlot[]): boolean;
enableServices(): void;
display(elementId: string): void;
setConfig?(config: GoogleTagConfig): void;
getConfig?(key: 'disableInitialLoad'): GoogleTagConfig | undefined;
_loaded_?: boolean;
}

Expand Down Expand Up @@ -409,37 +415,71 @@ function queueWinBillingBeacon(url: string): boolean {
/**
* Track whether the publisher disabled GPT initial load.
*
* GPT exposes no getter for the initial-load-disabled flag, so wrap
* `pubads().disableInitialLoad()` to record it on `window.tsjs`. With initial
* load disabled, `display()` only registers a slot — the ad request must come
* from a later `refresh()`. adInit() reads this to refresh its own freshly
* defined slots so they are not left blank.
* GPT's modern `getConfig()` getter may not report state set through the legacy
* `pubads().disableInitialLoad()` API, so read it when available and wrap both
* configuration APIs to record the state on `window.tsjs`. With
* initial load disabled, `display()` only registers a slot — the ad request
* must come from a later `refresh()`. adInit() reads this to refresh its own
* freshly defined slots so they are not left blank.
*
* Installed via the command queue so it runs before the publisher's own
* `disableInitialLoad()` call (the TS core script is injected ahead of the
* publisher's GPT setup). Idempotent per pubads service.
* Installed via the command queue so it runs before the publisher's own GPT
* configuration (the TS core script is injected ahead of the publisher's GPT
* setup). Idempotent per googletag object and pubads service.
*
* Only hooks an existing `googletag` stub — it never creates one. A plain module
* import that does not activate the GPT integration must not touch
* `window.googletag`. When the GPT shim is active it creates the stub before
* `installTsAdInit` runs, so the detector is still queued ahead of the
* publisher's GPT setup.
*/
function syncInitialLoadDisabled(gpt: Partial<GoogleTag>, ts: TsjsApi): boolean {
if (typeof gpt.getConfig !== 'function') return false;

const config = gpt.getConfig('disableInitialLoad');
if (!config || config.disableInitialLoad === undefined) return false;

ts.gptInitialLoadDisabled = config.disableInitialLoad === true;
return true;
}

function installInitialLoadDetector(ts: TsjsApi): void {
const win = window as GptWindow;
const cmd = win.googletag?.cmd;
if (!cmd) return;
cmd.push(() => {
const pubads = win.googletag?.pubads?.();
const gpt = win.googletag as
| (Partial<GoogleTag> & { __tsInitialLoadConfigHooked?: boolean })
| undefined;
if (!gpt) return;

syncInitialLoadDisabled(gpt, ts);

if (typeof gpt.setConfig === 'function' && !gpt.__tsInitialLoadConfigHooked) {
const originalSetConfig = gpt.setConfig.bind(gpt);
gpt.setConfig = function (...args: Parameters<typeof originalSetConfig>) {
const config = args[0];
const result = originalSetConfig(...args);
if (!syncInitialLoadDisabled(gpt, ts) && config && 'disableInitialLoad' in config) {
ts.gptInitialLoadDisabled = config.disableInitialLoad === true;
}
return result;
};
gpt.__tsInitialLoadConfigHooked = true;
}

const pubads = gpt.pubads?.();
if (!pubads) return;
const service = pubads as GoogleTagPubAdsService & { __tsInitialLoadHooked?: boolean };
if (typeof service.disableInitialLoad !== 'function' || service.__tsInitialLoadHooked) {
return;
}
const original = service.disableInitialLoad.bind(service);
const originalDisableInitialLoad = service.disableInitialLoad.bind(service);
service.disableInitialLoad = function () {
ts.gptInitialLoadDisabled = true;
return original();
const result = originalDisableInitialLoad();
if (!syncInitialLoadDisabled(gpt, ts)) {
ts.gptInitialLoadDisabled = true;
}
return result;
};
service.__tsInitialLoadHooked = true;
});
Expand Down Expand Up @@ -612,12 +652,13 @@ export function installTsAdInit(): void {
// Slots needing an explicit ad request via refresh(). Reused
// publisher-owned slots always need one to pick up the just-applied
// server-side targeting. TS-defined slots are normally fetched by the
// display() above — but when the publisher called
// pubads().disableInitialLoad(), display() only registers the slot and the
// ad request must come from refresh(). Without this, a TS-owned
// display() above — but when the publisher disabled initial load through
// setConfig() or the legacy pubads() method, display() only registers the
// slot and the ad request must come from refresh(). Without this, a TS-owned
// first-impression slot renders blank on initial-load-disabled pages. Only
// add them in that case; otherwise display() + refresh() would
// double-request the impression.
syncInitialLoadDisabled(g, ts);
const slotsNeedingRefresh = ts.gptInitialLoadDisabled
? slotsToRefresh.concat(newSlots)
: slotsToRefresh;
Expand Down
Loading
Loading