diff --git a/modules/.submodules.json b/modules/.submodules.json index daa8b1421ce..ffac9d89d88 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -35,6 +35,7 @@ "quantcastIdSystem", "sharedIdSystem", "tapadIdSystem", + "tncIdSystem", "trustpidSystem", "uid2IdSystem", "unifiedIdSystem", diff --git a/modules/tncIdSystem.js b/modules/tncIdSystem.js new file mode 100644 index 00000000000..24e3c79d4df --- /dev/null +++ b/modules/tncIdSystem.js @@ -0,0 +1,63 @@ +import { submodule } from '../src/hook.js'; +import { logInfo } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; + +const MODULE_NAME = 'tncId'; +let url = null; + +const waitTNCScript = (tncNS) => { + return new Promise((resolve, reject) => { + var tnc = window[tncNS]; + if (!tnc) reject(new Error('No TNC Object')); + if (tnc.tncid) resolve(tnc.tncid); + tnc.ready(() => { + tnc = window[tncNS]; + if (tnc.tncid) resolve(tnc.tncid); + else tnc.on('data-sent', () => resolve(tnc.tncid)); + }); + }); +} + +const loadRemoteScript = () => { + return new Promise((resolve) => { + loadExternalScript(url, MODULE_NAME, resolve); + }) +} + +const tncCallback = function (cb) { + let tncNS = '__tnc'; + let promiseArray = []; + if (!window[tncNS]) { + tncNS = '__tncPbjs'; + promiseArray.push(loadRemoteScript()); + } + + return Promise.all(promiseArray).then(() => waitTNCScript(tncNS)).then(cb).catch(() => cb()); +} + +export const tncidSubModule = { + name: MODULE_NAME, + decode(id) { + return { + tncid: id + }; + }, + gvlid: 750, + getId(config, consentData) { + const gdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; + const consentString = gdpr ? consentData.consentString : ''; + + if (gdpr && !consentString) { + logInfo('Consent string is required for TNCID module'); + return; + } + + if (config.params && config.params.url) { url = config.params.url; } + + return { + callback: function (cb) { return tncCallback(cb); } + } + } +} + +submodule('userId', tncidSubModule) diff --git a/modules/tncIdSystem.md b/modules/tncIdSystem.md new file mode 100644 index 00000000000..f0f98e9098f --- /dev/null +++ b/modules/tncIdSystem.md @@ -0,0 +1,33 @@ +# TNCID UserID Module + +### Prebid Configuration + +First, make sure to add the TNCID submodule to your Prebid.js package with: + +``` +gulp build --modules=tncIdSystem,userId +``` + +### TNCIDIdSystem module Configuration + +You can configure this submodule in your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'tncId', + params: { + url: 'https://js.tncid.app/remote.min.js' //Optional + } + }], + syncDelay: 5000 + } +}); +``` +#### Configuration Params + +| Param Name | Required | Type | Description | +| --- | --- | --- | --- | +| name | Required | String | ID value for the TNCID module: `"tncId"` | +| params.url | Optional | String | Provide TNC fallback script URL, this script is loaded if there is no TNC script on page | diff --git a/modules/userId/eids.js b/modules/userId/eids.js index a077043a228..e6f50012d60 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -307,11 +307,18 @@ export const USER_IDS_CONFIG = { } }, + // tncId + 'tncid': { + source: 'thenewco.it', + atype: 3 + }, + // Gravito MP ID 'gravitompId': { source: 'gravito.net', atype: 1 }, + }; // this function will create an eid object for the given UserId sub-module diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 2682eca8dbc..2372bb4b6fd 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -232,6 +232,13 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 3 }] + }, + { + source: 'thenewco.it', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] } ] ``` diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 77a8b4b771f..4a6a9b515a8 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -356,3 +356,20 @@ pbjs.setConfig({ } }); ``` +``` + +Example showing how to configure a `params` object to pass directly to bid adapters + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'tncId', + params: { + providerId: "c8549079-f149-4529-a34b-3fa91ef257d1" + } + }], + syncDelay: 5000 + } +}); +``` diff --git a/src/adloader.js b/src/adloader.js index b167079d488..d156bcaf9be 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -11,6 +11,7 @@ const _approvedLoadExternalJSList = [ 'browsi', 'brandmetrics', 'justtag', + 'tncId', 'akamaidap', 'ftrackId', 'inskin', diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index ca971c37832..614e32f1296 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -330,6 +330,20 @@ describe('eids array generation for known sub-modules', function() { }] }); }); + it('tncid', function() { + const userId = { + tncid: 'TEST_TNCID' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'thenewco.it', + uids: [{ + id: 'TEST_TNCID', + atype: 3 + }] + }); + }); it('pubProvidedId', function() { const userId = { pubProvidedId: [{ diff --git a/test/spec/modules/tncIdSystem_spec.js b/test/spec/modules/tncIdSystem_spec.js new file mode 100644 index 00000000000..57c5fa63645 --- /dev/null +++ b/test/spec/modules/tncIdSystem_spec.js @@ -0,0 +1,109 @@ +import { tncidSubModule } from 'modules/tncIdSystem'; + +const consentData = { + gdprApplies: true, + consentString: 'GDPR_CONSENT_STRING' +}; + +describe('TNCID tests', function () { + describe('name', () => { + it('should expose the name of the submodule', () => { + expect(tncidSubModule.name).to.equal('tncId'); + }); + }); + + describe('gvlid', () => { + it('should expose the vendor id', () => { + expect(tncidSubModule.gvlid).to.equal(750); + }); + }); + + describe('decode', () => { + it('should wrap the given value inside an object literal', () => { + expect(tncidSubModule.decode('TNCID_TEST_ID')).to.deep.equal({ + tncid: 'TNCID_TEST_ID' + }); + }); + }); + + describe('getId', () => { + afterEach(function () { + Object.defineProperty(window, '__tnc', {value: undefined, configurable: true}); + Object.defineProperty(window, '__tncPbjs', {value: undefined, configurable: true}); + }); + + it('Should NOT give TNCID if GDPR applies but consent string is missing', function () { + const res = tncidSubModule.getId({}, { gdprApplies: true }); + expect(res).to.be.undefined; + }); + + it('GDPR is OK and page has no TNC script on page, script goes in error, no TNCID is returned', function () { + const completeCallback = sinon.spy(); + const {callback} = tncidSubModule.getId({}, consentData); + + return callback(completeCallback).then(() => { + expect(completeCallback.calledOnce).to.be.true; + }) + }); + + it('GDPR is OK and page has TNC script with ns: __tnc, present TNCID is returned', function () { + Object.defineProperty(window, '__tnc', { + value: { + ready: (readyFunc) => { readyFunc() }, + on: (name, cb) => { cb() }, + tncid: 'TNCID_TEST_ID_1', + providerId: 'TEST_PROVIDER_ID_1', + }, + configurable: true + }); + + const completeCallback = sinon.spy(); + const {callback} = tncidSubModule.getId({}, { gdprApplies: false }); + + return callback(completeCallback).then(() => { + expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_1')).to.be.true; + }) + }); + + it('GDPR is OK and page has TNC script with ns: __tnc but not loaded, TNCID is assigned and returned', function () { + Object.defineProperty(window, '__tnc', { + value: { + ready: (readyFunc) => { readyFunc() }, + on: (name, cb) => { cb() }, + providerId: 'TEST_PROVIDER_ID_1', + }, + configurable: true + }); + + const completeCallback = sinon.spy(); + const {callback} = tncidSubModule.getId({}, { gdprApplies: false }); + + return callback(completeCallback).then(() => { + expect(completeCallback.calledOnceWithExactly(undefined)).to.be.true; + }) + }); + + it('GDPR is OK and page has TNC script with ns: __tncPbjs, TNCID is returned', function () { + Object.defineProperty(window, '__tncPbjs', { + value: { + ready: (readyFunc) => { readyFunc() }, + on: (name, cb) => { + window.__tncPbjs.tncid = 'TNCID_TEST_ID_2'; + cb(); + }, + providerId: 'TEST_PROVIDER_ID_1', + options: {}, + }, + configurable: true, + writable: true + }); + + const completeCallback = sinon.spy(); + const {callback} = tncidSubModule.getId({params: {url: 'TEST_URL'}}, consentData); + + return callback(completeCallback).then(() => { + expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_2')).to.be.true; + }) + }); + }); +}); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 035c86a3cc1..49c07bb1334 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -38,6 +38,7 @@ import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; import {mwOpenLinkIdSubModule} from 'modules/mwOpenLinkIdSystem.js'; import {tapadIdSubmodule} from 'modules/tapadIdSystem.js'; +import {tncidSubModule} from 'modules/tncIdSystem.js'; import {getPrebidInternal} from 'src/utils.js'; import {uid2IdSubmodule} from 'modules/uid2IdSystem.js'; import {admixerIdSubmodule} from 'modules/admixerIdSystem.js'; @@ -784,9 +785,9 @@ describe('User ID', function () { expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); - it('config with 21 configurations should result in 21 submodules add', function () { + it('config with 22 configurations should result in 22 submodules add', function () { init(config); - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule, tncidSubModule]); config.setConfig({ userSync: { syncDelay: 0, @@ -847,10 +848,12 @@ describe('User ID', function () { }, { name: 'qid', storage: {name: 'qid', type: 'html5'} + }, { + name: 'tncId' }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 21 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 22 submodules'); }); it('config syncDelay updates module correctly', function () {