Detect GPT initial load configured through setConfig#948
Detect GPT initial load configured through setConfig#948ChristianPavilonis wants to merge 2 commits into
Conversation
aram356
left a comment
There was a problem hiding this comment.
Summary
Extends initial-load detection to googletag.setConfig({ disableInitialLoad: true }) alongside the legacy pubads().disableInitialLoad(), in both the edge bootstrap and the bundle. The approach is right and the idempotency markers are correctly scoped per object, but the new setConfig hook is write-only: it can set gptInitialLoadDisabled and never clear it, which turns a publisher re-enabling initial load into a duplicate ad request on TS-owned slots.
Blocking
🔧 wrench
setConfignever clearsgptInitialLoadDisabled→ duplicate ad request:setConfigis a settings merge API, so{ disableInitialLoad: false }re-enables and{ disableInitialLoad: null }resets to default. Matching only=== trueleaves the flag stuck on, andadInit()then doesdisplay()andrefresh()on a TS-owned slot — the exact double-request the code comment warns against. Verified with a scratch vitest against this branch. Needs the same key-presence fix in both copies (gpt/index.ts:447,gpt_bootstrap.js:37).
Non-blocking
🤔 thinking
- Detection remains ordering-dependent: both hooks install from the
googletag.cmdqueue, so a publisher that configures GPT ahead of the TS injection point still lands in the old blank-slot failure mode.setConfigwidens coverage but does not remove the race. Longer term it may be more robust for TS to own the fetch for its own slots outright rather than inferring publisher state through wrappers.
♻️ refactor
- Missing negative coverage for the new hook (
ad_init.test.ts:246) — no test that asetConfigcall withoutdisableInitialLoadleaves the flag unset, and none that the__tsInitialLoadConfigHookedguard prevents double-wrapping. - Wrapper drops extra arguments (
gpt/index.ts:446) — forward with rest/spread instead of a fixed single parameter.
🏕 camp site / 📌 out of scope
- The new test case is a ~40-line verbatim clone of the preceding legacy test; a shared
setupGptMocks()helper would keep them in sync. gpt_bootstrap.jsstill has no behavioral tests — correctness rests on Rust substring assertions while the duplicated hooking logic grows. Follow-up issue suggested (gpt.rs:1265).
⛏ nitpick
setConfigis declared required onGoogleTagwhile every sibling runtime-guarded API is optional (gpt/index.ts:127).GoogleTagConfig extends Record<string, unknown>suppresses excess-property checking entirely (gpt/index.ts:112).
CI Status
GitHub, at time of review:
- format-typescript / format-docs: PASS
- vitest: PASS
- cargo test (ts CLI, native): PASS
- CodeQL (javascript-typescript, actions): PASS
- cargo fmt / clippy / test (fastly, axum, cloudflare, spin, parity): PENDING
Verified locally in a worktree at the PR head:
npx vitest run test/integrations/gpt/— 74 passedcargo test -p trusted-server-core --lib integrations::gpt::tests— 30 passedtsc --noEmit— no new errors in the changed files (pre-existing errors only, on untouched lines)
| if (typeof gpt.setConfig === 'function' && !gpt.__tsInitialLoadConfigHooked) { | ||
| const originalSetConfig = gpt.setConfig.bind(gpt); | ||
| gpt.setConfig = function (config: GoogleTagConfig) { | ||
| if (config?.disableInitialLoad === true) { |
There was a problem hiding this comment.
🔧 wrench — setConfig can only ever set the flag, never clear it, which produces a duplicate ad request.
setConfig is a settings merge API: { disableInitialLoad: false } re-enables initial load and { disableInitialLoad: null } resets it to the default. Matching only === true means that once a publisher disables and later re-enables initial load (a real pattern — disable while waiting on the CMP, re-enable after consent), ts.gptInitialLoadDisabled is stuck true. adInit() then takes the slotsToRefresh.concat(newSlots) branch and issues display() and refresh() on a TS-owned slot — exactly the double-request the comment below at line 641 says to avoid.
The asymmetry is new to this PR: the legacy path is safe only because GPT has no enableInitialLoad().
Verified against this branch with a scratch vitest, not theoretical:
setConfig({ disableInitialLoad: true });
setConfig({ disableInitialLoad: false });
→ gptInitialLoadDisabled === true
→ adInit(): display('div-atf-sidebar') AND pubads.refresh([slot])
Fix — key-presence, which matches GPT's merge semantics (an absent key means "unchanged"):
gpt.setConfig = function (config: GoogleTagConfig) {
if (config && 'disableInitialLoad' in config) {
ts.gptInitialLoadDisabled = config.disableInitialLoad === true;
}
return originalSetConfig(config);
};The same change is needed in gpt_bootstrap.js. Please add the re-enable case as a regression test alongside the new one.
| ) { | ||
| var originalSetConfig = gpt.setConfig.bind(gpt); | ||
| gpt.setConfig = function (config) { | ||
| if (config && config.disableInitialLoad === true) { |
There was a problem hiding this comment.
🔧 wrench — Same defect as the bundle (see the comment on gpt/index.ts:447): config.disableInitialLoad === true never clears the flag, so setConfig({ disableInitialLoad: false }) (or null) leaves ts.gptInitialLoadDisabled stuck true and adInit() double-requests TS-owned slots via display() + refresh().
Fix:
gpt.setConfig = function (config) {
if (config && "disableInitialLoad" in config) {
ts.gptInitialLoadDisabled = config.disableInitialLoad === true;
}
return originalSetConfig(config);
};|
|
||
| if (typeof gpt.setConfig === 'function' && !gpt.__tsInitialLoadConfigHooked) { | ||
| const originalSetConfig = gpt.setConfig.bind(gpt); | ||
| gpt.setConfig = function (config: GoogleTagConfig) { |
There was a problem hiding this comment.
♻️ refactor — The wrapper is signature-locked to today's single-argument API and silently drops anything else GPT may pass. Forwarding all arguments is future-proof at no cost:
gpt.setConfig = function (...args: [GoogleTagConfig]) {
const config = args[0];
// ...
return originalSetConfig(...args);
};| expect(mockPubads.refresh).toHaveBeenCalledWith([mockSlot]); | ||
| }); | ||
|
|
||
| it('refreshes TS-defined slots when setConfig disables GPT initial load', async () => { |
There was a problem hiding this comment.
♻️ refactor — Missing negative coverage for the new hook. This test only exercises the positive path; two cheap additions would have caught the wrench finding above and would pin down the wrapper's other contracts:
setConfig({ singleRequest: true })must leavegptInitialLoadDisabledunset and must not add TS-owned slots torefresh().- Calling
installTsAdInit()twice must not double-wrapsetConfig—__tsInitialLoadConfigHookedis currently untested, and a double wrap would invoke the originalsetConfigtwice.
Also 🏕 — this case is a ~40-line verbatim clone of the preceding legacy test, differing only in the setConfig mock. A shared setupGptMocks() helper would keep the pair from drifting.
| destroySlots(slots?: GoogleTagSlot[]): boolean; | ||
| enableServices(): void; | ||
| display(elementId: string): void; | ||
| setConfig(config: GoogleTagConfig): void; |
There was a problem hiding this comment.
⛏ nitpick — Every sibling GPT API the shim guards at runtime is declared optional (disableInitialLoad?(), _loaded_?), and the detector does check typeof gpt.setConfig === 'function'. setConfig?(config: GoogleTagConfig): void would be more honest about the surface actually being probed.
| disableInitialLoad?(): void; | ||
| } | ||
|
|
||
| interface GoogleTagConfig extends Record<string, unknown> { |
There was a problem hiding this comment.
⛏ nitpick — Extending Record<string, unknown> suppresses excess-property checking entirely, so the type documents disableInitialLoad without enforcing anything else about the config. Reasonable as an escape hatch for a partially-modelled third-party API — just noting the trade-off.
| assert!( | ||
| combined.contains("disableInitialLoad"), | ||
| "bootstrap should wrap disableInitialLoad() to detect the disabled state" | ||
| combined.contains("gpt.setConfig"), |
There was a problem hiding this comment.
📌 out of scope — gpt_bootstrap.js correctness is still asserted only by substring matching from Rust. This PR duplicates a second piece of non-trivial hooking logic across the bootstrap and the bundle, so the two can now silently diverge — and per the wrench finding, they need the same fix applied twice.
Worth a follow-up issue for a jsdom harness that evals GPT_BOOTSTRAP_JS and drives it the way ad_init.test.ts drives the bundle.
Summary
googletag.setConfig({ disableInitialLoad: true })API as well as the legacy pubads method.refresh()afterdisplay(), preventing GPT slots from stopping at fetch count zero.Changes
crates/trusted-server-core/src/integrations/gpt_bootstrap.jscrates/trusted-server-js/lib/src/integrations/gpt/index.tscrates/trusted-server-js/lib/src/core/types.tscrates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.tsgoogletag.setConfig().crates/trusted-server-core/src/integrations/gpt.rsCloses
Closes #946
Test plan
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute servesetConfig()call now setsgptInitialLoadDisabled.Checklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)