diff --git a/cspell.json b/cspell.json index b91c2462..df8b430d 100644 --- a/cspell.json +++ b/cspell.json @@ -49,6 +49,7 @@ "testdata", "Bytespider", "Timespans", - "googlequicksearchbox" + "googlequicksearchbox", + "cnstrc" ] } diff --git a/spec/src/modules/autocomplete.js b/spec/src/modules/autocomplete.js index 7ac2354e..4c16c892 100644 --- a/spec/src/modules/autocomplete.js +++ b/spec/src/modules/autocomplete.js @@ -38,6 +38,7 @@ describe(`ConstructorIO - Autocomplete${bundledDescriptionSuffix}`, () => { afterEach(() => { delete global.CLIENT_VERSION; delete window.CLIENT_VERSION; + delete window.cnstrc; cleanup(); fetchSpy = null; @@ -598,6 +599,67 @@ describe(`ConstructorIO - Autocomplete${bundledDescriptionSuffix}`, () => { autocomplete.getAutocompleteResults(query); }); + it('Should include window global userId when useWindowParameters is true and options.userId is absent', (done) => { + window.cnstrc = { userId: 'window-user-id' }; + const { autocomplete } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + autocomplete.getAutocompleteResults(query).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ui').to.equal('window-user-id'); + done(); + }); + }); + + it('Should include window global testCells when useWindowParameters is true and options.testCells is absent', (done) => { + window.cnstrc = { testCells: { experiment: 'variation_a' } }; + const { autocomplete } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + autocomplete.getAutocompleteResults(query).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ef-experiment').to.equal('variation_a'); + done(); + }); + }); + + it('Should include window global userSegments when useWindowParameters is true and options.segments is absent', (done) => { + window.cnstrc = { userSegments: ['vip', 'beta'] }; + const { autocomplete } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + autocomplete.getAutocompleteResults(query).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('us').to.deep.equal(['vip', 'beta']); + done(); + }); + }); + + it('Should not include window globals when useWindowParameters is false', (done) => { + window.cnstrc = { userId: 'window-user-id', testCells: { exp: 'var' }, userSegments: ['seg'] }; + const { autocomplete } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + autocomplete.getAutocompleteResults(query).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.not.have.property('ui'); + expect(requestedUrlParams).to.not.have.property('ef-exp'); + expect(requestedUrlParams).to.not.have.property('us'); + done(); + }); + }); + it('Should be rejected when invalid query is provided', () => { const { autocomplete } = new ConstructorIO({ apiKey: testApiKey }); diff --git a/spec/src/modules/browse.js b/spec/src/modules/browse.js index d3db86ef..a93e441b 100644 --- a/spec/src/modules/browse.js +++ b/spec/src/modules/browse.js @@ -39,6 +39,7 @@ describe(`ConstructorIO - Browse${bundledDescriptionSuffix}`, () => { afterEach(() => { delete global.CLIENT_VERSION; delete window.CLIENT_VERSION; + delete window.cnstrc; cleanup(); fetchSpy = null; @@ -153,6 +154,67 @@ describe(`ConstructorIO - Browse${bundledDescriptionSuffix}`, () => { }); }); + it('Should include window global userId when useWindowParameters is true and options.userId is absent', (done) => { + window.cnstrc = { userId: 'window-user-id' }; + const { browse } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + browse.getBrowseResults(filterName, filterValue).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ui').to.equal('window-user-id'); + done(); + }); + }); + + it('Should include window global testCells when useWindowParameters is true and options.testCells is absent', (done) => { + window.cnstrc = { testCells: { experiment: 'variation_a' } }; + const { browse } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + browse.getBrowseResults(filterName, filterValue).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ef-experiment').to.equal('variation_a'); + done(); + }); + }); + + it('Should include window global userSegments when useWindowParameters is true and options.segments is absent', (done) => { + window.cnstrc = { userSegments: ['vip', 'beta'] }; + const { browse } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + browse.getBrowseResults(filterName, filterValue).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('us').to.deep.equal(['vip', 'beta']); + done(); + }); + }); + + it('Should not include window globals when useWindowParameters is false', (done) => { + window.cnstrc = { userId: 'window-user-id', testCells: { exp: 'var' }, userSegments: ['seg'] }; + const { browse } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + browse.getBrowseResults(filterName, filterValue).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.not.have.property('ui'); + expect(requestedUrlParams).to.not.have.property('ef-exp'); + expect(requestedUrlParams).to.not.have.property('us'); + done(); + }); + }); + it('Should return a response with a valid filterName, filterValue and page', (done) => { const page = 1; const { browse } = new ConstructorIO({ diff --git a/spec/src/modules/recommendations.js b/spec/src/modules/recommendations.js index 53dfaf70..218c3cd9 100644 --- a/spec/src/modules/recommendations.js +++ b/spec/src/modules/recommendations.js @@ -38,6 +38,7 @@ describe(`ConstructorIO - Recommendations${bundledDescriptionSuffix}`, () => { afterEach(() => { delete global.CLIENT_VERSION; delete window.CLIENT_VERSION; + delete window.cnstrc; cleanup(); fetchSpy = null; @@ -574,5 +575,66 @@ describe(`ConstructorIO - Recommendations${bundledDescriptionSuffix}`, () => { )).to.eventually.be.rejectedWith(timeoutRejectionMessage); }); } + + it('Should include window global userId when useWindowParameters is true and options.userId is absent', (done) => { + window.cnstrc = { userId: 'window-user-id' }; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + recommendations.getRecommendations(podId, { itemIds: itemId }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ui').to.equal('window-user-id'); + done(); + }); + }); + + it('Should include window global testCells when useWindowParameters is true and options.testCells is absent', (done) => { + window.cnstrc = { testCells: { experiment: 'variation_a' } }; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + recommendations.getRecommendations(podId, { itemIds: itemId }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ef-experiment').to.equal('variation_a'); + done(); + }); + }); + + it('Should include window global userSegments when useWindowParameters is true and options.segments is absent', (done) => { + window.cnstrc = { userSegments: ['vip', 'beta'] }; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + recommendations.getRecommendations(podId, { itemIds: itemId }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('us').to.deep.equal(['vip', 'beta']); + done(); + }); + }); + + it('Should not include window globals when useWindowParameters is false', (done) => { + window.cnstrc = { userId: 'window-user-id', testCells: { exp: 'var' }, userSegments: ['seg'] }; + const { recommendations } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + recommendations.getRecommendations(podId, { itemIds: itemId }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.not.have.property('ui'); + expect(requestedUrlParams).to.not.have.property('ef-exp'); + expect(requestedUrlParams).to.not.have.property('us'); + done(); + }); + }); }); }); diff --git a/spec/src/modules/search.js b/spec/src/modules/search.js index aaa65890..5cbd9095 100644 --- a/spec/src/modules/search.js +++ b/spec/src/modules/search.js @@ -38,6 +38,7 @@ describe(`ConstructorIO - Search${bundledDescriptionSuffix}`, () => { afterEach(() => { delete global.CLIENT_VERSION; delete window.CLIENT_VERSION; + delete window.cnstrc; cleanup(); fetchSpy = null; @@ -150,6 +151,83 @@ describe(`ConstructorIO - Search${bundledDescriptionSuffix}`, () => { }); }); + it('Should include window global userId when useWindowParameters is true and options.userId is absent', (done) => { + window.cnstrc = { userId: 'window-user-id' }; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { section }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ui').to.equal('window-user-id'); + done(); + }); + }); + + it('Should include window global testCells when useWindowParameters is true and options.testCells is absent', (done) => { + window.cnstrc = { testCells: { experiment: 'variation_a' } }; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { section }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ef-experiment').to.equal('variation_a'); + done(); + }); + }); + + it('Should include window global userSegments when useWindowParameters is true and options.segments is absent', (done) => { + window.cnstrc = { userSegments: ['vip', 'beta'] }; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { section }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('us').to.deep.equal(['vip', 'beta']); + done(); + }); + }); + + it('Should prefer options.userId over window global when useWindowParameters is true', (done) => { + window.cnstrc = { userId: 'window-user-id' }; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + userId: 'options-user-id', + useWindowParameters: true, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { section }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ui').to.equal('options-user-id'); + done(); + }); + }); + + it('Should not include window globals when useWindowParameters is false', (done) => { + window.cnstrc = { userId: 'window-user-id', testCells: { exp: 'var' }, userSegments: ['seg'] }; + const { search } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + }); + + search.getSearchResults(query, { section }).then(() => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.not.have.property('ui'); + expect(requestedUrlParams).to.not.have.property('ef-exp'); + expect(requestedUrlParams).to.not.have.property('us'); + done(); + }); + }); + it('Should return a response with a valid query, section, and offset', (done) => { const offset = 1; const { search } = new ConstructorIO({ diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index fc275beb..a0698c49 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -75,6 +75,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { delete window.CLIENT_VERSION; delete global.CLIENT_VERSION; + delete window.cnstrc; cleanup(); setTimeout(done, delayBetweenTests); @@ -459,6 +460,50 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackSessionStart()).to.equal(true); }); + + it('Should include window globals in tracking requests when useWindowParameters is true', (done) => { + window.cnstrc = { userId: 'window-user-id', testCells: { exp: 'var' }, userSegments: ['seg1', 'seg2'] }; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', () => { + try { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ui').to.equal('window-user-id'); + expect(requestedUrlParams).to.have.property('ef-exp').to.equal('var'); + expect(requestedUrlParams).to.have.property('us').to.deep.equal(['seg1', 'seg2']); + done(); + } catch (e) { + done(e); + } + }); + + expect(tracker.trackSessionStart()).to.equal(true); + }); + + it('Should prefer options.userId over window global in tracking requests when useWindowParameters is true', (done) => { + window.cnstrc = { userId: 'window-user-id' }; + const instance = new ConstructorIO({ + apiKey: testApiKey, + useWindowParameters: true, + fetch: fetchSpy, + ...requestQueueOptions, + }); + + instance.setClientOptions({ userId: 'explicit-user-id' }); + + instance.tracker.on('success', () => { + const requestedUrlParams = helpers.extractUrlParamsFromFetch(fetchSpy); + expect(requestedUrlParams).to.have.property('ui').to.equal('explicit-user-id'); + done(); + }); + + expect(instance.tracker.trackSessionStart()).to.equal(true); + }); }); describe('trackInputFocus', () => { diff --git a/spec/src/utils/helpers.js b/spec/src/utils/helpers.js index 292e33c5..a0692986 100644 --- a/spec/src/utils/helpers.js +++ b/spec/src/utils/helpers.js @@ -27,6 +27,7 @@ const { trimUrl, cleanAndValidateUrl, toValidTestCells, + applyWindowParameterGetters, } = require('../../../test/utils/helpers'); // eslint-disable-line import/extensions const jsdom = require('./jsdom-global'); const store = require('../../../test/utils/store'); // eslint-disable-line import/extensions @@ -693,6 +694,172 @@ describe('ConstructorIO - Utils - Helpers', () => { expect(result.length).to.be.at.most(2000); }); }); + + describe('applyWindowParameterGetters', () => { + const jsdomLocal = require('./jsdom-global'); + let cleanup; + + beforeEach(() => { + cleanup = jsdomLocal({ url: 'http://localhost' }); + }); + + afterEach(() => { + delete window.cnstrc; + delete window.cnstrcUserId; + delete window.cnstrcTestCells; + delete window.cnstrcUserSegments; + cleanup(); + }); + + it('Should fall back to window.cnstrc.userId when options.userId is absent', () => { + window.cnstrc = { userId: 'cnstrc-user' }; + const options = {}; + applyWindowParameterGetters(options); + expect(options.userId).to.equal('cnstrc-user'); + }); + + it('Should fall back to window.cnstrcUserId when window.cnstrc does not exist', () => { + window.cnstrcUserId = 'global-user'; + const options = {}; + applyWindowParameterGetters(options); + expect(options.userId).to.equal('global-user'); + }); + + it('Should prefer window.cnstrc.userId over window.cnstrcUserId', () => { + window.cnstrc = { userId: 'cnstrc-user' }; + window.cnstrcUserId = 'global-user'; + const options = {}; + applyWindowParameterGetters(options); + expect(options.userId).to.equal('cnstrc-user'); + }); + + it('Should use options.userId when provided (priority over window globals)', () => { + window.cnstrc = { userId: 'cnstrc-user' }; + const options = { userId: 'options-user' }; + applyWindowParameterGetters(options); + expect(options.userId).to.equal('options-user'); + }); + + it('Should ignore non-string userId from window globals', () => { + window.cnstrc = { userId: 12345 }; + window.cnstrcUserId = { notAString: true }; + const options = {}; + applyWindowParameterGetters(options); + expect(options.userId).to.be.undefined; + }); + + it('Should ignore empty string userId from window globals', () => { + window.cnstrc = { userId: '' }; + const options = {}; + applyWindowParameterGetters(options); + expect(options.userId).to.be.undefined; + }); + + it('Should fall back to window.cnstrc.testCells when options.testCells is empty', () => { + window.cnstrc = { testCells: { foo: 'bar' } }; + const options = { testCells: {} }; + applyWindowParameterGetters(options); + expect(options.testCells).to.deep.equal({ foo: 'bar' }); + }); + + it('Should fall back to window.cnstrcTestCells when window.cnstrc does not exist', () => { + window.cnstrcTestCells = { baz: 'qux' }; + const options = { testCells: {} }; + applyWindowParameterGetters(options); + expect(options.testCells).to.deep.equal({ baz: 'qux' }); + }); + + it('Should prefer window.cnstrc.testCells over window.cnstrcTestCells', () => { + window.cnstrc = { testCells: { foo: 'bar' } }; + window.cnstrcTestCells = { baz: 'qux' }; + const options = { testCells: {} }; + applyWindowParameterGetters(options); + expect(options.testCells).to.deep.equal({ foo: 'bar' }); + }); + + it('Should use options.testCells when provided (priority over window globals)', () => { + window.cnstrc = { testCells: { foo: 'bar' } }; + const options = { testCells: { mine: 'value' } }; + applyWindowParameterGetters(options); + expect(options.testCells).to.deep.equal({ mine: 'value' }); + }); + + it('Should validate testCells from window through toValidTestCells', () => { + window.cnstrc = { testCells: { valid: 'yes', invalid: null, num: 123 } }; + const options = { testCells: {} }; + applyWindowParameterGetters(options); + expect(options.testCells).to.deep.equal({ valid: 'yes' }); + }); + + it('Should ignore non-object testCells from window globals', () => { + window.cnstrc = { testCells: 'not-an-object' }; + window.cnstrcTestCells = [1, 2, 3]; + const options = { testCells: {} }; + applyWindowParameterGetters(options); + expect(options.testCells).to.be.undefined; + }); + + it('Should fall back to window.cnstrc.userSegments when options.segments is empty', () => { + window.cnstrc = { userSegments: ['seg1', 'seg2'] }; + const options = { segments: [] }; + applyWindowParameterGetters(options); + expect(options.segments).to.deep.equal(['seg1', 'seg2']); + }); + + it('Should fall back to window.cnstrcUserSegments when window.cnstrc does not exist', () => { + window.cnstrcUserSegments = ['seg3']; + const options = {}; + applyWindowParameterGetters(options); + expect(options.segments).to.deep.equal(['seg3']); + }); + + it('Should prefer window.cnstrc.userSegments over window.cnstrcUserSegments', () => { + window.cnstrc = { userSegments: ['seg1'] }; + window.cnstrcUserSegments = ['seg2']; + const options = {}; + applyWindowParameterGetters(options); + expect(options.segments).to.deep.equal(['seg1']); + }); + + it('Should use options.segments when provided (priority over window globals)', () => { + window.cnstrc = { userSegments: ['window-seg'] }; + const options = { segments: ['option-seg'] }; + applyWindowParameterGetters(options); + expect(options.segments).to.deep.equal(['option-seg']); + }); + + it('Should ignore non-array userSegments from window globals', () => { + window.cnstrc = { userSegments: 'not-an-array' }; + window.cnstrcUserSegments = { notArray: true }; + const options = {}; + applyWindowParameterGetters(options); + expect(options.segments).to.be.undefined; + }); + + it('Should ignore empty array userSegments from window globals', () => { + window.cnstrc = { userSegments: [] }; + const options = {}; + applyWindowParameterGetters(options); + expect(options.segments).to.be.undefined; + }); + + it('Should allow setting values via setter after getters are applied', () => { + window.cnstrc = { userId: 'cnstrc-user' }; + const options = {}; + applyWindowParameterGetters(options); + expect(options.userId).to.equal('cnstrc-user'); + options.userId = 'new-user'; + expect(options.userId).to.equal('new-user'); + }); + + it('Should read latest window globals at access time', () => { + const options = {}; + applyWindowParameterGetters(options); + expect(options.userId).to.be.undefined; + window.cnstrc = { userId: 'late-user' }; + expect(options.userId).to.equal('late-user'); + }); + }); } describe('toValidTestCells', () => { diff --git a/src/constructorio.js b/src/constructorio.js index 8a469785..59764afa 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -58,6 +58,7 @@ class ConstructorIO { * @param {object} [parameters.networkParameters] - Parameters relevant to network requests * @param {number} [parameters.networkParameters.timeout] - Request timeout (in milliseconds) - may be overridden within individual method calls * @param {string} [parameters.humanityCheckLocation='session'] - Storage location for the humanity check flag ('session' for sessionStorage, 'local' for localStorage) + * @param {boolean} [parameters.useWindowParameters=false] - Indicates if window globals (cnstrc/cnstrcUserId/cnstrcTestCells/cnstrcUserSegments) should be used as fallback for userId, testCells, and segments * @property {object} search - Interface to {@link module:search} * @property {object} browse - Interface to {@link module:browse} * @property {object} autocomplete - Interface to {@link module:autocomplete} @@ -91,6 +92,7 @@ class ConstructorIO { beaconMode, networkParameters, humanityCheckLocation, + useWindowParameters, } = options; if (!apiKey || typeof apiKey !== 'string') { @@ -139,8 +141,13 @@ class ConstructorIO { beaconMode: (beaconMode === false) ? false : true, // Defaults to 'true', networkParameters: networkParameters || {}, humanityCheckLocation: humanityCheckLocation || 'session', + useWindowParameters: useWindowParameters === true, }; + if (useWindowParameters === true) { + helpers.applyWindowParameterGetters(this.options); + } + // Expose global modules this.search = new Search(this.options); this.browse = new Browse(this.options); @@ -187,12 +194,10 @@ class ConstructorIO { this.tracker.requests.sendTrackingEvents = sendTrackingEvents; } - // Set Session ID in dom-less environments only if (sessionId && !helpers.canUseDOM()) { this.options.sessionId = sessionId; } - // If User ID is passed if ('userId' in options) { this.options.userId = userId; } diff --git a/src/modules/recommendations.js b/src/modules/recommendations.js index ca839109..c30b6495 100644 --- a/src/modules/recommendations.js +++ b/src/modules/recommendations.js @@ -5,7 +5,7 @@ const helpers = require('../utils/helpers'); // Create URL from supplied parameters // eslint-disable-next-line complexity function createRecommendationsUrl(podId, parameters, options) { - const { apiKey, version, serviceUrl, sessionId, userId, clientId, segments } = options; + const { apiKey, version, serviceUrl, sessionId, userId, clientId, segments, testCells } = options; let queryParams = { c: version }; queryParams.key = apiKey; @@ -27,6 +27,13 @@ function createRecommendationsUrl(podId, parameters, options) { queryParams.ui = String(userId); } + // Pull test cells from options + if (testCells) { + Object.keys(testCells).forEach((testCellKey) => { + queryParams[`ef-${testCellKey}`] = testCells[testCellKey]; + }); + } + if (parameters) { const { numResults, diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 6ba5fc57..b8e853c2 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -70,6 +70,7 @@ export interface ConstructorClientOptions { beaconMode?: boolean; networkParameters?: NetworkParameters; humanityCheckLocation?: 'session' | 'local'; + useWindowParameters?: boolean; } export interface RequestFeature extends Record { diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 6ffc8396..3e3d5940 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -390,6 +390,54 @@ const utils = { return filtered; }, + applyWindowParameterGetters: (options) => { + if (!utils.canUseDOM()) { + return; + } + + const defineWindowGetter = (key, windowKey, validate) => { + const initial = options[key] || undefined; + let backing = initial; + + Object.defineProperty(options, key, { + get() { + if (validate(backing)) return backing; + const windowValue = (window.cnstrc && window.cnstrc[windowKey]) || window[`cnstrc${windowKey.charAt(0).toUpperCase()}${windowKey.slice(1)}`]; + const validated = validate(windowValue) ? windowValue : undefined; + return validated; + }, + set(value) { backing = value; }, + enumerable: true, + configurable: true, + }); + }; + + defineWindowGetter('userId', 'userId', (v) => typeof v === 'string' && v.length > 0); + + defineWindowGetter('segments', 'userSegments', (v) => Array.isArray(v) && v.length > 0); + + // testCells needs special handling for validation through toValidTestCells + const initialTestCells = options.testCells || undefined; + let backingTestCells = initialTestCells; + + Object.defineProperty(options, 'testCells', { + get() { + if (backingTestCells && Object.keys(backingTestCells).length > 0) { + return backingTestCells; + } + const windowTestCells = (window.cnstrc && window.cnstrc.testCells) || window.cnstrcTestCells; + const validated = utils.toValidTestCells(windowTestCells); + if (validated && Object.keys(validated).length > 0) { + return validated; + } + return undefined; + }, + set(value) { backingTestCells = value; }, + enumerable: true, + configurable: true, + }); + }, + getBehaviorUrl: (mediaServiceUrl) => { const baseUrl = new URL(mediaServiceUrl);