-
Notifications
You must be signed in to change notification settings - Fork 8
Support Sourcepoint GPP consent for EC generation #642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/edge-cookies-final
Are you sure you want to change the base?
Changes from all commits
1c14b3d
fca8979
a1bc657
1964872
fbb2457
ad6e790
e025198
6450637
a760c4c
c8abb71
eee99f0
bd366ad
05d1700
3d59d3d
67e55d5
f956e8a
08440a6
8e3ff80
fedcc34
24d4515
a2c6d83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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; | ||
|
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`; | ||
|
ChristianPavilonis marked this conversation as resolved.
|
||
| } | ||
|
|
||
| function clearCookie(name: string): void { | ||
| document.cookie = `${name}=; path=/; Secure; SameSite=Lax; Max-Age=0`; | ||
| } | ||
|
ChristianPavilonis marked this conversation as resolved.
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 }); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — When |
||
| } | ||
|
|
||
| window.setTimeout(retry, INITIAL_RETRY_DELAY_MS); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — Real
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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔧 wrench — Sourcepoint mirror clobbers other CMPs' The 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()) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — This branch only executes after we just wrote |
||
| 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(); | ||
There was a problem hiding this comment.
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-eidscookie. WithuserId.jsunconditionally 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 seedsts-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
getUserIdsAsEidsis correct (i.e. that no User ID Module = no EIDs to forward, so the cookie must not be stale).