-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
e1e9e6c
Adding sharedid submodule
0010b20
Updating with Shared ID Module
06ede75
SharedID test and sharedid eids
16db9be
Shared ID md changes
2d1d473
Shared ID md changes
81efb7d
Shared ID changes
c3777ba
Apply suggestions from code review
SKOCHERI 5f58ceb
Applying review suggestions
9a70c2e
Apply suggestions from code review
SKOCHERI 120f1c3
Reformatting and reorganizing sharedId submodule
1ce1d66
Reformatting and reorganizing sharedId submodule
16fa25c
Reformatting and reorganizing sharedId submodule
e175a29
Shared Id generation changes
76d5424
Adding cookie Sync
7d43ecc
Decode and sync cookie
ba08669
Updating endpoint
21dea57
Updaitng eids.md
649b593
Configured sync
584abb5
Refactor and md update
6729c10
Refactoring
46e9efa
Refactoring
91255b0
Updating sync to seconds
b6721a3
Updating configuration
4e16a0f
Reformatting
d832997
Reformatting
26d694b
Reformatting
b4f2f1f
Fixing review comments
f58abbb
Changes to id value
4d05d33
Updating documentation
d41dad2
Merge pull request #2 from SKOCHERI/DE-3448
SKOCHERI 40a849e
Documentation update
d864b64
Resolving merge conflicts
73c693d
updating userid_example.html
6cb4d98
Fixing review comments on test to separate sharedid opt out tests
7b7847e
Moving sharedID generation within sharedId module
5204703
Moving sharedID generation within sharedId module
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
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); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.