Skip to content

Select active GPT responsive slots - #978

Open
ChristianPavilonis wants to merge 2 commits into
fix/duplicate-gpt-slotsfrom
fix/gpt-display-target-responsive-slot
Open

Select active GPT responsive slots#978
ChristianPavilonis wants to merge 2 commits into
fix/duplicate-gpt-slotsfrom
fix/gpt-display-target-responsive-slot

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Resolve stable GPT slot prefixes to the unique active responsive sibling instead of the first matching DOM element.
  • Preserve exact-ID and unique lazy-slot behavior while failing closed when multiple prefix matches have no unique visible, laid-out candidate.
  • Keep the edge bootstrap and runtime implementation aligned with shared regression coverage.

This PR is stacked on #966, which already preserves GPT's display(string | Element | Slot) contract in the permanent slot-handoff implementation. It does not depend on #974.

Changes

File Change
crates/trusted-server-core/src/integrations/gpt_bootstrap.js Select a unique active responsive prefix match before defining or reusing a GPT slot.
crates/trusted-server-js/lib/src/integrations/gpt/index.ts Mirror responsive slot resolution in the runtime integration and fail closed on ambiguity.
crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts Cover runtime/bootstrap parity for element and container layout, hidden siblings, ambiguity, empty IDs, and publisher-owned slots.
crates/trusted-server-core/src/integrations/gpt.rs Update the injected-bootstrap source assertion for the shared resolver.

Closes

Closes #977

Test plan

  • cargo test-fastly && cargo test-axum && cargo test-cloudflare && cargo test-spin
  • cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity
  • cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare
  • cargo clippy-cloudflare-wasm && cargo clippy-spin-native && cargo clippy-spin-wasm
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run (430 passed)
  • JS lint and format: npm run lint && npm run format
  • JS production build: npm run build
  • Docs format: cd docs && npm run format
  • Fastly WASM release build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve

Checklist

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

@ChristianPavilonis ChristianPavilonis changed the title Fix GPT display gating for responsive slots Select active GPT responsive slots Jul 29, 2026
@ChristianPavilonis
ChristianPavilonis force-pushed the fix/gpt-display-target-responsive-slot branch from 4d95acc to 340d1ef Compare July 29, 2026 18:48
@ChristianPavilonis
ChristianPavilonis changed the base branch from rc/july to fix/duplicate-gpt-slots July 29, 2026 18:48

@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

Replaces first-match-in-document-order prefix resolution with "unique active prefix match", failing closed on ambiguity, and mirrors it across the edge bootstrap and the bundle with a shared test table. The direction is right and the parity coverage is good. One blocking issue: the resolver requires geometry before it will pick a sibling, so the single visible responsive sibling is still skipped when it reserves no space — and unlike before this PR, nothing is defined or requested at all in that case.

Blocking

🔧 wrench

  • Geometry gate drops the one visible responsive sibling when it reserves no space: slotElementHasLayout requires width > 0 && height > 0 on the element or its -container. An empty ad div awaiting its creative commonly has height 0, and its empty wrapper does too, so the active sibling fails the check and activeMatches.length === 0null → the slot is skipped entirely (no defineSlot, no ad request). Before this PR the first prefix match was defined and requested. Verified against both implementations with a throwaway probe (three display:none dupes plus one visible sibling, default 0×0 rects):

    GPT slot prefix did not resolve to one active element { divId: 'ad-probe-', prefixMatchCount: 4, activeMatchCount: 0 }
    → defineSlot not called, divToSlotId {}
    

    Adding a visibility-only tier before the geometry tier resolves the not-size-a/b/c/d pattern this PR targets without requiring reserved height. Details inline on gpt_bootstrap.js:121 and gpt/index.ts:85.

Non-blocking

🤔 thinking

  • Forced synchronous layout inside the SPA MutationObserver (crates/trusted-server-js/lib/src/integrations/gpt/index.ts:897): waitForSlotElements calls allPresent() on every mutation batch, and findSlotElementByDivId now runs getComputedStyle + getBoundingClientRect per prefix candidate whenever a prefix has more than one match. During framework hydration that is a forced reflow per batch. The presence check only needs existence, not activeness — consider a geometry-free probe there (or a resolver flag that stops at the prefix-match tier).
  • Ambiguity warn never fires from the edge bootstrap — see inline on gpt_bootstrap.js:129.

🌱 seedling

  • Resolver logic now exists twice (edge bootstrap JS and bundle TS), kept aligned by a substring assertion in gpt.rs. Worth a follow-up to generate the bootstrap from one source — see inline on gpt.rs:1216.

⛏ nitpick

  • Test table covers ambiguous, none-active, hidden-sibling and empty-div_id cases, but not "exactly one visible sibling, nothing laid out" — the case that motivates the blocking finding — and nothing asserts the warn payload (prefixMatchCount / activeMatchCount), which is the only diagnostic for the fail-closed path.

👍 praise

  • Empty-div_id guard and the runtime/bootstrap parity table — see inline on gpt/index.ts:74 and ad_init.test.ts:1298.

CI Status

Only the Integration Tests workflow ran on 340d1efb4 (4 checks, all pass). format.yml and test.yml trigger on pull_request: branches: [main, "feature/**"], so with this PR based on fix/duplicate-gpt-slots the fmt / clippy / Rust test / JS test gates did not run on CI. Verified locally on the head commit instead:

  • fmt: NOT RUN on CI
  • clippy: NOT RUN on CI
  • rust tests: PASS locally (cargo test -p trusted-server-core --target aarch64-apple-darwin gpt — 31 passed)
  • js tests: PASS locally (npx vitest run — 30 files, 430 passed)
  • integration tests: PASS on CI

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-core/src/integrations/gpt_bootstrap.js
Comment thread crates/trusted-server-core/src/integrations/gpt.rs
Comment thread crates/trusted-server-js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts
@aram356 aram356 linked an issue Jul 30, 2026 that may be closed by this pull request
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
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.

Fix GPT display gating for responsive slots

2 participants