diff --git a/spec/src/utils/humanity-check.js b/spec/src/utils/humanity-check.js index 9390a0e8..5f67c1b3 100644 --- a/spec/src/utils/humanity-check.js +++ b/spec/src/utils/humanity-check.js @@ -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; diff --git a/src/constructorio.js b/src/constructorio.js index 4c74668d..452f0054 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -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) * @property {object} search - Interface to {@link module:search} * @property {object} browse - Interface to {@link module:browse} * @property {object} autocomplete - Interface to {@link module:autocomplete} @@ -89,6 +90,7 @@ class ConstructorIO { idOptions, beaconMode, networkParameters, + humanityCheckLocation, } = options; if (!apiKey || typeof apiKey !== 'string') { @@ -136,6 +138,7 @@ class ConstructorIO { eventDispatcher, beaconMode: (beaconMode === false) ? false : true, // Defaults to 'true', networkParameters: networkParameters || {}, + humanityCheckLocation: humanityCheckLocation || 'session', }; // Expose global modules diff --git a/src/types/index.d.ts b/src/types/index.d.ts index ff95cde4..f81eaac7 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -64,6 +64,7 @@ export interface ConstructorClientOptions { eventDispatcher?: EventDispatcherOptions; beaconMode?: boolean; networkParameters?: NetworkParameters; + humanityCheckLocation?: 'session' | 'local'; } export interface RequestFeature extends Record { diff --git a/src/utils/humanity-check.js b/src/utils/humanity-check.js index 46eb7212..26876984 100644 --- a/src/utils/humanity-check.js +++ b/src/utils/humanity-check.js @@ -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); }); @@ -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; + } + + // Backward-compatible alias getIsHumanFromSessionStorage() { return !!store.session.get(storageKey) || false; } @@ -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; } diff --git a/src/utils/request-queue.js b/src/utils/request-queue.js index 2a691815..291d0724 100644 --- a/src/utils/request-queue.js +++ b/src/utils/request-queue.js @@ -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;