Skip to content

Commit

Permalink
Update AMXIdSystem logic, allow non-html5 storage
Browse files Browse the repository at this point in the history
update documentation
  • Loading branch information
nickjacob committed Apr 4, 2023
1 parent 4167bae commit 3ee32d6
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 53 deletions.
76 changes: 56 additions & 20 deletions modules/amxIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand All @@ -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;
Expand Down Expand Up @@ -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 = {
Expand All @@ -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;
Expand All @@ -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,
};
Expand All @@ -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);
Expand All @@ -142,9 +180,7 @@ export const amxIdSubmodule = {
},
},
params,
{
method: 'GET'
}
AJAX_OPTIONS
);

return { callback };
Expand Down
14 changes: 7 additions & 7 deletions modules/amxIdSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
82 changes: 56 additions & 26 deletions test/spec/modules/amxIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
{},
Expand Down

0 comments on commit 3ee32d6

Please sign in to comment.