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

UserId SharedId submodule #5315

Merged
merged 36 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e1e9e6c
Adding sharedid submodule
Apr 10, 2020
0010b20
Updating with Shared ID Module
Apr 13, 2020
06ede75
SharedID test and sharedid eids
Apr 14, 2020
16db9be
Shared ID md changes
Apr 14, 2020
2d1d473
Shared ID md changes
Apr 14, 2020
81efb7d
Shared ID changes
Apr 14, 2020
c3777ba
Apply suggestions from code review
SKOCHERI Apr 14, 2020
5f58ceb
Applying review suggestions
Apr 14, 2020
9a70c2e
Apply suggestions from code review
SKOCHERI Apr 14, 2020
120f1c3
Reformatting and reorganizing sharedId submodule
Apr 15, 2020
1ce1d66
Reformatting and reorganizing sharedId submodule
Apr 15, 2020
16fa25c
Reformatting and reorganizing sharedId submodule
Apr 15, 2020
e175a29
Shared Id generation changes
Apr 20, 2020
76d5424
Adding cookie Sync
Apr 23, 2020
7d43ecc
Decode and sync cookie
May 2, 2020
ba08669
Updating endpoint
May 4, 2020
21dea57
Updaitng eids.md
May 4, 2020
649b593
Configured sync
May 26, 2020
584abb5
Refactor and md update
May 26, 2020
6729c10
Refactoring
May 27, 2020
46e9efa
Refactoring
May 27, 2020
91255b0
Updating sync to seconds
May 27, 2020
b6721a3
Updating configuration
May 27, 2020
4e16a0f
Reformatting
May 28, 2020
d832997
Reformatting
May 28, 2020
26d694b
Reformatting
May 28, 2020
b4f2f1f
Fixing review comments
May 28, 2020
f58abbb
Changes to id value
May 28, 2020
4d05d33
Updating documentation
May 28, 2020
d41dad2
Merge pull request #2 from SKOCHERI/DE-3448
SKOCHERI May 28, 2020
40a849e
Documentation update
Jun 4, 2020
d864b64
Resolving merge conflicts
Jun 9, 2020
73c693d
updating userid_example.html
Jun 9, 2020
6cb4d98
Fixing review comments on test to separate sharedid opt out tests
Jun 10, 2020
7b7847e
Moving sharedID generation within sharedId module
Jun 15, 2020
5204703
Moving sharedID generation within sharedId module
Jun 15, 2020
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
12 changes: 11 additions & 1 deletion integrationExamples/gpt/userId_example.html
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,19 @@
name: 'idl_env',
expires: 30
}
}, {
name: "sharedId",
params: {
syncTime: 60 // in seconds, default is 24 hours
},
storage: {
type: "cookie",
name: "sharedid",
expires: 28
}
}],
syncDelay: 5000,
auctionDelay: 1000
auctionDelay: 1000
}
});
pbjs.addAdUnits(adUnits);
Expand Down
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"liveIntentIdSystem",
"criteoIdSystem",
"netIdSystem",
"identityLinkIdSystem"
"identityLinkIdSystem",
"sharedIdSystem"
],
"adpod": [
"freeWheelAdserverVideo",
Expand Down
333 changes: 333 additions & 0 deletions modules/sharedIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
/**
* This module adds Shared ID support to the User ID module
* The {@link module:modules/userId} module is required.
* @module modules/sharedIdSystem
* @requires module:modules/userId
*/

import * as utils from '../src/utils.js'
import {ajax} from '../src/ajax.js';
import {submodule} from '../src/hook.js';

const MODULE_NAME = 'sharedId';
const ID_SVC = 'https://id.sharedid.org/id';
const DEFAULT_24_HOURS = 86400;
const OPT_OUT_VALUE = '00000000000000000000000000';
// These values should NEVER change. If
// they do, we're no longer making ulids!
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; // Crockford's Base32
const ENCODING_LEN = ENCODING.length;
const TIME_MAX = Math.pow(2, 48) - 1;
const TIME_LEN = 10;
const RANDOM_LEN = 16;
const id = factory();
/**
* Constructs cookie value
* @param value
* @param needsSync
* @returns {string}
*/
function constructCookieValue(value, needsSync) {
const cookieValue = {};
cookieValue.id = value;
cookieValue.ts = utils.timestamp();
if (needsSync) {
cookieValue.ns = true;
}
utils.logInfo('SharedId: cookie Value: ' + JSON.stringify(cookieValue));
return cookieValue;
}

/**
* Checks if id needs to be synced
* @param configParams
* @param storedId
* @returns {boolean}
*/
function isIdSynced(configParams, storedId) {
const needSync = storedId.ns;
if (needSync) {
return true;
}
if (!configParams || typeof configParams.syncTime !== 'number') {
utils.logInfo('SharedId: Sync time is not configured or is not a number');
}
let syncTime = (!configParams || typeof configParams.syncTime !== 'number') ? DEFAULT_24_HOURS : configParams.syncTime;
if (syncTime > DEFAULT_24_HOURS) {
syncTime = DEFAULT_24_HOURS;
}
const cookieTimestamp = storedId.ts;
if (cookieTimestamp) {
var secondBetweenTwoDate = timeDifferenceInSeconds(utils.timestamp(), cookieTimestamp);
return secondBetweenTwoDate >= syncTime;
}
return false;
}

/**
* Gets time difference in secounds
* @param date1
* @param date2
* @returns {number}
*/
function timeDifferenceInSeconds(date1, date2) {
const diff = (date1 - date2) / 1000;
return Math.abs(Math.round(diff));
}

/**
* id generation call back
* @param result
* @param callback
* @returns {{success: success, error: error}}
*/
function idGenerationCallback(callback) {
return {
success: function (responseBody) {
let value = {};
if (responseBody) {
try {
let responseObj = JSON.parse(responseBody);
utils.logInfo('SharedId: Generated SharedId: ' + responseObj.sharedId);
value = constructCookieValue(responseObj.sharedId, false);
} catch (error) {
utils.logError(error);
}
}
callback(value);
},
error: function (statusText, responseBody) {
const value = constructCookieValue(id(), true);
utils.logInfo('SharedId: Ulid Generated SharedId: ' + value.id);
callback(value);
}
}
}

/**
* existing id generation call back
* @param result
* @param callback
* @returns {{success: success, error: error}}
*/
function existingIdCallback(storedId, callback) {
return {
success: function (responseBody) {
utils.logInfo('SharedId: id to be synced: ' + storedId.id);
if (responseBody) {
try {
let responseObj = JSON.parse(responseBody);
storedId = constructCookieValue(responseObj.sharedId, false);
utils.logInfo('SharedId: Older SharedId: ' + storedId.id);
} catch (error) {
utils.logError(error);
}
}
callback(storedId);
},
error: function () {
utils.logInfo('SharedId: Sync error for id : ' + storedId.id);
callback(storedId);
}
}
}

/**
* Encode the id
* @param value
* @returns {string|*}
*/
function encodeId(value) {
const result = {};
const sharedId = (value && typeof value['id'] === 'string') ? value['id'] : undefined;
if (sharedId == OPT_OUT_VALUE) {
return undefined;
}
if (sharedId) {
const bidIds = {
id: sharedId,
}
const ns = (value && typeof value['ns'] === 'boolean') ? value['ns'] : undefined;
if (ns == undefined) {
bidIds.third = sharedId;
}
result.sharedid = bidIds;
utils.logInfo('SharedId: Decoded value ' + JSON.stringify(result));
return result;
}
return sharedId;
}

/**
* the factory to generate unique identifier based on time and current pseudorandom number
* @param {string} the current pseudorandom number generator
* @returns {function(*=): *}
*/
function factory(currPrng) {
if (!currPrng) {
currPrng = detectPrng();
}
return function ulid(seedTime) {
if (isNaN(seedTime)) {
seedTime = Date.now();
}
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN, currPrng);
};
}

/**
* creates and logs the error message
* @function
* @param {string} error message
* @returns {Error}
*/
function createError(message) {
utils.logError(message);
const err = new Error(message);
err.source = 'sharedId';
return err;
}

/**
* gets a a random charcter from generated pseudorandom number
* @param {string} the generated pseudorandom number
* @returns {string}
*/
function randomChar(prng) {
let rand = Math.floor(prng() * ENCODING_LEN);
if (rand === ENCODING_LEN) {
rand = ENCODING_LEN - 1;
}
return ENCODING.charAt(rand);
}

/**
* encodes the time based on the length
* @param now
* @param len
* @returns {string} encoded time.
*/
function encodeTime (now, len) {
if (isNaN(now)) {
throw new Error(now + ' must be a number');
}

if (Number.isInteger(now) === false) {
throw createError('time must be an integer');
}

if (now > TIME_MAX) {
throw createError('cannot encode time greater than ' + TIME_MAX);
}
if (now < 0) {
throw createError('time must be positive');
}

if (Number.isInteger(len) === false) {
throw createError('length must be an integer');
}
if (len < 0) {
throw createError('length must be positive');
}

let mod;
let str = '';
for (; len > 0; len--) {
mod = now % ENCODING_LEN;
str = ENCODING.charAt(mod) + str;
now = (now - mod) / ENCODING_LEN;
}
return str;
}

/**
* encodes random character
* @param len
* @param prng
* @returns {string}
*/
function encodeRandom (len, prng) {
let str = '';
for (; len > 0; len--) {
str = randomChar(prng) + str;
}
return str;
}

/**
* detects the pseudorandom number generator and generates the random number
* @function
* @param {string} error message
* @returns {string} a random number
*/
function detectPrng(root) {
if (!root) {
root = typeof window !== 'undefined' ? window : null;
}
const browserCrypto = root && (root.crypto || root.msCrypto);
if (browserCrypto) {
return () => {
const buffer = new Uint8Array(1);
browserCrypto.getRandomValues(buffer);
return buffer[0] / 0xff;
};
}
return () => Math.random();
}

/** @type {Submodule} */
export const sharedIdSubmodule = {
/**
* 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 {{sharedid:{ id: string, third:string}} or undefined if value doesn't exists
*/
decode(value) {
return (value) ? encodeId(value) : undefined;
},

/**
* performs action to obtain id and return a value.
* @function
* @param {SubmoduleParams} [configParams]
* @returns {sharedId}
*/
getId(configParams) {
const resp = function (callback) {
utils.logInfo('SharedId: Sharedid doesnt exists, new cookie creation');
ajax(ID_SVC, idGenerationCallback(callback), undefined, {method: 'GET', withCredentials: true});
};
return {callback: resp};
},

/**
* performs actions even if the id exists and returns a value
* @param configParams
* @param storedId
* @returns {{callback: *}}
*/
extendId(configParams, storedId) {
utils.logInfo('SharedId: Existing shared id ' + storedId.id);
const resp = function (callback) {
const needSync = isIdSynced(configParams, storedId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't you rely on the refreshInSeconds feature instead of reproducing it?

https://github.com/prebid/Prebid.js/blob/master/modules/userId/index.js#L64

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couple of reason for not using "refreshInSeconds"

  1. The default of refreshInSeconds is the expiry of the cookies but we are looking at refreshing/syncing atleast once in 24 hours
  2. The refresh based on refreshInSeconds relies on getId function to reset where as we want to use extendId function

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @SKOCHERI, thanks for the feedback.

for 1, isn't that up to the publisher to configure, rather than forcing a refresh that they don't ask for?

for 2, why don't you want to use getId function to do the refresh? this is what the refreshInSeconds was designed to hit. By not using the existing functionality, publishers will have different configurations for different id providers for the same functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @smenzer ,
SharedId supports user opt-out/opt-in and in order to handle the opted-out/opted-in user, we intend to sync the ids atleast once in 24hrs.
If we were to rely on refreshInSeconds and if refreshInSeconds is not configured, the id sync/refresh would happen on cookie expiry and we would not be respecting the user opt-out/opt-in in this duration.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SKOCHERI ok. i'll note that I still think there are better ways to accomplish this (some of which may require userId module changes), but I think it's fine to leave as is and perhaps gets reviewed at another time down the road.

if (needSync) {
utils.logInfo('SharedId: Existing shared id ' + storedId + ' is not synced');
const sharedIdPayload = {};
sharedIdPayload.sharedId = storedId.id;
const payloadString = JSON.stringify(sharedIdPayload);
ajax(ID_SVC, existingIdCallback(storedId, callback), payloadString, {method: 'POST', withCredentials: true});
smenzer marked this conversation as resolved.
Show resolved Hide resolved
}
};
return {callback: resp};
}
};

// Register submodule for userId
submodule('userId', sharedIdSubmodule);
Loading