Skip to content

Implement APS render fix and TSJS resilience - #1002

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

Implement APS render fix and TSJS resilience#1002
aram356 wants to merge 646 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, with no backward-compatibility runtime or legacy API aliases.

  • serves the APS runner through live first-party proxy routes on Fastly, Axum, Cloudflare, and Spin without vendoring or pinning vendor bytes
  • implements the APS renderer/PUC lifecycle, reservation ownership, exact sizing, render diagnostics, and cleanup semantics
  • replaces the legacy TSJS assembly with one generated, phase-aware release and integration registry
  • emits rewritten creatives with an exact document-local core + render_runtime + creative boot artifact, including authenticated opaque-origin handling
  • preserves the adopted rc/july TSJS behavior through the executable adoption ledger and hard-cutover gates
  • upgrades the pinned TypeScript/Node toolchain and enforces type, architecture, release, bundle, and browser contracts

Scope boundaries

  • 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 or repository pin; runner access remains a live proxy concern
  • external Prebid remains a pure 10.26.0 artifact, separate from TSJS integration code
  • active code contains only the hard-cutover protocol; stale pages must reload

Current remediation checkpoint

  • head: 5b12c161daeed3e6f9e64a7a7012d8a6f5153f12
  • TSJS release: da6237db414516d27426d1ae0be03c024274dd0e869a335fb3c00bbc52afc0bd
  • base: main
  • all current inline review threads are resolved
  • source, release, bundle, adapter, workflow, documentation, formatting, and evidence-provenance review findings are remediated

Verification

Local verification at the current implementation checkpoint:

  • Rust: core (1,943), Fastly (119 + core), Axum (15 + 1 + 22), Cloudflare (18 + 19)
  • Rust formatting and all three adapter clippy matrices
  • TSJS: 90 files / 1,919 tests, no type errors
  • release contracts: 64 tests
  • lint, architecture, repository-wide Prettier, hard-cutover absence, and frozen bundle-budget checks
  • current GitHub checks

Open merge blocker: first-display load time

The automatic production-shaped candidate-versus-current-main gate is intentionally strict and remains red. The completed paired run measured:

  • current main p90: approximately 473.3 ms
  • allowed candidate p90 (1.10×): 520.63 ms
  • candidate p90: 2,163.6 ms
  • candidate/main ratio: approximately 4.57×

Performance run 31657180720

This is an architecture blocker, not an accepted budget reset. The current parser-blocking release carries roughly 395 KB raw for the production core + render_runtime + creative + gpt shape versus roughly 81 KB on current main. The next implementation checkpoint must introduce a lean first-display owner and move the full resilient lifecycle behind the protected first display while preserving APS/ADM/GPT correctness. The 1.10 threshold and actual-byte network measurement will not be weakened or relabeled.

The timeout/head-attestation defect exposed by the failing run is fixed at the current head, so subsequent failing runs retain complete schema-5 evidence and bind PR evidence to the source head SHA rather than GitHub's synthetic merge SHA.

Protected cutover prerequisites

This PR is not merge-ready until the first-display performance gate passes. Production activation also requires release coordination to supply the active Fastly service/version identity and the protected real-GAM environment. 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 29 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.
function isPermutiveSdkUrl(url: string): boolean {
const lower = url.toLowerCase();
return (
(lower.includes('.edge.permutive.app') || lower.includes('cdn.permutive.com')) &&
const MAX_ALIASES = 32;
const MAX_FACTS = 512;
const HASH = /^[0-9a-f]{64}$/;
const CAPABILITY = /^[a-z][a-z0-9_]*(?:[._][a-z0-9_]+)*$/;

function exactControllerInline(document: string): string {
const inline = [
...document.matchAll(/<script([^>]*)>([\s\S]*?)<\/script>/giu),
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