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 RTD : initial release #9852

Merged
merged 7 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
30 changes: 21 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,16 @@
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',
url: "http://localhost:8080/v2/rtd?"
}
}]
},
userSync: {
Expand All @@ -81,7 +93,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
ncolletti marked this conversation as resolved.
Show resolved Hide resolved

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 capabilities 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 build --modules=userId,rtdModule,growthCodeRtdProvider,appnexusBidAdapter`

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

```
pbjs.setConfig(
...
realTimeData: {
auctionDelay: 1000,
dataProviders: [
{
name: 'growthCodeRtd',
waitForIt: true,
params: {
pid: 'TEST01',
ncolletti marked this conversation as resolved.
Show resolved Hide resolved
}
}
]
}
...
}
```

### 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)
});
});
});