Skip to content

Commit

Permalink
ID5 UserId module - integrate with TrueLink Id (prebid#11802)
Browse files Browse the repository at this point in the history
* ID5 User Id module - integrate with TrueLinkId

* ID5 User Id module - eids documentation

* ID5 User Id module - changed the documentation, so it doesn't use multiplication

Some partners are setting refreshInSeconds as string with value '8*3600', taken from the configuration.
By using just a number we can still parse it (but we don't want to parse mathematical operations)
  • Loading branch information
abazylewicz-id5 authored and DecayConstant committed Jul 18, 2024
1 parent ddc6edd commit f634053
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 7 deletions.
33 changes: 31 additions & 2 deletions modules/id5IdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const LOCAL_STORAGE = 'html5';
const LOG_PREFIX = 'User ID - ID5 submodule: ';
const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid';
const ID5_DOMAIN = 'id5-sync.com';
const TRUE_LINK_SOURCE = 'true-link-id5-sync.com';

// order the legacy cookie names in reverse priority order so the last
// cookie in the array is the most preferred to use
Expand Down Expand Up @@ -134,12 +135,13 @@ export const id5IdSubmodule = {
* @returns {(Object|undefined)}
*/
decode(value, config) {
let universalUid;
let universalUid, publisherTrueLinkId;
let ext = {};

if (value && typeof value.universal_uid === 'string') {
universalUid = value.universal_uid;
ext = value.ext || ext;
publisherTrueLinkId = value.publisherTrueLinkId;
} else {
return undefined;
}
Expand All @@ -159,6 +161,12 @@ export const id5IdSubmodule = {
};
}

if (publisherTrueLinkId) {
responseObj.trueLinkId = {
uid: publisherTrueLinkId,
};
}

const abTestingResult = deepAccess(value, 'ab_testing.result');
switch (abTestingResult) {
case 'control':
Expand Down Expand Up @@ -263,7 +271,22 @@ export const id5IdSubmodule = {
return data.ext;
}
}
},
'trueLinkId': {
getValue: function (data) {
return data.uid;
},
getSource: function (data) {
return TRUE_LINK_SOURCE;
},
atype: 1,
getUidExt: function (data) {
if (data.ext) {
return data.ext;
}
}
}

}
};

Expand Down Expand Up @@ -380,6 +403,8 @@ export class IdFetchFlow {
const referer = getRefererInfo();
const signature = (this.cacheIdObj && this.cacheIdObj.signature) ? this.cacheIdObj.signature : getLegacyCookieSignature();
const nbPage = incrementAndResetNb(params.partner);
const trueLinkInfo = window.id5Bootstrap ? window.id5Bootstrap.getTrueLinkInfo() : {booted: false};

const data = {
'partner': params.partner,
'gdpr': hasGdpr,
Expand All @@ -392,7 +417,8 @@ export class IdFetchFlow {
'u': referer.stack[0] || window.location.href,
'v': '$prebid.version$',
'storage': this.submoduleConfig.storage,
'localStorage': storage.localStorageIsEnabled() ? 1 : 0
'localStorage': storage.localStorageIsEnabled() ? 1 : 0,
'true_link': trueLinkInfo
};

// pass in optional data, but only if populated
Expand Down Expand Up @@ -431,6 +457,9 @@ export class IdFetchFlow {
try {
if (fetchCallResponse.privacy) {
storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS);
if (window.id5Bootstrap && window.id5Bootstrap.setPrivacy) {
window.id5Bootstrap.setPrivacy(fetchCallResponse.privacy);
}
}
} catch (error) {
logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error);
Expand Down
69 changes: 67 additions & 2 deletions modules/id5IdSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pbjs.setConfig({
type: 'html5', // "html5" is the required storage type
name: 'id5id', // "id5id" is the required storage name
expires: 90, // storage lasts for 90 days
refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh
refreshInSeconds: 7200 // refresh ID every 2 hours to ensure it's fresh
}
}],
auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules
Expand All @@ -61,7 +61,7 @@ pbjs.setConfig({
| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` |
| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` |
| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` |
| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` |
| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 2 hours between refreshes | `7200` |

**ATTENTION:** As of Prebid.js v4.14.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [[email protected]](mailto:[email protected]).

Expand All @@ -73,3 +73,68 @@ To turn on A/B Testing, simply edit the configuration (see above table) to enabl

### A Note on Using Multiple Wrappers
If you or your monetization partners are deploying multiple Prebid wrappers on your websites, you should make sure you add the ID5 ID User ID module to *every* wrapper. Only the bidders configured in the Prebid wrapper where the ID5 ID User ID module is installed and configured will be able to pick up the ID5 ID. Bidders from other Prebid instances will not be able to pick up the ID5 ID.

### Provided eids
The module provides following eids:

```
[
{
source: 'id5-sync.com',
uids: [
{
id: 'some-random-id-value',
atype: 1,
ext: {
linkType: 2,
abTestingControlGroup: false
}
}
]
},
{
source: 'true-link-id5-sync.com',
uids: [
{
id: 'some-publisher-true-link-id',
atype: 1
}
]
},
{
source: 'uidapi.com',
uids: [
{
id: 'some-uid2',
atype: 3,
ext: {
provider: 'id5-sync.com'
}
}
]
}
]
```

The id from `id5-sync.com` should be always present (though the id provided will be '0' in case of no consent or optout)

The id from `true-link-id5-sync.com` will be available if the page is integrated with TrueLink (if you are an ID5 partner you can learn more at https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/true-link-integration)

The id from `uidapi.com` will be available if the partner that is used in ID5 user module has the EUID2 integration enabled (it has to be enabled on the ID5 side)


### Providing TrueLinkId as a Google PPID

TrueLinkId can be provided as a PPID - to use, it the `true-link-id5-sync.com` needs to be provided as a ppid source in prebid userSync configuration:

```javascript
pbjs.setConfig({
userSync: {
ppid: 'true-link-id5-sync.com',
userIds: [], //userIds modules should be configured here
}
});
```



82 changes: 79 additions & 3 deletions test/spec/modules/id5IdSystem_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('ID5 ID System', function () {
const ID5_MODULE_NAME = 'id5Id';
const ID5_EIDS_NAME = ID5_MODULE_NAME.toLowerCase();
const ID5_SOURCE = 'id5-sync.com';
const TRUE_LINK_SOURCE = 'true-link-id5-sync.com';
const ID5_TEST_PARTNER_ID = 173;
const ID5_ENDPOINT = `https://id5-sync.com/g/v2/${ID5_TEST_PARTNER_ID}.json`;
const ID5_API_CONFIG_URL = `https://id5-sync.com/api/config/prebid`;
Expand All @@ -48,10 +49,8 @@ describe('ID5 ID System', function () {
const EUID_STORED_ID = 'EUID_1';
const EUID_SOURCE = 'uidapi.com';
const ID5_STORED_OBJ_WITH_EUID = {
'universal_uid': ID5_STORED_ID,
'signature': ID5_STORED_SIGNATURE,
...ID5_STORED_OBJ,
'ext': {
'linkType': ID5_STORED_LINK_TYPE,
'euid': {
'source': EUID_SOURCE,
'uids': [{
Expand All @@ -61,6 +60,11 @@ describe('ID5 ID System', function () {
}
}
};
const TRUE_LINK_STORED_ID = 'TRUE_LINK_1';
const ID5_STORED_OBJ_WITH_TRUE_LINK = {
...ID5_STORED_OBJ,
publisherTrueLinkId: TRUE_LINK_STORED_ID
};
const ID5_RESPONSE_ID = 'newid5id';
const ID5_RESPONSE_SIGNATURE = 'abcdef';
const ID5_RESPONSE_LINK_TYPE = 2;
Expand Down Expand Up @@ -148,6 +152,16 @@ describe('ID5 ID System', function () {
});
}

function wrapAsyncExpects(done, expectsFn) {
return function () {
try {
expectsFn();
} catch (err) {
done(err);
}
}
}

class XhrServerMock {
currentRequestIdx = 0;
server;
Expand Down Expand Up @@ -837,6 +851,38 @@ describe('ID5 ID System', function () {
id5System.id5IdSubmodule.getId(getId5FetchConfig());
});
});

it('should pass true link info to ID5 server even when true link is not booted', function () {
let xhrServerMock = new XhrServerMock(server);
let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ);

return xhrServerMock.expectFetchRequest()
.then(fetchRequest => {
let requestBody = JSON.parse(fetchRequest.requestBody);
expect(requestBody.true_link).is.deep.equal({booted: false});
fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
return submoduleResponse;
});
});

it('should pass full true link info to ID5 server when true link is booted', function () {
let xhrServerMock = new XhrServerMock(server);
let trueLinkResponse = {booted: true, redirected: true, id: 'TRUE_LINK_ID'};
window.id5Bootstrap = {
getTrueLinkInfo: function () {
return trueLinkResponse;
}
};
let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), undefined, ID5_STORED_OBJ);

return xhrServerMock.expectFetchRequest()
.then(fetchRequest => {
let requestBody = JSON.parse(fetchRequest.requestBody);
expect(requestBody.true_link).is.deep.equal(trueLinkResponse);
fetchRequest.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE));
return submoduleResponse;
});
});
});

describe('Local storage', () => {
Expand Down Expand Up @@ -950,6 +996,31 @@ describe('ID5 ID System', function () {
}, {adUnits});
});

it('should add stored TRUE_LINK_ID from cache to bids', function (done) {
id5System.storeInLocalStorage(id5System.ID5_STORAGE_NAME, JSON.stringify(ID5_STORED_OBJ_WITH_TRUE_LINK), 1);

init(config);
setSubmoduleRegistry([id5System.id5IdSubmodule]);
config.setConfig(getFetchLocalStorageConfig());

requestBidsHook(wrapAsyncExpects(done, function () {
adUnits.forEach(unit => {
unit.bids.forEach(bid => {
expect(bid).to.have.deep.nested.property(`userId.trueLinkId`);
expect(bid.userId.trueLinkId.uid).is.equal(TRUE_LINK_STORED_ID);
expect(bid.userIdAsEids[1]).is.deep.equal({
source: TRUE_LINK_SOURCE,
uids: [{
id: TRUE_LINK_STORED_ID,
atype: 1,
}]
});
});
});
done();
}), {adUnits});
});

it('should add config value ID to bids', function (done) {
init(config);
setSubmoduleRegistry([id5System.id5IdSubmodule]);
Expand Down Expand Up @@ -1056,6 +1127,11 @@ describe('ID5 ID System', function () {
'ext': {'provider': ID5_SOURCE}
});
});
it('should decode trueLinkId from a stored object with trueLinkId', function () {
expect(id5System.id5IdSubmodule.decode(ID5_STORED_OBJ_WITH_TRUE_LINK, getId5FetchConfig()).trueLinkId).is.deep.equal({
'uid': TRUE_LINK_STORED_ID
});
});
});

describe('A/B Testing', function () {
Expand Down

0 comments on commit f634053

Please sign in to comment.