Skip to content

Commit

Permalink
Topics FPD module: initial release (#8630)
Browse files Browse the repository at this point in the history
* Topics FPD module

* Small improvements
  • Loading branch information
dgirardi authored and ahmadlob committed Jul 27, 2022
1 parent d9f3de5 commit 7097075
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 65 deletions.
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
],
"fpdModule": [
"enrichmentFpdModule",
"validationFpdModule"
"validationFpdModule",
"topicsFpdModule"
]
},
"libraries": {
Expand Down
22 changes: 16 additions & 6 deletions modules/fpdModule/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand All @@ -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() {
Expand Down
65 changes: 65 additions & 0 deletions modules/topicsFpdModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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) {
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;
});
}

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
});
127 changes: 69 additions & 58 deletions test/spec/modules/fpdModule_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
});
});
});

Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand All @@ -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 () {
Expand All @@ -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 () {
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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'}]}]);
});
});
});
});
Loading

0 comments on commit 7097075

Please sign in to comment.