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/mocha.helpers.js b/spec/mocha.helpers.js index fb7ac900..675be288 100644 --- a/spec/mocha.helpers.js +++ b/spec/mocha.helpers.js @@ -64,6 +64,13 @@ const extractBodyParamsFromFetch = (fetch) => { return null; }; +// Extract headers from the last fetch spy call +const extractHeadersFromFetch = (fetch) => { + const lastCallArguments = fetch && fetch.args && fetch.args[fetch.args.length - 1]; + const requestData = lastCallArguments && lastCallArguments[1]; + return (requestData && requestData.headers) || {}; +}; + const getUserDefinedWindowProperties = () => { const iframe = document.createElement('iframe'); @@ -85,5 +92,6 @@ module.exports = { clearStorage, extractUrlParamsFromFetch, extractBodyParamsFromFetch, + extractHeadersFromFetch, getUserDefinedWindowProperties, }; diff --git a/spec/src/constructorio.js b/spec/src/constructorio.js index de776485..5d03ff9b 100644 --- a/spec/src/constructorio.js +++ b/spec/src/constructorio.js @@ -741,6 +741,72 @@ if (!bundled) { expect(instance).to.have.property('tracker'); }); + it('Should store securityToken in options when provided', () => { + const securityToken = 'test-security-token'; + const instance = new ConstructorIO({ + apiKey: validApiKey, + clientId, + sessionId, + securityToken, + }); + + expect(instance.options).to.have.property('securityToken').to.equal(securityToken); + }); + + it('Should store empty string for securityToken when not provided', () => { + const instance = new ConstructorIO({ + apiKey: validApiKey, + clientId, + sessionId, + }); + + expect(instance.options).to.have.property('securityToken').to.equal(''); + }); + + it('Should store userIp in options when provided', () => { + const userIp = '127.0.0.1'; + const instance = new ConstructorIO({ + apiKey: validApiKey, + clientId, + sessionId, + userIp, + }); + + expect(instance.options).to.have.property('userIp').to.equal(userIp); + }); + + it('Should store empty string for userIp when not provided', () => { + const instance = new ConstructorIO({ + apiKey: validApiKey, + clientId, + sessionId, + }); + + expect(instance.options).to.have.property('userIp').to.equal(''); + }); + + it('Should store userAgent in options when provided', () => { + const userAgent = 'custom-agent/1.0'; + const instance = new ConstructorIO({ + apiKey: validApiKey, + clientId, + sessionId, + userAgent, + }); + + expect(instance.options).to.have.property('userAgent').to.equal(userAgent); + }); + + it('Should store empty string for userAgent when not provided', () => { + const instance = new ConstructorIO({ + apiKey: validApiKey, + clientId, + sessionId, + }); + + expect(instance.options).to.have.property('userAgent').to.equal(''); + }); + it('Should throw an error if client identifier is not provided', () => { expect(() => new ConstructorIO({ apiKey: validApiKey, diff --git a/spec/src/utils/request-queue.js b/spec/src/utils/request-queue.js index 767e5bed..f2492557 100644 --- a/spec/src/utils/request-queue.js +++ b/spec/src/utils/request-queue.js @@ -849,6 +849,131 @@ describe('ConstructorIO - Utils - Request Queue', function utilsRequestQueue() { }, waitInterval); }); }); + + describe('headers', () => { + describe('securityToken', () => { + it('Should send x-cnstrc-token header on POST requests when securityToken is provided', (done) => { + const requests = new RequestQueue({ + ...requestQueueOptions, + securityToken: 'test-security-token', + }); + + store.session.set(humanityStorageKey, true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start', 'POST', { action: 'session_start' }); + requests.send(); + + setTimeout(() => { + const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy); + expect(requestHeaders).to.have.property('x-cnstrc-token').to.equal('test-security-token'); + done(); + }, waitInterval); + }); + + it('Should send x-cnstrc-token header on GET requests when securityToken is provided', (done) => { + const requests = new RequestQueue({ + ...requestQueueOptions, + securityToken: 'test-security-token', + }); + + store.session.set(humanityStorageKey, true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start'); + requests.send(); + + setTimeout(() => { + const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy); + expect(requestHeaders).to.have.property('x-cnstrc-token').to.equal('test-security-token'); + done(); + }, waitInterval); + }); + + it('Should not send x-cnstrc-token header when securityToken is not provided', (done) => { + const requests = new RequestQueue(requestQueueOptions); + + store.session.set(humanityStorageKey, true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start', 'POST', { action: 'session_start' }); + requests.send(); + + setTimeout(() => { + const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy); + expect(requestHeaders).to.not.have.property('x-cnstrc-token'); + done(); + }, waitInterval); + }); + }); + + describe('userIp', () => { + it('Should send X-Forwarded-For header when userIp is provided', (done) => { + const requests = new RequestQueue({ + ...requestQueueOptions, + userIp: '127.0.0.1', + }); + + store.session.set(humanityStorageKey, true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start', 'POST', { action: 'session_start' }); + requests.send(); + + setTimeout(() => { + const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy); + expect(requestHeaders).to.have.property('X-Forwarded-For').to.equal('127.0.0.1'); + done(); + }, waitInterval); + }); + + it('Should not send X-Forwarded-For header when userIp is not provided', (done) => { + const requests = new RequestQueue(requestQueueOptions); + + store.session.set(humanityStorageKey, true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start', 'POST', { action: 'session_start' }); + requests.send(); + + setTimeout(() => { + const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy); + expect(requestHeaders).to.not.have.property('X-Forwarded-For'); + done(); + }, waitInterval); + }); + }); + + describe('userAgent', () => { + it('Should send User-Agent header when userAgent is provided', (done) => { + const requests = new RequestQueue({ + ...requestQueueOptions, + userAgent: 'custom-agent/1.0', + }); + + store.session.set(humanityStorageKey, true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start', 'POST', { action: 'session_start' }); + requests.send(); + + setTimeout(() => { + const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy); + expect(requestHeaders).to.have.property('User-Agent').to.equal('custom-agent/1.0'); + done(); + }, waitInterval); + }); + + it('Should not send User-Agent header when userAgent is not provided', (done) => { + const requests = new RequestQueue(requestQueueOptions); + + store.session.set(humanityStorageKey, true); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start', 'POST', { action: 'session_start' }); + requests.send(); + + setTimeout(() => { + const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy); + expect(requestHeaders).to.not.have.property('User-Agent'); + done(); + }, waitInterval); + }); + }); + }); }); describe('domless', () => { diff --git a/src/constructorio.js b/src/constructorio.js index 8a469785..b9db6c12 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -91,6 +91,9 @@ class ConstructorIO { beaconMode, networkParameters, humanityCheckLocation, + securityToken, + userIp, + userAgent, } = options; if (!apiKey || typeof apiKey !== 'string') { @@ -139,6 +142,9 @@ class ConstructorIO { beaconMode: (beaconMode === false) ? false : true, // Defaults to 'true', networkParameters: networkParameters || {}, humanityCheckLocation: humanityCheckLocation || 'session', + securityToken: securityToken || '', + userIp: userIp || '', + userAgent: userAgent || '', }; // Expose global modules diff --git a/src/utils/request-queue.js b/src/utils/request-queue.js index 291d0724..944898ba 100644 --- a/src/utils/request-queue.js +++ b/src/utils/request-queue.js @@ -112,16 +112,19 @@ class RequestQueue { } } + const headers = this.buildHeaders(); + if (nextInQueue.method === 'GET') { - request = fetch(nextInQueue.url, { signal }); + request = fetch(nextInQueue.url, { headers, signal }); } if (nextInQueue.method === 'POST') { + headers['Content-Type'] = 'text/plain'; request = fetch(nextInQueue.url, { method: nextInQueue.method, body: JSON.stringify(nextInQueue.body), mode: 'cors', - headers: { 'Content-Type': 'text/plain' }, + headers, signal, }); } @@ -186,6 +189,21 @@ class RequestQueue { } } + // Build request headers from options + buildHeaders() { + const headers = {}; + if (this.options.securityToken && typeof this.options.securityToken === 'string') { + headers['x-cnstrc-token'] = this.options.securityToken; + } + if (this.options.userIp && typeof this.options.userIp === 'string') { + headers['X-Forwarded-For'] = this.options.userIp; + } + if (this.options.userAgent && typeof this.options.userAgent === 'string') { + headers['User-Agent'] = this.options.userAgent; + } + return headers; + } + // Read from queue and send requests to server send() { if (this.sendTrackingEvents) {