Skip to content

Commit

Permalink
GrowthCode RTD : initial release (#9852)
Browse files Browse the repository at this point in the history
* The New RTD Module

* GrowthCode new RTD Module

* Fixed to Prebid
Added Testing
Added Docs

* Fixed to Prebid
Added Testing
Added Docs

* Completed testing spec

* Update the MD file to provide more infomation about what the module does

* Update sample to point to the correct server for testing.
  • Loading branch information
southern-growthcode authored May 30, 2023
1 parent 4a3d5a7 commit 122d624
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 9 deletions.
29 changes: 20 additions & 9 deletions integrationExamples/gpt/growthcode.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
<script async src="../../build/dev/prebid.js"></script>
<script async src="https://www.googletagservices.com/tag/js/gpt.js"></script>
<script>
var FAILSAFE_TIMEOUT = 3300;
var PREBID_TIMEOUT = 1000;
var FAILSAFE_TIMEOUT = 33000;
var PREBID_TIMEOUT = 10000;

var adUnits = [{
debugging: {
enabled: true
},
code: 'div-gpt-ad-1460505748561-0',
mediaTypes: {
banner: {
Expand All @@ -23,6 +26,11 @@
params: {
placementId: 13144370
}
},{
bidder: 'criteo',
params: {
zoneId: 497747
},
}],
}];

Expand Down Expand Up @@ -67,12 +75,15 @@
pbjs.setConfig({
debugging: {
enabled: true,
bids: [{
bidder: 'appnexus',
adUnitCode: '/19968336/header-bid-tag-0',
cpm: 1.5,
adId: '111111',
ad: '<html><body><img src="https://files.prebid.org/creatives/prebid300x250.png"></body></html>'
},
realTimeData: {
auctionDelay: 1000,
dataProviders: [{
name: 'growthCodeRtd',
waitForIt: true,
params: {
pid: 'TEST01',
}
}]
},
userSync: {
Expand All @@ -81,7 +92,7 @@
storage: {
type: "html5",
name: "_sharedID", // create a cookie with this name
expires: 365 // expires in 1 years
expires: 365 // expires in 1 year
}
},{
name: 'growthCodeId',
Expand Down
130 changes: 130 additions & 0 deletions modules/growthCodeRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* This module adds GrowthCode HEM and other Data to Bid Requests
* @module modules/growthCodeRtdProvider
*/
import { submodule } from '../src/hook.js'
import { getStorageManager } from '../src/storageManager.js';
import {
logMessage, logError, tryAppendQueryString, mergeDeep
} from '../src/utils.js';
import * as ajax from '../src/ajax.js';

const MODULE_NAME = 'growthCodeRtd';
const LOG_PREFIX = 'GrowthCodeRtd: ';
const ENDPOINT_URL = 'https://p2.gcprivacy.com/v2/rtd?'
const RTD_EXPIRE_KEY = 'gc_rtd_expires_at'
const RTD_CACHE_KEY = 'gc_rtd_items'

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

export const growthCodeRtdProvider = {
name: MODULE_NAME,
init: init,
getBidRequestData: alterBidRequests,
addData: addData,
callServer: callServer
};

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

/**
* Init The RTD Module
* @param config
* @param userConsent
* @returns {boolean}
*/
function init(config, userConsent) {
logMessage(LOG_PREFIX + 'Init RTB');

if (config == null) {
return false
}

const configParams = (config && config.params) || {};
let expiresAt = parseInt(storage.getDataFromLocalStorage(RTD_EXPIRE_KEY, null));

items = tryParse(storage.getDataFromLocalStorage(RTD_CACHE_KEY, null));

return callServer(configParams, items, expiresAt, userConsent);
}
function callServer(configParams, items, expiresAt, userConsent) {
// Expire Cache
let now = Math.trunc(Date.now() / 1000);
if ((!isNaN(expiresAt)) && (now > expiresAt)) {
expiresAt = NaN;
storage.removeDataFromLocalStorage(RTD_CACHE_KEY, null)
storage.removeDataFromLocalStorage(RTD_EXPIRE_KEY, null)
}
if ((items === null) && (isNaN(expiresAt))) {
let gcid = localStorage.getItem('gcid')

let url = configParams.url ? configParams.url : ENDPOINT_URL;
url = tryAppendQueryString(url, 'pid', configParams.pid);
url = tryAppendQueryString(url, 'u', window.location.href);
url = tryAppendQueryString(url, 'gcid', gcid);
if ((userConsent !== null) && (userConsent.gdpr !== null) && (userConsent.gdpr.consentData.getTCData.tcString)) {
url = tryAppendQueryString(url, 'tcf', userConsent.gdpr.consentData.getTCData.tcString)
}

ajax.ajaxBuilder()(url, {
success: response => {
let respJson = tryParse(response);
// If response is a valid json and should save is true
if (respJson && respJson.results >= 1) {
storage.setDataInLocalStorage(RTD_CACHE_KEY, JSON.stringify(respJson.items), null);
storage.setDataInLocalStorage(RTD_EXPIRE_KEY, respJson.expires_at, null)
} else {
storage.setDataInLocalStorage(RTD_EXPIRE_KEY, respJson.expires_at, null)
}
},
error: error => {
logError(LOG_PREFIX + 'ID fetch encountered an error', error);
}
}, undefined, {method: 'GET', withCredentials: true})
}

return true;
}

function addData(reqBidsConfigObj, items) {
let merge = false

for (let j = 0; j < items.length; j++) {
let item = items[j]
let data = JSON.parse(item.parameters);
if (item['attachment_point'] === 'data') {
mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, data)
merge = true
}
}
return merge
}

/**
* Alter the Bid Request for additional information such as HEM or 3rd Party Ids
* @param reqBidsConfigObj
* @param callback
* @param config
* @param userConsent
*/
function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) {
if (items != null) {
addData(reqBidsConfigObj, items)
}
callback();
}

submodule('realTimeData', growthCodeRtdProvider);
55 changes: 55 additions & 0 deletions modules/growthCodeRtdProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## GrowthCode Real-time Data Submodule

The [GrowthCode](https://growthcode.io) real-time data module in Prebid enables publishers to fully
leverage the potential of their first-party audiences and contextual data.
With an integrated cookieless GrowthCode identity, this module offers real-time
contextual and audience segmentation (IAB Taxonomy 2.2, cattax: 6) capabilities, and HEMs that can seamlessly
integrate into your existing Prebid deployment, making it easy to maximize
your advertising strategies.

## Building Prebid with GrowthCode Support

Compile the GrowthCode RTD module into your Prebid build:

`gulp serve --modules=userId,rtdModule,appnexusBidAdapter,growthCodeRtdProvider,sharedIdSystem,criteoBidAdapter`

Please visit https://growthcode.io/ for more information.

```
pbjs.setConfig(
...
realTimeData: {
auctionDelay: 1000,
dataProviders: [
{
name: 'growthCodeRtd',
waitForIt: true,
params: {
pid: 'TEST01',
}
}
]
}
...
}
```

### Parameter Descriptions for the GrowthCode Configuration Section

| Name | Type | Description | Notes |
|:---------------------------------|:--------|:--------------------------------------------------------------------------|:----------------------------|
| name | String | Real time data module name | Always 'growthCodeRtd' |
| waitForIt | Boolean | Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false |
| params | Object | | |
| params.pid | String | This is the Parter ID value obtained from GrowthCode | `TEST01` |
| params.url | String | Custom URL for server | Optional |

## Testing

To view an example of GrowthCode backends:

`gulp serve --modules=userId,rtdModule,appnexusBidAdapter,growthCodeRtdProvider,sharedIdSystem,criteoBidAdapter`

and then point your browser at:

`http://localhost:9999/integrationExamples/gpt/growthcode.html`
127 changes: 127 additions & 0 deletions test/spec/modules/growthCodeRtdProvider_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {config} from 'src/config.js';
import {growthCodeRtdProvider} from '../../../modules/growthCodeRtdProvider';
import sinon from 'sinon';
import * as ajaxLib from 'src/ajax.js';

const sampleConfig = {
name: 'growthCodeRtd',
waitForIt: true,
params: {
pid: 'TEST01',
}
}

describe('growthCodeRtdProvider', function() {
beforeEach(function() {
config.resetConfig();
});

afterEach(function () {
});

describe('growthCodeRtdSubmodule', function() {
it('test bad config instantiates', function () {
const ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => {
return (url, cbObj) => {
cbObj.success('{"status":"ok","version":"1.0.0","results":1,"items":[{"bidder":"client_a","attachment_point":"data","parameters":"{\\"client_a\\":{\\"user\\":{\\"ext\\":{\\"data\\":{\\"eids\\":[{\\"source\\":\\"\\",\\"uids\\":[{\\"id\\":\\"4254074976bb6a6d970f5f693bd8a75c\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemmd5\\"}},{\\"id\\":\\"d0ee291572ffcfba0bf7edb2b1c90ca7c32d255e5040b8b50907f5963abb1898\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemsha256\\"}}]}]}}}}}"}],"expires_at":1685029931}')
}
});
expect(growthCodeRtdProvider.init(null, null)).to.equal(false);
ajaxStub.restore()
});
it('successfully instantiates', function () {
const ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => {
return (url, cbObj) => {
cbObj.success('{"status":"ok","version":"1.0.0","results":1,"items":[{"bidder":"client_a","attachment_point":"data","parameters":"{\\"client_a\\":{\\"user\\":{\\"ext\\":{\\"data\\":{\\"eids\\":[{\\"source\\":\\"\\",\\"uids\\":[{\\"id\\":\\"4254074976bb6a6d970f5f693bd8a75c\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemmd5\\"}},{\\"id\\":\\"d0ee291572ffcfba0bf7edb2b1c90ca7c32d255e5040b8b50907f5963abb1898\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemsha256\\"}}]}]}}}}}"}],"expires_at":1685029931}')
}
});
expect(growthCodeRtdProvider.init(sampleConfig, null)).to.equal(true);
ajaxStub.restore()
});
it('successfully instantiates (cached)', function () {
const ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => {
return (url, cbObj) => {
cbObj.success('{"status":"ok","version":"1.0.0","results":1,"items":[{"bidder":"client_a","attachment_point":"data","parameters":"{\\"client_a\\":{\\"user\\":{\\"ext\\":{\\"data\\":{\\"eids\\":[{\\"source\\":\\"\\",\\"uids\\":[{\\"id\\":\\"4254074976bb6a6d970f5f693bd8a75c\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemmd5\\"}},{\\"id\\":\\"d0ee291572ffcfba0bf7edb2b1c90ca7c32d255e5040b8b50907f5963abb1898\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemsha256\\"}}]}]}}}}}"}],"expires_at":1685029931}')
}
});
const localStoreItem = '[{"bidder":"client_a","attachment_point":"data","parameters":"{\\"client_a\\":{\\"user\\":{\\"ext\\":{\\"data\\":{\\"eids\\":[{\\"source\\":\\"\\",\\"uids\\":[{\\"id\\":\\"4254074976bb6a6d970f5f693bd8a75c\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemmd5\\"}},{\\"id\\":\\"d0ee291572ffcfba0bf7edb2b1c90ca7c32d255e5040b8b50907f5963abb1898\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemsha256\\"}}]}]}}}}}"}]'
expect(growthCodeRtdProvider.callServer(sampleConfig, localStoreItem, '1965949885', null)).to.equal(true);
ajaxStub.restore()
});
it('successfully instantiates (cached,expire)', function () {
const ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => {
return (url, cbObj) => {
cbObj.success('{"status":"ok","version":"1.0.0","results":1,"items":[{"bidder":"client_a","attachment_point":"data","parameters":"{\\"client_a\\":{\\"user\\":{\\"ext\\":{\\"data\\":{\\"eids\\":[{\\"source\\":\\"\\",\\"uids\\":[{\\"id\\":\\"4254074976bb6a6d970f5f693bd8a75c\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemmd5\\"}},{\\"id\\":\\"d0ee291572ffcfba0bf7edb2b1c90ca7c32d255e5040b8b50907f5963abb1898\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemsha256\\"}}]}]}}}}}"}],"expires_at":1685029931}')
}
});
const localStoreItem = '[{"bidder":"client_a","attachment_point":"data","parameters":"{\\"client_a\\":{\\"user\\":{\\"ext\\":{\\"data\\":{\\"eids\\":[{\\"source\\":\\"\\",\\"uids\\":[{\\"id\\":\\"4254074976bb6a6d970f5f693bd8a75c\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemmd5\\"}},{\\"id\\":\\"d0ee291572ffcfba0bf7edb2b1c90ca7c32d255e5040b8b50907f5963abb1898\\",\\"atype\\":3,\\"ext\\":{\\"stype\\":\\"hemsha256\\"}}]}]}}}}}"}]'
expect(growthCodeRtdProvider.callServer(sampleConfig, localStoreItem, '1679188732', null)).to.equal(true);
ajaxStub.restore()
});

it('test no items response', function () {
const ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => {
return (url, cbObj) => {
cbObj.success('{}')
}
});
expect(growthCodeRtdProvider.callServer(sampleConfig, null, '1679188732', null)).to.equal(true);
ajaxStub.restore();
});

it('ajax error response', function () {
const ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => {
return (url, cbObj) => {
cbObj.error();
}
});
expect(growthCodeRtdProvider.callServer(sampleConfig, null, '1679188732', null)).to.equal(true);
ajaxStub.restore();
});

it('test alterBid data merge into ortb2 data (bidder)', function() {
const gcData =
{
'client_a':
{
'user':
{'ext':
{'data':
{'eids': [
{'source': 'test.com',
'uids': [
{
'id': '4254074976bb6a6d970f5f693bd8a75c',
'atype': 3,
'ext': {
'stype': 'hemmd5'}
}, {
'id': 'd0ee291572ffcfba0bf7edb2b1c90ca7c32d255e5040b8b50907f5963abb1898',
'atype': 3,
'ext': {
'stype': 'hemsha256'
}
}
]
}
]
}
}
}
}
};

const payload = [
{
'bidder': 'client_a',
'attachment_point': 'data',
'parameters': JSON.stringify(gcData)
}]

const bidConfig = {ortb2Fragments: {bidder: {}}};
growthCodeRtdProvider.addData(bidConfig, payload)

expect(bidConfig.ortb2Fragments.bidder).to.deep.equal(gcData)
});
});
});

0 comments on commit 122d624

Please sign in to comment.