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

GrowthCode ID Module: initial module release #9011

Merged
merged 13 commits into from
Sep 27, 2022
Merged
164 changes: 164 additions & 0 deletions modules/growthCodeIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* This module adds GrowthCodeId to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/growthCodeIdSystem
* @requires module:modules/userId
*/

import {logError, logInfo, tryAppendQueryString} from '../src/utils.js';
import {ajax} from '../src/ajax.js';
import { submodule } from '../src/hook.js'
import { getStorageManager } from '../src/storageManager.js';

const GCID_EXPIRY = 45;
const MODULE_NAME = 'growthCodeId';
const GC_DATA_KEY = '_gc_data';
const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb?'

export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME });

/**
* Read GrowthCode data from cookie or local storage
* @param key
* @return {string}
*/
export function readData(key) {
try {
if (storage.hasLocalStorage()) {
return storage.getDataFromLocalStorage(key);
}
if (storage.cookiesAreEnabled()) {
return storage.getCookie(key);
}
} catch (error) {
logError(error);
}
}

/**
* Store GrowthCode data in either cookie or local storage
* expiration date: 45 days
* @param key
* @param {string} value
*/
function storeData(key, value) {
try {
logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value);

if (value) {
if (storage.hasLocalStorage()) {
storage.setDataInLocalStorage(key, value);
}
const expiresStr = (new Date(Date.now() + (GCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString();
if (storage.cookiesAreEnabled()) {
storage.setCookie(key, value, expiresStr, 'LAX');
}
}
} catch (error) {
logError(error);
}
}

/**
* Parse json if possible, else return null
* @param data
* @param {object|null}
*/
function tryParse(data) {
try {
return JSON.parse(data);
} catch (err) {
logError(err);
return null;
}
}

/** @type {Submodule} */
export const growthCodeIdSubmodule = {
/**
* used to link submodule with config
* @type {string}
*/
name: MODULE_NAME,
/**
* decode the stored id value for passing to bid requests
* @function
* @param {{string}} value
* @returns {{growthCodeId: {string}}|undefined}
*/
decode(value) {
return value && value !== '' ? { 'growthCodeId': value } : undefined;
},
/**
* performs action to obtain id and return a value in the callback's response argument
* @function
* @param {SubmoduleConfig} [config]
* @returns {IdResponse|undefined}
*/
getId(config, consentData) {
const configParams = (config && config.params) || {};
if (!configParams || typeof configParams.pid !== 'string') {
logError('User ID - GrowthCodeID submodule requires a valid Partner ID to be defined');
return;
}

const gdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0;
const consentString = gdpr ? consentData.consentString : '';
if (gdpr && !consentString) {
logInfo('Consent string is required to call GrowthCode id.');
return;
}

let publisherId = configParams.publisher_id ? configParams.publisher_id : '_sharedID';

let sharedId;
if (configParams.publisher_id_storage === 'html5') {
sharedId = storage.getDataFromLocalStorage(publisherId, null) ? (storage.getDataFromLocalStorage(publisherId, null)) : null;
} else {
sharedId = storage.getCookie(publisherId, null) ? (storage.getCookie(publisherId, null)) : null;
}
if (!sharedId) {
logError('User ID - Publisher ID is not correctly setup.');
}

const resp = function(callback) {
let gcData = tryParse(readData(GC_DATA_KEY));
if (gcData) {
callback(gcData);
} else {
let segment = window.location.pathname.substr(1).replace(/\/+$/, '');
if (segment === '') {
segment = 'home';
}

let url = configParams.url ? configParams.url : ENDPOINT_URL;
url = tryAppendQueryString(url, 'pid', configParams.pid);
url = tryAppendQueryString(url, 'uid', sharedId);
url = tryAppendQueryString(url, 'u', window.location.href);
url = tryAppendQueryString(url, 'h', window.location.hostname);
url = tryAppendQueryString(url, 's', segment);
url = tryAppendQueryString(url, 'r', document.referrer);

ajax(url, {
success: response => {
let respJson = tryParse(response);
// If response is a valid json and should save is true
if (respJson) {
storeData(GC_DATA_KEY, JSON.stringify(respJson))
callback(respJson);
} else {
callback();
}
},
error: error => {
logError(MODULE_NAME + ': ID fetch encountered an error', error);
callback();
}
}, undefined, {method: 'GET', withCredentials: true})
}
};
return { callback: resp };
}
};

submodule('userId', growthCodeIdSubmodule);
37 changes: 37 additions & 0 deletions modules/growthCodeIdSystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## GrowthCode User ID Submodule

GrowthCode provides Id Enrichment for requests.

## Building Prebid with GrowthCode Support

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

```
gulp build --modules=growthCodeIdSystem,userId
```

The following configuration parameters are available:

```javascript
pbjs.setConfig({
userSync: {
userIds: [{
name: 'growthCodeId',
params: {
pid: 'TEST01', // Set your Partner ID here for production (obtained from Growthcode)
publisher_id: '_sharedID',
publisher_id_storage: 'html5'
}
}]
}
});
```

| Param under userSync.userIds[] | Scope | Type | Description | Example |
|--------------------------------|----------|--------| --- |-----------------|
| name | Required | String | The name of this module. | `"growthCodeId"` |
| params | Required | Object | Details of module params. | |
| params.pid | Required | String | This is the Parter ID value obtained from GrowthCode | `"TEST01"` |
| params.url | Optional | String | Custom URL for server | |
| params.publisher_id | Optional | String | Name if the variable that holds your publisher ID | `"_sharedID"` |
| params.publisher_id_storage | Optional | String | Publisher ID storage (cookie, html5) | `"html5"` |
19 changes: 19 additions & 0 deletions modules/userId/eids.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ export const USER_IDS_CONFIG = {

// key-name : {config}

// GrowthCode
'growthCodeId': {
getValue: function(data) {
return data.gc_id
},
source: 'growthcode.io',
atype: 1,
getUidExt: function(data) {
const extendedData = pick(data, [
'h1',
'h2',
'h3',
]);
if (Object.keys(extendedData).length) {
return extendedData;
}
}
},

// trustpid
'trustpid': {
source: 'trustpid.com',
Expand Down
83 changes: 83 additions & 0 deletions test/spec/modules/growthCodeIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { growthCodeIdSubmodule } from 'modules/growthCodeIdSystem.js';
import * as utils from 'src/utils.js';
import { server } from 'test/mocks/xhr.js';
import { uspDataHandler } from 'src/adapterManager.js';
import {expect} from 'chai';
import {getStorageManager} from '../../../src/storageManager.js';

const GCID_EXPIRY = 45;
const MODULE_NAME = 'growthCodeId';
const SHAREDID = 'fe9c5c89-7d56-4666-976d-e07e73b3b664';

export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME });

const getIdParams = {params: {
pid: 'TEST01',
publisher_id: '_sharedid',
publisher_id_storage: 'html5',
}};

describe('growthCodeIdSystem', () => {
let logErrorStub;

beforeEach(function () {
logErrorStub = sinon.stub(utils, 'logError');
storage.setDataInLocalStorage('_sharedid', SHAREDID);
const expiresStr = (new Date(Date.now() + (GCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString();
if (storage.cookiesAreEnabled()) {
storage.setCookie('_sharedid', SHAREDID, expiresStr, 'LAX');
}
});

afterEach(function () {
logErrorStub.restore();
});

describe('name', () => {
it('should expose the name of the submodule', () => {
expect(growthCodeIdSubmodule.name).to.equal('growthCodeId');
});
});

it('should NOT call the growthcode id endpoint if gdpr applies but consent string is missing', function () {
let submoduleCallback = growthCodeIdSubmodule.getId(getIdParams, { gdprApplies: true }, undefined);
expect(submoduleCallback).to.be.undefined;
});

it('should log an error if pid configParam was not passed when getId', function () {
growthCodeIdSubmodule.getId();
expect(logErrorStub.callCount).to.be.equal(1);
});

it('should log an error if sharedId (LocalStore) is not setup correctly', function () {
growthCodeIdSubmodule.getId({params: {
pid: 'TEST01',
publisher_id: '_sharedid_bad',
publisher_id_storage: 'html5',
}});
expect(logErrorStub.callCount).to.be.equal(1);
});

it('should log an error if sharedId (LocalStore) is not setup correctly', function () {
growthCodeIdSubmodule.getId({params: {
pid: 'TEST01',
publisher_id: '_sharedid_bad',
publisher_id_storage: 'cookie',
}});
expect(logErrorStub.callCount).to.be.equal(1);
});

it('should call the growthcode id endpoint', function () {
let callBackSpy = sinon.spy();
let submoduleCallback = growthCodeIdSubmodule.getId(getIdParams).callback;
submoduleCallback(callBackSpy);
let request = server.requests[0];
expect(request.url.substr(0, 85)).to.be.eq('https://p2.gcprivacy.com/v1/pb?pid=TEST01&uid=' + SHAREDID + '&u=');
request.respond(
200,
{},
JSON.stringify({})
);
expect(callBackSpy.calledOnce).to.be.true;
});
})