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

ID5 UserId module - integrate with TrueLink Id #11802

Merged
merged 3 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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