From c0a922cc59f074aa98a2784fe1d4e794c5da0a46 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 30 Jun 2022 12:05:48 -0700 Subject: [PATCH 1/4] Topics FPD module --- modules/.submodules.json | 3 +- modules/fpdModule/index.js | 22 ++- modules/topicsFpdModule.js | 63 ++++++++ test/spec/modules/fpdModule_spec.js | 127 ++++++++-------- test/spec/modules/topicsFpdModule_spec.js | 168 ++++++++++++++++++++++ 5 files changed, 318 insertions(+), 65 deletions(-) create mode 100644 modules/topicsFpdModule.js create mode 100644 test/spec/modules/topicsFpdModule_spec.js diff --git a/modules/.submodules.json b/modules/.submodules.json index d808b10051b..53f557fd082 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -69,7 +69,8 @@ ], "fpdModule": [ "enrichmentFpdModule", - "validationFpdModule" + "validationFpdModule", + "topicsFpdModule" ] }, "libraries": { diff --git a/modules/fpdModule/index.js b/modules/fpdModule/index.js index bd735e215a4..bc6461b57cc 100644 --- a/modules/fpdModule/index.js +++ b/modules/fpdModule/index.js @@ -4,6 +4,7 @@ */ import { config } from '../../src/config.js'; import { module, getHook } from '../../src/hook.js'; +import {logError} from '../../src/utils.js'; let submodules = []; @@ -17,19 +18,28 @@ export function reset() { export function processFpd({global = {}, bidder = {}} = {}) { let modConf = config.getConfig('firstPartyData') || {}; - + // TODO: convert this to GreedyPromise once #8626 gets merged + let result = Promise.resolve({global, bidder}); submodules.sort((a, b) => { return ((a.queue || 1) - (b.queue || 1)); }).forEach(submodule => { - ({global = global, bidder = bidder} = submodule.processFpd(modConf, {global, bidder})); + result = result.then( + ({global, bidder}) => Promise.resolve(submodule.processFpd(modConf, {global, bidder})) + .catch((err) => { + logError(`Error in FPD module ${submodule.name}`, err); + return {}; + }) + .then((result) => ({global: result.global || global, bidder: result.bidder || bidder})) + ); }); - - return {global, bidder}; + return result; } export function startAuctionHook(fn, req) { - Object.assign(req.ortb2Fragments, processFpd(req.ortb2Fragments)); - fn.call(this, req); + processFpd(req.ortb2Fragments).then((ortb2Fragments) => { + Object.assign(req.ortb2Fragments, ortb2Fragments); + fn.call(this, req); + }) } function setupHook() { diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js new file mode 100644 index 00000000000..4deb9bc3f99 --- /dev/null +++ b/modules/topicsFpdModule.js @@ -0,0 +1,63 @@ +import {logError, mergeDeep} from '../src/utils.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {submodule} from '../src/hook.js'; + +export function getTopicsData(name, topics) { + const data = Object.entries( + topics.reduce((byTaxVersion, topic) => { + const taxv = topic.taxonomyVersion; + if (!byTaxVersion.hasOwnProperty(taxv)) byTaxVersion[taxv] = []; + byTaxVersion[taxv].push(topic.topic); + return byTaxVersion; + }, {}) + ).map(([taxv, topics]) => ({ + ext: { + segtax: 600, + segclass: taxv + }, + segment: topics.map((topic) => ({id: topic.toString()})) + })); + if (name != null) { + data.forEach((datum) => { + datum.name = name; + }); + } + return data; +} + +export function getTopics(doc = document) { + let topics = null; + try { + if ('browsingTopics' in doc && doc.featurePolicy.allowsFeature('browsing-topics')) { + topics = doc.browsingTopics(); + } + } catch (e) { + logError('Could not call topics API', e); + } + if (topics == null) { + // TODO: convert this to GreedyPromise once #8626 gets merged + topics = Promise.resolve([]); + } + return topics; +} + +const topicsData = getTopics().then((topics) => getTopicsData(getRefererInfo().domain, topics)); + +export function processFpd(config, {global}, {data = topicsData} = {}) { + return data.then((data) => { + if (data.length) { + mergeDeep(global, { + user: { + data + } + }); + } + return {global}; + }); +} + +submodule('firstPartyData', { + name: 'topics', + queue: 1, + processFpd +}); diff --git a/test/spec/modules/fpdModule_spec.js b/test/spec/modules/fpdModule_spec.js index d43ae8309bd..498bed29243 100644 --- a/test/spec/modules/fpdModule_spec.js +++ b/test/spec/modules/fpdModule_spec.js @@ -5,9 +5,6 @@ import {processFpd, registerSubmodules, startAuctionHook, reset} from 'modules/f import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; import * as validationModule from 'modules/validationFpdModule/index.js'; -let enrichments = {...enrichmentModule}; -let validations = {...validationModule}; - describe('the first party data module', function () { afterEach(function () { config.resetConfig(); @@ -18,21 +15,37 @@ describe('the first party data module', function () { global: {key: 'value'}, bidder: {A: {bkey: 'bvalue'}} } - before(() => { + beforeEach(() => { reset(); + }); + + it('should run ortb2Fragments through fpd submodules', () => { registerSubmodules({ name: 'test', - queue: 2, processFpd: function () { return mockFpd; } }); - }) + const req = {ortb2Fragments: {}}; + return new Promise((resolve) => startAuctionHook(resolve, req)) + .then(() => { + expect(req.ortb2Fragments).to.eql(mockFpd); + }) + }); - it('should run ortb2Fragments through fpd submodules', () => { + it('should work with fpd submodules that return promises', () => { + registerSubmodules({ + name: 'test', + processFpd: function () { + return Promise.resolve(mockFpd); + } + }); const req = {ortb2Fragments: {}}; - startAuctionHook(() => null, req); - expect(req.ortb2Fragments).to.eql(mockFpd); + return new Promise((resolve) => { + startAuctionHook(resolve, req); + }).then(() => { + expect(req.ortb2Fragments).to.eql(mockFpd); + }); }); }); @@ -79,7 +92,6 @@ describe('the first party data module', function () { }); it('filters ortb2 data that is set', function () { - let validated; const global = { user: { data: {}, @@ -113,42 +125,42 @@ describe('the first party data module', function () { width = 1120; height = 750; - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal(getRefererInfo().ref || undefined); - expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); - expect(validated.site.domain).to.equal('domain.com'); - expect(validated.site.content.data).to.deep.equal([{segment: [{id: 'test'}], name: 'bar'}]); - expect(validated.user.data).to.be.undefined; - expect(validated.device).to.deep.to.equal({w: 1, h: 1}); - expect(validated.site.keywords).to.be.undefined; + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal(getRefererInfo().ref || undefined); + expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); + expect(validated.site.domain).to.equal('domain.com'); + expect(validated.site.content.data).to.deep.equal([{segment: [{id: 'test'}], name: 'bar'}]); + expect(validated.user.data).to.be.undefined; + expect(validated.device).to.deep.to.equal({w: 1, h: 1}); + expect(validated.site.keywords).to.be.undefined; + }); }); it('should not overwrite existing data with default settings', function () { - let validated; const global = { site: { ref: 'https://referer.com' } }; - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal('https://referer.com'); + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal('https://referer.com'); + }); }); it('should allow overwrite default data with setConfig', function () { - let validated; const global = { site: { ref: 'https://referer.com' } }; - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal('https://referer.com'); + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal('https://referer.com'); + }); }); it('should filter all data', function () { - let validated; let global = { imp: [], site: { @@ -179,15 +191,13 @@ describe('the first party data module', function () { adServerCurrency: 'USD' } }; - config.setConfig({'firstPartyData': {skipEnrichments: true}}); - - ({global: validated} = processFpd({global})); - expect(validated).to.deep.equal({}); + return processFpd({global}).then(({global: validated}) => { + expect(validated).to.deep.equal({}); + }); }); it('should add enrichments but not alter any arbitrary ortb2 data', function () { - let validated; let global = { site: { ext: { @@ -205,12 +215,12 @@ describe('the first party data module', function () { }, cur: ['USD'] }; - - ({global: validated} = processFpd({global})); - expect(validated.site.ref).to.equal(getRefererInfo().referer); - expect(validated.site.ext.data).to.deep.equal({inventory: ['value1']}); - expect(validated.user.ext.data).to.deep.equal({visitor: ['value2']}); - expect(validated.cur).to.deep.equal(['USD']); + return processFpd({global}).then(({global: validated}) => { + expect(validated.site.ref).to.equal(getRefererInfo().referer); + expect(validated.site.ext.data).to.deep.equal({inventory: ['value1']}); + expect(validated.user.ext.data).to.deep.equal({visitor: ['value2']}); + expect(validated.cur).to.deep.equal(['USD']); + }) }); it('should filter bidderConfig data', function () { @@ -230,12 +240,13 @@ describe('the first party data module', function () { } }; - const {bidder: validated} = processFpd({bidder}); - expect(validated.bidderA).to.not.be.undefined; - expect(validated.bidderA.user.data).to.be.undefined; - expect(validated.bidderA.user.keywords).to.equal('test'); - expect(validated.bidderA.site.keywords).to.equal('other'); - expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + return processFpd({bidder}).then(({bidder: validated}) => { + expect(validated.bidderA).to.not.be.undefined; + expect(validated.bidderA.user.data).to.be.undefined; + expect(validated.bidderA.user.keywords).to.equal('test'); + expect(validated.bidderA.site.keywords).to.equal('other'); + expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + }) }); it('should not filter bidderConfig data as it is valid', function () { @@ -255,17 +266,16 @@ describe('the first party data module', function () { } }; - const {bidder: validated} = processFpd({bidder}); - - expect(validated.bidderA).to.not.be.undefined; - expect(validated.bidderA.user.data).to.deep.equal([{segment: [{id: 'data1_id'}], name: 'data1'}]); - expect(validated.bidderA.user.keywords).to.equal('test'); - expect(validated.bidderA.site.keywords).to.equal('other'); - expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + return processFpd({bidder}).then(({bidder: validated}) => { + expect(validated.bidderA).to.not.be.undefined; + expect(validated.bidderA.user.data).to.deep.equal([{segment: [{id: 'data1_id'}], name: 'data1'}]); + expect(validated.bidderA.user.keywords).to.equal('test'); + expect(validated.bidderA.site.keywords).to.equal('other'); + expect(validated.bidderA.site.ref).to.equal('https://domain.com'); + }); }); it('should not set default values if skipEnrichments is turned on', function () { - let validated; config.setConfig({'firstPartyData': {skipEnrichments: true}}); let global = { @@ -281,15 +291,15 @@ describe('the first party data module', function () { } }; - ({global: validated} = processFpd({global})); - expect(validated.device).to.be.undefined; - expect(validated.site.ref).to.be.undefined; - expect(validated.site.page).to.be.undefined; - expect(validated.site.domain).to.be.undefined; + return processFpd({global}).then(({global: validated}) => { + expect(validated.device).to.be.undefined; + expect(validated.site.ref).to.be.undefined; + expect(validated.site.page).to.be.undefined; + expect(validated.site.domain).to.be.undefined; + }); }); it('should not validate ortb2 data if skipValidations is turned on', function () { - let validated; config.setConfig({'firstPartyData': {skipValidations: true}}); let global = { @@ -304,8 +314,9 @@ describe('the first party data module', function () { } }; - ({global: validated} = processFpd({global})); - expect(validated.user.data).to.deep.equal([{segment: [{id: 'nonfiltered'}]}]); + return processFpd({global}).then(({global: validated}) => { + expect(validated.user.data).to.deep.equal([{segment: [{id: 'nonfiltered'}]}]); + }); }); }); }); diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js new file mode 100644 index 00000000000..ea6d18c3f20 --- /dev/null +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -0,0 +1,168 @@ +import {getTopics, getTopicsData, processFpd} from '../../../modules/topicsFpdModule.js'; +import {deepClone} from '../../../src/utils.js'; + +describe('getTopicsData', () => { + function makeTopic(topic, taxv) { + return { + topic, + taxonomyVersion: taxv + } + } + + [ + { + t: 'no topics', + topics: [], + expected: [] + }, + { + t: 'single topic', + topics: [makeTopic(123, 'v1')], + expected: [ + { + ext: { + segclass: 'v1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics with the same taxonomy version', + topics: [makeTopic(123, 'v1'), makeTopic(321, 'v1')], + expected: [ + { + ext: { + segclass: 'v1' + }, + segment: [ + {id: '123'}, + {id: '321'} + ] + } + ] + }, + { + t: 'multiple topics with different taxonomy versions', + topics: [makeTopic(1, 'v1'), makeTopic(2, 'v1'), makeTopic(3, 'v2')], + expected: [ + { + ext: { + segclass: 'v1' + }, + segment: [ + {id: '1'}, + {id: '2'} + ] + }, + { + ext: { + segclass: 'v2' + }, + segment: [ + {id: '3'} + ] + } + ] + } + ].forEach(({t, topics, expected}) => { + it(`on ${t}`, () => { + const actual = getTopicsData('mockName', topics); + actual.forEach((data, i) => { + sinon.assert.match(data, expected[i]); + expect(data.name).to.equal('mockName'); + expect(data.ext.segtax).to.eql(600); + }) + }); + + it('should not set name if null', () => { + getTopicsData(null, topics).forEach((data) => { + expect(data.hasOwnProperty('name')).to.be.false; + }) + }) + }) +}); + +describe('getTopics', () => { + Object.entries({ + 'document with no browsingTopics': {}, + 'document that disallows topics': { + featurePolicy: { + allowsFeature: sinon.stub().returns(false) + } + }, + 'document that throws on featurePolicy': { + browsingTopics: sinon.stub(), + get featurePolicy() { + throw new Error() + } + }, + 'document that throws on browsingTopics': { + browsingTopics: sinon.stub().callsFake(() => { throw new Error(); }), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }, + }).forEach(([t, doc]) => { + it(`should resolve to an empty list on ${t}`, () => { + return getTopics(doc).then((topics) => { + expect(topics).to.eql([]); + }); + }) + }); + + it('should call `document.browsingTopics` when allowed', () => { + const topics = ['t1', 't2'] + return getTopics({ + browsingTopics: sinon.stub().returns(Promise.resolve(topics)), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }).then((actual) => { + expect(actual).to.eql(topics); + }) + }) +}) + +describe('processFpd', () => { + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}] + } + ]; + + it('should add topics data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) + .then(({global}) => { + expect(global.user.data).to.eql(mockData); + }); + }); + + it('should apppend to existing user.data', () => { + const global = { + user: { + data: [ + {name: 'preexisting'}, + ] + } + }; + return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) + .then((data) => { + expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); + }); + }); + + it('should not modify fpd when there is no data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve([])}) + .then((data) => { + expect(data.global).to.eql({}); + }); + }); +}); From c60a9859b9c814a00862f9e5e303737ae74f54cb Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 30 Jun 2022 15:36:34 -0700 Subject: [PATCH 2/4] Small improvements --- modules/topicsFpdModule.js | 28 ++++++++++++----------- test/spec/modules/topicsFpdModule_spec.js | 5 ++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 4deb9bc3f99..e87767b5dba 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -2,27 +2,29 @@ import {logError, mergeDeep} from '../src/utils.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {submodule} from '../src/hook.js'; +export const TOPICS_TAXONOMY = 600; + export function getTopicsData(name, topics) { - const data = Object.entries( + return Object.entries( topics.reduce((byTaxVersion, topic) => { const taxv = topic.taxonomyVersion; if (!byTaxVersion.hasOwnProperty(taxv)) byTaxVersion[taxv] = []; byTaxVersion[taxv].push(topic.topic); return byTaxVersion; }, {}) - ).map(([taxv, topics]) => ({ - ext: { - segtax: 600, - segclass: taxv - }, - segment: topics.map((topic) => ({id: topic.toString()})) - })); - if (name != null) { - data.forEach((datum) => { + ).map(([taxv, topics]) => { + const datum = { + ext: { + segtax: TOPICS_TAXONOMY, + segclass: taxv + }, + segment: topics.map((topic) => ({id: topic.toString()})) + }; + if (name != null) { datum.name = name; - }); - } - return data; + } + return datum; + }); } export function getTopics(doc = document) { diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js index ea6d18c3f20..017cfc11720 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -1,4 +1,4 @@ -import {getTopics, getTopicsData, processFpd} from '../../../modules/topicsFpdModule.js'; +import {getTopics, getTopicsData, processFpd, TOPICS_TAXONOMY} from '../../../modules/topicsFpdModule.js'; import {deepClone} from '../../../src/utils.js'; describe('getTopicsData', () => { @@ -70,10 +70,11 @@ describe('getTopicsData', () => { ].forEach(({t, topics, expected}) => { it(`on ${t}`, () => { const actual = getTopicsData('mockName', topics); + expect(actual.length).to.eql(expected.length); actual.forEach((data, i) => { sinon.assert.match(data, expected[i]); expect(data.name).to.equal('mockName'); - expect(data.ext.segtax).to.eql(600); + expect(data.ext.segtax).to.eql(TOPICS_TAXONOMY); }) }); From 2388b0674a50772e07da2f6ef9220903f38f7488 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Fri, 8 Jul 2022 14:46:29 -0700 Subject: [PATCH 3/4] Map taxonomyVersion to segtax, modelVersion to segclass --- modules/topicsFpdModule.js | 61 ++++++----- test/spec/modules/topicsFpdModule_spec.js | 120 +++++++++++++++++----- 2 files changed, 133 insertions(+), 48 deletions(-) diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index e87767b5dba..218be1055d1 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -1,30 +1,45 @@ -import {logError, mergeDeep} from '../src/utils.js'; +import {logError, logWarn, mergeDeep} from '../src/utils.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {submodule} from '../src/hook.js'; -export const TOPICS_TAXONOMY = 600; +const TAXONOMIES = { + // map from topic taxonomyVersion to IAB segment taxonomy + '1': 600 +} -export function getTopicsData(name, topics) { - return Object.entries( - topics.reduce((byTaxVersion, topic) => { - const taxv = topic.taxonomyVersion; - if (!byTaxVersion.hasOwnProperty(taxv)) byTaxVersion[taxv] = []; - byTaxVersion[taxv].push(topic.topic); - return byTaxVersion; - }, {}) - ).map(([taxv, topics]) => { - const datum = { - ext: { - segtax: TOPICS_TAXONOMY, - segclass: taxv - }, - segment: topics.map((topic) => ({id: topic.toString()})) - }; - if (name != null) { - datum.name = name; - } - return datum; - }); +function partitionBy(field, items) { + return items.reduce((partitions, item) => { + const key = item[field]; + if (!partitions.hasOwnProperty(key)) partitions[key] = []; + partitions[key].push(item); + return partitions; + }, {}); +} + +export function getTopicsData(name, topics, taxonomies = TAXONOMIES) { + return Object.entries(partitionBy('taxonomyVersion', topics)) + .filter(([taxonomyVersion]) => { + if (!taxonomies.hasOwnProperty(taxonomyVersion)) { + logWarn(`Unrecognized taxonomyVersion from Topics API: "${taxonomyVersion}"; topic will be ignored`); + return false; + } + return true; + }).flatMap(([taxonomyVersion, topics]) => + Object.entries(partitionBy('modelVersion', topics)) + .map(([modelVersion, topics]) => { + const datum = { + ext: { + segtax: taxonomies[taxonomyVersion], + segclass: modelVersion + }, + segment: topics.map((topic) => ({id: topic.topic.toString()})) + }; + if (name != null) { + datum.name = name; + } + return datum; + }) + ); } export function getTopics(doc = document) { diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js index 017cfc11720..3781768497b 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -1,14 +1,22 @@ -import {getTopics, getTopicsData, processFpd, TOPICS_TAXONOMY} from '../../../modules/topicsFpdModule.js'; +import {getTopics, getTopicsData, processFpd} from '../../../modules/topicsFpdModule.js'; import {deepClone} from '../../../src/utils.js'; describe('getTopicsData', () => { - function makeTopic(topic, taxv) { + function makeTopic(topic, modelv, taxv = '1') { return { topic, - taxonomyVersion: taxv + taxonomyVersion: taxv, + modelVersion: modelv } } + function byTaxClass(segments) { + return segments.reduce((memo, segment) => { + memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; + return memo; + }, {}) + } + [ { t: 'no topics', @@ -17,11 +25,12 @@ describe('getTopicsData', () => { }, { t: 'single topic', - topics: [makeTopic(123, 'v1')], + topics: [makeTopic(123, 'm1')], expected: [ { ext: { - segclass: 'v1' + segtax: 600, + segclass: 'm1' }, segment: [ {id: '123'} @@ -30,12 +39,13 @@ describe('getTopicsData', () => { ] }, { - t: 'multiple topics with the same taxonomy version', - topics: [makeTopic(123, 'v1'), makeTopic(321, 'v1')], + t: 'multiple topics with the same model version', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], expected: [ { ext: { - segclass: 'v1' + segtax: 600, + segclass: 'm1' }, segment: [ {id: '123'}, @@ -45,12 +55,13 @@ describe('getTopicsData', () => { ] }, { - t: 'multiple topics with different taxonomy versions', - topics: [makeTopic(1, 'v1'), makeTopic(2, 'v1'), makeTopic(3, 'v2')], + t: 'multiple topics with different model versions', + topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], expected: [ { ext: { - segclass: 'v1' + segtax: 600, + segclass: 'm1' }, segment: [ {id: '1'}, @@ -59,28 +70,87 @@ describe('getTopicsData', () => { }, { ext: { - segclass: 'v2' + segtax: 600, + segclass: 'm2' }, segment: [ {id: '3'} ] } ] + }, + { + t: 'multiple topics, some with a taxonomy version other than "1"', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics in multiple taxonomies', + taxonomies: { + '1': 600, + '2': 601 + }, + topics: [ + makeTopic(123, 'm1', '1'), + makeTopic(321, 'm1', '2'), + makeTopic(213, 'm2', '1'), + ], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + }, + { + ext: { + segtax: 601, + segclass: 'm1', + }, + segment: [ + {id: '321'} + ] + }, + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '213'} + ] + } + ] } - ].forEach(({t, topics, expected}) => { - it(`on ${t}`, () => { - const actual = getTopicsData('mockName', topics); - expect(actual.length).to.eql(expected.length); - actual.forEach((data, i) => { - sinon.assert.match(data, expected[i]); - expect(data.name).to.equal('mockName'); - expect(data.ext.segtax).to.eql(TOPICS_TAXONOMY); - }) - }); + ].forEach(({t, topics, expected, taxonomies}) => { + describe(`on ${t}`, () => { + it('should convert topics to user.data segments correctly', () => { + const actual = getTopicsData('mockName', topics, taxonomies); + expect(actual.length).to.eql(expected.length); + expected = byTaxClass(expected); + Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { + sinon.assert.match(datum, expected[key]); + expect(datum.name).to.equal('mockName'); + }) + }); - it('should not set name if null', () => { - getTopicsData(null, topics).forEach((data) => { - expect(data.hasOwnProperty('name')).to.be.false; + it('should not set name if null', () => { + getTopicsData(null, topics).forEach((data) => { + expect(data.hasOwnProperty('name')).to.be.false; + }) }) }) }) From 4f511811d3b5d3938215534cc93488c3e178c995 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 20 Jul 2022 10:41:05 -0700 Subject: [PATCH 4/4] Convert fpdModule & topicsFpdModule to use GreedyPromise --- modules/fpdModule/index.js | 6 +++--- modules/topicsFpdModule.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/fpdModule/index.js b/modules/fpdModule/index.js index bc6461b57cc..553cab47b66 100644 --- a/modules/fpdModule/index.js +++ b/modules/fpdModule/index.js @@ -5,6 +5,7 @@ import { config } from '../../src/config.js'; import { module, getHook } from '../../src/hook.js'; import {logError} from '../../src/utils.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; let submodules = []; @@ -18,13 +19,12 @@ export function reset() { export function processFpd({global = {}, bidder = {}} = {}) { let modConf = config.getConfig('firstPartyData') || {}; - // TODO: convert this to GreedyPromise once #8626 gets merged - let result = Promise.resolve({global, bidder}); + let result = GreedyPromise.resolve({global, bidder}); submodules.sort((a, b) => { return ((a.queue || 1) - (b.queue || 1)); }).forEach(submodule => { result = result.then( - ({global, bidder}) => Promise.resolve(submodule.processFpd(modConf, {global, bidder})) + ({global, bidder}) => GreedyPromise.resolve(submodule.processFpd(modConf, {global, bidder})) .catch((err) => { logError(`Error in FPD module ${submodule.name}`, err); return {}; diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 218be1055d1..dbd1d3e90e9 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -1,6 +1,7 @@ import {logError, logWarn, mergeDeep} from '../src/utils.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {submodule} from '../src/hook.js'; +import {GreedyPromise} from '../src/utils/promise.js'; const TAXONOMIES = { // map from topic taxonomyVersion to IAB segment taxonomy @@ -46,14 +47,13 @@ export function getTopics(doc = document) { let topics = null; try { if ('browsingTopics' in doc && doc.featurePolicy.allowsFeature('browsing-topics')) { - topics = doc.browsingTopics(); + topics = GreedyPromise.resolve(doc.browsingTopics()); } } catch (e) { logError('Could not call topics API', e); } if (topics == null) { - // TODO: convert this to GreedyPromise once #8626 gets merged - topics = Promise.resolve([]); + topics = GreedyPromise.resolve([]); } return topics; }