Skip to content

Implement APS render fix and TSJS resilience - #1002

Draft
aram356 wants to merge 647 commits into
mainfrom
spec/aps-tsjs-resilience-design
Draft

Implement APS render fix and TSJS resilience#1002
aram356 wants to merge 647 commits into
mainfrom
spec/aps-tsjs-resilience-design

Conversation

@aram356

@aram356 aram356 commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the APS render fix and TSJS resilience design as a coordinated hard cutover based on current main, with no backward-compatibility runtime or legacy API aliases.

  • serves the APS runner through an unversioned live first-party proxy on Fastly, Axum, Cloudflare, and Spin without vendoring, pinning, or caching vendor bytes
  • implements the versioned APS renderer, strict shared renderer contract, MessageChannel/nonce lifecycle, reservation ownership, exact sizing, render diagnostics, and cleanup semantics
  • replaces the old TSJS composition roots with one generated release catalog, one immutable server boot input, a bounded first-display agent, and atomic persistent-runtime takeover
  • preserves all current-main TSJS integration behavior, including DataDome and PBS Cache, while rebuilding only the TSJS concepts identified by the retired-branch audit
  • enforces content-addressed artifacts, exact release-ID validation, bundle admission, dependency direction, hard-cutover absence, browser lifecycle cleanup, and current-main performance comparison

Scope boundaries

  • main is the implementation and behavior authority; retired rc/july is an immutable concept checklist only and is never merged, rebased, cherry-picked, built, or performance-tested
  • no DynamoDB or Tinybird requirements
  • no new analytics, persistence, cache redesign, or experiment architecture
  • no vendored APS runner, GPT runtime, or PUC bytes
  • no APS runner cache, digest, version pin, or successful-response Cache-Control; runner access remains a validated live proxy concern
  • external Prebid remains a pure external artifact, separate from TSJS integration code
  • active code contains only the hard-cutover protocol; stale pages must reload

Final implementation checkpoint

  • head: 212040ba9d5f26bde87238191844f09646bff31c
  • TSJS release: a0ec892d39dcc22b13d5d17e7bd55857633f97fcf65e41825729882c4e45d8b7
  • base: current main at f6a2fb85ce623bf8a574e3941e1ee349acc3412d
  • ancestry: main is the exact merge base; branch is 0 commits behind
  • all current inline review threads are resolved

Load-time architecture

The former parser-blocking full persistent release is no longer the first-display path. The generated bootstrap selects one content-addressed artifact:

  • a size-admitted first-display agent for the exact document mask, followed by protected paint and atomic takeover; or
  • the persistent runtime directly when the mask is not admitted.

The current admission gate reports:

  • bootstrap: 9,860 raw / 3,639 gzip / 3,285 brotli bytes
  • largest admitted first-display artifact: 89,977 raw / 26,675 gzip / 23,140 brotli bytes
  • reference persistent release: 514,040 raw / 139,422 gzip / 107,034 brotli bytes
  • maximal total release: 926,877 raw / 228,411 gzip / 158,763 brotli bytes

All absolute ceilings pass. The production-shaped semantic transfer comparison against the exact freshly built current main also passes for raw, gzip, and brotli bytes. The PR-triggered browser performance workflow remains the authority for paired timing evidence; its threshold was not weakened or reset.

Verification

Local verification at the final checkpoint:

  • Rust tests: Fastly 119 + core 1,975 + OpenRTB 21; Axum 15 + 1 + 22; Cloudflare 18 + 19; Spin 42 + 34; cross-adapter parity 14
  • APS live-proxy corpus passed through all four actual adapters; each run first proved the hermetic fixture seams absent from the production artifact
  • Rust formatting and all Fastly/Axum/Cloudflare/Cloudflare-Wasm/Spin-native/Spin-Wasm clippy gates pass; integration-test format and clippy pass
  • TSJS source: 100 files / 2,010 tests, zero type errors
  • generated release contracts: 73 tests, including exact release-ID printer validation over the complete 35-artifact inventory
  • Prettier, ESLint, ad-tech-global isolation, architecture, APS contract generation, retired-concept audit, hard-cutover absence, bundle budgets, and 55-mutation performance-evidence validator pass
  • focused Chromium: direct runtime, first-display takeover/fallback cleanup, exact current-main transfer, APS/PUC lifecycle, creative sandbox click recovery, and GPT diagnostics all pass

Protected cutover prerequisites

The PR-triggered performance workflow and repository-required GitHub checks must pass on this head. The real-GAM suite remains a protected/manual attestation because it requires deployment credentials and live infrastructure. No production deployment or protected real-GAM run was performed from this worktree.

@aram356
aram356 changed the base branch from main to rc/july August 6, 2026 05:47
ChristianPavilonis and others added 28 commits August 6, 2026 18:41
The winning-bid `hb_auction_id` carried `AuctionRequest.id`, which is
`ts-{ec_id}` whenever an Edge Cookie ID exists. That value reached
`window.tsjs.bids` and the page-bids JSON for every visitor, handing any
script on the page the identifier the `ts-ec` cookie keeps HttpOnly, and
it could not distinguish one auction from the next because it is stable
per visitor.

Mint an unrelated `ts-auc-{uuid}` per auction instead, and emit it only
when the GPT diagnostics integration is enabled, since nothing else
consumes it. `AuctionRequest.id` is unchanged for SSPs.
Keep the operator API read-only: `window.tsjs.gptDiagnostics` now exposes
only snapshot, export, subscribe, show, and hide. The evidence writers
Trusted Server's own modules use move to a separate internal channel,
`window.tsjs.gptDiagnosticsRecorder`, so the documented contract matches
what the object actually offers.

Bound the store's deferred work by retained state rather than refresh
rate. Request-intent evidence now expires lazily when the slot is next
recorded or requested instead of owning a timer per source, which also
removes the WeakRef-absent fallback that strongly retained every marked
slot until expiry. Delivery-boundary notifications share one timer that
re-arms from retained cycles.

Stop reporting a source-agnostic GPT identifier as a reservation. Those
IDs are populated for reservation and backfill alike, so they classify
as `reservation` only alongside an explicit non-backfill fact.

Evict a creative attempt whose cycle rendered empty, so a late markup
response cannot claim a Trusted Server delivery against an empty render,
and label badges from the derived delivery state instead of re-deriving
the precedence rules from raw timestamps.

Also: attribute `refresh(null)`, make the presentation switches
exhaustive, hoist the snapshot out of the subscriber loop so every
subscriber sees one capture, and make the export's attribution fields
required.

Type-check the export contract: the `expectTypeOf` assertions were never
evaluated, and one of them was a genuine error. Scope `test.typecheck`
to the type tests, since a package-wide `tsc --noEmit` still fails on
pre-existing errors elsewhere.

Fix the replacement-after-eviction test, which passed vacuously because
ten open cycles made the final render ambiguous, and cover the paths the
review identified: publisher-refresh malformed input, the association
LRU trim, the out-of-order callback arms, a declined creative attempt,
the Prebid dispatch-context restore arm, and wrapper install ordering.

Correct the operator guide where it contradicted the implementation, and
record the auction-token revision in the design spec.
Conflict in the Prebid refresh handler: main (#965) documented that the
delegated refresh preserves the publisher's original bare form, while
this branch replaced that call with the diagnostics recording plus the
scoped dispatch context. Both hold — `dispatchPrebidRefresh` passes
`slots` and `opts` through unchanged — so the resolution keeps the
diagnostics calls and main's comment, extended to say the wrapper only
scopes the shared context.

Also add `bid_id` to the auction-ID test provider's `Bid` literal, a
field main added in #996 after this branch introduced the provider.
Brings in the PR #997 review fixes: the read-only diagnostics facade
split from the internal recorder channel, lazily expiring request-intent
evidence with one shared delivery-boundary timer, source-agnostic IDs no
longer reported as reservations, the empty-render attempt eviction, and
the scoped type-check gate for the export contract.

The branch also carries a merge of main, whose #965 and #996 arrive here
as squashes of work rc/july already implements more fully. Where the two
sides describe the same feature, rc/july's implementation is kept:

- APS, adserver_mock, auction/types.rs, auction/formats.rs — rc/july's
  OpenRTB provider, renderer-aware bid_id precedence, and typed renderer
  envelope supersede main's versions, which drop fields rc/july needs.
- prebid.rs — rc/july canonicalizes the excluded-suffix list at both the
  startup and build paths already, so main's `load_config` helper adds
  nothing. Main's test is taken instead of rc/july's: it builds from raw
  settings rather than reusing the config `validate_config_for_startup`
  already canonicalized, so it actually exercises the build path.
- prebid/index.ts — a bare refresh that filtered slots must deliver the
  resolved target list, not stay bare, so rc/july's `deliveredSlots`
  behavior and its test expectation both stand.

Three fixes are ported into rc/july's shapes rather than resolved away:

- The EC-derived auction ID reached page JavaScript here too, through
  different plumbing: both collect paths inlined `request.id.as_str()`
  into `write_bids_to_state`, and page-bids passed it to
  `build_bid_map_with_auction_id`. All three now mint a per-auction
  token via `diagnostics_auction_id()`, gated on the diagnostics
  integration being enabled.
- A blank Prebid Cache UUID no longer ships cache coordinates. It loses
  the hb_adid precedence to `adid` or the bid id, so the Universal
  Creative would fetch `?uuid=<non-cache-id>` and miss instead of using
  the inline adm. The gate moves from `is_some()` to `non_empty()`, and
  main's regression test comes along with rc/july's `Bid` fields added.
- The browser-side excluded-suffix list is validated before use. The
  server only de-duplicates it, so an empty suffix matched every ad unit
  path and pulled every slot out of the refresh auction, and a non-array
  value threw inside the publisher's own `refresh()`.

Also collapses a duplicated `hb_auction_id` write in `build_bid_map`
down to one guarded insert, and points the Prebid refresh recorder at
`gptDiagnosticsRecorder` to match the new channel.
@aram356 aram356 self-assigned this Aug 7, 2026
function isPermutiveSdkUrl(url: string): boolean {
const lower = url.toLowerCase();
return (
(lower.includes('.edge.permutive.app') || lower.includes('cdn.permutive.com')) &&
Comment thread crates/trusted-server-js/lib/src/shared/first_display_contracts.ts Fixed

function exactControllerInline(document: string): string {
const inline = [
...document.matchAll(/<script([^>]*)>([\s\S]*?)<\/script>/giu),
const MAX_ALIASES = 32;
const MAX_FACTS = 512;
const HASH = /^[0-9a-f]{64}$/;
const CAPABILITY = /^[a-z][a-z0-9_]*(?:[._][a-z0-9_]+)*$/;
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.

4 participants