-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add yandex id system * refactor: improve yandex user id adapter codestyle * tests: add unit tests for yandex user id module * fix: adjust eid key * refactor: remove explicit calls to cookie storage
- Loading branch information
Showing
3 changed files
with
284 additions
and
1 deletion.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
/** | ||
* The {@link module:modules/userId} module is required | ||
* @module modules/yandexIdSystem | ||
* @requires module:modules/userId | ||
*/ | ||
|
||
// @ts-check | ||
|
||
import { MODULE_TYPE_UID } from '../src/activities/modules.js'; | ||
import { submodule } from '../src/hook.js'; | ||
import { getStorageManager } from '../src/storageManager.js'; | ||
import { logError, logInfo } from '../src/utils.js'; | ||
|
||
// .com suffix is just a convention for naming the bidder eids | ||
// See https://github.com/prebid/Prebid.js/pull/11196#discussion_r1591165139 | ||
const BIDDER_EID_KEY = 'yandex.com'; | ||
const YANDEX_ID_KEY = 'yandexId'; | ||
export const BIDDER_CODE = 'yandex'; | ||
export const YANDEX_USER_ID_KEY = '_ym_uid'; | ||
export const YANDEX_COOKIE_STORAGE_TYPE = 'cookie'; | ||
export const YANDEX_MIN_EXPIRE_DAYS = 30; | ||
|
||
export const PREBID_STORAGE = getStorageManager({ | ||
moduleType: MODULE_TYPE_UID, | ||
moduleName: BIDDER_CODE, | ||
bidderCode: undefined | ||
}); | ||
|
||
export const yandexIdSubmodule = { | ||
/** | ||
* Used to link submodule with config. | ||
* @type {string} | ||
*/ | ||
name: BIDDER_CODE, | ||
/** | ||
* Decodes the stored id value for passing to bid requests. | ||
* @param {string} value | ||
*/ | ||
decode(value) { | ||
logInfo('decoded value yandexId', value); | ||
|
||
return { [YANDEX_ID_KEY]: value }; | ||
}, | ||
/** | ||
* @param {import('./userId/index.js').SubmoduleConfig} submoduleConfig | ||
* @param {unknown} [_consentData] | ||
* @param {string} [storedId] Id that was saved by the core previously. | ||
*/ | ||
getId(submoduleConfig, _consentData, storedId) { | ||
if (checkConfigHasErrorsAndReport(submoduleConfig)) { | ||
return; | ||
} | ||
|
||
if (storedId) { | ||
return { | ||
id: storedId | ||
}; | ||
} | ||
|
||
return { | ||
id: new YandexUidGenerator().generateUid(), | ||
}; | ||
}, | ||
eids: { | ||
[YANDEX_ID_KEY]: { | ||
source: BIDDER_EID_KEY, | ||
atype: 1, | ||
}, | ||
}, | ||
}; | ||
|
||
/** | ||
* @param {import('./userId/index.js').SubmoduleConfig} submoduleConfig | ||
* @returns {boolean} `true` - when there are errors, `false` - otherwise. | ||
*/ | ||
function checkConfigHasErrorsAndReport(submoduleConfig) { | ||
let error = false; | ||
|
||
const READABLE_MODULE_NAME = 'Yandex ID module'; | ||
|
||
if (submoduleConfig.storage == null) { | ||
logError(`Misconfigured ${READABLE_MODULE_NAME}. "storage" is required.`) | ||
return true; | ||
} | ||
|
||
if (submoduleConfig.storage?.name !== YANDEX_USER_ID_KEY) { | ||
logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.name" is required to be "${YANDEX_USER_ID_KEY}"`); | ||
error = true; | ||
} | ||
|
||
if (submoduleConfig.storage?.type !== YANDEX_COOKIE_STORAGE_TYPE) { | ||
logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.type" is required to be "${YANDEX_COOKIE_STORAGE_TYPE}"`); | ||
error = true; | ||
} | ||
|
||
if ((submoduleConfig.storage?.expires ?? 0) < YANDEX_MIN_EXPIRE_DAYS) { | ||
logError(`Misconfigured ${READABLE_MODULE_NAME}, "storage.expires" is required to be not less than "${YANDEX_MIN_EXPIRE_DAYS}"`); | ||
error = true; | ||
} | ||
|
||
return error; | ||
} | ||
|
||
/** | ||
* Yandex-specific generator for uid. Needs to be compatible with Yandex Metrica tag. | ||
* @see https://github.com/yandex/metrica-tag/blob/main/src/utils/uid/uid.ts#L51 | ||
*/ | ||
class YandexUidGenerator { | ||
/** | ||
* @param {number} min | ||
* @param {number} max | ||
*/ | ||
_getRandomInteger(min, max) { | ||
const generateRandom = this._getRandomGenerator(); | ||
|
||
return Math.floor(generateRandom() * (max - min)) + min; | ||
} | ||
|
||
_getCurrentSecTimestamp() { | ||
return Math.round(Date.now() / 1000); | ||
} | ||
|
||
generateUid() { | ||
return [ | ||
this._getCurrentSecTimestamp(), | ||
this._getRandomInteger(1000000, 999999999), | ||
].join(''); | ||
} | ||
|
||
_getRandomGenerator() { | ||
if (crypto) { | ||
return () => { | ||
const buffer = new Uint32Array(1); | ||
crypto.getRandomValues(buffer); | ||
|
||
return buffer[0] / 0xffffffff; | ||
}; | ||
} | ||
|
||
// Polyfill for environments that don't support Crypto API | ||
return () => Math.random(); | ||
} | ||
} | ||
|
||
submodule('userId', yandexIdSubmodule); |
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,137 @@ | ||
// @ts-check | ||
|
||
import { yandexIdSubmodule, PREBID_STORAGE, BIDDER_CODE, YANDEX_USER_ID_KEY, YANDEX_COOKIE_STORAGE_TYPE, YANDEX_MIN_EXPIRE_DAYS } from '../../../modules/yandexIdSystem.js'; | ||
import {createSandbox} from 'sinon' | ||
import * as utils from '../../../src/utils.js'; | ||
|
||
/** | ||
* @typedef {import('sinon').SinonStub} SinonStub | ||
* @typedef {import('sinon').SinonSpy} SinonSpy | ||
* @typedef {import('sinon').SinonSandbox} SinonSandbox | ||
*/ | ||
|
||
const MIN_METRICA_ID_LEN = 17; | ||
|
||
/** @satisfies {import('../../../modules/userId/index.js').SubmoduleConfig} */ | ||
const CORRECT_SUBMODULE_CONFIG = { | ||
name: BIDDER_CODE, | ||
storage: { | ||
expires: YANDEX_MIN_EXPIRE_DAYS, | ||
name: YANDEX_USER_ID_KEY, | ||
type: YANDEX_COOKIE_STORAGE_TYPE, | ||
refreshInSeconds: undefined, | ||
}, | ||
params: undefined, | ||
value: undefined, | ||
}; | ||
|
||
/** @type {import('../../../modules/userId/index.js').SubmoduleConfig[]} */ | ||
const INCORRECT_SUBMODULE_CONFIGS = [ | ||
{ | ||
...CORRECT_SUBMODULE_CONFIG, | ||
storage: { | ||
...CORRECT_SUBMODULE_CONFIG.storage, | ||
expires: 0, | ||
} | ||
}, | ||
{ | ||
...CORRECT_SUBMODULE_CONFIG, | ||
storage: { | ||
...CORRECT_SUBMODULE_CONFIG.storage, | ||
type: 'html5' | ||
} | ||
}, | ||
{ | ||
...CORRECT_SUBMODULE_CONFIG, | ||
storage: { | ||
...CORRECT_SUBMODULE_CONFIG.storage, | ||
name: 'custom_key' | ||
} | ||
}, | ||
]; | ||
|
||
describe('YandexId module', () => { | ||
/** @type {SinonSandbox} */ | ||
let sandbox; | ||
/** @type {SinonStub} */ | ||
let getCryptoRandomValuesStub; | ||
/** @type {SinonStub} */ | ||
let randomStub; | ||
/** @type {SinonSpy} */ | ||
let logErrorSpy; | ||
|
||
beforeEach(() => { | ||
sandbox = createSandbox(); | ||
logErrorSpy = sandbox.spy(utils, 'logError'); | ||
|
||
getCryptoRandomValuesStub = sandbox | ||
.stub(window.crypto, 'getRandomValues') | ||
.callsFake((bufferView) => { | ||
if (bufferView != null) { | ||
bufferView[0] = 10000; | ||
} | ||
|
||
return null; | ||
}); | ||
randomStub = sandbox.stub(window.Math, 'random').returns(0.555); | ||
}); | ||
|
||
afterEach(() => { | ||
sandbox.restore(); | ||
}); | ||
|
||
describe('getId()', () => { | ||
it('user id matches Yandex Metrica format', () => { | ||
const generatedId = yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG)?.id; | ||
|
||
expect(isNaN(Number(generatedId))).to.be.false; | ||
expect(generatedId).to.have.length.greaterThanOrEqual( | ||
MIN_METRICA_ID_LEN | ||
); | ||
}); | ||
|
||
it('uses stored id', () => { | ||
const storedId = '11111111111111111'; | ||
const generatedId = yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG, undefined, storedId)?.id; | ||
|
||
expect(generatedId).to.be.equal(storedId); | ||
}) | ||
|
||
describe('config validation', () => { | ||
INCORRECT_SUBMODULE_CONFIGS.forEach((config, i) => { | ||
it(`invalid config #${i} fails`, () => { | ||
const generatedId = yandexIdSubmodule.getId(config)?.id; | ||
|
||
expect(generatedId).to.be.undefined; | ||
expect(logErrorSpy.called).to.be.true; | ||
}) | ||
}) | ||
}) | ||
|
||
describe('crypto', () => { | ||
it('uses Math.random when crypto is not available', () => { | ||
sandbox.stub(window, 'crypto').value(undefined); | ||
|
||
yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG); | ||
|
||
expect(randomStub.calledOnce).to.be.true; | ||
expect(getCryptoRandomValuesStub.called).to.be.false; | ||
}); | ||
|
||
it('uses crypto when it is available', () => { | ||
yandexIdSubmodule.getId(CORRECT_SUBMODULE_CONFIG); | ||
|
||
expect(randomStub.called).to.be.false; | ||
expect(getCryptoRandomValuesStub.calledOnce).to.be.true; | ||
}); | ||
}); | ||
}); | ||
|
||
describe('decode()', () => { | ||
it('should not transform value', () => { | ||
const value = 'test value'; | ||
|
||
expect(yandexIdSubmodule.decode(value).yandexId).to.equal(value); | ||
}); | ||
}); | ||
}); |