From b7f7bbe206cd62bdc056ce4a208fbc8cec736c8d Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Fri, 14 Nov 2025 09:36:27 +0000 Subject: [PATCH 1/8] Permutive RTD Provider: remove deprecated _ppam local storage key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The _ppam local storage key is no longer written by the Permutive SDK and has been deprecated. This change removes all references to _ppam from the RTD provider module and its tests. Changes: - Removed _ppam from AC cohorts aggregation in modules/permutiveRtdProvider.js:326 - Updated test data and expectations in test/spec/modules/permutiveCombined_spec.js Tests passed: 36/36 tests passing Lint: Successful with no errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/permutiveRtdProvider.js | 3 +-- test/spec/modules/permutiveCombined_spec.js | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index bb06d2d138e..96022a4ed73 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -323,10 +323,9 @@ export function getSegments(maxSegs) { .filter((seg) => seg >= 1000000) .map(String), ) || []; - const _ppam = makeSafe(() => readSegments('_ppam', []).map(String)) || []; const _pcrprs = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; - return [..._pcrprs, ..._ppam, ...legacySegs]; + return [..._pcrprs, ...legacySegs]; }) || [], ix: diff --git a/test/spec/modules/permutiveCombined_spec.js b/test/spec/modules/permutiveCombined_spec.js index dbf82d68fee..e581e70c0a9 100644 --- a/test/spec/modules/permutiveCombined_spec.js +++ b/test/spec/modules/permutiveCombined_spec.js @@ -459,7 +459,6 @@ describe('permutiveRtdProvider', function () { it('should merge ortb2 correctly for ac and ssps', function () { const customTargetingData = { ...getTargetingData(), - '_ppam': [], '_psegs': [], '_pcrprs': ['abc', 'def', 'xyz'], '_pssps': { @@ -529,7 +528,6 @@ describe('permutiveRtdProvider', function () { _prubicons: [], _papns: [], _psegs: [], - _ppam: [], _pcrprs: [], _pindexs: [], _pssps: { ssps: [], cohorts: [] }, @@ -782,7 +780,7 @@ function transformedTargeting (data = getTargetingData()) { })() return { - ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)].map(String), + ac: [...data._pcrprs, ...data._psegs.filter(seg => seg >= 1000000)].map(String), appnexus: data._papns.map(String), ix: data._pindexs.map(String), rubicon: data._prubicons.map(String), @@ -801,7 +799,6 @@ function getTargetingData () { _prubicons: ['rubicon1', 'rubicon2'], _papns: ['appnexus1', 'appnexus2'], _psegs: ['1234', '1000001', '1000002'], - _ppam: ['ppam1', 'ppam2'], _pindexs: ['pindex1', 'pindex2'], _pcrprs: ['pcrprs1', 'pcrprs2', 'dup'], _pssps: { ssps: ['xyz', 'abc', 'dup'], cohorts: ['123', 'abc'] }, From 349d70510b92ccb7e635038ee1824bc961ad0b4a Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Fri, 14 Nov 2025 10:01:53 +0000 Subject: [PATCH 2/8] Permutive RTD Provider: improve code clarity with better naming and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit significantly improves the readability and maintainability of the Permutive RTD module by: 1. Adding comprehensive module-level documentation explaining: - All local storage keys and their purposes - Signal types (AC Signals vs SSP Signals) and distribution logic - ORTB2 locations and what data is written where 2. Improving variable names throughout: - `cohorts` -> `signalsForBidder` (eliminates confusing reuse) - `sspCohorts` -> `sspSignals` (clearer business terminology) - `segmentIDs` -> `mergedSignalIds` (describes actual content) - `sspSegmentIDs` -> `sspSignalIds` (consistent terminology) - `customCohortsData` -> `bidderCustomCohorts` (more descriptive) - `legacySegs` -> `standardCohorts` (accurate business term) 3. Adding structural section comments in updateOrtbConfig(): - === ORTB2.USER.DATA[] SETUP === - === ORTB2.USER.KEYWORDS SETUP === - === ORTB2.USER.EXT.DATA SETUP === - === ORTB2.SITE.EXT.PERMUTIVE SETUP === 4. Clarifying ORTB2 provider mappings: - "permutive.com": AC/SSP Signals (Standard + DCR + Curation) - "permutive": Bidder-specific custom cohorts - "permutive.com" with segtax: Privacy Sandbox Topics 5. Adding inline comments explaining cohort types and their sources These changes make the complex signal flow much easier to understand without changing any behavior. Changes in modules/permutiveRtdProvider.js: - Lines 9-62: Added comprehensive module-level documentation - Lines 158-204: Improved setBidderRtb with clearer variable names and comments - Lines 206-339: Enhanced updateOrtbConfig with detailed JSDoc and section headers - Lines 417-503: Clarified getSegments with better variable names and cohort type comments Tests passed: 36/36 tests passing Lint: Successful with no errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/permutiveRtdProvider.js | 221 +++++++++++++++++++++++++------- 1 file changed, 173 insertions(+), 48 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 96022a4ed73..94ce22c7bfe 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -5,6 +5,62 @@ * @module modules/permutiveRtdProvider * @requires module:modules/realTimeData */ + +/** + * LOCAL STORAGE KEYS READ BY THIS MODULE: + * + * Cohort Data: + * - _psegs: Raw Permutive segments (filtered to >= 1000000 for Standard Cohorts) + * - _pcrprs: Data Clean Room (DCR) cohorts from privacy-enhanced partnerships + * - _pssps: { ssps: ['bidder1', ...], cohorts: [...] } - SSP signals and recipient SSP bidder codes + * - _papns: AppNexus/Xandr-specific custom cohorts + * - _prubicons: Rubicon/Magnite-specific custom cohorts + * - _pindexs: Index Exchange-specific custom cohorts + * - _pdfps: Google Ad Manager-specific custom cohorts + * - _ppsts: Privacy Sandbox Topics, keyed by IAB taxonomy version (e.g., { '600': [...], '601': [...] }) + * + * Configuration: + * - permutive-prebid-rtd: Module configuration set by Permutive SDK + * + * SIGNAL TYPES & DISTRIBUTION: + * + * AC Signals (Standard Cohorts + DCR Cohorts): + * - Sent to: AC Bidders (configured via params.acBidders) + * - Source: _psegs (>= 1000000) + _pcrprs + * - Cohort types: Standard Cohorts, DCR Cohorts + * + * SSP Signals (Curation Signals): + * - Sent to: SSP Bidders (list provided in _pssps.ssps) + * - Source: _pssps.cohorts + * - Cohort types: Curated mix of DCR, Standard, and Curated cohorts + * + * Bidders that are BOTH AC and SSP: + * - Receive: AC Signals + SSP Signals (merged and deduped) + * + * Custom Cohorts (bidder-specific): + * - Sent to: Specific bidder only + * - Source: Bidder-specific keys (_papns, _prubicons, _pindexs, _pdfps) + * + * ORTB2 LOCATIONS & SIGNAL MAPPING: + * + * ortb2.user.data[] (array of provider objects): + * - Provider "permutive.com": AC Signals or AC+SSP Signals merged + * - Provider "permutive": Bidder-specific custom cohorts + * - Provider "permutive.com" with segtax: Privacy Sandbox Topics (per taxonomy) + * + * ortb2.user.keywords (comma-separated key=value pairs): + * - p_standard=: AC Signals or AC+SSP Signals merged + * - p_standard_aud=: SSP Signals only + * - permutive=: Bidder-specific custom cohorts + * + * ortb2.user.ext.data (first-party data extensions): + * - p_standard: AC Signals or AC+SSP Signals merged + * - permutive: Bidder-specific custom cohorts + * + * ortb2.site.ext.permutive (site-level extensions): + * - p_standard: AC Signals or AC+SSP Signals merged + */ + import {getGlobal} from '../src/prebidGlobal.js'; import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -104,67 +160,109 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const maxSegs = deepAccess(moduleConfig, 'params.maxSegs') const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] - const ssps = segmentData?.ssp?.ssps ?? [] - const sspCohorts = segmentData?.ssp?.cohorts ?? [] + // AC Signals: Standard Cohorts + DCR Cohorts + const acSignals = segmentData?.ac ?? [] + + // SSP Signals: Curation signals (curated mix of DCR, Standard, and Curated cohorts) + const sspBidderCodes = segmentData?.ssp?.ssps ?? [] + const sspSignals = segmentData?.ssp?.cohorts ?? [] + const topics = segmentData?.topics ?? {} - const bidders = new Set([...acBidders, ...ssps]) + // Process all bidders (union of AC bidders and SSP bidders) + const bidders = new Set([...acBidders, ...sspBidderCodes]) bidders.forEach(function (bidder) { const currConfig = { ortb2: bidderOrtb2[bidder] || {} } - let cohorts = [] - + // Determine which signals this bidder should receive const isAcBidder = acBidders.indexOf(bidder) > -1 + const isSspBidder = sspBidderCodes.indexOf(bidder) > -1 + + let signalsForBidder = [] + if (isAcBidder) { - cohorts = segmentData.ac + // AC Bidders receive AC Signals (Standard + DCR cohorts) + signalsForBidder = acSignals } - const isSspBidder = ssps.indexOf(bidder) > -1 if (isSspBidder) { - cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs) + // SSP Bidders receive SSP Signals (may also include AC Signals if bidder is both AC and SSP) + signalsForBidder = [...new Set([...signalsForBidder, ...sspSignals])].slice(0, maxSegs) } - const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, topics, transformationConfigs, segmentData) + const nextConfig = updateOrtbConfig( + bidder, + currConfig, + signalsForBidder, // Merged signals for this bidder (AC only, SSP only, or AC+SSP) + sspSignals, // SSP Signals (for p_standard_aud keyword) + topics, + transformationConfigs, + segmentData + ) bidderOrtb2[bidder] = nextConfig.ortb2 }) } /** - * Updates `user.data` object in existing bidder config with Permutive segments + * Updates ORTB2 config for a bidder with Permutive cohorts across multiple locations: + * + * ortb2.user.data[] providers: + * - "permutive.com": Contains AC Signals (Standard + DCR cohorts) or AC+SSP Signals merged + * - "permutive": Contains bidder-specific custom cohorts + * - "permutive.com" with segtax: Contains Privacy Sandbox Topics (per taxonomy version) + * + * ortb2.user.keywords: + * - p_standard=: AC Signals or AC+SSP Signals merged + * - p_standard_aud=: SSP Signals only (curation signals) + * - permutive=: Bidder-specific custom cohorts + * + * ortb2.user.ext.data: + * - p_standard: AC Signals or AC+SSP Signals merged + * - permutive: Bidder-specific custom cohorts + * + * ortb2.site.ext.permutive: + * - p_standard: AC Signals or AC+SSP Signals merged + * * @param {string} bidder - The bidder identifier * @param {Object} currConfig - Current bidder config - * @param {string[]} segmentIDs - Permutive segment IDs - * @param {string[]} sspSegmentIDs - Permutive SSP segment IDs + * @param {string[]} mergedSignalIds - Combined signals for this bidder (AC, SSP, or AC+SSP merged) + * @param {string[]} sspSignalIds - SSP Signals (curation signal IDs, used only for p_standard_aud keywords) * @param {Object} topics - Privacy Sandbox Topics, keyed by IAB taxonomy version (600, 601, etc.) - * @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine - * the transformations on user data to include the ORTB2 object - * @param {Object} segmentData - The segments available for targeting - * @return {Object} Merged ortb2 object + * @param {Object[]} transformationConfigs - Array of transformation configs (e.g., IAB taxonomy mappings) + * @param {Object} segmentData - All segment data (includes bidder-specific custom cohorts) + * @return {Object} Updated ortb2 config object */ -function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, transformationConfigs, segmentData) { +function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, topics, transformationConfigs, segmentData) { logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig }) - const customCohortsData = deepAccess(segmentData, bidder) || [] + // Get bidder-specific custom cohorts (e.g., _papns for AppNexus, _prubicons for Rubicon) + const bidderCustomCohorts = deepAccess(segmentData, bidder) || [] const name = 'permutive.com' + // === ORTB2.USER.DATA[] SETUP === + + // 1. "permutive.com" provider: AC Signals or AC+SSP Signals merged + // Contains: Standard Cohorts + DCR Cohorts (+ Curation Signals if bidder is both AC and SSP) const permutiveUserData = { name, - segment: segmentIDs.map(segmentId => ({ id: segmentId })), + segment: mergedSignalIds.map(segmentId => ({ id: segmentId })), } + // 2. Optional IAB taxonomy transformations on AC/SSP signals const transformedUserData = transformationConfigs .filter(({ id }) => ortb2UserDataTransformations.hasOwnProperty(id)) .map(({ id, config }) => ortb2UserDataTransformations[id](permutiveUserData, config)) + // 3. "permutive" provider: Bidder-specific custom cohorts + // Contains: Custom cohorts from bidder-specific local storage keys const customCohortsUserData = { name: PERMUTIVE_CUSTOM_COHORTS_KEYWORD, - segment: customCohortsData.map(cohortID => ({ id: cohortID })), + segment: bidderCustomCohorts.map(cohortID => ({ id: cohortID })), } - const ortbConfig = mergeDeep({}, currConfig) - const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] - + // 4. "permutive.com" provider with segtax: Privacy Sandbox Topics (one entry per taxonomy version) + // Contains: Google Topics API signals const topicsUserData = [] for (const [k, value] of Object.entries(topics)) { topicsUserData.push({ @@ -176,6 +274,9 @@ function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, }) } + // Merge all user.data[] entries, removing old Permutive entries first + const ortbConfig = mergeDeep({}, currConfig) + const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] const updatedUserData = currentUserData .filter(el => el.name !== permutiveUserData.name && el.name !== customCohortsUserData.name) .concat(permutiveUserData, transformedUserData, customCohortsUserData) @@ -184,16 +285,17 @@ function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, logger.logInfo(`Updating ortb2.user.data`, { bidder, user_data: updatedUserData }) deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData) - // Set ortb2.user.keywords + // === ORTB2.USER.KEYWORDS SETUP === + const currentKeywords = deepAccess(ortbConfig, 'ortb2.user.keywords') const keywordGroups = { - [PERMUTIVE_STANDARD_KEYWORD]: segmentIDs, - [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSegmentIDs, - [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: customCohortsData, + [PERMUTIVE_STANDARD_KEYWORD]: mergedSignalIds, // p_standard: AC Signals or AC+SSP Signals merged + [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSignalIds, // p_standard_aud: SSP Signals only + [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: bidderCustomCohorts, // permutive: Bidder-specific custom cohorts } // Transform groups of key-values into a single array of strings - // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3',' p_standard=4'] + // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3', 'p_standard=4'] const transformedKeywordGroups = Object.entries(keywordGroups) .flatMap(([keyword, ids]) => ids.map(id => `${keyword}=${id}`)) @@ -210,21 +312,26 @@ function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, topics, }) deepSetValue(ortbConfig, 'ortb2.user.keywords', keywords) - // Set user extensions - if (segmentIDs.length > 0) { - deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, segmentIDs) - logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, segmentIDs) + // === ORTB2.USER.EXT.DATA SETUP === + + // Set p_standard: AC Signals or AC+SSP Signals merged + if (mergedSignalIds.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignalIds) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignalIds) } - if (customCohortsData.length > 0) { - deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}`, customCohortsData.map(String)) - logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, customCohortsData) + // Set permutive: Bidder-specific custom cohorts + if (bidderCustomCohorts.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}`, bidderCustomCohorts.map(String)) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, bidderCustomCohorts) } - // Set site extensions - if (segmentIDs.length > 0) { - deepSetValue(ortbConfig, `ortb2.site.ext.permutive.${PERMUTIVE_STANDARD_KEYWORD}`, segmentIDs) - logger.logInfo(`Extending ortb2.site.ext.permutive with "${PERMUTIVE_STANDARD_KEYWORD}"`, segmentIDs) + // === ORTB2.SITE.EXT.PERMUTIVE SETUP === + + // Set p_standard: AC Signals or AC+SSP Signals merged at site level + if (mergedSignalIds.length > 0) { + deepSetValue(ortbConfig, `ortb2.site.ext.permutive.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignalIds) + logger.logInfo(`Extending ortb2.site.ext.permutive with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignalIds) } logger.logInfo(`Updated ortb2 config`, { bidder, config: ortbConfig }) @@ -308,54 +415,70 @@ export function isPermutiveOnPage () { } /** - * Get all relevant segment IDs in an object - * @param {number} maxSegs - Maximum number of segments to be included - * @return {Object} + * Reads cohort data from local storage keys written by the Permutive SDK. + * Returns segment data organized by signal type (AC, SSP) and bidder-specific custom cohorts. + * + * @param {number} maxSegs - Maximum number of segments per cohort type + * @return {Object} Segment data with AC signals, SSP signals, custom cohorts, and topics */ export function getSegments(maxSegs) { const segments = { + // AC Signals: Standard Cohorts + DCR Cohorts + // Sent to AC Bidders via p_standard keyword and ortb2.user.data "permutive.com" provider ac: makeSafe(() => { - const legacySegs = + // Standard Cohorts: Permutive's core audience segments (_psegs >= 1000000) + const standardCohorts = makeSafe(() => readSegments('_psegs', []) .map(Number) - .filter((seg) => seg >= 1000000) + .filter((seg) => seg >= 1000000) // Filter to only Standard Cohorts .map(String), ) || []; - const _pcrprs = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; - return [..._pcrprs, ...legacySegs]; + // DCR Cohorts: Data Clean Room cohorts from privacy-enhanced partnerships (_pcrprs) + const dcrCohorts = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; + + return [...dcrCohorts, ...standardCohorts]; }) || [], + // Bidder-specific custom cohorts (sent via "permutive" provider in ortb2.user.data) + + // Index Exchange custom cohorts (_pindexs) ix: makeSafe(() => { const _pindexs = readSegments('_pindexs', []); return _pindexs.map(String); }) || [], + // Rubicon/Magnite custom cohorts (_prubicons) rubicon: makeSafe(() => { const _prubicons = readSegments('_prubicons', []); return _prubicons.map(String); }) || [], + // AppNexus/Xandr custom cohorts (_papns) appnexus: makeSafe(() => { const _papns = readSegments('_papns', []); return _papns.map(String); }) || [], + // Google Ad Manager custom cohorts (_pdfps) gam: makeSafe(() => { const _pdfps = readSegments('_pdfps', []); return _pdfps.map(String); }) || [], + // SSP Signals: Curation signals (curated mix of DCR, Standard, and Curated cohorts) + // Sent to SSP Bidders via p_standard_aud keyword + // Includes both the signal IDs and the list of SSP bidder codes that should receive them ssp: makeSafe(() => { const _pssps = readSegments('_pssps', { - cohorts: [], - ssps: [], + cohorts: [], // SSP Signal IDs (curation signals) + ssps: [], // SSP bidder codes }); return { @@ -364,6 +487,8 @@ export function getSegments(maxSegs) { }; }), + // Privacy Sandbox Topics: Google Topics API signals, keyed by IAB taxonomy version + // Sent to all bidders via "permutive.com" provider with segtax in ortb2.user.data topics: makeSafe(() => { const _ppsts = readSegments('_ppsts', {}); From 2ce8f4963c6b0edf69f11206b9ccdae34f1c488b Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Fri, 14 Nov 2025 11:07:34 +0000 Subject: [PATCH 3/8] Permutive RTD Provider: Implement unified custom cohorts model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit simplifies the custom cohorts mechanism by moving from bidder-specific custom cohorts to a unified model where all configured bidders receive the same merged custom cohort list. Changes to modules/permutiveRtdProvider.js: - Added comprehensive module-level documentation mapping local storage keys to ORTB2 locations and explaining signal types (lines 9-64) - Added ccBidders config parameter for custom cohort target bidders (line 148) - Updated getSegments() to return unified customCohorts array by merging _pprebid + legacy keys (_papns, _prubicons, _pindexs, _pdfps) (lines 463-486) - Updated setBidderRtb() to send custom cohorts to bidders in ccBidders config + hardcoded legacy bidders (ix, rubicon, appnexus, gam) (lines 175-186) - Updated updateOrtbConfig() to accept customCohorts array parameter instead of bidder-specific cohorts (line 253) - Added maxSegs limiting to all segment types: AC signals (line 460), SSP cohorts (line 498), and Topics (line 511) - Improved variable names for clarity (cohorts → signalsForBidder, sspCohorts → sspSignals, etc.) Changes to test/spec/modules/permutiveCombined_spec.js: - Added ccBidders: [] to default config (line 51) - Updated getConfig() to set ccBidders matching acBidders so tests receive custom cohorts as expected (line 769) - Updated transformedTargeting() to return customCohorts array instead of individual bidder properties (lines 784-796) - Updated all test references from segmentsData[bidder] to segmentsData.customCohorts (lines 204, 271, 395, 447, 560, 600) - Updated "Getting segments" tests to check segments.customCohorts instead of segments.rubicon (lines 647, 664) - Cleared default custom cohort data in tests that set specific values to avoid merging with default data (lines 636-643, 653-660) Backwards compatibility: - Legacy local storage keys (_papns, _prubicons, _pindexs, _pdfps) are still read and merged into unified custom cohorts list - Legacy bidders (ix, rubicon, appnexus, gam) automatically receive custom cohorts without needing to be in ccBidders config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/permutiveRtdProvider.js | 133 ++++++++++---------- test/spec/modules/permutiveCombined_spec.js | 57 ++++++--- 2 files changed, 105 insertions(+), 85 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 94ce22c7bfe..cca3962c099 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -13,10 +13,11 @@ * - _psegs: Raw Permutive segments (filtered to >= 1000000 for Standard Cohorts) * - _pcrprs: Data Clean Room (DCR) cohorts from privacy-enhanced partnerships * - _pssps: { ssps: ['bidder1', ...], cohorts: [...] } - SSP signals and recipient SSP bidder codes - * - _papns: AppNexus/Xandr-specific custom cohorts - * - _prubicons: Rubicon/Magnite-specific custom cohorts - * - _pindexs: Index Exchange-specific custom cohorts - * - _pdfps: Google Ad Manager-specific custom cohorts + * - _pprebid: Custom cohorts (unified key) + * - _papns: AppNexus/Xandr-specific custom cohorts (LEGACY - merged into unified list) + * - _prubicons: Rubicon/Magnite-specific custom cohorts (LEGACY - merged into unified list) + * - _pindexs: Index Exchange-specific custom cohorts (LEGACY - merged into unified list) + * - _pdfps: Google Ad Manager-specific custom cohorts (LEGACY - merged into unified list) * - _ppsts: Privacy Sandbox Topics, keyed by IAB taxonomy version (e.g., { '600': [...], '601': [...] }) * * Configuration: @@ -37,25 +38,26 @@ * Bidders that are BOTH AC and SSP: * - Receive: AC Signals + SSP Signals (merged and deduped) * - * Custom Cohorts (bidder-specific): - * - Sent to: Specific bidder only - * - Source: Bidder-specific keys (_papns, _prubicons, _pindexs, _pdfps) + * Custom Cohorts (Unified Model): + * - Sent to: Bidders in params.ccBidders + legacy bidders (ix, rubicon, appnexus, gam) + * - Source: _pprebid + legacy keys (_papns, _prubicons, _pindexs, _pdfps) merged + * - Distribution: All target bidders receive the same unified list * * ORTB2 LOCATIONS & SIGNAL MAPPING: * * ortb2.user.data[] (array of provider objects): * - Provider "permutive.com": AC Signals or AC+SSP Signals merged - * - Provider "permutive": Bidder-specific custom cohorts + * - Provider "permutive": Custom cohorts (unified list) * - Provider "permutive.com" with segtax: Privacy Sandbox Topics (per taxonomy) * * ortb2.user.keywords (comma-separated key=value pairs): * - p_standard=: AC Signals or AC+SSP Signals merged * - p_standard_aud=: SSP Signals only - * - permutive=: Bidder-specific custom cohorts + * - permutive=: Custom cohorts (unified list) * * ortb2.user.ext.data (first-party data extensions): * - p_standard: AC Signals or AC+SSP Signals merged - * - permutive: Bidder-specific custom cohorts + * - permutive: Custom cohorts (unified list) * * ortb2.site.ext.permutive (site-level extensions): * - p_standard: AC Signals or AC+SSP Signals merged @@ -141,6 +143,7 @@ export function getModuleConfig(customModuleConfig) { params: { maxSegs: 500, acBidders: [], + ccBidders: [], overwrites: {}, }, }, @@ -169,14 +172,26 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const topics = segmentData?.topics ?? {} - // Process all bidders (union of AC bidders and SSP bidders) + // Custom Cohorts: Unified list + const customCohorts = segmentData?.customCohorts ?? [] + + // Determine which bidders should receive custom cohorts + // Combine configured ccBidders + hardcoded legacy bidders + const ccBidders = deepAccess(moduleConfig, 'params.ccBidders') || [] + const legacyCustomCohortBidders = ['ix', 'rubicon', 'appnexus', 'gam'] + const customCohortTargetBidders = new Set([...ccBidders, ...legacyCustomCohortBidders]) + + // Process all bidders (union of AC bidders, SSP bidders, and custom cohort bidders) const bidders = new Set([...acBidders, ...sspBidderCodes]) + customCohortTargetBidders.forEach(bidder => bidders.add(bidder)) + bidders.forEach(function (bidder) { const currConfig = { ortb2: bidderOrtb2[bidder] || {} } // Determine which signals this bidder should receive const isAcBidder = acBidders.indexOf(bidder) > -1 const isSspBidder = sspBidderCodes.indexOf(bidder) > -1 + const isCustomCohortBidder = customCohortTargetBidders.has(bidder) let signalsForBidder = [] @@ -190,14 +205,17 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { signalsForBidder = [...new Set([...signalsForBidder, ...sspSignals])].slice(0, maxSegs) } + // Custom cohorts for this bidder (empty array if not a custom cohort target bidder) + const customCohortsForBidder = isCustomCohortBidder ? customCohorts : [] + const nextConfig = updateOrtbConfig( bidder, currConfig, - signalsForBidder, // Merged signals for this bidder (AC only, SSP only, or AC+SSP) - sspSignals, // SSP Signals (for p_standard_aud keyword) + signalsForBidder, // Merged signals for this bidder (AC only, SSP only, or AC+SSP) + sspSignals, // SSP Signals (for p_standard_aud keyword) topics, transformationConfigs, - segmentData + customCohortsForBidder // Custom cohorts (unified list or empty) ) bidderOrtb2[bidder] = nextConfig.ortb2 }) @@ -208,17 +226,17 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { * * ortb2.user.data[] providers: * - "permutive.com": Contains AC Signals (Standard + DCR cohorts) or AC+SSP Signals merged - * - "permutive": Contains bidder-specific custom cohorts + * - "permutive": Contains custom cohorts (unified list) * - "permutive.com" with segtax: Contains Privacy Sandbox Topics (per taxonomy version) * * ortb2.user.keywords: * - p_standard=: AC Signals or AC+SSP Signals merged * - p_standard_aud=: SSP Signals only (curation signals) - * - permutive=: Bidder-specific custom cohorts + * - permutive=: Custom cohorts (unified list) * * ortb2.user.ext.data: * - p_standard: AC Signals or AC+SSP Signals merged - * - permutive: Bidder-specific custom cohorts + * - permutive: Custom cohorts (unified list) * * ortb2.site.ext.permutive: * - p_standard: AC Signals or AC+SSP Signals merged @@ -229,14 +247,14 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { * @param {string[]} sspSignalIds - SSP Signals (curation signal IDs, used only for p_standard_aud keywords) * @param {Object} topics - Privacy Sandbox Topics, keyed by IAB taxonomy version (600, 601, etc.) * @param {Object[]} transformationConfigs - Array of transformation configs (e.g., IAB taxonomy mappings) - * @param {Object} segmentData - All segment data (includes bidder-specific custom cohorts) + * @param {string[]} customCohorts - Custom cohorts for this bidder (unified list from _pprebid + legacy keys) * @return {Object} Updated ortb2 config object */ -function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, topics, transformationConfigs, segmentData) { +function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, topics, transformationConfigs, customCohorts) { logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig }) - // Get bidder-specific custom cohorts (e.g., _papns for AppNexus, _prubicons for Rubicon) - const bidderCustomCohorts = deepAccess(segmentData, bidder) || [] + // Custom cohorts passed directly (unified list for all configured bidders) + const bidderCustomCohorts = customCohorts const name = 'permutive.com' @@ -416,10 +434,10 @@ export function isPermutiveOnPage () { /** * Reads cohort data from local storage keys written by the Permutive SDK. - * Returns segment data organized by signal type (AC, SSP) and bidder-specific custom cohorts. + * Returns segment data organized by signal type (AC, SSP, custom cohorts) and topics. * * @param {number} maxSegs - Maximum number of segments per cohort type - * @return {Object} Segment data with AC signals, SSP signals, custom cohorts, and topics + * @return {Object} Segment data with AC signals, SSP signals, unified custom cohorts, and topics */ export function getSegments(maxSegs) { const segments = { @@ -439,37 +457,32 @@ export function getSegments(maxSegs) { // DCR Cohorts: Data Clean Room cohorts from privacy-enhanced partnerships (_pcrprs) const dcrCohorts = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; - return [...dcrCohorts, ...standardCohorts]; - }) || [], - - // Bidder-specific custom cohorts (sent via "permutive" provider in ortb2.user.data) - - // Index Exchange custom cohorts (_pindexs) - ix: - makeSafe(() => { - const _pindexs = readSegments('_pindexs', []); - return _pindexs.map(String); + return [...dcrCohorts, ...standardCohorts].slice(0, maxSegs); }) || [], - // Rubicon/Magnite custom cohorts (_prubicons) - rubicon: + // Custom Cohorts: Unified list from new key + all legacy keys merged + // Sent to bidders in ccBidders config + legacy bidders (ix, rubicon, appnexus, gam) + customCohorts: makeSafe(() => { - const _prubicons = readSegments('_prubicons', []); - return _prubicons.map(String); - }) || [], - - // AppNexus/Xandr custom cohorts (_papns) - appnexus: - makeSafe(() => { - const _papns = readSegments('_papns', []); - return _papns.map(String); - }) || [], - - // Google Ad Manager custom cohorts (_pdfps) - gam: - makeSafe(() => { - const _pdfps = readSegments('_pdfps', []); - return _pdfps.map(String); + // Read new unified custom cohorts key (_pprebid) + const unifiedCustomCohorts = makeSafe(() => + readSegments('_pprebid', []).map(String) + ) || []; + + // Read legacy bidder-specific keys for backwards compatibility + const legacyAppnexus = makeSafe(() => readSegments('_papns', []).map(String)) || []; + const legacyRubicon = makeSafe(() => readSegments('_prubicons', []).map(String)) || []; + const legacyIndex = makeSafe(() => readSegments('_pindexs', []).map(String)) || []; + const legacyGam = makeSafe(() => readSegments('_pdfps', []).map(String)) || []; + + // Merge and deduplicate all custom cohorts into a single unified list + return [...new Set([ + ...unifiedCustomCohorts, + ...legacyAppnexus, + ...legacyRubicon, + ...legacyIndex, + ...legacyGam + ])].slice(0, maxSegs); }) || [], // SSP Signals: Curation signals (curated mix of DCR, Standard, and Curated cohorts) @@ -482,7 +495,7 @@ export function getSegments(maxSegs) { }); return { - cohorts: makeSafe(() => _pssps.cohorts.map(String)) || [], + cohorts: (makeSafe(() => _pssps.cohorts.map(String)) || []).slice(0, maxSegs), ssps: makeSafe(() => _pssps.ssps.map(String)) || [], }; }), @@ -495,27 +508,13 @@ export function getSegments(maxSegs) { const topics = {}; for (const [k, value] of Object.entries(_ppsts)) { - topics[k] = makeSafe(() => value.map(String)) || []; + topics[k] = (makeSafe(() => value.map(String)) || []).slice(0, maxSegs); } return topics; }) || {}, }; - for (const bidder in segments) { - if (bidder === 'ssp') { - if (segments[bidder].cohorts && Array.isArray(segments[bidder].cohorts)) { - segments[bidder].cohorts = segments[bidder].cohorts.slice(0, maxSegs) - } - } else if (bidder === 'topics') { - for (const taxonomy in segments[bidder]) { - segments[bidder][taxonomy] = segments[bidder][taxonomy].slice(0, maxSegs) - } - } else { - segments[bidder] = segments[bidder].slice(0, maxSegs) - } - } - logger.logInfo(`Read segments`, segments) return segments; } diff --git a/test/spec/modules/permutiveCombined_spec.js b/test/spec/modules/permutiveCombined_spec.js index e581e70c0a9..eef3e8ef371 100644 --- a/test/spec/modules/permutiveCombined_spec.js +++ b/test/spec/modules/permutiveCombined_spec.js @@ -48,6 +48,7 @@ describe('permutiveRtdProvider', function () { params: { maxSegs: 500, acBidders: [], + ccBidders: [], overwrites: {}, }, }) @@ -200,7 +201,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] expect(bidderConfig[bidder].user.data).to.deep.include.members([ { name: 'permutive.com', @@ -267,7 +268,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] expect(bidderConfig[bidder].user.data).to.not.deep.include.members([...sampleOrtbConfig.user.data]) expect(bidderConfig[bidder].user.data).to.deep.include.members([ @@ -391,7 +392,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohortsData = segmentsData[bidder] || [] + const customCohortsData = segmentsData.customCohorts || [] const keywordGroups = { [PERMUTIVE_STANDARD_KEYWORD]: segmentsData.ac, [PERMUTIVE_STANDARD_AUD_KEYWORD]: segmentsData.ssp.cohorts, @@ -443,7 +444,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - const customCohortsData = segmentsData[bidder] || [] + const customCohortsData = segmentsData.customCohorts || [] const expectedKeywords = [ ...existingKeywords, @@ -556,7 +557,7 @@ describe('permutiveRtdProvider', function () { p_standard: segmentsData.ac, } - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] if (customCohorts.length > 0) { deepSetValue(userExtData, 'permutive', customCohorts) } @@ -572,10 +573,8 @@ describe('permutiveRtdProvider', function () { const bidderConfig = {} const segmentsData = transformedTargeting() - moduleConfig.params.acBidders.forEach((bidder) => { - // Remove custom cohorts - delete segmentsData[bidder] - }) + // Remove custom cohorts by setting to empty array + segmentsData.customCohorts = [] setBidderRtb(bidderConfig, moduleConfig, segmentsData) @@ -598,7 +597,7 @@ describe('permutiveRtdProvider', function () { setBidderRtb(bidderConfig, moduleConfig, segmentsData) moduleConfig.params.acBidders.forEach(bidder => { - const customCohorts = segmentsData[bidder] || [] + const customCohorts = segmentsData.customCohorts || [] if (customCohorts.length > 0) { expect(bidderConfig[bidder].user.ext.data).to.deep .eq({ permutive: customCohorts }) @@ -634,21 +633,35 @@ describe('permutiveRtdProvider', function () { }) it('should coerce numbers to strings', function () { - setLocalStorage({ _prubicons: [1, 2, 3], _pssps: { ssps: ['foo', 'bar'], cohorts: [4, 5, 6] } }) + setLocalStorage({ + _prubicons: [1, 2, 3], + _pssps: { ssps: ['foo', 'bar'], cohorts: [4, 5, 6] }, + // Clear other custom cohort keys + _pdfps: [], + _papns: [], + _pindexs: [] + }) const segments = getSegments(200) - expect(segments.rubicon).to.deep.equal(['1', '2', '3']) + expect(segments.customCohorts).to.deep.equal(['1', '2', '3']) expect(segments.ssp.ssps).to.deep.equal(['foo', 'bar']) expect(segments.ssp.cohorts).to.deep.equal(['4', '5', '6']) }) it('should return empty values on unexpected format', function () { - setLocalStorage({ _prubicons: 'a string instead?', _pssps: 123 }) + setLocalStorage({ + _prubicons: 'a string instead?', + _pssps: 123, + // Clear other custom cohort keys + _pdfps: [], + _papns: [], + _pindexs: [] + }) const segments = getSegments(200) - expect(segments.rubicon).to.deep.equal([]) + expect(segments.customCohorts).to.deep.equal([]) expect(segments.ssp.ssps).to.deep.equal([]) expect(segments.ssp.cohorts).to.deep.equal([]) }) @@ -765,6 +778,7 @@ function getConfig () { waitForIt: true, params: { acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], + ccBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], // Same as acBidders so tests receive custom cohorts maxSegs: 500 } } @@ -779,12 +793,19 @@ function transformedTargeting (data = getTargetingData()) { return topics })() + // Merge all custom cohorts (from _pprebid + legacy keys) + const pprebid = data._pprebid || [] + const customCohorts = [...new Set([ + ...pprebid, + ...data._papns, + ...data._prubicons, + ...data._pindexs, + ...data._pdfps + ])].map(String) + return { ac: [...data._pcrprs, ...data._psegs.filter(seg => seg >= 1000000)].map(String), - appnexus: data._papns.map(String), - ix: data._pindexs.map(String), - rubicon: data._prubicons.map(String), - gam: data._pdfps.map(String), + customCohorts: customCohorts, ssp: { ssps: data._pssps.ssps.map(String), cohorts: data._pssps.cohorts.map(String) From 0722e9d1de84a63ae3d1d89d5a1ab95bc8995b0a Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Fri, 14 Nov 2025 11:32:08 +0000 Subject: [PATCH 4/8] Permutive RTD Provider: Improve code consistency and update documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code consistency improvements: - Standardized variable names: sspBidderCodes → sspBidders, legacyCustomCohortBidders → legacyCcBidders - Standardized signal variable naming: customCohortsForBidder → ccSignals, bidderCustomCohorts → ccSignals - Simplified bidder set construction to single line (line 140) - Renamed customCohortsUserData → ccUserData for consistency - Consistent use of isCcBidder instead of isCustomCohortBidder - Added Topics to signal types in module documentation Documentation updates (permutiveRtdProvider.md): - Added params.ccBidders parameter to Parameters table - Updated Custom Cohorts section with: - Example configuration showing ccBidders usage - List of legacy bidders that automatically receive custom cohorts - Explanation of _pprebid local storage key - Clearer description of unified custom cohorts model Changes maintain full backwards compatibility. All tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/permutiveRtdProvider.js | 216 +++++++++----------------------- modules/permutiveRtdProvider.md | 30 +++-- 2 files changed, 79 insertions(+), 167 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index cca3962c099..a7ee7b6f011 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -7,60 +7,25 @@ */ /** - * LOCAL STORAGE KEYS READ BY THIS MODULE: - * - * Cohort Data: + * LOCAL STORAGE KEYS: * - _psegs: Raw Permutive segments (filtered to >= 1000000 for Standard Cohorts) - * - _pcrprs: Data Clean Room (DCR) cohorts from privacy-enhanced partnerships - * - _pssps: { ssps: ['bidder1', ...], cohorts: [...] } - SSP signals and recipient SSP bidder codes - * - _pprebid: Custom cohorts (unified key) - * - _papns: AppNexus/Xandr-specific custom cohorts (LEGACY - merged into unified list) - * - _prubicons: Rubicon/Magnite-specific custom cohorts (LEGACY - merged into unified list) - * - _pindexs: Index Exchange-specific custom cohorts (LEGACY - merged into unified list) - * - _pdfps: Google Ad Manager-specific custom cohorts (LEGACY - merged into unified list) - * - _ppsts: Privacy Sandbox Topics, keyed by IAB taxonomy version (e.g., { '600': [...], '601': [...] }) - * - * Configuration: - * - permutive-prebid-rtd: Module configuration set by Permutive SDK - * - * SIGNAL TYPES & DISTRIBUTION: - * - * AC Signals (Standard Cohorts + DCR Cohorts): - * - Sent to: AC Bidders (configured via params.acBidders) - * - Source: _psegs (>= 1000000) + _pcrprs - * - Cohort types: Standard Cohorts, DCR Cohorts - * - * SSP Signals (Curation Signals): - * - Sent to: SSP Bidders (list provided in _pssps.ssps) - * - Source: _pssps.cohorts - * - Cohort types: Curated mix of DCR, Standard, and Curated cohorts - * - * Bidders that are BOTH AC and SSP: - * - Receive: AC Signals + SSP Signals (merged and deduped) + * - _pcrprs: Data Clean Room (DCR) cohorts + * - _pssps: { ssps: [...], cohorts: [...] } - SSP signals and bidder codes + * - _pprebid: Custom cohorts + * - _papns, _prubicons, _pindexs, _pdfps: Legacy custom cohorts (merged with _pprebid) + * - _ppsts: Privacy Sandbox Topics by taxonomy version * - * Custom Cohorts (Unified Model): - * - Sent to: Bidders in params.ccBidders + legacy bidders (ix, rubicon, appnexus, gam) - * - Source: _pprebid + legacy keys (_papns, _prubicons, _pindexs, _pdfps) merged - * - Distribution: All target bidders receive the same unified list + * SIGNAL TYPES: + * - AC Signals: Standard Cohorts + DCR Cohorts → AC bidders (params.acBidders) + * - SSP Signals: Curation signals → SSP bidders (from _pssps.ssps) + * - CC Signals: Custom cohorts → CC bidders (params.ccBidders) + legacy bidders (ix, rubicon, appnexus, gam) + * - Topics: Privacy Sandbox Topics → All bidders * - * ORTB2 LOCATIONS & SIGNAL MAPPING: - * - * ortb2.user.data[] (array of provider objects): - * - Provider "permutive.com": AC Signals or AC+SSP Signals merged - * - Provider "permutive": Custom cohorts (unified list) - * - Provider "permutive.com" with segtax: Privacy Sandbox Topics (per taxonomy) - * - * ortb2.user.keywords (comma-separated key=value pairs): - * - p_standard=: AC Signals or AC+SSP Signals merged - * - p_standard_aud=: SSP Signals only - * - permutive=: Custom cohorts (unified list) - * - * ortb2.user.ext.data (first-party data extensions): - * - p_standard: AC Signals or AC+SSP Signals merged - * - permutive: Custom cohorts (unified list) - * - * ortb2.site.ext.permutive (site-level extensions): - * - p_standard: AC Signals or AC+SSP Signals merged + * ORTB2 LOCATIONS: + * - ortb2.user.data "permutive.com": AC/SSP Signals + * - ortb2.user.data "permutive": CC Signals + * - ortb2.user.keywords: p_standard, p_standard_aud, permutive + * - ortb2.user.ext.data: p_standard, permutive */ import {getGlobal} from '../src/prebidGlobal.js'; @@ -163,124 +128,79 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const maxSegs = deepAccess(moduleConfig, 'params.maxSegs') const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] - // AC Signals: Standard Cohorts + DCR Cohorts const acSignals = segmentData?.ac ?? [] - - // SSP Signals: Curation signals (curated mix of DCR, Standard, and Curated cohorts) - const sspBidderCodes = segmentData?.ssp?.ssps ?? [] + const sspBidders = segmentData?.ssp?.ssps ?? [] const sspSignals = segmentData?.ssp?.cohorts ?? [] - const topics = segmentData?.topics ?? {} - - // Custom Cohorts: Unified list const customCohorts = segmentData?.customCohorts ?? [] - // Determine which bidders should receive custom cohorts - // Combine configured ccBidders + hardcoded legacy bidders const ccBidders = deepAccess(moduleConfig, 'params.ccBidders') || [] - const legacyCustomCohortBidders = ['ix', 'rubicon', 'appnexus', 'gam'] - const customCohortTargetBidders = new Set([...ccBidders, ...legacyCustomCohortBidders]) + const legacyCcBidders = ['ix', 'rubicon', 'appnexus', 'gam'] - // Process all bidders (union of AC bidders, SSP bidders, and custom cohort bidders) - const bidders = new Set([...acBidders, ...sspBidderCodes]) - customCohortTargetBidders.forEach(bidder => bidders.add(bidder)) + const bidders = new Set([...acBidders, ...sspBidders, ...ccBidders, ...legacyCcBidders]) bidders.forEach(function (bidder) { const currConfig = { ortb2: bidderOrtb2[bidder] || {} } - // Determine which signals this bidder should receive const isAcBidder = acBidders.indexOf(bidder) > -1 - const isSspBidder = sspBidderCodes.indexOf(bidder) > -1 - const isCustomCohortBidder = customCohortTargetBidders.has(bidder) + const isSspBidder = sspBidders.indexOf(bidder) > -1 + const isCcBidder = ccBidders.indexOf(bidder) > -1 || legacyCcBidders.indexOf(bidder) > -1 let signalsForBidder = [] if (isAcBidder) { - // AC Bidders receive AC Signals (Standard + DCR cohorts) signalsForBidder = acSignals } if (isSspBidder) { - // SSP Bidders receive SSP Signals (may also include AC Signals if bidder is both AC and SSP) signalsForBidder = [...new Set([...signalsForBidder, ...sspSignals])].slice(0, maxSegs) } - // Custom cohorts for this bidder (empty array if not a custom cohort target bidder) - const customCohortsForBidder = isCustomCohortBidder ? customCohorts : [] + const ccSignals = isCcBidder ? customCohorts : [] const nextConfig = updateOrtbConfig( bidder, currConfig, - signalsForBidder, // Merged signals for this bidder (AC only, SSP only, or AC+SSP) - sspSignals, // SSP Signals (for p_standard_aud keyword) + signalsForBidder, + sspSignals, topics, transformationConfigs, - customCohortsForBidder // Custom cohorts (unified list or empty) + ccSignals ) bidderOrtb2[bidder] = nextConfig.ortb2 }) } /** - * Updates ORTB2 config for a bidder with Permutive cohorts across multiple locations: - * - * ortb2.user.data[] providers: - * - "permutive.com": Contains AC Signals (Standard + DCR cohorts) or AC+SSP Signals merged - * - "permutive": Contains custom cohorts (unified list) - * - "permutive.com" with segtax: Contains Privacy Sandbox Topics (per taxonomy version) - * - * ortb2.user.keywords: - * - p_standard=: AC Signals or AC+SSP Signals merged - * - p_standard_aud=: SSP Signals only (curation signals) - * - permutive=: Custom cohorts (unified list) - * - * ortb2.user.ext.data: - * - p_standard: AC Signals or AC+SSP Signals merged - * - permutive: Custom cohorts (unified list) - * - * ortb2.site.ext.permutive: - * - p_standard: AC Signals or AC+SSP Signals merged - * - * @param {string} bidder - The bidder identifier - * @param {Object} currConfig - Current bidder config - * @param {string[]} mergedSignalIds - Combined signals for this bidder (AC, SSP, or AC+SSP merged) - * @param {string[]} sspSignalIds - SSP Signals (curation signal IDs, used only for p_standard_aud keywords) - * @param {Object} topics - Privacy Sandbox Topics, keyed by IAB taxonomy version (600, 601, etc.) - * @param {Object[]} transformationConfigs - Array of transformation configs (e.g., IAB taxonomy mappings) - * @param {string[]} customCohorts - Custom cohorts for this bidder (unified list from _pprebid + legacy keys) - * @return {Object} Updated ortb2 config object + * Updates ORTB2 config for a bidder with Permutive signals + * @param {string} bidder + * @param {Object} currConfig + * @param {string[]} mergedSignalIds - AC/SSP Signals for this bidder + * @param {string[]} sspSignalIds - SSP Signals (for p_standard_aud) + * @param {Object} topics - Privacy Sandbox Topics + * @param {Object[]} transformationConfigs - IAB taxonomy transformations + * @param {string[]} ccSignals - CC Signals for this bidder + * @return {Object} Updated ortb2 config */ -function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, topics, transformationConfigs, customCohorts) { +function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, topics, transformationConfigs, ccSignals) { logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig }) - // Custom cohorts passed directly (unified list for all configured bidders) - const bidderCustomCohorts = customCohorts - const name = 'permutive.com' - // === ORTB2.USER.DATA[] SETUP === - - // 1. "permutive.com" provider: AC Signals or AC+SSP Signals merged - // Contains: Standard Cohorts + DCR Cohorts (+ Curation Signals if bidder is both AC and SSP) const permutiveUserData = { name, segment: mergedSignalIds.map(segmentId => ({ id: segmentId })), } - // 2. Optional IAB taxonomy transformations on AC/SSP signals const transformedUserData = transformationConfigs .filter(({ id }) => ortb2UserDataTransformations.hasOwnProperty(id)) .map(({ id, config }) => ortb2UserDataTransformations[id](permutiveUserData, config)) - // 3. "permutive" provider: Bidder-specific custom cohorts - // Contains: Custom cohorts from bidder-specific local storage keys - const customCohortsUserData = { + const ccUserData = { name: PERMUTIVE_CUSTOM_COHORTS_KEYWORD, - segment: bidderCustomCohorts.map(cohortID => ({ id: cohortID })), + segment: ccSignals.map(cohortID => ({ id: cohortID })), } - // 4. "permutive.com" provider with segtax: Privacy Sandbox Topics (one entry per taxonomy version) - // Contains: Google Topics API signals const topicsUserData = [] for (const [k, value] of Object.entries(topics)) { topicsUserData.push({ @@ -292,24 +212,21 @@ function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, top }) } - // Merge all user.data[] entries, removing old Permutive entries first const ortbConfig = mergeDeep({}, currConfig) const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] const updatedUserData = currentUserData - .filter(el => el.name !== permutiveUserData.name && el.name !== customCohortsUserData.name) - .concat(permutiveUserData, transformedUserData, customCohortsUserData) + .filter(el => el.name !== permutiveUserData.name && el.name !== ccUserData.name) + .concat(permutiveUserData, transformedUserData, ccUserData) .concat(topicsUserData) logger.logInfo(`Updating ortb2.user.data`, { bidder, user_data: updatedUserData }) deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData) - // === ORTB2.USER.KEYWORDS SETUP === - const currentKeywords = deepAccess(ortbConfig, 'ortb2.user.keywords') const keywordGroups = { - [PERMUTIVE_STANDARD_KEYWORD]: mergedSignalIds, // p_standard: AC Signals or AC+SSP Signals merged - [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSignalIds, // p_standard_aud: SSP Signals only - [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: bidderCustomCohorts, // permutive: Bidder-specific custom cohorts + [PERMUTIVE_STANDARD_KEYWORD]: mergedSignalIds, + [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSignalIds, + [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: ccSignals, } // Transform groups of key-values into a single array of strings @@ -330,23 +247,16 @@ function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, top }) deepSetValue(ortbConfig, 'ortb2.user.keywords', keywords) - // === ORTB2.USER.EXT.DATA SETUP === - - // Set p_standard: AC Signals or AC+SSP Signals merged if (mergedSignalIds.length > 0) { deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignalIds) logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignalIds) } - // Set permutive: Bidder-specific custom cohorts - if (bidderCustomCohorts.length > 0) { - deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}`, bidderCustomCohorts.map(String)) - logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, bidderCustomCohorts) + if (ccSignals.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}`, ccSignals.map(String)) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, ccSignals) } - // === ORTB2.SITE.EXT.PERMUTIVE SETUP === - - // Set p_standard: AC Signals or AC+SSP Signals merged at site level if (mergedSignalIds.length > 0) { deepSetValue(ortbConfig, `ortb2.site.ext.permutive.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignalIds) logger.logInfo(`Extending ortb2.site.ext.permutive with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignalIds) @@ -433,51 +343,42 @@ export function isPermutiveOnPage () { } /** - * Reads cohort data from local storage keys written by the Permutive SDK. - * Returns segment data organized by signal type (AC, SSP, custom cohorts) and topics. - * - * @param {number} maxSegs - Maximum number of segments per cohort type - * @return {Object} Segment data with AC signals, SSP signals, unified custom cohorts, and topics + * Reads cohort data from local storage and returns organized by signal type + * @param {number} maxSegs - Maximum number of segments per signal type + * @return {Object} Segment data with AC, SSP, CC signals and topics */ export function getSegments(maxSegs) { const segments = { - // AC Signals: Standard Cohorts + DCR Cohorts - // Sent to AC Bidders via p_standard keyword and ortb2.user.data "permutive.com" provider + // AC Signals ac: makeSafe(() => { - // Standard Cohorts: Permutive's core audience segments (_psegs >= 1000000) const standardCohorts = makeSafe(() => readSegments('_psegs', []) .map(Number) - .filter((seg) => seg >= 1000000) // Filter to only Standard Cohorts + .filter((seg) => seg >= 1000000) .map(String), ) || []; - // DCR Cohorts: Data Clean Room cohorts from privacy-enhanced partnerships (_pcrprs) const dcrCohorts = makeSafe(() => readSegments('_pcrprs', []).map(String)) || []; return [...dcrCohorts, ...standardCohorts].slice(0, maxSegs); }) || [], - // Custom Cohorts: Unified list from new key + all legacy keys merged - // Sent to bidders in ccBidders config + legacy bidders (ix, rubicon, appnexus, gam) + // CC Signals customCohorts: makeSafe(() => { - // Read new unified custom cohorts key (_pprebid) - const unifiedCustomCohorts = makeSafe(() => + const pprebid = makeSafe(() => readSegments('_pprebid', []).map(String) ) || []; - // Read legacy bidder-specific keys for backwards compatibility const legacyAppnexus = makeSafe(() => readSegments('_papns', []).map(String)) || []; const legacyRubicon = makeSafe(() => readSegments('_prubicons', []).map(String)) || []; const legacyIndex = makeSafe(() => readSegments('_pindexs', []).map(String)) || []; const legacyGam = makeSafe(() => readSegments('_pdfps', []).map(String)) || []; - // Merge and deduplicate all custom cohorts into a single unified list return [...new Set([ - ...unifiedCustomCohorts, + ...pprebid, ...legacyAppnexus, ...legacyRubicon, ...legacyIndex, @@ -485,13 +386,11 @@ export function getSegments(maxSegs) { ])].slice(0, maxSegs); }) || [], - // SSP Signals: Curation signals (curated mix of DCR, Standard, and Curated cohorts) - // Sent to SSP Bidders via p_standard_aud keyword - // Includes both the signal IDs and the list of SSP bidder codes that should receive them + // SSP Signals ssp: makeSafe(() => { const _pssps = readSegments('_pssps', { - cohorts: [], // SSP Signal IDs (curation signals) - ssps: [], // SSP bidder codes + cohorts: [], + ssps: [], }); return { @@ -500,8 +399,7 @@ export function getSegments(maxSegs) { }; }), - // Privacy Sandbox Topics: Google Topics API signals, keyed by IAB taxonomy version - // Sent to all bidders via "permutive.com" provider with segtax in ortb2.user.data + // Privacy Sandbox Topics topics: makeSafe(() => { const _ppsts = readSegments('_ppsts', {}); diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 3cf3ed2b367..40de1e29fba 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -46,6 +46,7 @@ as well as enabling settings for specific use cases mentioned above (e.g. acbidd | waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | | params | Object | | - | | params.acBidders | String[] | An array of bidder codes to share cohorts with in certain versions of Prebid, see below | `[]` | +| params.ccBidders | String[] | An array of bidder codes to receive custom cohorts, see Custom Cohorts section below | `[]` | | params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | #### Context @@ -100,17 +101,30 @@ For Equativ: Please ensure you are using Prebid.js 7.26 (or later) #### Custom Cohorts -The Permutive RTD module also supports passing any of the **Custom** Cohorts created in the dashboard to some SSP partners for targeting -e.g. setting up publisher deals. For these activations, cohort IDs are set in bidder-specific locations per ad unit (custom parameters). +The Permutive RTD module supports passing **Custom Cohorts** created in the dashboard to specified bidders for targeting (e.g., setting up publisher deals). -Currently, bidders with known support for custom cohort targeting are: +To enable custom cohorts, add bidder codes to the `ccBidders` parameter: -- Xandr -- Magnite +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'permutive', + params: { + ccBidders: ['appnexus', 'rubicon', 'ozone'] + } + }] + } +}) +``` + +**Note:** The following bidders automatically receive custom cohorts for backwards compatibility, even if not included in `ccBidders`: +- Index Exchange (`ix`) +- Rubicon/Magnite (`rubicon`) +- AppNexus/Xandr (`appnexus`) +- Google Ad Manager (`gam`) -When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. -There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. -Permutive cohorts will be sent in the permutive key-value. +Custom cohorts are read from the `_pprebid` local storage key (set by the Permutive SDK) and are sent to all target bidders in the `permutive` key-value. ### _Enabling Advertiser Cohorts_ From 6772c8d386b4cc56f5524d1ddf5e6c4604742f79 Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Fri, 14 Nov 2025 13:11:01 +0000 Subject: [PATCH 5/8] Permutive RTD Provider: Refactor updateOrtbConfig to accept individual signal types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improved code clarity by refactoring updateOrtbConfig() to accept individual signal types (acSignals, sspSignals, ccSignals) instead of pre-merged signals. Changes to modules/permutiveRtdProvider.js: - Updated updateOrtbConfig() signature to accept acSignals, sspSignals, ccSignals separately - Moved signal merging logic inside updateOrtbConfig() (line 179) - Simplified setBidderRtb() call site - now passes individual signals based on bidder type - Added maxSegs parameter to updateOrtbConfig() for proper signal limiting Benefits: - Self-documenting: Function signature clearly shows which signal types are available - Logic colocation: Merging happens right before use, making it obvious why - Clearer data flow: Easy to see "AC+SSP merge → p_standard", "SSP only → p_standard_aud" - No ambiguity: acSignals always means AC signals, never pre-merged Changes to test/spec/modules/permutiveCombined_spec.js: - Fixed test to only expect p_standard_aud keywords for bidders that are actually SSP bidders - Test now correctly checks if bidder is in SSP bidders list before including SSP signals All tests passing (36/36). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/permutiveRtdProvider.js | 51 +++++++++------------ test/spec/modules/permutiveCombined_spec.js | 6 +-- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index a7ee7b6f011..23347bf81b3 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -146,26 +146,15 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const isSspBidder = sspBidders.indexOf(bidder) > -1 const isCcBidder = ccBidders.indexOf(bidder) > -1 || legacyCcBidders.indexOf(bidder) > -1 - let signalsForBidder = [] - - if (isAcBidder) { - signalsForBidder = acSignals - } - - if (isSspBidder) { - signalsForBidder = [...new Set([...signalsForBidder, ...sspSignals])].slice(0, maxSegs) - } - - const ccSignals = isCcBidder ? customCohorts : [] - const nextConfig = updateOrtbConfig( bidder, currConfig, - signalsForBidder, - sspSignals, + isAcBidder ? acSignals : [], + isSspBidder ? sspSignals : [], + isCcBidder ? customCohorts : [], topics, transformationConfigs, - ccSignals + maxSegs ) bidderOrtb2[bidder] = nextConfig.ortb2 }) @@ -175,21 +164,25 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { * Updates ORTB2 config for a bidder with Permutive signals * @param {string} bidder * @param {Object} currConfig - * @param {string[]} mergedSignalIds - AC/SSP Signals for this bidder - * @param {string[]} sspSignalIds - SSP Signals (for p_standard_aud) + * @param {string[]} acSignals - AC Signals for this bidder + * @param {string[]} sspSignals - SSP Signals for this bidder + * @param {string[]} ccSignals - CC Signals for this bidder * @param {Object} topics - Privacy Sandbox Topics * @param {Object[]} transformationConfigs - IAB taxonomy transformations - * @param {string[]} ccSignals - CC Signals for this bidder + * @param {number} maxSegs - Maximum segments per signal type * @return {Object} Updated ortb2 config */ -function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, topics, transformationConfigs, ccSignals) { +function updateOrtbConfig(bidder, currConfig, acSignals, sspSignals, ccSignals, topics, transformationConfigs, maxSegs) { logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig }) + // Merge AC + SSP signals for p_standard and permutive.com provider + const mergedSignals = [...new Set([...acSignals, ...sspSignals])].slice(0, maxSegs) + const name = 'permutive.com' const permutiveUserData = { name, - segment: mergedSignalIds.map(segmentId => ({ id: segmentId })), + segment: mergedSignals.map(segmentId => ({ id: segmentId })), } const transformedUserData = transformationConfigs @@ -224,13 +217,11 @@ function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, top const currentKeywords = deepAccess(ortbConfig, 'ortb2.user.keywords') const keywordGroups = { - [PERMUTIVE_STANDARD_KEYWORD]: mergedSignalIds, - [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSignalIds, + [PERMUTIVE_STANDARD_KEYWORD]: mergedSignals, + [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSignals, [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: ccSignals, } - // Transform groups of key-values into a single array of strings - // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3', 'p_standard=4'] const transformedKeywordGroups = Object.entries(keywordGroups) .flatMap(([keyword, ids]) => ids.map(id => `${keyword}=${id}`)) @@ -247,9 +238,9 @@ function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, top }) deepSetValue(ortbConfig, 'ortb2.user.keywords', keywords) - if (mergedSignalIds.length > 0) { - deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignalIds) - logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignalIds) + if (mergedSignals.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignals) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignals) } if (ccSignals.length > 0) { @@ -257,9 +248,9 @@ function updateOrtbConfig(bidder, currConfig, mergedSignalIds, sspSignalIds, top logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, ccSignals) } - if (mergedSignalIds.length > 0) { - deepSetValue(ortbConfig, `ortb2.site.ext.permutive.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignalIds) - logger.logInfo(`Extending ortb2.site.ext.permutive with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignalIds) + if (mergedSignals.length > 0) { + deepSetValue(ortbConfig, `ortb2.site.ext.permutive.${PERMUTIVE_STANDARD_KEYWORD}`, mergedSignals) + logger.logInfo(`Extending ortb2.site.ext.permutive with "${PERMUTIVE_STANDARD_KEYWORD}"`, mergedSignals) } logger.logInfo(`Updated ortb2 config`, { bidder, config: ortbConfig }) diff --git a/test/spec/modules/permutiveCombined_spec.js b/test/spec/modules/permutiveCombined_spec.js index eef3e8ef371..dee64c5c749 100644 --- a/test/spec/modules/permutiveCombined_spec.js +++ b/test/spec/modules/permutiveCombined_spec.js @@ -393,14 +393,14 @@ describe('permutiveRtdProvider', function () { acBidders.forEach(bidder => { const customCohortsData = segmentsData.customCohorts || [] + const isSspBidder = segmentsData.ssp.ssps.includes(bidder) + const keywordGroups = { [PERMUTIVE_STANDARD_KEYWORD]: segmentsData.ac, - [PERMUTIVE_STANDARD_AUD_KEYWORD]: segmentsData.ssp.cohorts, + [PERMUTIVE_STANDARD_AUD_KEYWORD]: isSspBidder ? segmentsData.ssp.cohorts : [], [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: customCohortsData } - // Transform groups of key-values into a single array of strings - // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3',' p_standard=4'] const transformedKeywordGroups = Object.entries(keywordGroups) .flatMap(([keyword, ids]) => ids.map(id => `${keyword}=${id}`)) From 3be1fb3e2e357dbde7485e6ecda40442cfa81b41 Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Fri, 14 Nov 2025 13:26:36 +0000 Subject: [PATCH 6/8] Permutive RTD Module: improve naming consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename customCohorts variable to ccSignals for consistency with acSignals/sspSignals (modules/permutiveRtdProvider.js:135, 154) - Update setBidderRtb comment to accurately reflect it handles all bidder types, not just AC bidders (modules/permutiveRtdProvider.js:121) - Fix typo in bidderOrtb2 parameter description (modules/permutiveRtdProvider.js:122) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- modules/permutiveRtdProvider.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 23347bf81b3..a3fc84d4abd 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -118,8 +118,8 @@ export function getModuleConfig(customModuleConfig) { } /** - * Sets ortb2 config for ac bidders - * @param {Object} bidderOrtb2 - The ortb2 object for the all bidders + * Sets ortb2 config for bidders with Permutive signals + * @param {Object} bidderOrtb2 - The ortb2 object for all bidders * @param {Object} moduleConfig - Publisher config for module * @param {Object} segmentData - Segment data grouped by bidder or type */ @@ -132,7 +132,7 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const sspBidders = segmentData?.ssp?.ssps ?? [] const sspSignals = segmentData?.ssp?.cohorts ?? [] const topics = segmentData?.topics ?? {} - const customCohorts = segmentData?.customCohorts ?? [] + const ccSignals = segmentData?.customCohorts ?? [] const ccBidders = deepAccess(moduleConfig, 'params.ccBidders') || [] const legacyCcBidders = ['ix', 'rubicon', 'appnexus', 'gam'] @@ -151,7 +151,7 @@ export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { currConfig, isAcBidder ? acSignals : [], isSspBidder ? sspSignals : [], - isCcBidder ? customCohorts : [], + isCcBidder ? ccSignals : [], topics, transformationConfigs, maxSegs From 6b199e60acc132d6c5c6d9009544720e25bde8ae Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Mon, 17 Nov 2025 10:00:31 +0000 Subject: [PATCH 7/8] Updates integration examples --- .../gpt/permutiveRtdProvider_example.html | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html index dc7bb0d111a..b5cea5ed860 100644 --- a/integrationExamples/gpt/permutiveRtdProvider_example.html +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -10,13 +10,21 @@ function setLocalStorageData () { const data = { + // AC Signals + _psegs: ['1234', '1000001', '1000002'], // Standard cohorts (>= 1000000) + _pcrprs: ['pcrprs1', 'pcrprs2'], // DCR cohorts + + // SSP Signals + _pssps: { ssps: ['appnexus', 'some other'], cohorts: ['abcd', 'efgh', 'ijkl'] }, + + // CC Signals - New unified custom cohorts model + _pprebid: ['custom1', 'custom2', 'custom3'], // Primary custom cohorts + + // CC Signals - Legacy keys (merged with _pprebid) _pdfps: ['gam1', 'gam2'], _prubicons: ['rubicon1', 'rubicon2'], _papns: ['appnexus1', 'appnexus2'], - _psegs: ['1234', '1000001', '1000002'], - _ppam: ['ppam1', 'ppam2'], - _pcrprs: ['pcrprs1', 'pcrprs2'], - _pssps: { ssps: ['appnexus', 'some other'], cohorts: ['abcd', 'efgh', 'ijkl'] }, + _pindexs: ['index1', 'index2'], } for (let key in data) { @@ -91,15 +99,6 @@ ], ozoneData: {} } - }, - { - bidder: 'trustx', - params: { - uid: 45, - keywords: { - test_kv: ['true'] - } - } } ] }, @@ -150,7 +149,8 @@ name: 'permutive', waitForIt: true, params: { - acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], + acBidders: ['appnexus', 'rubicon', 'ozone', 'ix'], + ccBidders: ['ozone'], // Custom cohort bidders (ix, rubicon, appnexus, gam get them automatically) maxSegs: 500, transformations: [ { @@ -179,6 +179,7 @@ ] } }); + pbjs.setBidderConfig({ bidders: ['appnexus', 'rubicon', 'ix'], config: { From 4b53160cdffdb6a4e4cf144da7781f3fcd96a61a Mon Sep 17 00:00:00 2001 From: Josh Forman-Gornall Date: Mon, 17 Nov 2025 10:26:45 +0000 Subject: [PATCH 8/8] Tidy up and improve Permutive RTD module documentation --- modules/permutiveRtdProvider.js | 14 +-- modules/permutiveRtdProvider.md | 192 ++++++++++++++++++-------------- 2 files changed, 113 insertions(+), 93 deletions(-) diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index a3fc84d4abd..ba919d5c32d 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -30,11 +30,10 @@ import {getGlobal} from '../src/prebidGlobal.js'; import {submodule} from '../src/hook.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; import {getStorageManager} from '../src/storageManager.js'; import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse, prefixLog} from '../src/utils.js'; -import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; - /** * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule */ @@ -427,9 +426,9 @@ function readSegments (key, defaultValue) { const unknownIabSegmentId = '_unknown_' /** - * Functions to apply to ORT2B2 `user.data` objects. - * Each function should return an a new object containing a `name`, (optional) `ext` and `segment` - * properties. The result of the each transformation defined here will be appended to the array + * Functions to apply to ORTB2 `user.data` objects. + * Each function should return a new object containing `name`, (optional) `ext` and `segment` + * properties. The result of each transformation defined here will be appended to the array * under `user.data` in the bid request. */ const ortb2UserDataTransformations = { @@ -454,9 +453,8 @@ function iabSegmentId(permutiveSegmentId, iabIds) { /** * Pull the latest configuration and cohort information and update accordingly. - * - * @param reqBidsConfigObj - Bidder provided config for request - * @param moduleConfig - Publisher provided config + * @param {Object} reqBidsConfigObj - Bidder provided config for request + * @param {Object} moduleConfig - Publisher provided config */ export function readAndSetCohorts(reqBidsConfigObj, moduleConfig) { const segmentData = getSegments(deepAccess(moduleConfig, 'params.maxSegs')) diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 40de1e29fba..34acf9c01d6 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -1,10 +1,25 @@ -## Prebid Config for Permutive RTD Module +# Permutive Real-time Data Submodule -This module reads cohorts from Permutive and attaches them as targeting keys to bid requests. +## Overview -### _Permutive Real-time Data Submodule_ + Module Name: Permutive Rtd Provider + Module Type: Rtd Provider + Maintainer: support@permutive.com + +## Description + +The Permutive real-time data module enables publishers to enrich bid requests with Permutive audience segments and targeting data. The module reads cohort data from local storage (set by the Permutive SDK) and attaches it to bid requests as first-party data following OpenRTB 2.x conventions. + +Supported cohort types include: +- **Standard Cohorts**: IAB-compliant audience segments (segment IDs ≥ 1000000) +- **Custom Cohorts**: Publisher-defined audiences created in the Permutive dashboard +- **DCR Cohorts**: Data Clean Room cohorts for privacy-safe audience activation +- **Curation Cohorts**: SSP-specific curation signals for supply-side optimization + +## Usage + +### Build -#### Usage Compile the Permutive RTD module into your Prebid build: ``` @@ -13,97 +28,92 @@ gulp build --modules=rtdModule,permutiveRtdProvider > Note that the global RTD module, `rtdModule`, is a prerequisite of the Permutive RTD module. -You then need to enable the Permutive RTD in your Prebid configuration. Below is an example of the format: +### Configuration + +Enable the Permutive RTD module in your Prebid configuration: ```javascript pbjs.setConfig({ - ..., realTimeData: { auctionDelay: 50, // optional auction delay dataProviders: [{ name: 'permutive', - waitForIt: true, // should be true if there's an `auctionDelay` + waitForIt: true, // should be true if there's an auctionDelay params: { - acBidders: ['appnexus'] + acBidders: ['appnexus', 'rubicon'], + ccBidders: ['ozone'] } }] - }, - ... + } }) ``` -#### Parameters - -The parameters below provide configurability for general behaviours of the RTD submodule, -as well as enabling settings for specific use cases mentioned above (e.g. acbidders). +### Parameters -## Parameters - -{: .table .table-bordered .table-striped } | Name | Type | Description | Default | | ---------------------- | -------------------- | --------------------------------------------------------------------------------------------- | ------------------ | -| name | String | This should always be `permutive` | - | -| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | -| params | Object | | - | -| params.acBidders | String[] | An array of bidder codes to share cohorts with in certain versions of Prebid, see below | `[]` | -| params.ccBidders | String[] | An array of bidder codes to receive custom cohorts, see Custom Cohorts section below | `[]` | -| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | +| name | String | Real-time data module name (always `permutive`) | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined | `false` | +| params | Object | Module configuration parameters | - | +| params.acBidders | String[] | Bidder codes to receive Standard Cohorts and DCR Cohorts (see Standard Cohorts section) | `[]` | +| params.ccBidders | String[] | Bidder codes to receive Custom Cohorts (see Custom Cohorts section) | `[]` | +| params.maxSegs | Integer | Maximum number of cohorts per cohort type | `500` | -#### Context +## GDPR and TCF Configuration -While Permutive is listed as a TCF vendor (ID: 361), Permutive does not obtain consent directly from the TCF. As we act as a processor on behalf of our publishers consent is given to the Permutive SDK by the publisher, not by the [GDPR Consent Management Module](https://prebid-docs.atre.net/dev-docs/modules/consentManagement.html). +While Permutive is listed as a TCF vendor (ID: 361), Permutive does not obtain consent directly from the TCF. As a data processor, consent is managed by the Permutive SDK on behalf of publishers, not by Prebid's [GDPR Consent Management Module](https://docs.prebid.org/dev-docs/modules/consentManagement.html). -This means that if GDPR enforcement is configured within the Permutive SDK _and_ the user consent isn’t given for Permutive to fire, no cohorts will populate. +If GDPR enforcement is configured within the Permutive SDK and user consent is not granted, no cohorts will be passed to bidders. -If you are also using the [TCF Control Module](https://docs.prebid.org/dev-docs/modules/tcfControl.html), in order to prevent Permutive from being blocked, it needs to be labeled within the Vendor Exceptions. +### TCF Control Module Configuration -#### Instructions +If you are using the [TCF Control Module](https://docs.prebid.org/dev-docs/modules/tcfControl.html), Permutive must be added as a vendor exception to prevent it from being blocked: -1. Publisher enables rules within Prebid.js configuration. -2. Label Permutive as an exception, as shown below. ```javascript -[ - { - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: ["permutive"] - }, - { - purpose: 'basicAds', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] +pbjs.setConfig({ + consentManagement: { + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: ['permutive'] + }, { + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }] + } } -] +}) ``` -Before making any updates to this configuration, please ensure that this approach aligns with internal policies and current regulations regarding consent. - -## Cohort Activation with Permutive RTD Module +Before implementing this configuration, ensure it aligns with your organization's privacy policies and regulatory requirements. -**Note**: Publishers must be enabled on the above Permutive RTD Submodule to enable Standard Cohorts. +## Cohort Configuration -### _Enabling Publisher Cohorts_ +### Standard Cohorts -#### Standard Cohorts +Standard Cohorts are IAB-compliant audience segments that can be shared with demand partners. The module automatically includes DCR Cohorts (Data Clean Room) alongside Standard Cohorts when sharing with bidders. -The Permutive RTD module sets Standard Cohort IDs as bidder-specific ortb2.user.data first-party data, following the Prebid ortb2 convention. Cohorts will be sent in the `p_standard` key-value. +**Prebid.js Version Requirements:** +- **Version 7.29.0+**: Standard Cohorts are shared via OpenRTB 2.x first-party data. Configure eligible bidders in the Permutive dashboard (see Managing acBidders below). +- **Version 7.13.0 - 7.28.x**: Use `params.acBidders` to specify which bidders should receive Standard Cohorts. +- **Version < 7.13.0**: Limited support. Upgrade recommended. -For Prebid versions below 7.29.0, populate the acbidders config in the Permutive RTD with an array of bidder codes with whom you wish to share Standard Cohorts with. You also need to permission the bidders by communicating the bidder list to the Permutive team at strategicpartnershipops@permutive.com. +**Bidder-Specific Requirements:** +- **PubMatic or OpenX**: Prebid.js 7.13+ +- **Xandr**: Prebid.js 7.29+ +- **Equativ**: Prebid.js 7.26+ -For Prebid versions 7.29.0 and above, do not populate bidder codes in acbidders for the purpose of sharing Standard Cohorts (Note: there may be other business needs that require you to populate acbidders for Prebid versions 7.29.0+, see Advertiser Cohorts below). To share Standard Cohorts with bidders in Prebid versions 7.29.0 and above, communicate the bidder list to the Permutive team at strategicpartnershipops@permutive.com. +Standard Cohorts are sent to bidders in the `p_standard` keyword and as `ortb2.user.data` with provider name `permutive.com`. Curation Cohorts from SSPs are sent in the `p_standard_aud` keyword when applicable. -#### _Bidder Specific Requirements for Standard Cohorts_ -For PubMatic or OpenX: Please ensure you are using Prebid.js 7.13 (or later) -For Xandr: Please ensure you are using Prebid.js 7.29 (or later) -For Equativ: Please ensure you are using Prebid.js 7.26 (or later) +### Custom Cohorts -#### Custom Cohorts +Custom Cohorts are publisher-defined audience segments created in the Permutive dashboard, typically used for direct deals and private marketplace (PMP) setups. -The Permutive RTD module supports passing **Custom Cohorts** created in the dashboard to specified bidders for targeting (e.g., setting up publisher deals). - -To enable custom cohorts, add bidder codes to the `ccBidders` parameter: +To enable Custom Cohorts for specific bidders, add their bidder codes to the `ccBidders` parameter: ```javascript pbjs.setConfig({ @@ -118,51 +128,63 @@ pbjs.setConfig({ }) ``` -**Note:** The following bidders automatically receive custom cohorts for backwards compatibility, even if not included in `ccBidders`: +**Legacy Bidder Support:** + +The following bidders automatically receive Custom Cohorts for backwards compatibility, even if not included in `ccBidders`: - Index Exchange (`ix`) - Rubicon/Magnite (`rubicon`) - AppNexus/Xandr (`appnexus`) - Google Ad Manager (`gam`) -Custom cohorts are read from the `_pprebid` local storage key (set by the Permutive SDK) and are sent to all target bidders in the `permutive` key-value. - +Custom Cohorts are read from the `_pprebid` local storage key (set by the Permutive SDK) and sent to target bidders in the `permutive` keyword and as `ortb2.user.data` with provider name `permutive`. -### _Enabling Advertiser Cohorts_ +### Advertiser Cohorts -If you are connecting to an Advertiser seat within Permutive to share Advertiser Cohorts, populate the acbidders config in the Permutive RTD with an array of bidder codes with whom you wish to share Advertiser Cohorts with. +If you are using Permutive's Advertiser product to share cohorts with demand partners, add the relevant bidder codes to `params.acBidders` to enable Advertiser Cohort sharing. -### _Managing acbidders_ +### Managing acBidders -If your business needs require you to populate acbidders with bidder codes based on the criteria above, there are **two** ways to manage it. +For Prebid.js version 7.13.0 and above, bidders can be managed directly in the Permutive Dashboard. -#### Option 1 - Automated +#### Dashboard Configuration -If you are using Prebid.js v7.13.0+, bidders may be added to or removed from the acbidders config directly within the Permutive Dashboard. +1. **Enable Prebid Integration**: Navigate to the integrations page in your Permutive dashboard settings and enable the Prebid integration. -**Permutive can do this on your behalf**. Simply contact your Permutive CSM with strategicpartnershipops@permutive.com on cc, -indicating which bidders you would like added. + > **Note on Revenue Insights:** The Prebid integration includes a Revenue Insights feature, which is optional and not required for cohort activation. See the [Revenue Insights documentation](https://support.permutive.com/hc/en-us/articles/360019044079-Revenue-Insights) for more details. -Or, a publisher may do this themselves within the Permutive Dashboard using the below instructions. +2. **Configure Bidders**: In the "Data Provider config" section, enter valid bidder codes to enable Standard Cohorts, DCR Cohorts, or Advertiser Cohorts for specific partners. Refer to the [Prebid bidder codes list](https://docs.prebid.org/dev-docs/bidders.html) for valid values. -##### Create Integration +3. **Manual Override**: Bidders configured via the dashboard will automatically populate `params.acBidders`. If you have manually defined bidders in your Prebid configuration, dashboard settings will not override them. -In order to manage acbidders via the Permutive dashboard, it is necessary to first enable the Prebid integration via the integrations page (settings). +#### Manual Configuration -**Note on Revenue Insights:** The prebid integration includes a feature for revenue insights, -which is not required for the purpose of updating acbidders config. -Please see [this document](https://support.permutive.com/hc/en-us/articles/360019044079-Revenue-Insights) for more information about revenue insights. +Alternatively, you can manually define bidders in your Prebid configuration: -##### Update acbidders +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'permutive', + params: { + acBidders: ['appnexus', 'rubicon', 'pubmatic'] + } + }] + } +}) +``` -The input for the “Data Provider config” is a multi-input free text. A valid “bidder code” needs to be entered in order to enable Standard or Advertiser Cohorts to be passed to the desired partner. The [prebid Bidders page](https://docs.prebid.org/dev-docs/bidders.html) contains instructions and a link to a list of possible bidder codes. +**Note:** Manually configured bidders must be removed manually if no longer needed, as dashboard settings will not override them. -Bidders can be added or removed from acbidders using this feature, however, this will not impact any bidders that have been applied using the manual method below. +## Testing -#### Option 2 - Manual +To view an example of the Permutive RTD module: -As a secondary option, bidders may be added manually. +``` +gulp serve --modules=rtdModule,permutiveRtdProvider,appnexusBidAdapter +``` -To do so, define which bidders should receive Standard or Advertiser Cohorts by -including the _bidder code_ of any bidder in the `acBidders` array. +Then navigate to: -**Note:** If you ever need to remove a manually-added bidder, the bidder will also need to be removed manually. +``` +http://localhost:9999/integrationExamples/gpt/permutiveRtdProvider_example.html +```