Skip to content

Implement APS render fix and TSJS resilience - #1002

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

Implement APS render fix and TSJS resilience#1002
aram356 wants to merge 609 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
prk-Jr and others added 29 commits August 6, 2026 12:14
Carry bid_id through adserver_mock mediation. The reconstruction restored
nurl, burl, ad_id and the cache fields from the original SSP bid but
hard-coded bid_id to None, so a mediated bid whose only hb_adid source is
the OpenRTB bid id lost it and never rendered — the exact failure this
branch fixes, re-opened on the mediated path. Mediated APS bids were
worse off still: they carry no ad_id or cache_id for the restore to
recover, so they reached the page with no hb_adid at all. The mediation
response is itself OpenRTB, so prefer the mediated bid's own id and fall
back to the original SSP bid's.

Reject blank identity strings before applying the cache_id -> ad_id ->
bid_id precedence. Option::or treats Some("") as present, so a blank
cacheId or adid outranked a valid bid id and emitted an empty hb_adid —
falsey on the page, so GPT skips the targeting key and the render bridge
has nothing to match. Prebid's parse_bid likewise treats an empty
OpenRTB id as absent.

Emit hb_cache_host and hb_cache_path only alongside a real Prebid Cache
UUID. PBS reports the cache url and cacheId independently, so a bid with
coordinates but no UUID pointed the Universal Creative at
?uuid=<non-cache-id>, a guaranteed miss, instead of letting it fall
through to the inline adm.

Warn when the chosen hb_adid exceeds GAM's 40-character targeting value
limit. GAM drops an over-long value, so the creative echoes nothing and
the bridge's equality check never matches. Log rather than truncate: a
truncated id is no longer unique per bid, which is what lets one slot's
render claim another slot's creative.
Brings the PR #996 review fixes onto rc/july. The original fallback commit
was already merged; this adds the follow-ups plus two conflict
resolutions where rc/july had since moved.

hb_adid precedence keeps rc/july's renderer tier and gains blank
rejection on every tier, so a bidder emitting an empty cacheId or adid no
longer outranks a usable identifier:

    non_empty(cache_id) -> non_empty(renderer_bid_id)
      -> non_empty(ad_id) -> non_empty(bid_id)

Cache coordinates keep rc/july's placement inside the processed_adm match
and gain the cache_id requirement as a second, independent condition:
absent a UUID the coordinates would send the Universal Creative to
?uuid=<non-cache-id>.

Mediation bid_id precedence is inverted relative to the main-targeted
branch, deliberately. On main, APS bids carry no bid_id, so preferring the
mediation response's own id was the only way to give them an hb_adid. On
rc/july, aps.rs populates bid_id and a typed renderer envelope is minted
against it, and build_bid_map derives hb_adid from that pairing — so
substituting the mediator's id would key targeting to an id the renderer
does not know. The original SSP bid's id therefore wins here, with the
mediated id as the fallback for an upstream bid that carried none.
Bring the GPT delivery-evidence and refresh-attribution diagnostics onto the
July release branch. rc/july already carried a later evolution of the auction-ID
plumbing (PR #922 render tracing, the delivered-winner-slot telemetry, and the
APS renderer bridge), so the overlapping Rust and JS changes keep the rc/july
implementation and layer the new diagnostics on top of it.

Conflict resolutions:
- publisher.rs: keep the rc/july `write_bids_to_state` -> delivered winner slots
  contract and prebuilt page-bids bid map; keep both new tests; update the
  incoming test provider to the current `ProviderRequestOutcome` trait.
- gpt/index.ts: keep the render-trace and APS renderer paths and add the
  creative request/response/failure diagnostics around them. The attempt is
  recorded after the APS branch, which is served by the APS universal creative
  and has no Trusted Server creative response to resolve.
- prebid/index.ts: keep the resolved bare-refresh slot list and dispatch it
  through the diagnostics-aware refresh wrapper.
- store.ts: adopt the incoming response-based `slotOnload` correlation.
- Drop the pre-squash `gpt_diagnostics_bootstrap.js` and its test, which the
  upstream #974 squash removed as superseded by server-recognized activation.

Verified: cargo fmt, clippy (fastly/axum/cloudflare/cloudflare-wasm/spin-native/
spin-wasm), test-fastly, test-axum, test-cloudflare, test-spin, parity,
vitest (812 tests), JS and docs format.
Log whether protection_test_bypass is enabled when registering the DataDome
integration and include configured header name when enabled. Keep credential
secret out of logs.,
# Conflicts:
#	crates/trusted-server-core/src/integrations/datadome.rs
#	crates/trusted-server-core/src/integrations/datadome/protection.rs
#	crates/trusted-server-core/src/publisher.rs
#	docs/guide/integrations/datadome.md
#	docs/superpowers/plans/2026-08-03-datadome-ip-excluded-client-tag.md
#	docs/superpowers/specs/2026-08-03-datadome-ip-excluded-client-tag-design.md
@aram356
aram356 marked this pull request as ready for review August 12, 2026 17:45
@aram356
aram356 changed the base branch from rc/july to main August 12, 2026 19:38
Comment thread crates/trusted-server-js/lib/src/adapters/prebid.ts Fixed
Comment thread crates/trusted-server-js/lib/src/adapters/prebid.ts Fixed
Comment thread crates/trusted-server-js/lib/src/adapters/messaging.ts Fixed
@aram356
aram356 marked this pull request as draft August 13, 2026 16:24
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