Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1c14b3d
Add design spec for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
fca8979
Add implementation plan for Sourcepoint GPP consent support (#640)
ChristianPavilonis Apr 15, 2026
a1bc657
Add us_sale_opt_out field to GppConsent
ChristianPavilonis Apr 15, 2026
1964872
Decode US sale opt-out from GPP sections
ChristianPavilonis Apr 15, 2026
fbb2457
Recognize GPP US sale opt-out in EC consent gating
ChristianPavilonis Apr 15, 2026
ad6e790
Add Sourcepoint JS integration for GPP consent cookie mirroring
ChristianPavilonis Apr 15, 2026
e025198
Add design spec for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
6450637
Add implementation plan for Prebid User ID Module support
ChristianPavilonis Apr 16, 2026
a760c4c
Fix ESM path resolution in Prebid User ID plan regression guard
ChristianPavilonis Apr 16, 2026
c8abb71
Add Vitest coverage for Prebid ts-eids cookie sync
ChristianPavilonis Apr 16, 2026
eee99f0
Bundle Prebid User ID core and submodules in Prebid integration
ChristianPavilonis Apr 16, 2026
bd366ad
Correct Prebid User ID plan + spec — drop pubCommonIdSystem (removed …
ChristianPavilonis Apr 16, 2026
05d1700
Drop liveIntentIdSystem from Prebid bundle
ChristianPavilonis Apr 16, 2026
3d59d3d
Make Prebid User ID submodule set configurable at build time
ChristianPavilonis Apr 16, 2026
67e55d5
Clear stale consent cookies and aggregate US GPP opt-outs
ChristianPavilonis Apr 16, 2026
f956e8a
Add Secure flag and Max-Age to Sourcepoint GPP cookies
ChristianPavilonis Apr 16, 2026
08440a6
support ec partners map for env overrides
ChristianPavilonis Apr 17, 2026
8e3ff80
Scope Sourcepoint consent PR and address review feedback
ChristianPavilonis Apr 21, 2026
fedcc34
Wire request signing to RuntimeServices store primitives (PR 9) (#609…
prk-Jr Apr 29, 2026
24d4515
Remove generated Prebid user ID shim
ChristianPavilonis May 1, 2026
a2c6d83
Address Sourcepoint consent review feedback
ChristianPavilonis May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/js/lib/src/integrations/prebid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ function fitAuctionEidsToCookie(eids: AuctionEid[]): AuctionEid[] | undefined {
function syncPrebidEidsCookie(): void {
try {
if (typeof pbjs.getUserIdsAsEids !== 'function') {
clearPrebidEidsCookie();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 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).

return;
}

Expand Down
151 changes: 151 additions & 0 deletions crates/js/lib/src/integrations/sourcepoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { log } from '../../core/log';

const SP_CONSENT_PREFIX = '_sp_user_consent_';
const GPP_COOKIE_NAME = '__gpp';
const GPP_SID_COOKIE_NAME = '__gpp_sid';
const GPP_SOURCE_COOKIE_NAME = '_ts_gpp_src';
const GPP_SOURCE_SOURCEPOINT = 'sp';
const INITIAL_RETRY_DELAY_MS = 500;

interface SourcepointGppData {
gppString: string;
applicableSections: number[];
}

interface SourcepointConsentPayload {
gppData?: SourcepointGppData;
}

let initialized = false;

function findSourcepointConsent(): SourcepointConsentPayload | null {
// Sourcepoint stores one consent payload per property under `_sp_user_consent_*`.
// We intentionally take the first valid match and mirror that origin-scoped payload.
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key?.startsWith(SP_CONSENT_PREFIX)) continue;

const raw = localStorage.getItem(key);
if (!raw) continue;

try {
const payload = JSON.parse(raw) as SourcepointConsentPayload;
if (payload.gppData?.gppString) {
return payload;
}
} catch {
log.debug('sourcepoint: failed to parse localStorage value', { key });
}
}
return null;
Comment thread
ChristianPavilonis marked this conversation as resolved.
}

function readCookie(name: string): string | undefined {
const prefix = `${name}=`;
const cookie = document.cookie.split('; ').find((entry) => entry.startsWith(prefix));
return cookie?.slice(prefix.length);
}

function hasSourcepointMarker(): boolean {
return readCookie(GPP_SOURCE_COOKIE_NAME) === GPP_SOURCE_SOURCEPOINT;
}

function writeCookie(name: string, value: string): void {
document.cookie = `${name}=${value}; path=/; Secure; SameSite=Lax`;
Comment thread
ChristianPavilonis marked this conversation as resolved.
}

function clearCookie(name: string): void {
document.cookie = `${name}=; path=/; Secure; SameSite=Lax; Max-Age=0`;
}
Comment thread
ChristianPavilonis marked this conversation as resolved.
Comment thread
ChristianPavilonis marked this conversation as resolved.

function clearSourcepointCookies(): void {
if (!hasSourcepointMarker()) {
return;
}

clearCookie(GPP_COOKIE_NAME);
clearCookie(GPP_SID_COOKIE_NAME);
clearCookie(GPP_SOURCE_COOKIE_NAME);
}

function mirrorOnVisible(): void {
if (document.visibilityState === 'visible') {
mirrorSourcepointConsent();
}
}

function scheduleInitialRetry(): void {
const retry = (): void => {
mirrorSourcepointConsent();
};

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', retry, { once: true });
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkingDOMContentLoaded + 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.

}

window.setTimeout(retry, INITIAL_RETRY_DELAY_MS);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 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);
  // …
}

}

/**
* Reads Sourcepoint consent from localStorage and mirrors it into
* `__gpp` and `__gpp_sid` cookies for Trusted Server to read.
*
* Returns `true` if cookies were written, `false` otherwise.
*/
export function mirrorSourcepointConsent(): boolean {
if (typeof localStorage === 'undefined' || typeof document === 'undefined') {
return false;
}

const payload = findSourcepointConsent();
if (!payload?.gppData) {
clearSourcepointCookies();
log.debug('sourcepoint: no GPP data found in localStorage');
return false;
}

const { gppString, applicableSections } = payload.gppData;
if (!gppString) {
clearSourcepointCookies();
log.debug('sourcepoint: gppString is empty');
return false;
}

writeCookie(GPP_SOURCE_COOKIE_NAME, GPP_SOURCE_SOURCEPOINT);
writeCookie(GPP_COOKIE_NAME, gppString);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 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()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkinghasSourcepointMarker() 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.

clearCookie(GPP_SID_COOKIE_NAME);
}

log.info('sourcepoint: mirrored GPP consent to cookies', {
gppLength: gppString.length,
sections: applicableSections,
});

return true;
}

/**
* Initializes Sourcepoint consent mirroring and bounded refresh hooks.
*/
export function initializeSourcepointConsentMirror(): void {
if (initialized || typeof window === 'undefined' || typeof document === 'undefined') {
return;
}

initialized = true;

if (!mirrorSourcepointConsent()) {
scheduleInitialRetry();
}

// Sourcepoint persists consent changes to localStorage. Re-mirror when a
// user returns to the page so session cookies do not remain stale.
document.addEventListener('visibilitychange', mirrorOnVisible);
window.addEventListener('focus', mirrorSourcepointConsent);
}

initializeSourcepointConsentMirror();
8 changes: 5 additions & 3 deletions crates/js/lib/test/integrations/prebid/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ const {
mockProcessQueue,
mockRequestBids,
mockRegisterBidAdapter,
mockGetUserIdsAsEids,
mockPbjs,
mockGetBidAdapter,
mockGetUserIdsAsEids,
mockAdapterManager,
} = vi.hoisted(() => {
const mockSetConfig = vi.fn();
const mockProcessQueue = vi.fn();
const mockRequestBids = vi.fn();
const mockRegisterBidAdapter = vi.fn();
const mockGetBidAdapter = vi.fn();
const mockGetUserIdsAsEids = vi.fn();
const mockGetUserIdsAsEids = vi.fn(
() => [] as Array<{ source: string; uids?: Array<{ id: string; atype?: number }> }>
);
const mockPbjs = {
setConfig: mockSetConfig,
processQueue: mockProcessQueue,
Expand All @@ -33,9 +35,9 @@ const {
mockProcessQueue,
mockRequestBids,
mockRegisterBidAdapter,
mockGetUserIdsAsEids,
mockPbjs,
mockGetBidAdapter,
mockGetUserIdsAsEids,
mockAdapterManager,
};
});
Expand Down
Loading
Loading