Skip to content

Commit

Permalink
TNC Id Module : user input url validation and optimizations (#12527)
Browse files Browse the repository at this point in the history
* Bug Fixes:
modules/tncIdSystem.js  - Optimized User ID Recovery: Replaced the existing user ID recovery function with a faster and more efficient method, improving performance.
modules/userId/userId.md - Documentation Correction: Resolved inconsistencies in the documentation, ensuring accurate information for module configuration and usage.

* - Tests fixed

* - TNCID module fix: "getTNCID is not a function" error

* modules/tncIdSystem.js
      - user input URL validations added
      - TNCID recovered from cookie storage if available
      - code optimizations for faster TNCID load
________________________________________
test/spec/modules/tncIdSystem_spec.js
      - added tests for new functions
________________________________________
modules/tncIdSystem.md
      - updated documentation

* - fixed lint errors

* - Sales description removed

* Looking forward for code approval.
  • Loading branch information
annavane authored Dec 26, 2024
1 parent 9b15b22 commit 24e1780
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 65 deletions.
130 changes: 108 additions & 22 deletions modules/tncIdSystem.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,114 @@
/**
* This module adds TncId to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/tncIdSystem
* @requires module:modules/userId
*/

import { submodule } from '../src/hook.js';
import { logInfo } from '../src/utils.js';
import { parseUrl, buildUrl, logInfo, logMessage, logError } from '../src/utils.js';
import { getStorageManager } from '../src/storageManager.js';
import { loadExternalScript } from '../src/adloader.js';
import { MODULE_TYPE_UID } from '../src/activities/modules.js';

/**
* @typedef {import('../modules/userId/index.js').Submodule} Submodule
* @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
* @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
* @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
*/

const MODULE_NAME = 'tncId';
let url = null;
const TNC_API_URL = 'https://js.tncid.app/remote.js';
const TNC_DEFAULT_NS = '__tnc';
const TNC_PREBID_NS = '__tncPbjs';
const TNC_PREBIDJS_PROVIDER_ID = 'c8549079-f149-4529-a34b-3fa91ef257d1';
const TNC_LOCAL_VALUE_KEY = 'tncid';
let moduleConfig = null;

export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME});

function fixURL(config, ns) {
config.params = (config && config.params) ? config.params : {};
config.params.url = config.params.url || TNC_API_URL;
let url = parseUrl(config.params.url);
url.search = url.search || {};
let providerId = config.params.publisherId || config.params.providerId || url.search.publisherId || url.search.providerId || TNC_PREBIDJS_PROVIDER_ID;
delete url.search.publisherId;
url.search.providerId = providerId;
url.search.ns = ns;
return url;
}

const waitTNCScript = (tncNS) => {
const loadRemoteScript = function(url) {
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(async () => {
let tncid = await tnc.getTNCID('prebid');
resolve(tncid);
});
let endpoint = buildUrl(url);
logMessage('TNC Endpoint', endpoint);
loadExternalScript(endpoint, MODULE_TYPE_UID, MODULE_NAME, resolve);
});
}

const loadRemoteScript = () => {
return new Promise((resolve) => {
loadExternalScript(url, MODULE_TYPE_UID, MODULE_NAME, resolve);
})
function TNCObject(ns) {
let tnc = window[ns];
tnc = typeof tnc !== 'undefined' && tnc !== null && typeof tnc.ready == 'function' ? tnc : {
ready: function(f) { this.ready.q = this.ready.q || []; return typeof f == 'function' ? (this.ready.q.push(f), this) : new Promise(resolve => this.ready.q.push(resolve)); },
};
window[ns] = tnc;
return tnc;
}

const tncCallback = function (cb) {
let tncNS = '__tnc';
let promiseArray = [];
if (!window[tncNS]) {
tncNS = '__tncPbjs';
promiseArray.push(loadRemoteScript());
function getlocalValue(key) {
let value;
if (storage.hasLocalStorage()) {
value = storage.getDataFromLocalStorage(key);
}
if (!value) {
value = storage.getCookie(key);
}

if (typeof value === 'string') {
// if it's a json object parse it and return the tncid value, otherwise assume the value is the id
if (value.charAt(0) === '{') {
try {
const obj = JSON.parse(value);
if (obj) {
return obj.tncid;
}
} catch (e) {
logError(e);
}
} else {
return value;
}
}
return null;
}

const tncCallback = async function(cb) {
try {
let tncNS = TNC_DEFAULT_NS;
let tncid = getlocalValue(TNC_LOCAL_VALUE_KEY);

if (!window[tncNS] || typeof window[tncNS].ready !== 'function') {
tncNS = TNC_PREBID_NS; // Register a new namespace for TNC global object
let url = fixURL(moduleConfig, tncNS);
if (!url) return cb();
TNCObject(tncNS); // create minimal TNC object
await loadRemoteScript(url); // load remote script
}
if (!tncid) {
await new Promise(resolve => window[tncNS].ready(resolve));
tncid = await window[tncNS].getTNCID('prebid'); // working directly with (possibly) overridden TNC Object
logMessage('tncId Module - tncid retrieved from remote script', tncid);
} else {
logMessage('tncId Module - tncid already exists', tncid);
window[tncNS].ready(() => window[tncNS].getTNCID('prebid'));
}
return cb(tncid);
} catch (err) {
logMessage('tncId Module', err);
return cb();
}
return Promise.all(promiseArray).then(() => waitTNCScript(tncNS)).then(cb).catch(() => cb());
}

export const tncidSubModule = {
Expand All @@ -42,6 +119,14 @@ export const tncidSubModule = {
};
},
gvlid: 750,
/**
* performs action to obtain id
* Use a tncid cookie first if it is present, otherwise callout to get a new id
* @function
* @param {SubmoduleConfig} [config] Config object with params and storage properties
* @param {ConsentData} [consentData] GDPR consent
* @returns {IdResponse}
*/
getId(config, consentData) {
const gdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0;
const consentString = gdpr ? consentData.consentString : '';
Expand All @@ -51,10 +136,11 @@ export const tncidSubModule = {
return;
}

if (config.params && config.params.url) { url = config.params.url; }
moduleConfig = config;

return {
callback: function (cb) { return tncCallback(cb); }
// callback: tncCallback
}
},
eids: {
Expand Down
32 changes: 22 additions & 10 deletions modules/tncIdSystem.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# TNCID UserID Module
# Overview

### Prebid Configuration
Module Name: tncIdSystem

## Prebid Configuration

First, make sure to add the TNCID submodule to your Prebid.js package with:

```
```bash
gulp build --modules=tncIdSystem,userId
```

### TNCIDIdSystem module Configuration
## TNCIdSystem module Configuration

Disclosure: This module loads external script unreviewed by the prebid.js community

Expand All @@ -20,16 +22,26 @@ pbjs.setConfig({
userIds: [{
name: 'tncId',
params: {
url: 'https://js.tncid.app/remote.min.js' //Optional
url: 'TNC-fallback-script-url' // Fallback url, not required if onpage tag is present (ask TNC for it)
},
storage: {
type: "cookie",
name: "tncid",
expires: 365 // in days
}
}],
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 |
## Configuration Params

The following configuration parameters are available:

| Param under userSync.userIds[] | Scope | Type | Description | Example |
| --- | --- | --- | --- | --- |
| name | Required | String | The name of this sub-module | `"tncId"` |
| params ||| Details for the sub-module initialization ||
| params.url | Optional | String | TNC script fallback URL - This script is loaded if there is no TNC script on page | `"https://js.tncid.app/remote.min.js"` |
| params.publisherId | Optional | String | Publisher ID used in TNC fallback script - As default Prebid specific Publisher ID is used | `"c8549079-f149-4529-a34b-3fa91ef257d1"` |
70 changes: 37 additions & 33 deletions test/spec/modules/tncIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { tncidSubModule } from 'modules/tncIdSystem';
import {attachIdSystem} from '../../../modules/userId/index.js';
import {createEidsArray} from '../../../modules/userId/eids.js';
import {expect} from 'chai/index.mjs';
import { attachIdSystem } from '../../../modules/userId/index.js';
import { createEidsArray } from '../../../modules/userId/eids.js';

const consentData = {
gdprApplies: true,
Expand Down Expand Up @@ -40,71 +39,76 @@ describe('TNCID tests', function () {
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 () {
it('Should NOT give TNCID if there is no TNC script on page and no fallback url in configuration', async function () {
const completeCallback = sinon.spy();
const {callback} = tncidSubModule.getId({}, consentData);

return callback(completeCallback).then(() => {
expect(completeCallback.calledOnce).to.be.true;
})
await callback(completeCallback);
expect(callback).to.be.an('function');
expect(completeCallback.calledOnceWithExactly()).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() },
tncid: 'TNCID_TEST_ID_1',
providerId: 'TEST_PROVIDER_ID_1',
},
configurable: true
});
it('Should NOT give TNCID if fallback script is not loaded correctly', async function () {
const completeCallback = sinon.spy();
const {callback} = tncidSubModule.getId({
params: { url: 'www.thenewco.tech' }
}, consentData);

await callback(completeCallback);
expect(completeCallback.calledOnceWithExactly()).to.be.true;
});

it(`Should call external script if TNC is not loaded on page`, async function() {
const completeCallback = sinon.spy();
const {callback} = tncidSubModule.getId({}, { gdprApplies: false });
const {callback} = tncidSubModule.getId({params: {url: 'https://www.thenewco.tech?providerId=test'}}, { gdprApplies: false });

return callback(completeCallback).then(() => {
expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_1')).to.be.true;
})
await callback(completeCallback);
expect(window).to.contain.property('__tncPbjs');
});

it('GDPR is OK and page has TNC script with ns: __tnc but not loaded, TNCID is assigned and returned', function () {
it('TNCID is returned if page has TNC script with ns: __tnc', async function () {
Object.defineProperty(window, '__tnc', {
value: {
ready: async (readyFunc) => { await readyFunc() },
ready: (readyFunc) => { readyFunc() },
getTNCID: async (name) => { return '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;
})
await callback(completeCallback);
expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_1')).to.be.true;
});

it('GDPR is OK and page has TNC script with ns: __tncPbjs, TNCID is returned', function () {
it('TNC script with ns __tncPbjs is created', async function () {
const completeCallback = sinon.spy();
const {callback} = tncidSubModule.getId({params: {url: 'TEST_URL'}}, consentData);

await callback(completeCallback);
expect(window).to.contain.property('__tncPbjs');
});

it('TNCID is returned if page has TNC script with ns: __tncPbjs', async function () {
Object.defineProperty(window, '__tncPbjs', {
value: {
ready: async (readyFunc) => { await readyFunc() },
ready: (readyFunc) => { readyFunc() },
getTNCID: async (name) => { return 'TNCID_TEST_ID_2' },
providerId: 'TEST_PROVIDER_ID_1',
options: {},
},
configurable: true,
writable: true
});

const completeCallback = sinon.spy();
const {callback} = tncidSubModule.getId({params: {url: 'TEST_URL'}}, consentData);
const {callback} = tncidSubModule.getId({params: {url: 'www.thenewco.tech'}}, consentData);

return callback(completeCallback).then(() => {
expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_2')).to.be.true;
})
await callback(completeCallback);
expect(completeCallback.calledOnceWithExactly('TNCID_TEST_ID_2')).to.be.true;
});
});

describe('eid', () => {
before(() => {
attachIdSystem(tncidSubModule);
Expand Down

0 comments on commit 24e1780

Please sign in to comment.