Skip to content

Prevent competing refreshes before initial ad render - #1191

Open
prk-Jr wants to merge 11 commits into
mainfrom
spec/1190-initial-ad-render-ownership
Open

prk-Jr wants to merge 11 commits into
mainfrom
spec/1190-initial-ad-render-ownership

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

A publisher auction starting after the TS-owned slot's slotRequested event but before its initial slotRenderEnded could issue a second refresh and replace the first creative. Keep initial ownership open through render settlement, retain the original losing disposition when a wrapped callback repeats, and preserve legitimate post-render refreshes.

This PR includes the revised spec, implementation plan, runtime/bootstrap changes, unit regressions, and a portable production-bundle browser regression together. The controlled test reproduced two native GPT-fixture calls before the fix and one afterward. Live-site attribution remains unverified: sampled staging loads had no SSAT bids, and supplied server diagnostics reported no-bid/HTTP 400 responses.

Changes

Files Change
first_impression.ts, gpt_bootstrap.js Close initial registration on render instead of request; make settlement terminal and retain losing tokens through delayed/repeated callbacks; reuse a claim-scoped denial token at the 16-token limit and record one diagnostic if rendering remains pending after five seconds.
Prebid integration Do not restore initial targeting over a settled slot's later targeting.
Prebid/bootstrap tests Cover request-to-render overlap, lease expiry, callback failure/replay, filled/empty settlement, capacity overflow, pending diagnostics, and later refresh compatibility.
browser/initial-render/ Exercise real TSJS, Prebid, and Universal Creative with controlled GPT events; test runtime and persistent bootstrap listeners, with page-escaping tests and both browser variants enforced in CI and evidence uploaded.
Spec and implementation plan Record confirmed evidence, narrow the scope, and document compatibility limits.

Issue

Closes #1190. This corrects the demonstrated overlap; it does not claim the reported live flicker has been independently reproduced.

Test plan

  • JS: 1,053 tests, ESLint, Prettier, production bundle build.
  • Browser: 10 scenarios, runtime-only and bootstrap-first; verified the unfixed runtime fails the overlap assertion.
  • Rust: formatting and all eight target-matched clippy configurations.
  • Rust tests: Fastly, Axum, Cloudflare, Spin, native CLI/codegen, and adapter parity.
  • Docs: formatting, lint, VitePress build.
  • Template-cache CI harnesses: ESI 22 checks, inline 9 checks.

Fastly/Axum/CLI tests passed after allowing macOS certificate access and local socket binds outside the sandbox.

Deployment note

The five-second expiry no longer releases Trusted Server initial-render ownership.
A missing slotRenderEnded keeps competing delivery suppressed until the existing
element/navigation lifetime ends. After five seconds, a one-time diagnostic records
pending state but never unlocks the slot. It is available via debug logging and
pendingRenderDiagnostic on the claim, not the GPT diagnostics panel or JSON export.
Publisher fallback leases and diagnostics correlation windows are unchanged.

Scope limits

GPT/GAM selection is simulated in the browser regression; it verifies actual creative DOM through Universal Creative. Arbitrary publisher targeting writes and replay of a consumed pending bid outside the wrapped callback remain outside this correction. A missing initial render event keeps the slot protected until its existing element/navigation lifetime ends. The 16-token bound remains unchanged: overlapping auctions reuse a retained denial token at capacity. Publisher-owned capacity behavior and the existing 256-claim limit remain unchanged. Pending rendering produces a single diagnostic snapshot without releasing ownership.

Checklist

  • Spec, code, and regression coverage are in this PR.
  • No new dependencies or server auction/configuration changes.
  • Fixtures use fictional endpoints and contain no credentials.
  • Bootstrap and runtime use the same initial-render boundary.

@prk-Jr prk-Jr self-assigned this Sep 22, 2026
@prk-Jr
prk-Jr marked this pull request as ready for review September 22, 2026 13:56

@dhruv8sh dhruv8sh 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 TS initial-slot ownership from slotRequested to slotRenderEnded in both the runtime and the bootstrap, keeps losing publisher tokens so repeated or late callbacks stay suppressed, and bounds overflow by reusing a denial token at capacity. The change is well scoped, well tested, and matches the spec. The comments below are non-blocking.

2 of the inline comments below carry a one-click GitHub suggestion. The rest describe the change in prose.

Non-blocking

🤔 thinking

  • No bounded recovery if slotRenderEnded never arrives — see inline at crates/trusted-server-js/lib/src/core/first_impression.ts:192
  • Empty TS render still suppresses the overlapping publisher delivery — see inline at crates/trusted-server-js/lib/src/core/first_impression.ts:398

♻️ refactor

  • Failing scenario's Playwright trace is never saved — see inline at crates/trusted-server-integration-tests/browser/initial-render/run.cjs:386

⛏ nitpick

  • FIRST_IMPRESSION_LEASE_MS doc comment is now too narrow — see inline at crates/trusted-server-js/lib/src/core/first_impression.ts:10
  • Redundant inline require("node:child_process") — see inline at crates/trusted-server-integration-tests/browser/initial-render/run.cjs:450

👍 praise

  • The browser regression drives real TSJS, Prebid and Universal Creative bundles, checks the actual creative DOM, runs both the runtime-only and bootstrap-first setups, and has a --baseline mode that shows the unfixed code fails. The unit tests cover every row of the spec's verification table, including callback replay, the capacity overflow, and "a later slotRequested can't reopen the slot".

CI Status

  • integration tests: PASS
  • browser integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • CodeQL: PASS
  • Analyze (javascript-typescript) ×2: PASS
  • Analyze (actions): PASS
  • Analyze (rust): PASS
  • vitest: PASS
  • format-typescript: PASS (required)
  • format-docs: PASS (required)
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (ts CLI, native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (axum native): PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • CLAUDE.md symlink guard: PASS

Comment thread crates/trusted-server-js/lib/src/core/first_impression.ts
Comment thread crates/trusted-server-js/lib/src/core/first_impression.ts
Comment thread crates/trusted-server-integration-tests/browser/initial-render/run.cjs Outdated
Comment thread crates/trusted-server-js/lib/src/core/first_impression.ts Outdated
Comment thread crates/trusted-server-integration-tests/browser/initial-render/run.cjs Outdated
@prk-Jr
prk-Jr requested a review from dhruv8sh September 23, 2026 04:49
Save the failing scenario's Playwright trace by tracking the per-scenario
trace path and context at loop scope and draining them in the existing
outer finally, instead of re-indenting the whole scenario body. Use the
already-imported execFileSync for the artifact metadata.

Document the deliberate scope decisions the review asked about: the TS
claim's unbounded expiry when slotRenderEnded never arrives, and empty
renders settling identically to filled ones so already-retained denial
tokens keep suppressing. Both are recorded in contract item 3 of the
design spec and at the code sites. Widen the lease constant's doc comment
to cover the auction lease and the pending-render diagnostic delay.

@dhruv8sh dhruv8sh 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-reviewed at 6f8f1bee. All five earlier comments are addressed:

  • The unbounded TS claim and the empty-render suppression are now explicit, documented scope decisions, in code comments and in spec contract item 3. The reasoning holds: element identity and page navigation already retire stale claims, and auctions that start after settlement get no denial token.
  • The Playwright trace is now saved on failure via the outer finally.
  • The two nit suggestions were applied as proposed.

CI is green, and the touched Vitest suites, the JS build and the browser-page unit tests pass locally.

👍 Nice job keeping the trace fix to about 20 lines instead of a 290-line re-indent, and verifying it with an injected failure.

Optional, non-blocking: the gpt_bootstrap.js mirror of the Number.POSITIVE_INFINITY assignments (lines 232 and 245) could carry the same "deliberately unbounded" comment as the runtime.

@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-reviewed at 6f8f1bee. The ownership change holds up. Registration now stays open until render. Losing tokens survive consume, release and a repeated callback. At the 16-token limit, overlapping auctions reuse a denial token. Post-render targeting is no longer restored, and the rendered phase can no longer regress. The runtime and bootstrap agree on all of this. The touched Vitest suites (700 tests), the JS build and cargo check-fastly pass locally.

One path the spec names is untested, and it blocks: the pending-render diagnostic after a publisher-to-TS fallback. The rest are small CI, docs and test-harness fixes.

3 of the inline comments below carry a one-click GitHub suggestion. The other 2 describe the fix in prose because they touch a separate range or other files.

Blocking

🔧 wrench

  • Fallback-transition pending diagnostic is untested - see inline at crates/trusted-server-js/lib/src/core/first_impression.ts:186

Non-blocking

♻️ refactor

  • A failure in the new browser regression skips the Next.js browser suite - see inline at .github/workflows/integration-tests.yml:221

📝 note

  • Lazy-loaded slots record the pending diagnostic routinely - see inline at docs/guide/integrations/gpt-diagnostics.md:514

⛏ nitpick

  • The bootstrap copy is missing the "deliberately unbounded" comment - see inline at crates/trusted-server-core/src/integrations/gpt_bootstrap.js:232
  • pages.test.cjs loads jsdom from another package's node_modules - see inline at crates/trusted-server-integration-tests/browser/initial-render/pages.test.cjs:3

CI Status

  • cargo fmt: PASS (required)
  • format-typescript: PASS (required)
  • cargo test: PASS (required)
  • format-docs: PASS (required)
  • vitest: PASS
  • browser integration tests: PASS
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • CodeQL: PASS
  • Analyze (rust): PASS
  • Analyze (javascript-typescript): PASS (x2)
  • Analyze (actions): PASS
  • cargo test (ts CLI, native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (axum native): PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • CLAUDE.md symlink guard: PASS

Comment thread crates/trusted-server-js/lib/src/core/first_impression.ts
Comment thread .github/workflows/integration-tests.yml
Comment thread docs/guide/integrations/gpt-diagnostics.md Outdated
Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js
Comment thread crates/trusted-server-integration-tests/browser/initial-render/pages.test.cjs Outdated
@prk-Jr
prk-Jr requested a review from aram356 September 24, 2026 08:25

This branch has not been deployed

No deployments
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.

Prevent ad flicker when a client-side auction runs against a slot that already has an SSAT result

4 participants