Skip to content

Detect GPT initial load configured through setConfig - #948

Merged
ChristianPavilonis merged 13 commits into
mainfrom
fix/gpt-set-config-initial-load-detection
Jul 30, 2026
Merged

Detect GPT initial load configured through setConfig#948
ChristianPavilonis merged 13 commits into
mainfrom
fix/gpt-set-config-initial-load-detection

Conversation

@ChristianPavilonis

Copy link
Copy Markdown
Collaborator

Summary

  • Detect GPT initial-load disabling through the modern googletag.setConfig({ disableInitialLoad: true }) API as well as the legacy pubads method.
  • Ensure TS-owned slots receive the required refresh() after display(), preventing GPT slots from stopping at fetch count zero.

Changes

File Change
crates/trusted-server-core/src/integrations/gpt_bootstrap.js Wrap both GPT initial-load configuration APIs in the early page bootstrap.
crates/trusted-server-js/lib/src/integrations/gpt/index.ts Add equivalent detection to the bundled GPT integration and preserve the existing refresh behavior.
crates/trusted-server-js/lib/src/core/types.ts Document both supported initial-load configuration paths.
crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts Add regression coverage for googletag.setConfig().
crates/trusted-server-core/src/integrations/gpt.rs Assert that the injected bootstrap includes both detectors.

Closes

Closes #946

Test plan

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: Cloudflare and Spin tests/clippy passed; Playwright confirmed the current publisher setConfig() call now sets gptInitialLoadDisabled.

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses tracing macros (not println!)
  • New code has tests
  • No secrets or credentials committed

@aram356
aram356 requested a review from prk-Jr July 22, 2026 20:58

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • setConfig never clears gptInitialLoadDisabled → duplicate ad request: setConfig is a settings merge API, so { disableInitialLoad: false } re-enables and { disableInitialLoad: null } resets to default. Matching only === true leaves the flag stuck on, and adInit() then does display() and refresh() 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.cmd queue, so a publisher that configures GPT ahead of the TS injection point still lands in the old blank-slot failure mode. setConfig widens 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 a setConfig call without disableInitialLoad leaves the flag unset, and none that the __tsInitialLoadConfigHooked guard 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.js still has no behavioral tests — correctness rests on Rust substring assertions while the duplicated hooking logic grows. Follow-up issue suggested (gpt.rs:1265).

⛏ nitpick

  • setConfig is declared required on GoogleTag while 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 passed
  • cargo test -p trusted-server-core --lib integrations::gpt::tests — 30 passed
  • tsc --noEmit — no new errors in the changed files (pre-existing errors only, on untouched lines)

Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts Outdated
Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts Outdated
Comment thread crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-core/src/integrations/gpt.rs

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The PR adds detection for the modern googletag.setConfig({ disableInitialLoad: true }) path, but the new hook tracks attempted setter calls instead of GPT's effective configuration. That state can become stale and cause duplicate requests for TS-owned slots.

Blocking

🔧 wrench

  • Initial-load state can diverge from GPT's effective configuration: the flag is only set to true, never cleared, and it is updated before GPT processes the configuration. Both the bundle and inline bootstrap need to read the authoritative GPT state and cover re-enabling in regression tests.

CI Status

  • All GitHub checks currently report PASS, including formatting, Rust and JavaScript analysis, adapter tests, browser integration tests, and Vitest.

Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts Outdated
@ChristianPavilonis
ChristianPavilonis requested review from aram356 and prk-Jr and removed request for aram356 and prk-Jr July 23, 2026 17:35

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The move from "record what was attempted" to "read GPT's effective state" is the right direction, and the idempotency guards, argument forwarding, and jsdom bootstrap harness all land well. One blocking defect remains: getConfig() is now treated as authoritative for the legacy pubads().disableInitialLoad() path too, and per GPT's own documentation it never reflects that API. When a page mixes both APIs, the legacy detection this PR is meant to preserve is silently overwritten with false and TS-owned slots go back to rendering blank — the exact failure #946 fixes.

Blocking

🔧 wrench

  • getConfig() overrides legacy disableInitialLoad() state: syncInitialLoadDisabled() wins over the legacy fallback, but GPT's getConfig() only reports setConfig() state, so a mixed-API page loses the disabled flag (crates/trusted-server-js/lib/src/integrations/gpt/index.ts:483, crates/trusted-server-core/src/integrations/gpt_bootstrap.js:73).
  • null is treated as an authoritative false: the unset guard checks only undefined, while GoogleTagConfig.disableInitialLoad is typed boolean | null and null is GPT's documented "reset to default" (crates/trusted-server-js/lib/src/integrations/gpt/index.ts:443, crates/trusted-server-core/src/integrations/gpt_bootstrap.js:33).

Both were reproduced against this branch with a scratch vitest run; details and suggested fixes are inline.

Non-blocking

♻️ refactor

  • Bootstrap coverage stops at the legacy path: the new jsdom harness never drives the bootstrap's setConfig wrapper, which is the feature this PR adds (crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts:252).

⛏ nitpick

  • Test resolves the bootstrap through process.cwd(): breaks if vitest is invoked from the repo root (crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts:291).
  • getConfig type is narrower than the real API: GPT's signature is variadic and values may be null (crates/trusted-server-js/lib/src/integrations/gpt/index.ts:128).

👍 praise

  • Effective-state re-sync immediately before slotsNeedingRefresh, plus __tsInitialLoadConfigHooked sharing between bootstrap and bundle so the wrapper is installed exactly once, and full argument forwarding on both wrappers (crates/trusted-server-js/lib/src/integrations/gpt/index.ts:668).

CI Status

  • fmt: PASS
  • clippy / CodeQL analyze: PASS
  • rust tests (fastly, cloudflare, CLI, cross-adapter parity): PASS
  • js tests (vitest) + TS/docs format: PASS
  • pending at review time: axum native, spin native + wasm32-wasip1, integration artifacts

Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js
Comment thread crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts Outdated
Comment thread crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts Outdated
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Re-review after the update responding to the earlier REQUEST_CHANGES. The prior blocking issue — the setConfig hook could only ever set gptInitialLoadDisabled, never clear it, producing a duplicate ad request on re-enable — is resolved: the code now reads GPT's authoritative getConfig('disableInitialLoad') at detection and at adInit() decision time, and falls back to key-presence ('disableInitialLoad' in config) with === true. I traced the true → false → null sequence and confirmed the flag now clears and adInit() no longer double-requests. Verified against Google's GPT reference that getConfig('disableInitialLoad') returns a frozen object { disableInitialLoad: ... } and that disableInitialLoad is a real page-level setConfig/getConfig key, so the read is sound.

Approving. The remaining notes are non-blocking.

Non-blocking

♻️ refactor / test-coverage

  • The getConfig-absent fallback is untested (gpt/index.ts:466, gpt_bootstrap.js:51) — every new setConfig test injects a getConfig mock, so syncInitialLoadDisabled always returns true and the fallback branch never runs. That branch exists precisely for older GPT that has setConfig but not getConfig; a regression test with getConfig absent would lock it in. Verified working via a scratch probe.

🤔 thinking

  • Residual ordering dependency (gpt/index.ts:668) — the authoritative getConfig re-read at adInit() now catches setConfig state regardless of call order (a real gain). The only remaining blind spot is a publisher using the legacy pubads().disableInitialLoad() before the detector wraps it, on a GPT build where getConfig does not reflect legacy state. Narrow; noting for completeness.

⛏ nitpick

  • getConfig?(key: 'disableInitialLoad') types the key as only that literal; real GPT accepts string | string[] (gpt/index.ts:128).
  • syncInitialLoadDisabled is duplicated near-identically between the bootstrap (JS) and the bundle (TS) — inherent to the two-implementation design and now partly guarded by the new bootstrap eval-test, but the divergence risk remains (gpt_bootstrap.js:30).

CI Status

All checks green on GitHub: cargo fmt, clippy (all adapters), cargo test (fastly / axum / cloudflare / spin / parity), vitest, browser + Fastly EC integration, CodeQL, format-typescript, format-docs.

Verified locally at the PR head: integrations::gpt::tests 30 passed; GPT vitest 90 passed; tsc --noEmit clean on the changed files.

Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js
@ChristianPavilonis
ChristianPavilonis requested review from prk-Jr and removed request for prk-Jr July 28, 2026 20:55
ChristianPavilonis added a commit that referenced this pull request Jul 28, 2026
# Conflicts:
#	crates/trusted-server-core/src/integrations/gpt_bootstrap.js
#	crates/trusted-server-js/lib/src/integrations/gpt/index.ts
#	crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts
@ChristianPavilonis
ChristianPavilonis merged commit a436ffe into main Jul 30, 2026
19 checks passed
aram356 added a commit that referenced this pull request Jul 31, 2026
Adopts the gating revert and the hardened GPT slot handoff from #978:
- Removes the publisher initial-request gate (initialRequestGate,
  heldPublisherRequests, GptInitialRequestGate) that #978 reverted
- Takes matchingHandoff/displayTargetElementId and the responsive-slot
  helpers with ambiguous-hydration protection
- Keeps rc-only content intact: gpt_diagnostics types, the #948
  disableInitialLoad sync (syncInitialLoadDisabled wired into the
  refresh-selection path), and the #945 scheduleInitialAdInit coverage
- Drops the obsolete held-display test from schedule_initial_ad_init
  and ports the two #948 setConfig tests to the new zero-arg
  runGptBootstrap harness
@aram356
aram356 deleted the fix/gpt-set-config-initial-load-detection branch August 1, 2026 05:05
aram356 added a commit that referenced this pull request Aug 1, 2026
Brings in the #988 browser-spec fix through its PR lineage and, because
#988 stacks on #963's head, refreshes rc's stale #963 absorption with
the July 29-30 rework:
- bid.meta second descriptor carrier and bidAccepted registration
  replacing the requestId stash (prebid shim and Rust provider)
- Hardened APS auction delivery: sanitized publisher page identity
  (query/fragment stripped), delivery drop telemetry with
  dropped_winner_count/reasons, imp disposition counters
- ProviderLaunchState/ProviderRequestOutcome orchestrator refactor with
  parse_state threading and Immediate outcomes
- as_aps() Option accessor, fail-closed render-bridge stop, responsive
  slot-root helpers, case-insensitive APS exclusion tests

Preserved rc-only systems the #963 branch predates: #956 opt-in creative
processing (process_auction_creative, sanitize_creatives), #967
decoupled prebid shim (public markWinningBidAsUsed instead of prebid.js
internals), #948/#912 GPT sync, #865 platform timeout canonicalization
(restored at both launch paths and both mediator paths, with the
duplicate backend-name pre/post-launch guards and their test suite
ported to the new provider API), and the provider-validation startup
checks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect GPT initial load configured through setConfig

3 participants