Repository navigation
Conversation
An APS bid that wins Prebid targeting is served by Ad Manager as a 1x1 universal creative that resizes itself only after the creative draws. Every guard on that render path returned silently, so a slot that never drew was indistinguishable from one that did: Ad Manager reports a non-empty 1x1 render either way, and the tester framework reports "filled". Name the guard that stopped the render instead. The sandboxed renderer document now reports bad_hash, nonce_mismatch, descriptor_keys, descriptor_fields, descriptor_envelope, and amazon_script_error on the existing failure message. Reporting is one-shot and answers through the parent, never the sender. The source check stays first, so a window that is not the parent is dropped before any reporting happens and cannot consume the frame's single report. The Universal Creative source labels its own frame_timeout and frame_load_error and relays whichever reason it holds to the top window. That relay crosses an origin boundary, so reasons resolve through a null-prototype allowlist that drops anything unlisted and leaves a hostile __proto__ or constructor as undefined. The table is built on first use so it does not become a module-level side effect that defeats tree-shaking and grows the Prebid shim, which imports two unrelated helpers from this module. The failure message now carries a third key, which the direct render path's listener rejected: it exact-matched two keys, so the frame survived hidden over publisher content until the 10s timeout rather than failing fast. That listener now accepts a reason-bearing failure, including the nonce-less bad_hash report a frame sends before it has learned a nonce. Reasons are fixed categories. A descriptor is never echoed back.
The reasons the render path now emits had nowhere to land. The creative failure union in types.ts is the compile-time contract, but the runtime contract was a hand-written allowlist in the diagnostics store, and the overlay rendered facts from a separate exhaustive switch. Three lists, kept in agreement by hand, and a reason missing from the store's list was dropped silently. Derive the union from one const array and validate against the same array, so a category that exists at compile time is recordable at runtime by construction. The overlay keeps its exhaustive switch with no default, which is what makes a new category a compile error there rather than an undefined fact. The bridge consumes the relay the creative frame posts to the top window. Every field is untrusted: the reason resolves through the allowlist, and the sender must be a frame inside the ad unit the capability was served to, so a frame that merely knows a live ad ID cannot forge a failure. The ad ID is not secret from the ad-serving chain, since it is published as hb_adid targeting. The consumed-ad-ID tombstone carries the ad unit it was served to for exactly that check. Recording only; the branch never answers the sender and never influences delivery. Attribution of the relayed failure to the render it belongs to still depends on the attempt lifecycle work tracked separately: a response recorded when the descriptor is posted closes the attempt before the frame reports, and the slot association API plants a request intent that belongs to the next cycle. Tests drive a real store rather than a mock recorder, so a reason that the store would reject fails the test. Both new store and bridge tests were checked against the pre-fix allowlist and the unwired relay, and fail there.
The Universal Creative source changed, so its version constant moved 4 to 5, and the browser spec asserted the literal 4 against the Prebid Response payload. Bumping the constant is the normal consequence of touching that source, so a literal here turns every such change into a failure that looks unrelated to the change that caused it. Read the constant from render.ts instead, the way the spec already reads the built bundles from that crate.
#1300 replaces #1052, reverted from this RC in the previous commit. Integration decisions: - gpt_diagnostics/store.ts keeps the trace imports beside #1300's creative failure list import. - #1300 adds eight aps_* creative failure reasons that #1107's trace projection does not accept, so any cycle carrying one failed trace capture. The projection now validates against the full diagnostics list and drops the APS reasons; unknown values are still rejected. The viewer bundle is unchanged; the pinned trace source digest moves.
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Labelling the guard that stopped an APS render is the right fix, and the security shape of the relay (source check first, null-prototype allowlist, fixed categories, one-shot reporting) is sound. However, the relay records nothing in a realistic configuration: the Prebid path keys the store by the Prebid ad unit code while associations are keyed by the Trusted Server slot id, and the server-side APS path is never consulted. Both were reproduced against a real GptDiagnosticsStore using this suite's own homepage_header / div-header fixture.
3 of the inline comments below carry a one-click GitHub
suggestion— use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The remaining comments describe the fix in prose because the change touches multiple files or lines outside the diff and can't be auto-applied. Each suggestion was applied alone and as a batch in a scratch worktree: prettier, the full vitest suite (1225 passed, including the Prebid shim size guard), and the coreintegrations::apstests all pass.
Blocking
🔧 wrench
- Prebid-path relay records under the Prebid ad unit code, not the Trusted Server slot id — see inline at
crates/trusted-server-js/lib/src/integrations/gpt/index.ts:2007 - Server-side APS render failures are never relayed — see inline at
crates/trusted-server-js/lib/src/integrations/gpt/index.ts:2005
Non-blocking
🤔 thinking / ♻️ refactor / 📝 note / ⛏ nitpick
- 🤔
nonce_mismatchcan never be recorded (suggestion) — see inline atcrates/trusted-server-core/src/integrations/aps.rs:126 - 🤔 Nonce-less failure acceptance makes reasons forgeable from inside the renderer frame (suggestions) — see inline at
crates/trusted-server-js/lib/src/integrations/aps/render.ts:767and:812 - ♻️ No test executes the renderer document's reasons — see inline at
crates/trusted-server-core/src/integrations/aps.rs:3327 - 📝
aps_frame_load_erroris effectively unreachable — see inline atcrates/trusted-server-js/lib/src/integrations/gpt_diagnostics/overlay.ts:201 - ⛏ "Adding a member here is sufficient" overstates — see inline at
crates/trusted-server-js/lib/src/core/types.ts:189
👍 praise
- Single source of truth for the failure vocabulary — see inline at
crates/trusted-server-js/lib/src/core/types.ts:196
Cross-cutting / body-level findings
- ♻️ Operator docs still list four failure categories — the "Creative-bridge failures" table in
docs/guide/integrations/gpt-diagnostics.md(around line 225) documents onlymissing_render_source,cache_fetch_failed,invalid_cache_payload, andresponse_post_failed. This PR adds tenaps_*categories that now appear in the overlay; each needs a row describing where it is observed (renderer document vs. Universal Creative source) and, ideally, a sentence that APS reasons are relayed diagnostics, not bridge failures. - 🌱 The direct render path discards the reason —
renderApsCreative(reached fromcore/request.ts:59, the client/auctionpath) now accepts reason-bearing failures but ignoresreason, andfail()logsAPS renderer: frame load failedfor every cause, including the 10s timeout. Logging the allowlisted reason (viaapsRenderFailureReason) would make that path debuggable at no cost; recording it can follow the attempt-lifecycle work in #1299. - 📝 Repeated relays are bounded — checked while reviewing: a frame inside the ad unit can post the relay repeatedly, but
recordTrustedServerCreativeRequestreturns the existing live attempt for the cycle and failures are deduplicated, so this cannot flood the store. No action needed.
CI Status
- browser integration tests: PASS
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- CodeQL: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (axum native): PASS
- CLAUDE.md symlink guard: PASS
- Analyze (actions): PASS
- Analyze (python): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- Analyze (javascript-typescript): PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo test: PASS (required)
- cargo fmt: PASS (required)
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- prepare integration artifacts: PASS
- vitest: PASS
- Analyze (rust): PASS
dhruv8sh
left a comment
There was a problem hiding this comment.
Summary
Giving each APS render guard a name, and having one array define the failure categories, are both the right moves. The renderer-document and direct-path changes look correct. However, the relay that is meant to carry a reason to ts_console cannot record anything in a real browser yet. Three independent gaps each drop the reason, and this review adds one that the unit tests cannot see: the message is posted from PUC's hidden renderer frame, so it fails the bridge's source check.
None of the inline comments below carry a one-click
suggestion. The fix touches the Universal Creative source and its tests together, so it is described in prose.
Blocking
🔧 wrench
- Relay is posted from PUC's hidden renderer frame, so the source check always fails — see inline at
crates/trusted-server-js/lib/src/integrations/aps/render.ts:808
Cross-cutting / body-level findings
- 🔧 I agree with the two open wrenches from the earlier review, and they add to this one. The Prebid-path relay records under the Prebid ad unit code instead of the Trusted Server slot id (
gpt/index.ts:2007). The server-side APS path is never consulted (gpt/index.ts:2005). I confirmed both againstGptDiagnosticsStore.recordTrustedServerCreativeRequest, which looks uptrustedServerSlotsby slot id. To get a single reason recorded end to end, all three have to be fixed. The newad_init.test.tscaserecords a relayed APS render failure against a real diagnostics storepasses only because it uses a syntheticsourceand associates the slot under'div-header'. Neither happens in production. - 🌱 The tombstone can expire before the frame times out. The relay looks up
consumedPrebidApsIds, and each tombstone expires at the renderer entry'sexpiresAt. A capability consumed with less than 10s of TTL left will lose the tombstone beforeframe_timeoutfires, and the relay will be dropped. That is rare with the default 300s TTL. Once the relay works, keeping the tombstone until at leastnow + RENDERER_READY_TIMEOUT_MSwould close the gap. It also keeps the ad ID blocked longer, which is the safer direction.
CI Status
- browser integration tests: PASS
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- CodeQL: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (axum native): PASS
- CLAUDE.md symlink guard: PASS
- Analyze (actions): PASS
- Analyze (python): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- Analyze (javascript-typescript): PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo test: PASS (required)
- cargo fmt: PASS (required)
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- prepare integration artifacts: PASS
- vitest: PASS
- Analyze (rust): PASS
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Approved with non-blocking comments on the APS failure-reporting path:
- Send the relay from the creative window so source authentication accepts it, rather than rejecting PUC’s dynamic-renderer iframe.
- Recognize server-side APS winners as well as Prebid winners, preserving slot-scoped ownership.
- Resolve the Trusted Server slot ID before recording failures when it differs from the Prebid div code.
- Report nonce mismatches using the frame’s expected nonce so receivers retain
nonce_mismatchrather than falling back toframe_timeout.
Review found that the relay recorded nothing in a real browser. Four independent gaps each dropped the reason, and the new bridge test passed only because it used keys and a sender that production never produces. The relay was posted from the wrong realm. Universal Creative runs the renderer source inside a hidden frame it creates in the creative document, so a postMessage call made from that source carries the hidden frame as event.source whatever window it targets: the source is the realm of the running function, not the window the method is called on. The top-window listener authenticates a sender by matching it against iframes in the publisher document, which the hidden frame is not, so every relay was dropped. Reports now go through Universal Creative's own sendMessage helper, which is a creative-realm function, so a report arrives as the creative iframe the listener already recognizes. Verified in headless Chromium against a page mirroring Universal Creative's structure: the previous call reports the hidden renderer frame and matches no slot iframe, the helper reports the creative iframe and matches. The Prebid path recorded under the publisher's ad unit code, which is a div id, while the store keys associations by the configured Trusted Server slot id. Whenever they differ the lookup missed, the failure was dropped, and a creative_request_without_slot attribution issue was recorded in its place, so a render failure surfaced as a misleading attribution problem. The relay now resolves the slot id through divToSlotId. Server-side APS winners are served the same renderer and emit the same relay, but are tracked per slot rather than through the Prebid tombstone, so their relays were dropped. They are now resolved too, requiring the sender to sit in the slot that consumed the ad ID because hb_adid is not unique per slot. nonce_mismatch could never be recorded: the report echoed the mismatching nonce, which no parent accepts. It now echoes the nonce the frame expects, which only the parent receives, and the parent is the window that set it. Accepting any nonce-less failure let code running inside the renderer frame tear the frame down without knowing the nonce. bad_hash is the only report a frame sends before it has learned one, so nonce-less acceptance is narrowed to that reason in both parents. A tombstone could also expire before the frame's timeout fired, dropping the reason for a capability consumed late in its TTL. Tombstones now live at least as long as that timeout, which also keeps the ad ID blocked longer. The browser spec now asserts the reasons the renderer document actually emits, including the echoed nonce. That is the only coverage that executes the document rather than matching substrings in its Rust source, and it is where the nonce_mismatch bug would have been caught. The direct render path logs the reason instead of reporting every cause as a load failure. Operator docs describe the ten categories and where each is observed.
|
Addressed at a90f2f8. All four blocking findings were reproduced before fixing, and every fix is pinned by a test that fails without it. Realm (@dhruv8sh, @ChristianPavilonis). Confirmed, and the diagnosis was exactly right. Verified in headless Chromium against a page mirroring Universal Creative's structure, with the real One correction to the suggested fix: Slot-id keying (@prk-Jr, @ChristianPavilonis). Confirmed against the real store with production's association: keying on the div id gives Server-side path (@prk-Jr, @ChristianPavilonis). Confirmed: both branches serve the same renderer (
Nonce-less forgery (@prk-Jr). Narrowed to Tombstone expiry (@dhruv8sh 🌱). Fixed rather than deferred: tombstones now live at least Renderer-document coverage (@prk-Jr ♻️). Added to Also done: operator docs now have an APS table covering all ten categories and where each is observed, with a note that an HTTP error on the renderer route surfaces as Local gates: Attempt-lifecycle work remains deferred to #1299 and is unchanged by this round. |
dhruv8sh
left a comment
There was a problem hiding this comment.
Summary
The follow-up commit closes every blocking finding from the earlier reviews. The relay now goes out through Universal Creative's sendMessage helper, so it arrives from the creative iframe. Failures are recorded under the Trusted Server slot id, and server-side APS winners are relayed too. nonce_mismatch is now reachable, nonce-less acceptance is narrowed to bad_hash, the browser spec executes the renderer document, and the docs list all ten categories. What remains is non-blocking.
2 of the inline comments below carry a one-click GitHub
suggestion. Use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The remaining comment is a note with no action required.
Non-blocking
🤔 thinking / ⛏ nitpick / 📝 note
- 🤔 The tombstone minimum has no margin, so a late
frame_timeoutrelay is still dropped (suggestion). See inline atcrates/trusted-server-js/lib/src/integrations/gpt/index.ts:1951. - ⛏ The Rust test's doc comment overstates the JS-side coverage (suggestion). See inline at
crates/trusted-server-core/src/integrations/aps.rs:3328. - 📝
aps_unknownis effectively unreachable, and an unrecognised wire reason is dropped silently. See inline atcrates/trusted-server-js/lib/src/integrations/aps/render.ts:834.
Cross-cutting / body-level findings
-
♻️ No real-browser test covers the main fix (which window the relay comes from). Routing the relay through
sendMessageonly matters in a real browser, because the sending window is determined by where the posting code runs. The vitest relay tests use apucHelperstand-in that posts from the top window. The new browser-spec assertion covers the renderer document's ownrenderer-failedmessages, not the relay. The commit message describes a manual headless-Chromium check, but nothing automated would catch a return tow.top.postMessage. The real-PUC test incrates/trusted-server-integration-tests/browser/tests/shared/aps-renderer.spec.ts(around line 228) already loadsPUC_BANNERand is the natural place to lock it in. Apply manually: the test is outside this PR's diff hunks.// In "renders through real PUC and expands only its authenticated 1x1 shell", // make the renderer document report a failure (e.g. serve it a descriptor with // a bad envelope), then assert the top window received the relay from the // creative iframe itself: const relay = await page.evaluate(() => (window as unknown as { apsRelays: Array<{ fromCreative: boolean; data: Record<string, unknown> }>; }).apsRelays.find(({ data }) => data.message === 'trusted-server/aps/render-failed')); expect(relay).toEqual({ fromCreative: true, data: expect.objectContaining({ reason: 'descriptor_envelope' }) });
-
👍 Thorough follow-up. The fix commit addresses every earlier finding and explains each one. The new browser-spec assertion executes the real renderer document and checks the echoed nonce. That is the coverage that would have caught the
nonce_mismatchbug. The relay also keeps per-slot sender checks on both the Prebid and server-side paths.
CI Status
- browser integration tests: PASS
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- CodeQL: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- CLAUDE.md symlink guard: PASS
- format-docs: PASS (required)
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- Analyze (javascript-typescript): PASS
- cargo fmt: PASS (required)
- cargo test (axum native): PASS
- Analyze (actions): PASS
- format-typescript: PASS (required)
- Analyze (rust): PASS
- cargo test: PASS (required)
- vitest: PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- cargo test (cross-adapter parity): PASS
- prepare integration artifacts: PASS
- Analyze (python): PASS
prk-Jr
left a comment
There was a problem hiding this comment.
The follow-up fixes the earlier blocking findings: the failure relay uses Universal Creative's creative-realm helper, resolves exact div IDs to Trusted Server slot IDs, covers server-side APS winners, and reports nonce mismatches using the expected nonce. The descriptor validation, sandbox boundaries, fixed reason vocabulary, and source-first authentication remain intact.
One remaining correctness issue affects supported prefix-based ad units: authentication resolves the actual creative frame, but the new slot lookup uses the unresolved prefix as a map key. The inline comment includes a verified one-line correction.
The already-reported tombstone timeout margin and real-browser relay regression coverage remain relevant; this review does not duplicate those comments. Attempt association and post-completion evidence remain explicitly deferred to #1299, so this PR does not establish reliable attribution across refresh cycles.
Validation: all 22 GitHub checks pass. The three targeted JS suites pass locally under Node 24: 304 tests covering APS rendering, the GPT bridge, and the diagnostics store. Rust and browser execution are covered by the passing remote checks; they were not rerun locally.
An ad unit code can be a configured prefix rather than a DOM id, and source authentication supports that: a capability registered for `div-` is legitimately served to the iframe under `div-header`. The relay then looked the code up in `divToSlotId`, which only ever holds resolved ids, so the lookup missed and every relayed failure for that supported configuration was dropped after authentication had already succeeded. Resolve the slot through the source resolver instead, which handles exact ids, configured prefixes, and ambiguity. The capability's own source check still runs first, so a sender outside the served ad unit is rejected before any slot is resolved. Reproduced by changing only the existing relay test's ad unit code from `div-header` to `div-`: the capability is served and the expected failure becomes undefined. That case is now its own regression test beside the exact-id one. Also assert the relay end to end in the browser, driving real Universal Creative with a renderer route that never acknowledges, so the reason arrives as `aps_frame_timeout` through a prefix-configured slot. Only a real browser shows which realm a relay comes from and which slot the authenticated frame resolves to, which is the class of gap that the unit tests could not see.
|
Addressed at 64a0545. Prefix-based ad units. Confirmed and fixed as suggested. I reproduced it exactly as described: changing only the existing relay test's Real-browser relay coverage. This was the right thing to push on, so I did it rather than defer it. The last round added renderer-document reason coverage, but nothing asserted the relay reaching diagnostics in a browser, which is precisely the class of gap the realm bug and this prefix bug both fell into. Tombstone timeout margin. Already fixed in the previous round (a90f2f8) rather than deferred: Attempt association and post-completion evidence stay deferred to #1299, as you note. Agreed that this PR does not establish reliable attribution across refresh cycles, and the PR description says so. Local gates: |
The previous tombstone minimum was exactly one frame timeout measured from consumption, but the frame starts its own timer only after the capability round trip, so its timeout relay always lands slightly later than that. A capability consumed with little TTL left therefore still lost its tombstone before the relay arrived, and the reason was dropped. The margin now covers two timeout windows. Bound it by the renderer frame's own timeout rather than the native-mode constant. Both are ten seconds today, but the tombstone has to outlive the frame's timer specifically, and the two could drift apart. A relay arriving just after consumption plus one timeout is now a regression test; it fails against the single-window margin. Also correct two claims that overstated what the code does. The Rust test's doc comment named JavaScript coverage for every reason, when the browser spec executes only bad_hash, nonce_mismatch, and descriptor_keys. The aps_unknown docs row described a render with no reason available, when the category is reached only by an empty reason string: a reason the top window does not recognize, which can happen when the renderer document and the script bundle are on different versions, is dropped rather than recorded.
Why
A blank APS creative render is indistinguishable from a delivered one, and nothing on the render path says why. An APS bid that wins Prebid targeting is served by Ad Manager as a 1x1 universal creative that resizes itself only after the creative draws, so Ad Manager reports a non-empty 1x1 render either way and the tester framework reports "filled". Every guard on the render path returned silently: a frame that rejected the descriptor, one that never received it, and one that timed out all looked identical.
This replaces #1052, which is being closed rather than revived. That branch had not moved since 2026-09-01, carried five unaddressed
CHANGES_REQUESTEDreviews, conflicts with currentmain, and was anchored to the closed #1019. Its security shape was sound and is carried forward here; the six defects that stopped it are fixed or deferred explicitly.Companion issue: #1299.
What changed
The sandboxed renderer document (
aps.rs) reports which guard stopped it, on the existing failure message:bad_hash,nonce_mismatch,descriptor_keys,descriptor_fields,descriptor_envelope,amazon_script_error.The Universal Creative source (
render.ts) labels its ownframe_timeoutandframe_load_errorand relays whichever reason it holds to the top window.The GPT bridge (
gpt/index.ts) consumes that relay and records the reason against the ad unit the capability was served to.The diagnostics store and overlay learn the new categories, so a reason is recorded and displayed rather than discarded. This is the part #1052 was missing, and the reason nothing it emitted ever reached
ts_console.Three deliberate differences from #1052
One source of truth for the failure vocabulary. #1052 widened the type union in
types.tswhile the store's runtime allowlist and the overlay's switch stayed as they were, so all fifteen new reasons were dropped before being recorded. Here the union is derived from a singleconstarray and the store validates against that same array, so a category that exists at compile time is recordable at runtime by construction. The overlay keeps its exhaustive switch with nodefault, which is what makes a new category a compile error there instead of anundefinedfact.The source check stays first. #1052 moved it below the shape check so a non-parent sender could be told
source_mismatch. That reason is dropped here and the ordering is left alone: a sender that is not the parent is rejected before any reporting, so it cannot consume the frame's single report. Thesource_mismatchcategory does not exist.The relay is source-checked. #1052 accepted a relayed failure with no check on
e.source, so any frame with atophandle and a live ad ID could forge one, including the security-signal categories. The tombstone now carries the ad unit it was served to, and the relay requires the sender to be a frame inside it. The ad ID is not secret from the ad-serving chain, since it is published ashb_adidtargeting.The direct-path regression, fixed
Adding
reasonmakes the failure message three keys.renderApsCreative's listener exact-matched two, so it dropped every failure and the hidden frame survived over publisher content for the full 10s timeout instead of failing fast. That listener now accepts a reason-bearing failure, including the nonce-lessbad_hashreport a frame sends before it has learned a nonce, while still rejecting a foreign nonce and still requiring an exact key set on thereadymessage. #1052 shipped this regression untested; it has four tests here.Notes for review
The renderer bundle version moves 4 to 5 because the Universal Creative source changed, and the one test that hardcoded
4now reads the constant.The Prebid shim imports two unrelated helpers from
aps/render.ts, and a module-scopeObject.freezeon the reason table defeated tree-shaking and pushed the shim from 40787 to 41225 bytes, past the 41000-byte guard inprebid-artifact-integration.test.mjs. The table is built on first use instead, so the shim stays at its previous size and the guard is unchanged. The guard's comment asks for growth to be re-justified rather than raised, so it was not raised.Security posture, since a reason crosses an origin boundary:
parent, never the sender.__proto__,constructor, ortoStringresolves toundefined.Deferred, tracked in #1299
This does not attempt the attempt-lifecycle redesign, so a relayed failure is not yet attributed to the render it belongs to in every case:
recordTrustedServerCreativeResponseis recorded when the descriptor is posted, which closes the attempt before the frame can report. A narrower store API, or a post-completion failure state, is needed.Both are described in #1299 with the measurements behind them. They need design review rather than a port, which is why they are not here.
Testing
Gates run locally, all green:
cargo fmt --all -- --checkcargo clippy-fastlycargo test -p trusted-server-core(2912 passed)cd crates/trusted-server-js/lib && npx vitest run(1225 passed, 50 files, no type errors)cd crates/trusted-server-js/lib && npm run formattsc --noEmitoversrc/was compared against the merge base: the error set is byte-identical, so this branch introduces no new type errors. (The repo has 352 pre-existing ones and no CI gate for them.)New coverage:
render.test.ts: direct-path teardown on a reason-bearing failure, on nonce-lessbad_hash, rejection of a foreign nonce, rejection of areadymessage with an extra key; the allowlist including inherited-key and non-string rejection; every mapped reason resolving to a category the store accepts; UC relay of a frame reason, frame timeout, and frame load errorstore.test.ts: every declared category is recordable, iterating the array so a future addition cannot silently become unrecordablead_init.test.ts: a relayed failure recorded against a realGptDiagnosticsStorerather than a mock recorder, plus drops for a foreign sender and for unlisted reasonsThe two tests guarding the #1052 defects were checked against the broken versions and fail there: the store test fails with
expected undefined to deeply equal [ 'aps_bad_hash' ]against the old hand-written allowlist, and the bridge test fails withexpected undefined to deeply equal [ 'aps_frame_timeout' ]with the relay recording disabled.