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
36 changes: 34 additions & 2 deletions src/listeners/__tests__/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ beforeAll(() => {
});
});

// clean mocks
afterEach(() => {
// clear mocks
beforeEach(() => {
(global.window.addEventListener as jest.Mock).mockClear();
(global.window.removeEventListener as jest.Mock).mockClear();
if (global.window.navigator.sendBeacon) (global.window.navigator.sendBeacon as jest.Mock).mockClear();
Object.values(fakeSplitApi).forEach(method => method.mockClear());
});

// delete mocks from global
Expand Down Expand Up @@ -227,3 +228,34 @@ test('Browser JS listener / standalone mode / Impressions debug mode without sen
// restore sendBeacon API
global.navigator.sendBeacon = sendBeacon;
});

test('Browser JS listener / standalone mode / user consent status', () => {
const syncManagerMock = {};
const settings = { ...fullSettings };

// @ts-expect-error
const listener = new BrowserSignalListener(syncManagerMock, settings, fakeStorageOptimized as IStorageSync, fakeSplitApi);

listener.start();

settings.userConsent = 'UNKNOWN';
triggerUnloadEvent();
settings.userConsent = 'DECLINED';
triggerUnloadEvent();

// Unload event was triggered when user consent was unknown and declined. Thus sendBeacon and post services should not be called
expect(global.window.navigator.sendBeacon).toBeCalledTimes(0);
expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled();
expect(fakeSplitApi.postEventsBulk).not.toBeCalled();
expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled();

settings.userConsent = 'GRANTED';
triggerUnloadEvent();
settings.userConsent = undefined;
triggerUnloadEvent();

// Unload event was triggered when user consent was granted and undefined. Thus sendBeacon should be called 6 times (3 times per event in optimized mode).
expect(global.window.navigator.sendBeacon).toBeCalledTimes(6);

listener.stop();
});
22 changes: 13 additions & 9 deletions src/listeners/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { OPTIMIZED, DEBUG } from '../utils/constants';
import { objectAssign } from '../utils/lang/objectAssign';
import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants';
import { ISyncManager } from '../sync/types';
import { isConsentGranted } from '../utils/consent';

// 'unload' event is used instead of 'beforeunload', since 'unload' is not a cancelable event, so no other listeners can stop the event from occurring.
const UNLOAD_DOM_EVENT = 'unload';
Expand Down Expand Up @@ -65,15 +66,18 @@ export class BrowserSignalListener implements ISignalListener {
flushData() {
if (!this.syncManager) return; // In consumer mode there is not sync manager and data to flush

const eventsUrl = this.settings.urls.events;
const extraMetadata = {
// sim stands for Sync/Split Impressions Mode
sim: this.settings.sync.impressionsMode === OPTIMIZED ? OPTIMIZED : DEBUG
};
// Flush data if there is user consent
if (isConsentGranted(this.settings)) {
const eventsUrl = this.settings.urls.events;
const extraMetadata = {
// sim stands for Sync/Split Impressions Mode
sim: this.settings.sync.impressionsMode === OPTIMIZED ? OPTIMIZED : DEBUG
};

this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
}

// Close streaming connection
if (this.syncManager.pushManager) this.syncManager.pushManager.stop();
Expand All @@ -84,7 +88,7 @@ export class BrowserSignalListener implements ISignalListener {
if (!cache.isEmpty()) {
const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.state()) : cache.state();
if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exceptions
postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exception
}
cache.clear();
}
Expand Down
14 changes: 4 additions & 10 deletions src/sync/syncManagerOnline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IPushManager } from './streaming/types';
import { IPollingManager, IPollingManagerCS } from './polling/types';
import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants';
import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants';
import { CONSENT_GRANTED } from '../utils/constants';
import { isConsentGranted } from '../utils/consent';

/**
* Online SyncManager factory.
Expand All @@ -26,7 +26,7 @@ export function syncManagerOnlineFactory(
*/
return function (params: ISyncManagerFactoryParams): ISyncManagerCS {

const { log, streamingEnabled } = params.settings;
const { settings, settings: { log, streamingEnabled } } = params;

/** Polling Manager */
const pollingManager = pollingManagerFactory && pollingManagerFactory(params);
Expand All @@ -40,12 +40,6 @@ export function syncManagerOnlineFactory(
// It is not inyected as push and polling managers, because at the moment it is required
const submitter = submitterManagerFactory(params);

function isUserConsentGranted() {
const userConsent = params.settings.userConsent;
// undefined userConsent is handled as granted to avoid a breaking change with users that don't use this features
return !userConsent || userConsent === CONSENT_GRANTED;
}

/** Sync Manager logic */

function startPolling() {
Expand Down Expand Up @@ -102,7 +96,7 @@ export function syncManagerOnlineFactory(
}

// start periodic data recording (events, impressions, telemetry).
if (isUserConsentGranted()) submitter.start();
if (isConsentGranted(settings)) submitter.start();
},

/**
Expand All @@ -124,7 +118,7 @@ export function syncManagerOnlineFactory(
},

flush() {
if (isUserConsentGranted()) return submitter.execute();
if (isConsentGranted(settings)) return submitter.execute();
else return Promise.resolve();
},

Expand Down
8 changes: 8 additions & 0 deletions src/utils/consent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ISettings } from '../types';
import { CONSENT_GRANTED } from './constants';

export function isConsentGranted(settings: ISettings) {
const userConsent = settings.userConsent;
// undefined userConsent is handled as granted (default)
return !userConsent || userConsent === CONSENT_GRANTED;
}