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

Permutive Identity Manager: initial implementation #12337

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"novatiqIdSystem",
"oneKeyIdSystem",
"operaadsIdSystem",
"permutiveIdSystem",
"pubProvidedIdSystem",
"publinkIdSystem",
"quantcastIdSystem",
Expand Down
153 changes: 153 additions & 0 deletions modules/permutiveIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {MODULE_TYPE_UID} from '../src/activities/modules.js'
import {submodule} from '../src/hook.js'
import {getStorageManager} from '../src/storageManager.js'
import {prefixLog, safeJSONParse} from '../src/utils.js'
/**
* @typedef {import('../modules/userId/index.js').Submodule} Submodule
* @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
* @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
* @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
*/

const MODULE_NAME = 'permutiveId'
const PERMUTIVE_ID_DATA_STORAGE_KEY = 'permutive-prebid-id'

const ID5_DOMAIN = 'id5-sync.com'
const LIVERAMP_DOMAIN = 'liveramp.com'
const UID_DOMAIN = 'uidapi.com'

const PRIMARY_IDS = ['id5id', 'idl_env', 'uid2']

export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME})

const logger = prefixLog('[PermutiveID]')

const readFromSdkLocalStorage = () => {
const data = safeJSONParse(storage.getDataFromLocalStorage(PERMUTIVE_ID_DATA_STORAGE_KEY))
talbotja marked this conversation as resolved.
Show resolved Hide resolved
const id = {}
if (data && typeof data === 'object') {
const now = Date.now()
for (const [idName, value] of Object.entries(data)) {
if (PRIMARY_IDS.includes(idName) && value.userId) {
if (!value.expiryTime || value.expiryTime > now) {
id[idName] = value.userId
}
}
}
}
return id
}

/**
* Catch and log errors
* @param {function} fn - Function to safely evaluate
*/
function makeSafe (fn) {
try {
return fn()
} catch (e) {
logger.logError(e)
}
}

const waitAndRetrieveFromSdk = (timeoutMs) =>
new Promise(
resolve => {
const fallback = setTimeout(() => {
logger.logInfo('timeout expired waiting for SDK - attempting read from local storage again')
resolve(readFromSdkLocalStorage())
}, timeoutMs)
if (window.permutive) {
window.permutive.ready(() => makeSafe(() => {
talbotja marked this conversation as resolved.
Show resolved Hide resolved
logger.logInfo('Permutive SDK is ready')
const sdkIdentityManagerApi = window.permutive.addon('identityManager')
talbotja marked this conversation as resolved.
Show resolved Hide resolved
if (sdkIdentityManagerApi && 'prebid' in sdkIdentityManagerApi && 'onReady' in sdkIdentityManagerApi.prebid) {
talbotja marked this conversation as resolved.
Show resolved Hide resolved
sdkIdentityManagerApi.prebid.onReady((ids) => {
logger.logInfo('Permutive SDK has provided ids')
resolve(ids)
clearTimeout(fallback)
})
} else {
logger.logError('Permutive SDK initialised but identity manager prebid api not present')
}
}))
}
}
)

/** @type {Submodule} */
export const permutiveIdSubmodule = {
/**
* used to link submodule with config
* @type {string}
*/
name: MODULE_NAME,

/**
* decode the stored id value for passing to bid requests
* @function decode
* @param {(Object|string)} value
* @param {SubmoduleConfig|undefined} config
* @returns {(Object|undefined)}
*/
decode(value, config) {
return value
},

/**
* performs action to obtain id and return a value in the callback's response argument
* @function getId
* @param {SubmoduleConfig} submoduleConfig
* @param {ConsentData} consentData
* @param {(Object|undefined)} cacheIdObj
* @returns {IdResponse|undefined}
*/
getId(submoduleConfig, consentData, cacheIdObj) {
const id = readFromSdkLocalStorage()
if (Object.entries(id).length > 0) {
logger.logInfo('found id in sdk storage')
return { id }
} else if ('params' in submoduleConfig && submoduleConfig.params.ajaxTimeout) {
logger.logInfo('failed to find id in sdk storage - waiting for sdk')
// Is ajaxTimeout an appropriate timeout to use here?
return { callback: (done) => waitAndRetrieveFromSdk(submoduleConfig.params.ajaxTimeout).then(done) }
} else {
logger.logInfo('failed to find id in sdk storage and no wait time specified')
}
},

primaryIds: PRIMARY_IDS,

eids: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question to prebid maintainers, if we were to make this a getter property such that we could read the eids config directly from our SDK and use what we have defined here as a fallback, would this propagate to other parts of the identity system as it would be called frequently? Or is the initial object returned stored by reference somewhere?

i.e.

get eids() {
  const data = read.from.permutive.sdk
  if (data) {
    return data
  }

  return { id5id: ... }
}

'id5id': {
getValue: function (data) {
return data.uid
},
source: ID5_DOMAIN,
atype: 1,
getUidExt: function (data) {
if (data.ext) {
return data.ext
}
}
},
'idl_env': {
source: LIVERAMP_DOMAIN,
atype: 3,
},
'uid2': {
source: UID_DOMAIN,
atype: 3,
getValue: function(data) {
return data.id
},
getUidExt: function(data) {
if (data.ext) {
return data.ext
}
}
}
}
}

submodule('userId', permutiveIdSubmodule)
58 changes: 58 additions & 0 deletions modules/permutiveIdSystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Permutive Identity Manager

This module supports [Permutive](https://permutive.com/) customers in using Permutive's Identity Manager functionality.

To use this Prebid.js module it is assumed that the site includes Permutive's SDK, with Identity Manager configuration
enabled. See Permutive's user documentation for more information on Identity Manager.

## Building Prebid.js with Permutive Identity Manager Support

Prebid.js must be built with the `permutiveIdSystem` module in order for Permutive's Identity Manager to be able to
activate relevant user identities to Prebid.

To build Prebid.js with the `permutiveIdSystem` module included:

```
gulp build --modules=userId,permutiveIdSystem
```

## Prebid configuration

There is minimal configuration required to be set on Prebid.js, since the bulk of the behaviour is managed through
Permutive's dashboard and SDK.

It is recommended to keep the Prebid.js caching for this module short, since the mechanism by which Permutive's SDK
communicates with Prebid.js is effectively a local cache anyway.

```
pbjs.setConfig({
...
userSync: {
userIds: [
{
name: 'permutiveId',
params: {
ajaxTimeout: 90
},
storage: {
type: 'html5',
name: 'permutiveId',
refreshInSeconds: 5
}
}
],
auctionDelay: 100
},
...
});
```

### ajaxTimeout

By default this module will read IDs provided by the Permutive SDK from local storage when requested by prebid, and if
nothing is found, will not provide any identities. If a timeout is provided via the `ajaxTimeout` parameter, it will
instead wait for up to the specified number of milliseconds for Permutive's SDK to become available, and will retrieve
identities from the SDK directly if/when this happens.

This value should be set to a value smaller than the `auctionDelay` set on the `userSync` configuration object, since
there is no point waiting longer than this as the auction will already have been triggered.
136 changes: 136 additions & 0 deletions test/spec/modules/permutiveIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { permutiveIdSubmodule, storage } from 'modules/permutiveIdSystem'
import { deepSetValue } from 'src/utils.js'

const STORAGE_KEY = 'permutive-prebid-id'

describe('permutiveIdSystem', () => {
afterEach(() => {
storage.removeDataFromLocalStorage(STORAGE_KEY)
})

describe('decode', () => {
it('returns the input unchanged', () => {
const input = {
id5id: {
uid: '0',
ext: {
abTestingControlGroup: false,
linkType: 2,
pba: 'somepba'
}
}
}
const result = permutiveIdSubmodule.decode(input)
expect(result).to.be.equal(input)
})
})

describe('getId', () => {
it('returns relevant IDs from localStorage and does not return unexpected IDs', () => {
const data = getUserIdData()
storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(data))
const result = permutiveIdSubmodule.getId({})
const expected = {
'id': {
'id5id': {
'uid': '0',
'linkType': 0,
'ext': {
'abTestingControlGroup': false,
'linkType': 0,
'pba': 'EVqgf9vY0fSrsrqJZMOm+Q=='
}
}
}
}
expect(result).to.deep.equal(expected)
})

it('returns undefined if no relevant IDs are found in localStorage', () => {
storage.setDataInLocalStorage(STORAGE_KEY, '{}')
const result = permutiveIdSubmodule.getId({})
expect(result).to.be.undefined
})

it('will optionally wait for Permutive SDK if no identities are in local storage already', async () => {
const cleanup = setWindowPermutive()
const result = permutiveIdSubmodule.getId({params: {ajaxTimeout: 20}})
expect(result).not.to.be.undefined
expect(result.id).to.be.undefined
expect(result.callback).not.to.be.undefined
let r
result.callback((a) => r = a)
await sleep(25)
talbotja marked this conversation as resolved.
Show resolved Hide resolved
const expected = {
'id5id': {
'uid': '0',
'linkType': 0,
'ext': {
'abTestingControlGroup': false,
'linkType': 0,
'pba': 'EVqgf9vY0fSrsrqJZMOm+Q=='
}
}
}
expect(r).to.deep.equal(expected)
cleanup()
})
})
})

const setWindowPermutive = () => {
// Read from Permutive
const backup = window.permutive

deepSetValue(window, 'permutive.ready', (f) => {
setTimeout(() => f(), 5)
})

deepSetValue(window, 'permutive.addon', (name) => {
if (name === 'identityManager') {
return {
prebid: {
onReady: (f) => {
setTimeout(() => f(sdkUserIdData()), 5)
}
}
}
}
})

// Cleanup
return () => window.permutive = backup
}

const sleep = (timeMs) => new Promise(resolve => setTimeout(resolve, timeMs));

const sdkUserIdData = () => ({
'id5id': {
'uid': '0',
'linkType': 0,
'ext': {
'abTestingControlGroup': false,
'linkType': 0,
'pba': 'EVqgf9vY0fSrsrqJZMOm+Q=='
}
},
})

const getUserIdData = () => ({
'id5id': {
'userId': {
'uid': '0',
'linkType': 0,
'ext': {
'abTestingControlGroup': false,
'linkType': 0,
'pba': 'EVqgf9vY0fSrsrqJZMOm+Q=='
}
}
},
'fooid': {
'userId': {
'id': '1'
}
}
})