-
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
Open
ChristianPavilonis
wants to merge
28
commits into
feature/edge-cookies-final
Choose a base branch
from
edge-cookie-sourcepoint-consent
base: feature/edge-cookies-final
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
dcac51f
Add EC module with lifecycle management, consent gating, and config m…
ChristianPavilonis 374efc4
Add KV identity graph with CAS concurrency control
ChristianPavilonis 132ec58
Add partner registry and admin registration endpoint
ChristianPavilonis 72e6a11
Fix 8 EC spec deviations identified in branch audit
ChristianPavilonis da70b83
Migrate admin endpoints to /_ts/admin namespace
ChristianPavilonis 0998cd1
Add design spec for Sourcepoint GPP consent support (#640)
ChristianPavilonis a301437
Add implementation plan for Sourcepoint GPP consent support (#640)
ChristianPavilonis 304009f
Add us_sale_opt_out field to GppConsent
ChristianPavilonis f3f16d6
Decode US sale opt-out from GPP sections
ChristianPavilonis 6c0fce0
Recognize GPP US sale opt-out in EC consent gating
ChristianPavilonis e41a21a
Add Sourcepoint JS integration for GPP consent cookie mirroring
ChristianPavilonis fd7fcc7
tmp fix for toml
ChristianPavilonis 0825f06
Add design spec for Prebid User ID Module support
ChristianPavilonis 30f1988
Add implementation plan for Prebid User ID Module support
ChristianPavilonis 76bf59f
Fix ESM path resolution in Prebid User ID plan regression guard
ChristianPavilonis bb6fe99
Add Vitest coverage for Prebid ts-eids cookie sync
ChristianPavilonis 948035d
Bundle Prebid User ID core and submodules in Prebid integration
ChristianPavilonis 770d831
Correct Prebid User ID plan + spec — drop pubCommonIdSystem (removed …
ChristianPavilonis a3ef683
Drop liveIntentIdSystem from Prebid bundle
ChristianPavilonis 7524c3c
revert toml change
ChristianPavilonis f242d4a
Make Prebid User ID submodule set configurable at build time
ChristianPavilonis 22f34d1
Clear stale consent cookies and aggregate US GPP opt-outs
ChristianPavilonis 9e4f097
Add Secure flag and Max-Age to Sourcepoint GPP cookies
ChristianPavilonis 6597543
Revert accidental proxy_secret change in trusted-server.toml
ChristianPavilonis f21dded
Remove orphaned sync_pixel.rs (deleted in base branch)
ChristianPavilonis 1e33bc3
Fix leftover conflict markers from rebase — restore base branch versions
ChristianPavilonis 7518a35
support ec partners map for env overrides
ChristianPavilonis 8a6df3a
Scope Sourcepoint consent PR and address review feedback
ChristianPavilonis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
crates/js/lib/src/integrations/prebid/_user_ids.generated.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // Auto-generated by build-all.mjs — manual edits will be overwritten at build time. | ||
| // | ||
| // Controls which Prebid.js User ID submodules are included in the bundle. | ||
| // Set the TSJS_PREBID_USER_IDS environment variable to a comma-separated | ||
| // list of submodule filenames without the `.js` extension | ||
| // (e.g. "sharedIdSystem,id5IdSystem,criteoIdSystem") before building. | ||
| // The userId.js core module is always included via a static import in | ||
| // index.ts and is not configurable here. | ||
|
|
||
| import 'prebid.js/modules/sharedIdSystem.js'; | ||
| import 'prebid.js/modules/criteoIdSystem.js'; | ||
| import 'prebid.js/modules/33acrossIdSystem.js'; | ||
| import 'prebid.js/modules/pubProvidedIdSystem.js'; | ||
| import 'prebid.js/modules/quantcastIdSystem.js'; | ||
| import 'prebid.js/modules/id5IdSystem.js'; | ||
| import 'prebid.js/modules/identityLinkIdSystem.js'; | ||
| import 'prebid.js/modules/uid2IdSystem.js'; | ||
| import 'prebid.js/modules/euidIdSystem.js'; | ||
| import 'prebid.js/modules/intentIqIdSystem.js'; | ||
| import 'prebid.js/modules/lotamePanoramaIdSystem.js'; | ||
| import 'prebid.js/modules/connectIdSystem.js'; | ||
| import 'prebid.js/modules/merkleIdSystem.js'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { log } from '../../core/log'; | ||
|
|
||
| const SP_CONSENT_PREFIX = '_sp_user_consent_'; | ||
| const GPP_COOKIE_NAME = '__gpp'; | ||
| const GPP_SID_COOKIE_NAME = '__gpp_sid'; | ||
|
|
||
| interface SourcepointGppData { | ||
| gppString: string; | ||
| applicableSections: number[]; | ||
| } | ||
|
|
||
| interface SourcepointConsentPayload { | ||
| gppData?: SourcepointGppData; | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| function writeCookie(name: string, value: string): void { | ||
| document.cookie = `${name}=${value}; path=/; Secure; SameSite=Lax`; | ||
| } | ||
|
|
||
| function clearCookie(name: string): void { | ||
| // Trusted Server is the only intended writer for these mirrored cookies, so | ||
| // clearing the origin-scoped cookie is sufficient for this integration. | ||
| document.cookie = `${name}=; path=/; Secure; SameSite=Lax; Max-Age=0`; | ||
| } | ||
|
ChristianPavilonis marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * 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) { | ||
| clearCookie(GPP_COOKIE_NAME); | ||
| clearCookie(GPP_SID_COOKIE_NAME); | ||
| log.debug('sourcepoint: no GPP data found in localStorage'); | ||
| return false; | ||
| } | ||
|
|
||
| const { gppString, applicableSections } = payload.gppData; | ||
| if (!gppString) { | ||
| clearCookie(GPP_COOKIE_NAME); | ||
| clearCookie(GPP_SID_COOKIE_NAME); | ||
| log.debug('sourcepoint: gppString is empty'); | ||
| return false; | ||
| } | ||
|
|
||
| writeCookie(GPP_COOKIE_NAME, gppString); | ||
|
|
||
| if (Array.isArray(applicableSections) && applicableSections.length > 0) { | ||
| writeCookie(GPP_SID_COOKIE_NAME, applicableSections.join(',')); | ||
| } else { | ||
| clearCookie(GPP_SID_COOKIE_NAME); | ||
| } | ||
|
|
||
| log.info('sourcepoint: mirrored GPP consent to cookies', { | ||
| gppLength: gppString.length, | ||
| sections: applicableSections, | ||
| }); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| if (typeof window !== 'undefined') { | ||
| mirrorSourcepointConsent(); | ||
| } | ||
|
|
||
| export default mirrorSourcepointConsent; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
crates/js/lib/test/integrations/sourcepoint/index.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { mirrorSourcepointConsent } from '../../../src/integrations/sourcepoint'; | ||
|
|
||
| describe('integrations/sourcepoint', () => { | ||
| function clearAllCookies(): void { | ||
| document.cookie.split(';').forEach((c) => { | ||
| const name = c.split('=')[0].trim(); | ||
| if (name) document.cookie = `${name}=; path=/; Max-Age=0`; | ||
| }); | ||
| } | ||
|
|
||
| function getCookie(name: string): string | undefined { | ||
| const match = document.cookie.split('; ').find((c) => c.startsWith(`${name}=`)); | ||
| return match ? match.split('=').slice(1).join('=') : undefined; | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| // Clear cookies and localStorage before each test. | ||
| clearAllCookies(); | ||
| localStorage.clear(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| clearAllCookies(); | ||
| localStorage.clear(); | ||
| }); | ||
|
|
||
| it('mirrors __gpp and __gpp_sid from _sp_user_consent_* localStorage as session cookies', () => { | ||
| const payload = { | ||
| gppData: { | ||
| gppString: 'DBABLA~BVQqAAAAAgA.QA', | ||
| applicableSections: [7], | ||
| }, | ||
| }; | ||
| localStorage.setItem('_sp_user_consent_36026', JSON.stringify(payload)); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(true); | ||
| expect(document.cookie).toContain('__gpp=DBABLA~BVQqAAAAAgA.QA'); | ||
| expect(document.cookie).toContain('__gpp_sid=7'); | ||
| }); | ||
|
|
||
| it('handles multiple applicable sections', () => { | ||
| const payload = { | ||
| gppData: { | ||
| gppString: 'DBABLA~BVQqAAAAAgA.QA', | ||
| applicableSections: [7, 8], | ||
| }, | ||
| }; | ||
| localStorage.setItem('_sp_user_consent_99999', JSON.stringify(payload)); | ||
|
|
||
| mirrorSourcepointConsent(); | ||
|
|
||
| expect(document.cookie).toContain('__gpp_sid=7,8'); | ||
| }); | ||
|
|
||
| it('returns false when no _sp_user_consent_* key exists', () => { | ||
| localStorage.setItem('unrelated_key', 'value'); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(document.cookie).not.toContain('__gpp='); | ||
| expect(document.cookie).not.toContain('__gpp_sid='); | ||
| }); | ||
|
|
||
| it('clears stale mirrored cookies when no valid Sourcepoint payload exists', () => { | ||
| document.cookie = '__gpp=stale-gpp; path=/'; | ||
| document.cookie = '__gpp_sid=7,8; path=/'; | ||
| localStorage.setItem('unrelated_key', 'value'); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(getCookie('__gpp')).toBeUndefined(); | ||
| expect(getCookie('__gpp_sid')).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns false for malformed JSON in localStorage', () => { | ||
| localStorage.setItem('_sp_user_consent_12345', 'not-json!!!'); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(document.cookie).not.toContain('__gpp='); | ||
| }); | ||
|
|
||
| it('skips malformed entries when a later Sourcepoint key is valid', () => { | ||
| localStorage.setItem('_sp_user_consent_12345', 'not-json!!!'); | ||
| localStorage.setItem( | ||
| '_sp_user_consent_67890', | ||
| JSON.stringify({ | ||
| gppData: { | ||
| gppString: 'DBABLA~BVQqAAAAAgA.QA', | ||
| applicableSections: [7], | ||
| }, | ||
| }) | ||
| ); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(true); | ||
| expect(getCookie('__gpp')).toBe('DBABLA~BVQqAAAAAgA.QA'); | ||
| expect(getCookie('__gpp_sid')).toBe('7'); | ||
| }); | ||
|
|
||
| it('returns false when gppData is missing from payload', () => { | ||
| localStorage.setItem('_sp_user_consent_12345', JSON.stringify({ otherField: true })); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(document.cookie).not.toContain('__gpp='); | ||
| }); | ||
|
|
||
| it('returns false when gppString is empty', () => { | ||
| const payload = { | ||
| gppData: { | ||
| gppString: '', | ||
| applicableSections: [7], | ||
| }, | ||
| }; | ||
| localStorage.setItem('_sp_user_consent_12345', JSON.stringify(payload)); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(document.cookie).not.toContain('__gpp='); | ||
| }); | ||
|
|
||
| it('clears stale __gpp_sid when the payload has no applicable sections', () => { | ||
| document.cookie = '__gpp_sid=7,8; path=/'; | ||
| localStorage.setItem( | ||
| '_sp_user_consent_12345', | ||
| JSON.stringify({ | ||
| gppData: { | ||
| gppString: 'DBABLA~BVQqAAAAAgA.QA', | ||
| applicableSections: [], | ||
| }, | ||
| }) | ||
| ); | ||
|
|
||
| const result = mirrorSourcepointConsent(); | ||
|
|
||
| expect(result).toBe(true); | ||
| expect(getCookie('__gpp')).toBe('DBABLA~BVQqAAAAAgA.QA'); | ||
| expect(getCookie('__gpp_sid')).toBeUndefined(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.