Support Sourcepoint GPP consent for EC generation#642
Support Sourcepoint GPP consent for EC generation#642ChristianPavilonis wants to merge 21 commits intofeature/edge-cookies-finalfrom
Conversation
7009838 to
43df212
Compare
0e5c07c to
a8b5b1c
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Sourcepoint GPP consent is a small, well-motivated slice — the GPP-US decoder, the allows_ec_creation gating branch, and their tests are solid. But as shipped, the feature is non-functional (the JS module is never served to browsers), the PR fails CI, and it bundles several large unrelated changes that should be separate PRs.
Blocking
🔧 wrench
-
Sourcepoint JS module is built but never served.
crates/trusted-server-core/src/integrations/registry.rs:790-792hardcodesJS_ALWAYS = &["creative"]. The design spec (docs/superpowers/specs/2026-04-15-sourcepoint-gpp-consent-design.md:38) says the integration follows the same "JS-only, no Rust registration" pattern ascreative, but that pattern requiresJS_ALWAYSto list the module.integrations/mod.rshas nosourcepoint::registerandJS_ALWAYShas no"sourcepoint"entry, soIntegrationRegistry::js_module_ids()will never include it —/static/tsjs=…will never concatenatetsjs-sourcepoint.jsinto a served bundle. End-to-end, no browser will ever executemirrorSourcepointConsent(). Fix: add"sourcepoint"toJS_ALWAYSand a test asserting it ships injs_module_ids_immediate(). -
Orphaned dead-code file
crates/trusted-server-core/src/ec/admin.rs. 380 lines implementingPOST /_ts/admin/partners/register, butec/mod.rscontains nopub mod admin;declaration (grep -rn "mod admin\|ec::admin::" crates/returns zero hits). The base branch (feature/edge-cookies-final) replaced the KV-backed partner registry with config-based partners in commit049a501e; that change droppedadmin.rsbut the rebase on this branch reinstated it unreferenced. Fix: deletecrates/trusted-server-core/src/ec/admin.rs. -
Clippy
-D dead_codefailures block CI. Three dead symbols introduced by the same rebase and carrying no callers:crates/trusted-server-core/src/platform/test_support.rs:174—recorded_request_headersmethodcrates/trusted-server-core/src/platform/test_support.rs:336—build_services_with_http_clientfunctioncrates/integration-tests/tests/common/runtime.rs:56—PartnerRegistrationFailedvariant (this is the actual failing CI log message)
Confirmed locally:
cargo clippy --workspace --all-targets --all-features -- -D warningsfails in the PR's worktree. Fix: delete them all — nothing references any of the three. -
Undisclosed scope: creative iframe sandbox lockdown.
crates/js/lib/src/core/render.ts:7-18removesallow-scripts,allow-same-origin, andallow-formsfrom the creative iframesandbox=attribute, andcrates/trusted-server-core/src/creative.rs:361-367strips<iframe>elements entirely during HTML sanitization. Together, any creative relying on JavaScript (viewability pixels, click tracking, rich media, VPAID) or on third-party ad-tag iframes will stop rendering. This is a major ad-tech behavioral change wholly unrelated to Sourcepoint GPP consent, and it is not mentioned in the PR body. It needs its own PR with a threat model, product sign-off, and a rollout plan. Fix: extract these two changes to a dedicated PR.
❓ question
- Why is Prebid User ID Module support shipping inside this PR? The title and body are "Support Sourcepoint GPP consent for EC generation," but the diff also contains ~132 lines of
build-all.mjsUser-ID-submodule generation, a new_user_ids.generated.ts, ~290 lines of new Prebid test coverage, a 212-line design spec (specs/2026-04-16-prebid-user-id-module-design.md), and a 599-line implementation plan. Two independent features in one review is hard to evaluate. Can Prebid User ID Module move to its own PR?
Non-blocking
📌 out of scope
- No re-mirror after mid-session CMP interaction.
mirrorSourcepointConsent()runs once when the module is parsed. If a user opens the Sourcepoint CMP and updates consent mid-session,__gpp/__gpp_sidwon't reflect the new state until the next page load — meaning the same session can continue to block EC creation even after consent is granted. Follow-up: subscribe to__gppapievents (or astoragelistener) and re-run on change.
|
Addressed the requested review feedback in 3bbdb1d. Summary:
Validation run locally:
I also replied to and resolved the inline threads above. |
3bbdb1d to
8a6df3a
Compare
9261993 to
d8c943d
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Follow-up review focused on areas not covered in the prior pass. Sourcepoint flow (mirror + GPP US decoding + EC gating) is functional and well-tested, but two doc/file-stale issues from the scoping commit (8a6df3af) need correction before merge, and the always-shipped Sourcepoint module has cross-CMP failure modes worth designing around.
PR effective scope note: vs main the diff is 80 commits / 111 files, because the intended base (feature/edge-cookies-final) has merged. The Sourcepoint-specific changes are a small subset (~14 files); reviewers landing on this PR via GitHub should read the description with that in mind.
Blocking
🔧 wrench
- Stale Prebid User ID docs reference removed
TSJS_PREBID_USER_IDSenv var (docs/guide/integrations/prebid.md:226-261) _user_ids.generated.tsclaims to be auto-generated but is now hand-edited (crates/js/lib/src/integrations/prebid/_user_ids.generated.ts:1)
Non-blocking
🤔 thinking
- Sourcepoint hardcoded as always-shipped will clobber
__gppset by other CMPs (registry.rs:792+sourcepoint/index.ts:60-72) - First page load race: mirror runs before Sourcepoint CMP populates localStorage (
sourcepoint/index.ts:91-93) - TCF presence silently overrides explicit GPP US
sale_opt_out=Yesin US states (consent/mod.rs:506-515) - Session-scoped cookie + run-once mirror leaves stale
__gppafter mid-session retraction (sourcepoint/index.ts:39)
♻️ refactor
- Stale comment displaced by
us_sale_opt_outinsertion (consent/gpp.rs:74-75) - Make clearing logic CMP-safe by tracking write source (
sourcepoint/index.ts:42-46)
⛏ nitpick
- Unused/inconsistent default export (
sourcepoint/index.ts:95)
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS (1001 lib + 21 misc)
- js tests: PASS (294)
Prebid's liveIntentIdSystem.js uses a dynamic require() inside a build-flag-guarded branch that their gulp pipeline dead-codes via constant folding. esbuild leaves the require() in the output, causing ReferenceError: require is not defined at browser runtime. Remove from the bundle until we add an esbuild resolver plugin (or switch to Prebid's own build pipeline) — tracked as a follow-up in the design spec.
Introduces TSJS_PREBID_USER_IDS env var (mirroring TSJS_PREBID_ADAPTERS) to control which Prebid User ID submodules are bundled. The hardcoded imports in index.ts are replaced with a generated file written by build-all.mjs at build time, defaulting to the same 13-submodule set. - build-all.mjs: generatePrebidUserIds() validates names, denylists liveIntentIdSystem, and writes _user_ids.generated.ts. Existence check also probes dist/src/public/ to handle modules shipped as .ts in sources (sharedIdSystem). - index.ts: replaces 13 hardcoded submodule imports with import './_user_ids.generated' - _user_ids.generated.ts: committed default with all 13 submodules - Tests: updated mocks and regression guard; added 9 syncPrebidEidsCookie behavior tests - Docs: new "User ID Modules" section in prebid.md with TSJS_PREBID_USER_IDS usage; spec follow-up #1 marked complete
__gpp and __gpp_sid are read by the Rust server over HTTPS; they must be Secure. Also sets Max-Age=86400 (matching ts-eids) so stale consent state doesn't outlast the session, and replaces the legacy expires= deletion pattern with Max-Age=0.
… and Move logging initialization into Fastly adapter (#610) * Rename crates to trusted-server-core and trusted-server-adapter-fastly Rename crates/common → crates/trusted-server-core and crates/fastly → crates/trusted-server-adapter-fastly following the EdgeZero naming convention. Add EdgeZero workspace dependencies pinned to rev 170b74b. Update all references across docs, CI workflows, scripts, agent files, and configuration. * Add platform abstraction layer with traits and RuntimeServices Introduces trusted-server-core::platform with PlatformConfigStore, PlatformSecretStore, PlatformKvStore, PlatformBackend, PlatformHttpClient, and PlatformGeo traits alongside ClientInfo, PlatformError, and RuntimeServices. Wires the Fastly adapter implementations and threads RuntimeServices into route_request. Moves GeoInfo to platform/types as platform-neutral data and adds geo_from_fastly for field mapping. * Address platform layer review feedback - Defer KV store opening: replace early error return with a local UnavailableKvStore fallback so routes that do not need synthetic ID access succeed when the KV store is missing or temporarily unavailable - Use ConfigStore::try_open + try_get and SecretStore::try_get throughout FastlyPlatformConfigStore and FastlyPlatformSecretStore to honour the Result contract instead of panicking on open/lookup failure - Encapsulate RuntimeServices service fields as pub(crate) with public getter methods (config_store, secret_store, backend, http_client, geo) and a pub new() constructor; adapter updated to use new() - Reference #487 in FastlyPlatformHttpClient stub (PR 6 implements it) - Remove unused KvPage re-export from platform/mod.rs - Use super::KvHandle shorthand in RuntimeServices::kv_handle() * Reject host strings containing control characters in BackendConfig * Fix clippy error * Validate scheme and host for control characters in BackendConfig * Address review findings on platform abstraction layer * Address review findings on platform abstraction layer * Add config store read path and storage module split - Split fastly_storage.rs into storage/{config_store,secret_store,api_client,mod}.rs - Add PlatformConfigStore read path via FastlyPlatformConfigStore::get using ConfigStore::try_open/try_get - Add PlatformError::NotImplemented variant; stub write methods on FastlyPlatformConfigStore and FastlyPlatformSecretStore - Add StoreName/StoreId newtypes with From<String>, From<&str>, AsRef<str> - Add UnavailableKvStore to core platform module - Add RuntimeServicesBuilder replacing 7-arg constructor - Migrate get_active_jwks and handle_trusted_server_discovery to use &RuntimeServices - Update call sites in signing.rs, rotation.rs, main.rs - Add success-path test for handle_trusted_server_discovery using StubJwksConfigStore - Fix test_parse_cookies_to_jar_empty typo (was emtpy) * Harden legacy config-store reads and align Fastly adapter stubs * Address storage review feedback * Resolved github-advanced-security bot problems * Address PR review feedback on platform abstraction layer - Make StoreName and StoreId inner fields private; From/AsRef provide all needed construction and access - Add #[deprecated] to GeoInfo::from_request with #[allow(deprecated)] at the three legacy call sites to track migration progress - Enumerate the six platform traits in the platform module doc comment - Extract backend_config_from_spec helper to remove duplicate BackendConfig construction in predict_name and ensure - Replace .into_iter().collect() with .to_vec() on secret plaintext bytes - Remove unused bytes dependency from trusted-server-adapter-fastly - Add comment on SecretStore::open clarifying it already returns Result (unlike ConfigStore::open which panics) * Add PR 4 design spec for secret store trait (read-only) * Clarify test scope and deferred branches in PR 4 spec * Add implementation plan for PR 4 secret store trait * Add test for get_secret_bytes open-failure path * Add NotImplemented tests for FastlyPlatformSecretStore write stubs * Inline StoreId binding and add section comment in write-stub tests * Remove plan * Add PR 6 design spec for backend and HTTP client traits * Address spec review findings on PR 6 design * Implement PlatformHttpClient and thread RuntimeServices through proxy layer - Add PlatformHttpClient trait with send(), send_async(), and select() methods - Add PlatformBackend trait with predict_name() and ensure() methods - Add PlatformResponse wrapper around EdgeZero HTTP responses - Add PlatformPendingRequest and PlatformSelectResult for auction fan-out - Thread RuntimeServices through IntegrationProxy::handle(), IntegrationRegistry::handle_proxy(), and all first-party proxy endpoints so handlers can reach the HTTP client - Add StubHttpClient and StubBackend test stubs with build_services_with_http_client helper - Add proxy_request_calls_platform_http_client_send integration test - Fix proxy_with_redirects to stay within 7-arg clippy limit via ProxyRequestHeaders struct - Document Body::Stream limitation in edge_request_to_fastly with warning log - Document intentional duplication of platform_response_to_fastly across proxy and orchestrator - Remove spec file (promoted to plan + implementation) * Address pr review findings * Resolve pr review findings * Add PR7 design spec for geo lookup + client info extract-once Documents the call site migration plan: five Fastly SDK extraction points in trusted-server-core replaced by RuntimeServices::client_info reads, following Phase 1 injection pattern from the EdgeZero migration design. * Fix spec review issues in PR7 design doc - Correct erroneous claim about generate_synthetic_id being called twice via DeviceInfo; it is called once (line 91 for fresh_id), DeviceInfo.ip is a separate req.get_client_ip_addr() call fixed independently - Add before/after snippet for handle_publisher_request call site in main.rs - Add noop_services import instruction for http_util.rs test module - Clarify _services rename (drop underscore, not add new param) in didomi.rs - Clarify nextjs #[allow(deprecated)] annotations are out of scope (different function) * Update PR7 spec to address all five agent review findings - Change RequestInfo::from_request signature to &ClientInfo (not &RuntimeServices) so prebid can call it with context.client_info - Scope SDK-call acceptance criteria to active non-deprecated code only - List all six AuctionContext construction sites including two production sites in orchestrator.rs and three test helpers in orchestrator/prebid - Add explicit warn-and-continue pattern for publisher.rs geo lookup - Correct testing table: formats.rs and endpoints.rs have no test modules; add orchestrator.rs and prebid.rs test helper update rows * Add PR7 implementation plan and address plan review findings Plan covers 6 tasks in compilation-safe order: AuctionContext struct change first, then from_request signature, then synthetic.rs cascade, then publisher geo, then didomi. Includes two new copy_headers unit tests (Some/None). Spec fixes: clarify injection pattern exceptions for &ClientInfo and Option<IpAddr>; reword acceptance criterion to reflect that provider-layer reads flow through AuctionContext.client_info. * Fix three plan review findings and two open questions - Finding 1 (High): Add missing publisher.rs test call site at line ~695 for get_or_generate_synthetic_id — was omitted from Task 3 Step 6 - Finding 2 (Medium): Remove crate::geo::GeoInfo import from endpoints.rs rather than replacing it — type is not used by name after the change, keeping any import fails clippy -D warnings - Finding 3 (Low): Replace interactive git add -p in Task 6 with explicit file staging instruction - Open Q1: Add Task 2 step to update stale handle_publisher_request signature in auction/README.md - Open Q2: Add Task 2 step to update from_request doc comment to reflect ClientInfo-based TLS detection instead of Fastly SDK calls * Broaden two low-severity doc cleanup steps in PR7 plan - Step 7: cover all four stale Fastly-SDK-specific locations in http_util.rs (SPOOFABLE_FORWARDED_HEADERS doc, RequestInfo struct doc, from_request doc, detect_request_scheme doc) - Step 8: replace the whole routing snippet in auction/README.md, not just the one handle_publisher_request line — handle_auction and integration_registry.handle_proxy are also stale in that snippet * Fix two remaining low findings in PR7 plan - Add missing Location 2 (RequestInfo.scheme field doc, line ~67) to Step 7; renumber subsequent locations 3-5 - Replace &runtime_services with runtime_services in Step 5 and README snippet — runtime_services is already &RuntimeServices in route_request * Fix count drift in Step 7: four → five locations * Add client_info field to AuctionContext and fix all construction sites * Change RequestInfo::from_request to take &ClientInfo, thread services into handle_publisher_request * Add Task 2 follow-up coverage and README route fixes * Add services param to generate_synthetic_id, remove Fastly IP/geo calls in formats and endpoints * Revert premature publisher geo change from Task 3 * Replace deprecated GeoInfo::from_request in publisher.rs with services.geo().lookup() * Remove Fastly IP extraction from Didomi copy_headers, use ClientInfo instead * Move IpAddr import to test module level in didomi.rs * Apply rustfmt formatting to didomi.rs, publisher.rs, and synthetic.rs Fix multi-line function call style in didomi.rs, line-break wrapping in publisher.rs test, and import ordering in synthetic.rs test module. * Add test coverage for generate_synthetic_id with concrete client IP Adds noop_services_with_client_ip helper to test_support and a new test that verifies the client_ip path through generate_synthetic_id by asserting the HMAC differs when the IP changes. * Align geo lookup warn log format with codebase convention ({e} not {e:?}) * Apply Prettier formatting to PR7 plan and spec docs * Document content rewriting as platform-agnostic in platform module * Document html_processor as platform-agnostic * Document streaming_processor as platform-agnostic * Fix unresolved doc link: replace EdgeRequest with edgezero_core::http::Request * Add plan for content rewriting * Add plan for PR9: wire signing to store primitives * Add build_services_with_config_and_secret to test_support * Add FastlyManagementApiClient to adapter * Implement FastlyPlatformConfigStore and FastlyPlatformSecretStore write methods via management API Replace FastlyApiClient with FastlyManagementApiClient in the put/delete methods of FastlyPlatformConfigStore and the create/delete methods of FastlyPlatformSecretStore. Remove the now-unused FastlyApiClient import. * Add services to AuctionContext; remove deprecated from_config shim Thread RuntimeServices into AuctionContext so auction providers can access platform stores directly. Update PrebidAuctionProvider to use RequestSigner::from_services(context.services) instead of the now- removed from_config() shim. All construction sites and test helpers updated accordingly. This satisfies the final acceptance criterion of #490: no FastlyConfigStore/FastlySecretStore construction remains in the request_signing/ modules. * Fix rotate/delete atomicity, HTTP verb, idempotent deletes, and weak tests - Revert proxy.rs merge artifact: restore per-request allowed_domains at both redirect_is_permitted call sites; remove dead_code allow and stale comment — integration proxies defaulting to &[] get open mode again as documented - Drop unused trusted-server-js dep from adapter Cargo.toml - Fix check_response: gate body read behind error branch so 2xx paths do not buffer and discard the response body - Remove self-referential SECRET_UPSERT_METHOD test - Reorder write-cost doc so outbound HTTPS round-trip leads; handle-open caching noted as negligible - Refactor make_request to take fastly::http::Method; drop string match and unreachable arm; remove SECRET_UPSERT_METHOD const - Add SigningStoreIds named struct in endpoints.rs; update both call sites to destructure by name * Move logging initialization into Fastly adapter (PR 10) (#610) Move logging initialization into Fastly adapter (PR 10) (#610) - Reverted gratuitous _message rename and record.args() usage in logging.rs, returning to the idiomatic message parameter inside the fern format closure. - Refactored target_label to use .rsplit_once("::") rather than .split("::").last(). This provides a more explicit and robust way to extract the final module segment. - Expanded target_label test coverage to explicitly test edge cases such as inputs without :: separators, empty strings, and inputs with trailing ::.
8643550 to
24d4515
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Follow-up review focused on what's new since the last pass and on items I think were missed. The marker-cookie response in a2c6d831 substantively addresses most prior feedback (clearing is now CMP-safe, retries cover the first-load race, the stale comment is fixed, the default export is gone). The remaining issues are: the symmetric write-path clobber that the marker doesn't cover, a real CI failure inherited from the merge-base, and continued scope creep beyond the Sourcepoint feature.
Local verification on the PR HEAD passes: cargo check, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo test --workspace, and npx vitest run (301 tests). CI fails because the prepare integration artifacts job hits a build-time validation panic.
Blocking
🔧 wrench
-
CI: integration test passphrase below 32 chars.
prepare integration artifactspanics inbuild.rs:42viaEc::validate_passphrase—MIN_PASSPHRASE_LENGTHwas bumped from 8 to 32 in commit8e08e642(which is on the merge-base, hence inherited), but the integration env still setsTRUSTED_SERVER__EC__PASSPHRASE=integration-test-ec-secret(26 chars) in:.github/actions/setup-integration-test-env/action.yml:83.github/workflows/test.yml:58scripts/integration-tests.sh:56scripts/integration-tests-browser.sh:35
These files are not in this PR's diff, so the fix is either land it here or in a small precursor PR — but the PR cannot merge with red CI. Suggested value:
integration-test-ec-secret-padded-32(or longer). -
Sourcepoint mirror clobbers other CMPs'
__gppon the write path — see inline atsourcepoint/index.ts:115.
Non-blocking
🤔 thinking
-
Scope: PR still bundles unrelated commits after the partial scope-down.
8e3ff806removed the Prebid User ID feature, but the diff vsfeature/edge-cookies-finalstill containsfedcc347("Wire request signing… and Move logging initialization") which re-applies already-merged PRs #609/#610 — once the base branch is rebased onto currentmain, this commit is duplicate work and a guaranteed merge-conflict source.24d45156("Remove generated Prebid user ID shim") and theclearPrebidEidsCookiechange atprebid/index.ts:454are also outside the Sourcepoint feature. Suggestion: rebasefeature/edge-cookies-finalonto currentmainfirst to dropfedcc347, and split the Prebid User ID shim removal into its own PR. -
__gpp_sidmarker-check on the write path is dead code — see inline. -
scheduleInitialRetryleaks a realsetTimeoutacross Vitest tests — see inline. -
DOMContentLoaded+setTimeout(500)can double-firemirrorSourcepointConsent— see inline. -
Prebid
clearPrebidEidsCookieon missinggetUserIdsAsEidsis a behavior change — see inline.
♻️ refactor
decode_us_sale_opt_outaccumulator can dropsaw_not_opted_out— see inline.
🌱 seedling
- Cookie size limits not enforced client-side. Server caps at
MAX_GPP_STRING_LEN: 8192inconsent/gpp.rs:33, but browsers cap individual cookies near ~4096 bytes. A long Sourcepoint GPP payload (compound multi-section) can be silently truncated by the browser, leaving the server unable to decode. Worth a follow-up: cap and log client-side instead of writing a doomed cookie.
📌 out of scope
- TCF-vs-GPP-US precedence is documented but not configurable. The spec section "TCF-before-GPP precedence is intentional" makes a permanent policy choice some publishers may want to override. Tracking issue would prevent this from getting buried.
- Sourcepoint integration runs on every Trusted Server page (
registry.rs:803,sourcepoint/index.ts:151). With the marker cookie this is correctness-safe, but every visitor on every site pays ~3 KB of payload + a localStorage scan + cookie writes for a feature that only applies to Sourcepoint customers. Worth a follow-up: opt-in via publisher config, or only register listeners after a first valid mirror.
⛏ nitpick
- Spec lists test file at the wrong path — see inline.
CI Status
- fmt: PASS (local)
- clippy: PASS (local,
--all-targets --all-features -- -D warnings) - rust tests: PASS (local, 1073 + 21 + 19)
- js tests: PASS (local, 301 tests / 24 files)
- integration tests: FAIL (
prepare integration artifactspanics; see Blocking above)
| } | ||
|
|
||
| writeCookie(GPP_SOURCE_COOKIE_NAME, GPP_SOURCE_SOURCEPOINT); | ||
| writeCookie(GPP_COOKIE_NAME, gppString); |
There was a problem hiding this comment.
🔧 wrench — Sourcepoint mirror clobbers other CMPs' __gpp on the write path.
The _ts_gpp_src=sp marker added in a2c6d831 correctly addresses the prior reviewer's clearing-clobber concern (#3164911741), but on the very first run with a valid Sourcepoint payload, this code unconditionally overwrites whatever __gpp and __gpp_sid another CMP wrote on the same origin. The marker only protects the symmetric clear path.
Suggested fix (one of):
// Option A: only write when we own the cookie or it's empty.
const existing = readCookie(GPP_COOKIE_NAME);
if (existing && existing !== gppString && !hasSourcepointMarker()) {
log.debug('sourcepoint: __gpp already set by another writer — skipping');
return false;
}Option B: keep current behavior but document explicitly in the spec that Sourcepoint writes always win on its origin (the spec currently only documents clearing safety, not writing safety).
|
|
||
| if (Array.isArray(applicableSections) && applicableSections.length > 0) { | ||
| writeCookie(GPP_SID_COOKIE_NAME, applicableSections.join(',')); | ||
| } else if (hasSourcepointMarker()) { |
There was a problem hiding this comment.
🤔 thinking — hasSourcepointMarker() here is dead code.
This branch only executes after we just wrote _ts_gpp_src=sp on line 114, so the marker is always present. Either drop the conditional (else { clearCookie(GPP_SID_COOKIE_NAME); }) or hoist the marker check to before the marker write so it actually gates anything.
| document.addEventListener('DOMContentLoaded', retry, { once: true }); | ||
| } | ||
|
|
||
| window.setTimeout(retry, INITIAL_RETRY_DELAY_MS); |
There was a problem hiding this comment.
🤔 thinking — Real setTimeout at module load leaks across Vitest tests.
window.setTimeout(retry, 500) is scheduled at import time with no clearTimeout and no idempotency guard beyond initialized. Every Vitest test that imports this module schedules one of these. Only the explicit retry test uses fake timers; other tests can have the deferred retry fire mid-assertion against whatever localStorage state they've set up. Tests pass today because the suite is fast, but the isolation is brittle.
Suggested fix: track the timeout id and clear it once a mirror succeeds (and on subsequent successful mirrors), e.g.:
let retryTimer: number | undefined;
function scheduleInitialRetry(): void {
if (retryTimer !== undefined) return;
retryTimer = window.setTimeout(() => {
retryTimer = undefined;
mirrorSourcepointConsent();
}, INITIAL_RETRY_DELAY_MS);
// …
}| }; | ||
|
|
||
| if (document.readyState === 'loading') { | ||
| document.addEventListener('DOMContentLoaded', retry, { once: true }); |
There was a problem hiding this comment.
🤔 thinking — DOMContentLoaded + setTimeout(500) can fire mirrorSourcepointConsent twice.
When readyState === 'loading', both handlers register and both run — they don't cancel each other. Idempotent today (reading localStorage and writing the same cookies has no observable side effect), but if the mirror later gains a non-idempotent step (event emit, beacon, counter), this would silently double. Cheap fix: a retried boolean inside scheduleInitialRetry that short-circuits the second call.
| function syncPrebidEidsCookie(): void { | ||
| try { | ||
| if (typeof pbjs.getUserIdsAsEids !== 'function') { | ||
| clearPrebidEidsCookie(); |
There was a problem hiding this comment.
🤔 thinking — Behavior change buried in a Sourcepoint PR.
Previously this branch was a silent no-op; now it clears the ts-eids cookie. With userId.js unconditionally imported at line 20, the missing-function branch should be unreachable, so this is defensive — but it's a behavior change that doesn't belong with the Sourcepoint feature. If anything else ever seeds ts-eids (server response, manual injection), this will now wipe it.
Either (a) split into a separate PR with its own justification, or (b) add a one-line comment here explaining why clearing on missing getUserIdsAsEids is correct (i.e. that no User ID Module = no EIDs to forward, so the cookie must not be stale).
| /// - `None` if no US section is present or no decodable US section yields a | ||
| /// usable `sale_opt_out` signal | ||
| fn decode_us_sale_opt_out(parsed: &iab_gpp::v1::GPPString) -> Option<bool> { | ||
| let mut saw_not_opted_out = false; |
There was a problem hiding this comment.
♻️ refactor — Aggregation can be expressed without saw_not_opted_out.
let mut result: Option<bool> = None;
for us_section_id in parsed
.section_ids()
.filter(|id| US_SECTION_ID_RANGE.contains(&(**id as u16)))
{
match parsed.decode_section(*us_section_id) {
Ok(section) => match us_sale_opt_out_from_section(§ion) {
Some(true) => return Some(true),
Some(false) => result = Some(false),
None => {}
},
Err(e) => log::warn!("Failed to decode US GPP section {us_section_id}: {e}"),
}
}
resultSame behavior, drops the intermediate flag. Optional.
| | File | Change | | ||
| |---|---| | ||
| | `crates/js/lib/src/integrations/sourcepoint/index.ts` | New — localStorage auto-discovery, cookie mirroring | | ||
| | `crates/js/lib/src/integrations/sourcepoint/index.test.ts` | New — Vitest tests | |
There was a problem hiding this comment.
⛏ nitpick — Wrong path. Tests live at crates/js/lib/test/integrations/sourcepoint/index.test.ts (per the project's src vs test split), not under src/integrations/.
Summary
_sp_user_consent_*from localStorage and mirrors GPP consent into__gpp/__gpp_sidcookiessale_opt_outfrom US GPP sections (IDs 7–23)allows_ec_creation()between existing TCF andus_privacychecksCloses #640
Test plan
cargo test --workspace— 992 tests including 8 new)npx vitest run— 288 tests including 6 new)🤖 Generated with Claude Code