diff --git a/modules/amxIdSystem.js b/modules/amxIdSystem.js index 9dbab496f2c7..e66f60ead388 100644 --- a/modules/amxIdSystem.js +++ b/modules/amxIdSystem.js @@ -10,28 +10,23 @@ import {ajaxBuilder} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {deepAccess, logError} from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; const NAME = 'amxId'; const GVL_ID = 737; const ID_KEY = NAME; -const version = '1.0'; +const version = '1.1'; const SYNC_URL = 'https://id.a-mx.com/sync/'; const AJAX_TIMEOUT = 300; +const AJAX_OPTIONS = {method: 'GET', withCredentials: true, contentType: 'text/plain'}; -function validateConfig(config) { - if (config == null || config.storage == null) { - logError(`${NAME}: config.storage is required.`); - return false; - } - - if (config.storage.type !== 'html5') { - logError( - `${NAME} only supports storage.type "html5". ${config.storage.type} was provided` - ); - return false; - } +export const storage = getStorageManager({ gvlid: GVL_ID, moduleName: NAME }); +const AMUID_KEY = '__amuidpb'; +const getBidAdapterID = () => storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(AMUID_KEY) : null; +function validateConfig(config) { if ( + config.storage != null && typeof config.storage.expires === 'number' && config.storage.expires > 30 ) { @@ -44,7 +39,7 @@ function validateConfig(config) { return true; } -function handleSyncResponse(client, response, callback) { +function handleSyncResponse(client, response, params, callback) { if (response.id != null && response.id.length > 0) { callback(response.id); return; @@ -72,7 +67,22 @@ function handleSyncResponse(client, response, callback) { logError(`${NAME} invalid value`, complete); callback(null); }, - }); + }, params, AJAX_OPTIONS); +} + +const TEST_COOKIE_VALUE = Date.now() + ''; +function cookieTest(domain) { + if (domain == null || domain.indexOf('.') === -1) { + return false; + } + + const testCookieName = `_amDOT${Date.now()}`; + storage.setCookie(testCookieName, TEST_COOKIE_VALUE, undefined, undefined, domain); + const value = storage.getCookie(testCookieName); + + // delete cookie + storage.setCookie(testCookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, domain); + return value === TEST_COOKIE_VALUE; } export const amxIdSubmodule = { @@ -97,6 +107,28 @@ export const amxIdSubmodule = { ? { [ID_KEY]: value } : undefined, + /** + * Similar to sharedIdSystem domainOverride + * if on a subdomain, try to set the cookie on the root domain + */ + domainOverride() { + if (!cookieTest(document.domain)) { + return undefined; + } + + let components = document.domain.split('.'); + + while (components.length > 1) { + if (cookieTest(components.slice(1).join('.'))) { + components.shift(); + } else { + break; + } + } + + return components.join('.'); + }, + getId(config, consentData, _extant) { if (!validateConfig(config)) { return undefined; @@ -109,12 +141,18 @@ export const amxIdSubmodule = { const params = { tagId: deepAccess(config, 'params.tagId', ''), - // TODO: are these referer values correct? + ref: ref.ref, u: ref.location, + tl: ref.topmostLocation, + nf: ref.numIframes, + rt: ref.reachedTop, + v: '$prebid.version$', + av: version, vg: '$$PREBID_GLOBAL$$', us_privacy: usp, + am: getBidAdapterID(), gdpr: consent.gdprApplies ? 1 : 0, gdpr_consent: consent.consentString, }; @@ -131,7 +169,7 @@ export const amxIdSubmodule = { if (responseText != null && responseText.length > 0) { try { const parsed = JSON.parse(responseText); - handleSyncResponse(client, parsed, done); + handleSyncResponse(client, parsed, params, done); return; } catch (e) { logError(`${NAME} invalid response`, responseText); @@ -142,9 +180,7 @@ export const amxIdSubmodule = { }, }, params, - { - method: 'GET' - } + AJAX_OPTIONS ); return { callback }; diff --git a/modules/amxIdSystem.md b/modules/amxIdSystem.md index f67fefe261ec..aa949b39bfba 100644 --- a/modules/amxIdSystem.md +++ b/modules/amxIdSystem.md @@ -29,15 +29,15 @@ pbjs.setConfig({ | Param under `userSync.userIds[]` | Scope | Type | Description | Example | | -------------------------------- | -------- | ------ | --------------------------- | ----------------------------------------- | | name | Required | string | ID for the amxId module | `"amxId"` | -| storage | Required | Object | Settings for amxId storage | See [storage settings](#storage-settings) | +| storage | Optional | Object | Settings for amxId storage | See [storage settings](#storage-settings) | | params | Optional | Object | Parameters for amxId module | See [params](#params) | ### Storage Settings -The following settings are available for the `storage` property in the `userSync.userIds[]` object: +The following settings are suggested for the `storage` property in the `userSync.userIds[]` object: -| Param under `storage` | Scope | Type | Description | Example | -| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | -| name | Required | String | Where the ID will be stored | `"amxId"` | -| type | Required | String | This must be `"html5"` | `"html5"` | -| expires | Required | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | +| Param under `storage` | Type | Description | Example | +| --------------------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| name | String | Where the ID will be stored | `"amxId"` | +| type | String | For best performnace, this should be `"html5"` | `"html5"` | +| expires | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | diff --git a/test/spec/modules/amxIdSystem_spec.js b/test/spec/modules/amxIdSystem_spec.js index dea79e87baa0..2a9aad24dda6 100644 --- a/test/spec/modules/amxIdSystem_spec.js +++ b/test/spec/modules/amxIdSystem_spec.js @@ -1,4 +1,4 @@ -import { amxIdSubmodule } from 'modules/amxIdSystem.js'; +import { amxIdSubmodule, storage } from 'modules/amxIdSystem.js'; import { server } from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; @@ -38,6 +38,49 @@ describe('decode', () => { }); }); +describe('domainOverride', () => { + let sandbox, domain, cookies, rejectCookiesFor; + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(document, 'domain').get(() => domain); + cookies = {}; + sandbox.stub(storage, 'getCookie').callsFake((key) => cookies[key]); + rejectCookiesFor = null; + sandbox.stub(storage, 'setCookie').callsFake((key, value, expires, sameSite, domain) => { + if (domain !== rejectCookiesFor) { + if (expires != null) { + expires = new Date(expires); + } + if (expires == null || expires > Date.now()) { + cookies[key] = value; + } else { + delete cookies[key]; + } + } + }); + }); + + afterEach(() => sandbox.restore()) + + it('will return the root domain when given a subdomain', () => { + domain = 'subdomain.greatpublisher.com' + rejectCookiesFor = 'com' + expect(amxIdSubmodule.domainOverride()).to.equal('greatpublisher.com'); + }); + + it(`If we can't set cookies on the root domain, we'll return the subdomain`, () => { + domain = 'subdomain.greatpublisher.com' + rejectCookiesFor = 'greatpublisher.com' + expect(amxIdSubmodule.domainOverride()).to.equal('subdomain.greatpublisher.com'); + }); + + it('Will return undefined if we can\'t set cookies on the root domain or the subdomain', () => { + domain = 'subdomain.greatpublisher.com' + rejectCookiesFor = 'subdomain.greatpublisher.com' + expect(amxIdSubmodule.domainOverride()).to.equal(undefined); + }); +}); + describe('validateConfig', () => { let logErrorSpy; @@ -48,38 +91,17 @@ describe('validateConfig', () => { logErrorSpy.restore(); }); - it('should return undefined if config.storage is not present', () => { - expect( - amxIdSubmodule.getId( - { - ...config, - storage: null, - }, - null, - null - ) - ).to.equal(undefined); - - expect(logErrorSpy.calledOnce).to.be.true; - expect(logErrorSpy.lastCall.lastArg).to.contain('storage is required'); - }); - - it('should return undefined if config.storage.type !== "html5"', () => { + it('should allow configuration with no storage', () => { expect( amxIdSubmodule.getId( { ...config, - storage: { - type: 'cookie', - }, + storage: undefined }, null, null ) - ).to.equal(undefined); - - expect(logErrorSpy.calledOnce).to.be.true; - expect(logErrorSpy.lastCall.lastArg).to.contain('cookie'); + ).to.not.equal(undefined); }); it('should return undefined if expires > 30', () => { @@ -111,10 +133,18 @@ describe('getId', () => { }); it('should call the sync endpoint and accept a valid response', () => { + storage.setDataInLocalStorage('__amuidpb', TEST_ID); + const { callback } = amxIdSubmodule.getId(config, null, null); callback(spy); const [request] = server.requests; + expect(request.withCredentials).to.be.true + expect(request.requestHeaders['Content-Type']).to.match(/text\/plain/) + + const { search } = utils.parseUrl(request.url); + expect(search.av).to.equal(amxIdSubmodule.version); + expect(search.am).to.equal(TEST_ID); expect(request.method).to.equal('GET'); request.respond( @@ -187,7 +217,7 @@ describe('getId', () => { ); const [, secondRequest] = server.requests; - expect(secondRequest.url).to.be.equal(intermediateValue); + expect(secondRequest.url).to.match(new RegExp(`^${intermediateValue}\?`)); secondRequest.respond( 200, {},