Skip to content
Merged
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
64 changes: 64 additions & 0 deletions spec/src/utils/humanity-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,70 @@ describe('ConstructorIO - Utils - Humanity Check', () => {
});
});

describe('humanityCheckLocation', () => {
const storageKey = '_constructorio_is_human';
let cleanup;

beforeEach(() => {
global.CLIENT_VERSION = 'cio-mocha';

cleanup = jsdom();
});

afterEach(() => {
delete global.CLIENT_VERSION;
cleanup();

helpers.clearStorage();
});

it('Should write to sessionStorage by default (no option)', () => {
const _ = new HumanityCheck();

helpers.triggerResize();
expect(store.session.get(storageKey)).to.equal(true);
expect(store.local.get(storageKey)).to.equal(null);
});

it('Should write to sessionStorage when humanityCheckLocation is "session"', () => {
const _ = new HumanityCheck({ humanityCheckLocation: 'session' });

helpers.triggerResize();
expect(store.session.get(storageKey)).to.equal(true);
expect(store.local.get(storageKey)).to.equal(null);
});

it('Should write to localStorage when humanityCheckLocation is "local"', () => {
const _ = new HumanityCheck({ humanityCheckLocation: 'local' });

helpers.triggerResize();
expect(store.local.get(storageKey)).to.equal(true);
expect(store.session.get(storageKey)).to.equal(null);
});

it('Should read existing localStorage value on construction when location is "local"', () => {
store.local.set(storageKey, true);
const humanity = new HumanityCheck({ humanityCheckLocation: 'local' });

expect(humanity.hasPerformedHumanEvent).to.equal(true);
});

it('Should NOT read sessionStorage when humanityCheckLocation is "local" (isolation)', () => {
store.session.set(storageKey, true);
const humanity = new HumanityCheck({ humanityCheckLocation: 'local' });

expect(humanity.hasPerformedHumanEvent).to.equal(false);
});

it('Should return false from isBot after human action when using localStorage mode', () => {
const humanity = new HumanityCheck({ humanityCheckLocation: 'local' });

expect(humanity.isBot()).to.equal(true);
helpers.triggerResize();
expect(humanity.isBot()).to.equal(false);
});
});

describe('isBot', () => {
const storageKey = '_constructorio_is_human';
let cleanup;
Expand Down
3 changes: 3 additions & 0 deletions src/constructorio.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class ConstructorIO {
* @param {boolean} [parameters.eventDispatcher.waitForBeacon=true] - Wait for beacon before dispatching events
* @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)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

The JSDoc for humanityCheckLocation is typed as {string}, but the implementation and TypeScript types only support 'session' | 'local'. Consider tightening the JSDoc type (e.g., a string-literal union) so generated docs better reflect the supported values.

Suggested change
* @param {string} [parameters.humanityCheckLocation='session'] - Storage location for the humanity check flag ('session' for sessionStorage, 'local' for localStorage)
* @param {'session'|'local'} [parameters.humanityCheckLocation='session'] - Storage location for the humanity check flag ('session' for sessionStorage, 'local' for localStorage)

Copilot uses AI. Check for mistakes.
* @property {object} search - Interface to {@link module:search}
* @property {object} browse - Interface to {@link module:browse}
* @property {object} autocomplete - Interface to {@link module:autocomplete}
Expand Down Expand Up @@ -89,6 +90,7 @@ class ConstructorIO {
idOptions,
beaconMode,
networkParameters,
humanityCheckLocation,
} = options;

if (!apiKey || typeof apiKey !== 'string') {
Expand Down Expand Up @@ -136,6 +138,7 @@ class ConstructorIO {
eventDispatcher,
beaconMode: (beaconMode === false) ? false : true, // Defaults to 'true',
networkParameters: networkParameters || {},
humanityCheckLocation: humanityCheckLocation || 'session',
};

// Expose global modules
Expand Down
1 change: 1 addition & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface ConstructorClientOptions {
eventDispatcher?: EventDispatcherOptions;
beaconMode?: boolean;
networkParameters?: NetworkParameters;
humanityCheckLocation?: 'session' | 'local';
}

export interface RequestFeature extends Record<string, any> {
Expand Down
20 changes: 15 additions & 5 deletions src/utils/humanity-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@ const humanEvents = [
];

class HumanityCheck {
constructor() {
constructor(options = {}) {
const { humanityCheckLocation } = options;

// Resolve storage backend: 'local' for localStorage, 'session' (default) for sessionStorage
this.store = humanityCheckLocation === 'local' ? store.local : store.session;

// Check if a human event has been performed in the past
this.hasPerformedHumanEvent = this.getIsHumanFromSessionStorage();
this.hasPerformedHumanEvent = this.getIsHumanFromStorage();

// Humanity proved, remove handlers
const remove = () => {
this.hasPerformedHumanEvent = true;

store.session.set(storageKey, true);
this.store.set(storageKey, true);
humanEvents.forEach((eventType) => {
helpers.removeEventListener(eventType, remove, true);
});
Expand All @@ -39,7 +44,12 @@ class HumanityCheck {
}
}

// Helper function to grab the human variable from session storage
// Helper function to grab the human variable from storage
getIsHumanFromStorage() {
return !!this.store.get(storageKey) || false;
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

getIsHumanFromStorage has redundant boolean logic: !!this.store.get(storageKey) || false always returns a boolean already. Consider simplifying to Boolean(this.store.get(storageKey)) (or just !!...) to improve readability and avoid unnecessary operations.

Suggested change
return !!this.store.get(storageKey) || false;
return Boolean(this.store.get(storageKey));

Copilot uses AI. Check for mistakes.
}

// Backward-compatible alias
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

getIsHumanFromSessionStorage() is now an alias for getIsHumanFromStorage() and may read from localStorage when humanityCheckLocation is 'local'. Consider clarifying this in the method comment (or renaming the alias comment) to avoid misleading callers who may expect it to always read sessionStorage.

Suggested change
// Backward-compatible alias
// Backward-compatible alias for getIsHumanFromStorage();
// may read from localStorage when humanityCheckLocation is 'local'

Copilot uses AI. Check for mistakes.
getIsHumanFromSessionStorage() {
return !!store.session.get(storageKey) || false;
}
Expand All @@ -58,7 +68,7 @@ class HumanityCheck {
}

// If the user hasn't performed a human event, it indicates it is a bot
if (!this.getIsHumanFromSessionStorage()) {
if (!this.getIsHumanFromStorage()) {
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/utils/request-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class RequestQueue {
constructor(options, eventemitter) {
this.options = options;
this.eventemitter = eventemitter;
this.humanity = new HumanityCheck();
this.humanity = new HumanityCheck(options);
this.requestPending = false;
this.pageUnloading = false;

Expand Down
Loading