Skip to content

Prevent duplicate GPT slot requests - #966

Open
ChristianPavilonis wants to merge 8 commits into
mainfrom
fix/duplicate-gpt-slots
Open

Prevent duplicate GPT slot requests#966
ChristianPavilonis wants to merge 8 commits into
mainfrom
fix/duplicate-gpt-slots

Conversation

@ChristianPavilonis

Copy link
Copy Markdown
Collaborator

Summary

  • Prevent competing GPT requests when TS initializes a placement before the publisher defines its inner div.
  • Hand the TS-created inner-div slot to the late publisher definition and preserve SPA ownership cleanup.

Changes

File Change
crates/trusted-server-js/lib/src/integrations/gpt/index.ts Define fallback slots on the inner div and add scoped late-publisher definition/display/refresh handoff wrappers.
crates/trusted-server-js/lib/src/core/types.ts Add shared window.tsjs handoff lifecycle state.
crates/trusted-server-core/src/integrations/gpt_bootstrap.js Mirror the initial-load handoff behavior in the head bootstrap.
crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts Add request-level tests for late definitions and disableInitialLoad().
crates/trusted-server-core/src/integrations/gpt.rs Assert the injected bootstrap uses inner-div handoff behavior.
docs/superpowers/{specs,plans}/ Add the design specification and implementation plan.

Closes

Closes #944

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: cargo test-axum; target-matched trusted-server-core GPT tests; cargo clippy-axum

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

prk-Jr added a commit that referenced this pull request Jul 27, 2026
googletag.display()/refresh() may pass a Slot object rather than a string id,
which made elementId.startsWith throw and abort the GPT command queue — no ads
requested. Coerce a non-string arg to its getSlotElementId() (empty string if
unresolvable) before the string matching, keeping the gate-publisher-requests
feature intact (rather than reverting it as #966 did). Applied to the inline
bootstrap (gpt_bootstrap.js) and the bundle twin (gpt/index.ts).
@ChristianPavilonis
ChristianPavilonis requested review from aram356 and prk-Jr and removed request for aram356 July 27, 2026 15:43
@ChristianPavilonis
ChristianPavilonis marked this pull request as ready for review July 27, 2026 15:43

@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

The inner-div fallback plus narrowly scoped defineSlot/display/refresh wrappers with a shared window.tsjs registry is a clean solution to the competing container-slot problem, and the test suite is thorough (options preservation, global-refresh filtering, ambiguity rejection, bootstrap-eval parity). However, the wrappers introduce two reproducible crash paths — one of which breaks publisher defineSlot calls for slots unrelated to TS placements — so this needs another pass before merge. Details inline.

Non-blocking

🌱 seedling

  • Handoff registry is never pruned: gptSlotHandoffs entries and hydration aliases accumulate for the page lifetime across SPA navigations; correctness is protected only by the live-slot lookup (findGptSlotByElementId) on every claim. Consider deleting entries when adInit() destroys prevGptSlots, as future-proofing against a matching path that forgets the liveness check.

CI Status

No GitHub checks have run on this branch. Verified locally:

  • JS tests (npx vitest run): PASS (418/418)
  • Core GPT Rust tests (cargo test -p trusted-server-core --target aarch64-apple-darwin integrations::gpt): PASS (31/31)
  • Prettier on gated lib/ files: PASS

Both inline crash findings were reproduced with Vitest against the built module.

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-js/lib/src/integrations/gpt/index.ts Outdated

@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

Solid, conservative design for the late-publisher slot handoff: single-candidate ambiguity guard, internal-call guard, idempotence markers shared between bootstrap and bundle, and ownership transfer out of destroySlots(). One blocking correctness issue in the prefix-matching path, plus five non-blocking items.

Blocking

🔧 wrench

  • Prefix handoff can hijack a sibling placement's slot: the prefix path can alias a publisher defineSlot() for a different sibling div to the TS slot and suppress its display(), leaving that placement blank (crates/trusted-server-js/lib/src/integrations/gpt/index.ts:495, crates/trusted-server-core/src/integrations/gpt_bootstrap.js:63). See inline comments — a DOM-presence guard on handoff.slotElementId fixes it without breaking the new hydration tests.

Non-blocking

🤔 thinking

  • One-shot suppressPublisherRefresh can swallow a genuinely-later refresh (index.ts:554): documented spec tradeoff, no action requested.
  • JSON.stringify size comparison defeats valid GPT shorthand (index.ts:497): a publisher passing [300, 250] un-nested skips the handoff and reintroduces the duplicate slot; consider normalizing before comparing.

♻️ refactor

  • Bootstrap parity gap in tests (ad_init.test.ts:589): the eval'd-bootstrap suite covers only the hydrated handoff and div-less define; the disabled-load global-refresh filter, refresh-options preservation, and ambiguity cases run only against the bundle.

📌 out of scope

  • gptInitialLoadDisabled recorded even when the call was too late to take effect (index.ts:451, unchanged by this PR): GPT ignores disableInitialLoad() called after enableServices(), but the detector still records true. On exactly this PR's target pages (hydration-deferred GPT setup), TS's adInit() enables services first, so the publisher's later disableInitialLoad() is a GPT no-op — yet every subsequent SPA-navigation adInit() both display()s (which really requests) and refresh()es TS-owned slots: a double request, the bug class this PR fixes. Pre-existing detector behavior — worth a follow-up issue (record only when !ts.servicesEnabled at call time).

⛏ nitpick

  • gpt_bootstrap.js is no longer prettier-clean (gpt_bootstrap.js:105): two over-width lines; outside the CI format glob but main's copy is clean.

CI Status

GitHub checks: only CodeQL Analyze ran — PASS; fmt/clippy/test workflows have not run on this PR.

Local verification:

  • fmt: PASS (cargo fmt --all -- --check)
  • rust tests: PASS (cargo test-fastly gpt, 31/31 including the new bootstrap assertion test)
  • js tests: PASS (npx vitest run, 477/477)
  • clippy: not run (only Rust change is a test)

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 Outdated
Comment thread crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts
Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js Outdated
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.

Duplicate GPT slots per ad unit (TS container slot competes with publisher inner-div slot)

2 participants