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
58 changes: 48 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,62 @@
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 effective
// googletag.getConfig() value when available, and wrap googletag.setConfig()
// and the legacy pubads().disableInitialLoad() method so changes are
// synchronized immediately and still detected when getConfig() is
// unavailable. 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.
Comment thread
ChristianPavilonis marked this conversation as resolved.
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 (
Comment thread
ChristianPavilonis marked this conversation as resolved.
!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)) {
Comment thread
ChristianPavilonis marked this conversation as resolved.
ts.gptInitialLoadDisabled = true;
}
return result;
};
pubads.__tsInitialLoadHooked = true;
});
Expand Down Expand Up @@ -150,6 +187,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 @@ -130,11 +130,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.
* Whether the publisher disabled GPT initial load through
* `googletag.setConfig()` or `googletag.pubads().disableInitialLoad()`.
* TS synchronizes this from GPT's getter and wraps both configuration APIs as
* a fallback when the getter is unavailable.
* 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
75 changes: 60 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,14 @@ interface GoogleTagPubAdsService {
disableInitialLoad?(): void;
}

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

interface GoogleTagEffectiveConfig {
disableInitialLoad?: boolean;
}

interface GoogleTag {
cmd: Array<() => void>;
pubads(): GoogleTagPubAdsService;
Expand All @@ -120,6 +128,8 @@ interface GoogleTag {
destroySlots(slots?: GoogleTagSlot[]): boolean;
Comment thread
ChristianPavilonis marked this conversation as resolved.
enableServices(): void;
display(elementId: string): void;
setConfig?(config: GoogleTagConfig): void;
getConfig?(keys: string | string[]): GoogleTagEffectiveConfig | undefined;
_loaded_?: boolean;
}

Expand Down Expand Up @@ -413,37 +423,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.
* Read GPT's effective state through `getConfig()` when available and wrap both
* configuration APIs so changes are synchronized immediately. The wrappers also
* provide a fallback for runtimes where the getter is unavailable. 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;
Comment thread
ChristianPavilonis marked this conversation as resolved.

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);
Comment thread
ChristianPavilonis marked this conversation as resolved.
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)) {
Comment thread
ChristianPavilonis marked this conversation as resolved.
ts.gptInitialLoadDisabled = true;
}
return result;
};
service.__tsInitialLoadHooked = true;
});
Expand Down Expand Up @@ -619,12 +663,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
Comment thread
ChristianPavilonis marked this conversation as resolved.
// 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);
Comment thread
ChristianPavilonis marked this conversation as resolved.
const slotsNeedingRefresh = ts.gptInitialLoadDisabled
? slotsToRefresh.concat(newSlots)
: slotsToRefresh;
Expand Down
Loading
Loading