Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Topics FPD module: initial release #8630

Merged
merged 4 commits into from
Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Collaborator

@vkimcm vkimcm Jul 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing on Chrome beta, when you get topics like this from API,
[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":301,"version":"chrome.1:1:2206021246"},{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":3,"version":"chrome.1:1:2206021246"}]
datum looks like this
[{"name":"domain","ext":{"segtax":600,"segclass":"1"},"segment":[{"id":"301"},{"id":"3"}]}]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! just to confirm, "domain" is a placeholder you chose, not the actual value of name - right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it was placeholder

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

segclass should be "2206021246" here @dgirardi , not 1

});
}

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