Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"testdata",
"Bytespider",
"Timespans",
"googlequicksearchbox"
"googlequicksearchbox",
"cnstrc"
]
}
8 changes: 8 additions & 0 deletions spec/mocha.helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -85,5 +92,6 @@ module.exports = {
clearStorage,
extractUrlParamsFromFetch,
extractBodyParamsFromFetch,
extractHeadersFromFetch,
getUserDefinedWindowProperties,
};
66 changes: 66 additions & 0 deletions spec/src/constructorio.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
125 changes: 125 additions & 0 deletions spec/src/utils/request-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Comment on lines +907 to +924
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header support is implemented for both GET and POST requests, but the tests for userIp currently only cover POST. Add a GET-coverage test to ensure X-Forwarded-For is included when userIp is provided on GET requests as well (and absent when not provided).

Copilot uses AI. Check for mistakes.

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);
});
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header support is implemented for both GET and POST requests, but the tests for userAgent currently only cover POST. Add a GET-coverage test to verify the User-Agent header behavior (present when provided, absent otherwise) for GET requests too.

Suggested change
});
});
it('Should send User-Agent header for GET 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', 'GET');
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 for GET 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', 'GET');
requests.send();
setTimeout(() => {
const requestHeaders = helpers.extractHeadersFromFetch(fetchSpy);
expect(requestHeaders).to.not.have.property('User-Agent');
done();
}, waitInterval);
});

Copilot uses AI. Check for mistakes.
});
});
});

describe('domless', () => {
Expand Down
6 changes: 6 additions & 0 deletions src/constructorio.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class ConstructorIO {
beaconMode,
networkParameters,
humanityCheckLocation,
securityToken,
userIp,
userAgent,
} = options;
Comment on lines 91 to 97
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New client options securityToken, userIp, and userAgent were added, but they aren’t included in the constructor JSDoc options list above. Please update the JSDoc so consumers know these options exist and what they do (including any environment limitations like browser vs Node).

Copilot uses AI. Check for mistakes.

if (!apiKey || typeof apiKey !== 'string') {
Expand Down Expand Up @@ -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 || '',
Comment on lines 144 to +147
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions userIP, but the code introduces userIp (camelCase). If external users are expected to configure this, consider either aligning naming (or accepting both userIP and userIp as aliases) and ensure docs/README match the actual option name to avoid integration confusion.

Copilot uses AI. Check for mistakes.
};

// Expose global modules
Expand Down
22 changes: 20 additions & 2 deletions src/utils/request-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Expand Down Expand Up @@ -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') {
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildHeaders sets a User-Agent header when options.userAgent is provided. In browser environments, User-Agent is a forbidden request header and will be silently dropped (or may trigger issues in some runtimes), so this option won’t work as expected outside of Node/server-side fetch. Consider gating this header to DOM-less environments (e.g., !helpers.canUseDOM()) or documenting clearly that it’s only supported when using a server-side/custom fetch implementation.

Suggested change
if (this.options.userAgent && typeof this.options.userAgent === 'string') {
// Only set User-Agent in DOM-less environments (e.g., server-side/custom fetch)
if (!helpers.canUseDOM() && this.options.userAgent && typeof this.options.userAgent === 'string') {

Copilot uses AI. Check for mistakes.
headers['User-Agent'] = this.options.userAgent;
}
return headers;
}

// Read from queue and send requests to server
send() {
if (this.sendTrackingEvents) {
Expand Down
Loading