From 0dd964a08a3c4a7a7c8b06b7fe68f1ae65b50df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Tue, 9 Jan 2024 15:04:45 +0100 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20[RUM-2445]=20introduce=20init?= =?UTF-8?q?=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/configuration.spec.ts | 35 +++++++++++++++---- .../src/domain/configuration/configuration.ts | 12 +++++++ packages/core/src/domain/trackingConsent.ts | 5 +++ packages/core/src/index.ts | 1 + 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/domain/trackingConsent.ts diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index c77d381be7..5a3653006c 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -5,12 +5,19 @@ import { isExperimentalFeatureEnabled, resetExperimentalFeatures, } from '../../tools/experimentalFeatures' +import { TrackingConsent } from '../trackingConsent' import type { InitConfiguration } from './configuration' import { validateAndBuildConfiguration } from './configuration' describe('validateAndBuildConfiguration', () => { const clientToken = 'some_client_token' + let displaySpy: jasmine.Spy + + beforeEach(() => { + displaySpy = spyOn(display, 'error') + }) + afterEach(() => { resetExperimentalFeatures() }) @@ -44,12 +51,6 @@ describe('validateAndBuildConfiguration', () => { }) describe('validate init configuration', () => { - let displaySpy: jasmine.Spy - - beforeEach(() => { - displaySpy = spyOn(display, 'error') - }) - it('requires the InitConfiguration to be defined', () => { expect(validateAndBuildConfiguration(undefined as unknown as InitConfiguration)).toBeUndefined() expect(displaySpy).toHaveBeenCalledOnceWith('Client Token is not configured, we will not send any data.') @@ -160,7 +161,6 @@ describe('validateAndBuildConfiguration', () => { throw myError } const configuration = validateAndBuildConfiguration({ clientToken, beforeSend })! - const displaySpy = spyOn(display, 'error') expect(configuration.beforeSend!(null, {})).toBeUndefined() expect(displaySpy).toHaveBeenCalledWith('beforeSend threw an error:', myError) }) @@ -186,4 +186,25 @@ describe('validateAndBuildConfiguration', () => { ).toBeTrue() }) }) + + describe('trackingConsent', () => { + it('defaults to "granted"', () => { + expect(validateAndBuildConfiguration({ clientToken: 'yes' })!.trackingConsent).toBe(TrackingConsent.GRANTED) + }) + + it('is set to provided value', () => { + expect( + validateAndBuildConfiguration({ clientToken: 'yes', trackingConsent: TrackingConsent.NOT_GRANTED })! + .trackingConsent + ).toBe(TrackingConsent.NOT_GRANTED) + expect( + validateAndBuildConfiguration({ clientToken: 'yes', trackingConsent: TrackingConsent.GRANTED })!.trackingConsent + ).toBe(TrackingConsent.GRANTED) + }) + + it('rejects invalid values', () => { + expect(validateAndBuildConfiguration({ clientToken: 'yes', trackingConsent: 'foo' as any })).toBeUndefined() + expect(displaySpy).toHaveBeenCalledOnceWith('Tracking Consent should be either "granted" or "not-granted"') + }) + }) }) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 574b4afe5e..bd2f7b6dae 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -10,6 +10,7 @@ import { objectHasValue } from '../../tools/utils/objectUtils' import { assign } from '../../tools/utils/polyfills' import { selectSessionStoreStrategyType } from '../session/sessionStore' import type { SessionStoreStrategyType } from '../session/storeStrategies/sessionStoreStrategy' +import { TrackingConsent } from '../trackingConsent' import type { TransportConfiguration } from './transportConfiguration' import { computeTransportConfiguration } from './transportConfiguration' @@ -30,6 +31,7 @@ export interface InitConfiguration { allowFallbackToLocalStorage?: boolean | undefined allowUntrustedEvents?: boolean | undefined storeContextsAcrossPages?: boolean | undefined + trackingConsent?: TrackingConsent | undefined // transport options proxy?: string | ProxyFn | undefined @@ -84,6 +86,7 @@ export interface Configuration extends TransportConfiguration { service: string | undefined silentMultipleInit: boolean allowUntrustedEvents: boolean + trackingConsent: TrackingConsent // Event limits eventRateLimiterThreshold: number // Limit the maximum number of actions, errors and logs per minutes @@ -120,6 +123,14 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati return } + if ( + initConfiguration.trackingConsent !== undefined && + !objectHasValue(TrackingConsent, initConfiguration.trackingConsent) + ) { + display.error('Tracking Consent should be either "granted" or "not-granted"') + return + } + // Set the experimental feature flags as early as possible, so we can use them in most places if (Array.isArray(initConfiguration.enableExperimentalFeatures)) { addExperimentalFeatures( @@ -140,6 +151,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati service: initConfiguration.service, silentMultipleInit: !!initConfiguration.silentMultipleInit, allowUntrustedEvents: !!initConfiguration.allowUntrustedEvents, + trackingConsent: initConfiguration.trackingConsent ?? TrackingConsent.GRANTED, /** * beacon payload max queue size implementation is 64kb diff --git a/packages/core/src/domain/trackingConsent.ts b/packages/core/src/domain/trackingConsent.ts new file mode 100644 index 0000000000..6bce98b4bc --- /dev/null +++ b/packages/core/src/domain/trackingConsent.ts @@ -0,0 +1,5 @@ +export const TrackingConsent = { + GRANTED: 'granted', + NOT_GRANTED: 'not-granted', +} as const +export type TrackingConsent = (typeof TrackingConsent)[keyof typeof TrackingConsent] diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b318d99b5e..777942b950 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,6 +10,7 @@ export { INTAKE_SITE_US1_FED, INTAKE_SITE_EU1, } from './domain/configuration' +export { TrackingConsent } from './domain/trackingConsent' export { isExperimentalFeatureEnabled, addExperimentalFeatures, From d944a0afe3df79df190c53dd9ef08c6c9fe5cf43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Fri, 12 Jan 2024 14:27:10 +0100 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20[RUM-2445]=20wait=20for=20conse?= =?UTF-8?q?nt=20before=20starting=20SDKs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/domain/trackingConsent.spec.ts | 71 +++++++++++++++++++ packages/core/src/domain/trackingConsent.ts | 33 +++++++++ packages/core/src/index.ts | 2 +- .../core/src/tools/experimentalFeatures.ts | 1 + packages/logs/src/boot/preStartLogs.spec.ts | 35 ++++++++- packages/logs/src/boot/preStartLogs.ts | 6 +- .../rum-core/src/boot/preStartRum.spec.ts | 47 +++++++++++- packages/rum-core/src/boot/preStartRum.ts | 6 +- 8 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/domain/trackingConsent.spec.ts diff --git a/packages/core/src/domain/trackingConsent.spec.ts b/packages/core/src/domain/trackingConsent.spec.ts new file mode 100644 index 0000000000..b61ef354bc --- /dev/null +++ b/packages/core/src/domain/trackingConsent.spec.ts @@ -0,0 +1,71 @@ +import { mockExperimentalFeatures } from '../../test' +import { ExperimentalFeature } from '../tools/experimentalFeatures' +import { TrackingConsent, createTrackingConsentState } from './trackingConsent' + +describe('createTrackingConsentState', () => { + describe('with tracking_consent enabled', () => { + beforeEach(() => { + mockExperimentalFeatures([ExperimentalFeature.TRACKING_CONSENT]) + }) + + it('creates a tracking consent state', () => { + const trackingConsentState = createTrackingConsentState() + expect(trackingConsentState).toBeDefined() + }) + + it('defaults to not granted', () => { + const trackingConsentState = createTrackingConsentState() + expect(trackingConsentState.isGranted()).toBeFalse() + }) + + it('can be created with a default consent state', () => { + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) + expect(trackingConsentState.isGranted()).toBeTrue() + }) + + it('can be set to granted', () => { + const trackingConsentState = createTrackingConsentState() + trackingConsentState.set(TrackingConsent.GRANTED) + expect(trackingConsentState.isGranted()).toBeTrue() + }) + + it('notifies when the consent is set', () => { + const spy = jasmine.createSpy() + const trackingConsentState = createTrackingConsentState() + trackingConsentState.observable.subscribe(spy) + trackingConsentState.set(TrackingConsent.GRANTED) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('can set a consent state if not defined', () => { + const trackingConsentState = createTrackingConsentState() + trackingConsentState.setIfNotDefined(TrackingConsent.GRANTED) + expect(trackingConsentState.isGranted()).toBeTrue() + }) + + it('does not set a consent state if already defined', () => { + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) + trackingConsentState.setIfNotDefined(TrackingConsent.NOT_GRANTED) + expect(trackingConsentState.isGranted()).toBeTrue() + }) + }) + + describe('with tracking_consent disabled', () => { + it('creates a tracking consent state', () => { + const trackingConsentState = createTrackingConsentState() + expect(trackingConsentState).toBeDefined() + }) + + it('is always granted', () => { + let trackingConsentState = createTrackingConsentState() + expect(trackingConsentState.isGranted()).toBeTrue() + + trackingConsentState = createTrackingConsentState(TrackingConsent.NOT_GRANTED) + expect(trackingConsentState.isGranted()).toBeTrue() + + trackingConsentState = createTrackingConsentState() + trackingConsentState.set(TrackingConsent.NOT_GRANTED) + expect(trackingConsentState.isGranted()).toBeTrue() + }) + }) +}) diff --git a/packages/core/src/domain/trackingConsent.ts b/packages/core/src/domain/trackingConsent.ts index 6bce98b4bc..1e16dd0074 100644 --- a/packages/core/src/domain/trackingConsent.ts +++ b/packages/core/src/domain/trackingConsent.ts @@ -1,5 +1,38 @@ +import { ExperimentalFeature, isExperimentalFeatureEnabled } from '../tools/experimentalFeatures' +import { Observable } from '../tools/observable' + export const TrackingConsent = { GRANTED: 'granted', NOT_GRANTED: 'not-granted', } as const export type TrackingConsent = (typeof TrackingConsent)[keyof typeof TrackingConsent] + +export interface TrackingConsentState { + setIfNotDefined: (trackingConsent: TrackingConsent) => void + set: (trackingConsent: TrackingConsent) => void + isGranted: () => boolean + observable: Observable +} + +export function createTrackingConsentState(currentConsent?: TrackingConsent): TrackingConsentState { + const observable = new Observable() + + return { + setIfNotDefined(trackingConsent: TrackingConsent) { + if (!currentConsent) { + currentConsent = trackingConsent + } + }, + set(trackingConsent: TrackingConsent) { + currentConsent = trackingConsent + observable.notify() + }, + isGranted() { + return ( + !isExperimentalFeatureEnabled(ExperimentalFeature.TRACKING_CONSENT) || + currentConsent === TrackingConsent.GRANTED + ) + }, + observable, + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 777942b950..d2e0a4e63e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,7 +10,7 @@ export { INTAKE_SITE_US1_FED, INTAKE_SITE_EU1, } from './domain/configuration' -export { TrackingConsent } from './domain/trackingConsent' +export { TrackingConsent, TrackingConsentState, createTrackingConsentState } from './domain/trackingConsent' export { isExperimentalFeatureEnabled, addExperimentalFeatures, diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts index 94748fe737..2d582edf91 100644 --- a/packages/core/src/tools/experimentalFeatures.ts +++ b/packages/core/src/tools/experimentalFeatures.ts @@ -18,6 +18,7 @@ export enum ExperimentalFeature { ZERO_LCP_TELEMETRY = 'zero_lcp_telemetry', DISABLE_REPLAY_INLINE_CSS = 'disable_replay_inline_css', WRITABLE_RESOURCE_GRAPHQL = 'writable_resource_graphql', + TRACKING_CONSENT = 'tracking_consent', } const enabledExperimentalFeatures: Set = new Set() diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index efcdf88e8b..72e010c853 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -1,6 +1,12 @@ -import { mockClock, type Clock, deleteEventBridgeStub, initEventBridgeStub } from '@datadog/browser-core/test' +import { + mockClock, + type Clock, + deleteEventBridgeStub, + initEventBridgeStub, + mockExperimentalFeatures, +} from '@datadog/browser-core/test' import type { TimeStamp } from '@datadog/browser-core' -import { ONE_SECOND, display } from '@datadog/browser-core' +import { ExperimentalFeature, ONE_SECOND, TrackingConsent, display } from '@datadog/browser-core' import type { CommonContext } from '../rawLogsEvent.types' import type { HybridInitConfiguration, LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' import { StatusType, type Logger } from '../domain/logger' @@ -201,4 +207,29 @@ describe('preStartLogs', () => { expect(strategy.getInternalContext()).toBeUndefined() }) }) + + describe('tracking consent', () => { + describe('with tracking_consent enabled', () => { + it('does not start logs if tracking consent is not granted at init', () => { + mockExperimentalFeatures([ExperimentalFeature.TRACKING_CONSENT]) + const strategy = createPreStartStrategy(getCommonContextSpy, doStartLogsSpy) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartLogsSpy).not.toHaveBeenCalled() + }) + }) + + describe('with tracking_consent disabled', () => { + it('ignores the trackingConsent init param', () => { + const strategy = createPreStartStrategy(getCommonContextSpy, doStartLogsSpy) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartLogsSpy).toHaveBeenCalled() + }) + }) + }) }) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index ee112277a2..66ff2415cd 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -2,6 +2,7 @@ import { BoundedBuffer, assign, canUseEventBridge, + createTrackingConsentState, display, displayAlreadyInitializedError, noop, @@ -23,9 +24,11 @@ export function createPreStartStrategy( const bufferApiCalls = new BoundedBuffer() let cachedInitConfiguration: LogsInitConfiguration | undefined let cachedConfiguration: LogsConfiguration | undefined + const trackingConsentState = createTrackingConsentState() + trackingConsentState.observable.subscribe(tryStartLogs) function tryStartLogs() { - if (!cachedConfiguration || !cachedInitConfiguration) { + if (!cachedConfiguration || !cachedInitConfiguration || !trackingConsentState.isGranted()) { return } @@ -59,6 +62,7 @@ export function createPreStartStrategy( } cachedConfiguration = configuration + trackingConsentState.setIfNotDefined(configuration.trackingConsent) tryStartLogs() }, diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index c85db81074..e4b4ed5fbb 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -1,11 +1,20 @@ import type { DeflateWorker, RelativeTime, TimeStamp } from '@datadog/browser-core' -import { display, getTimeStamp, noop, relativeToClocks, clocksNow } from '@datadog/browser-core' +import { + display, + getTimeStamp, + noop, + relativeToClocks, + clocksNow, + TrackingConsent, + ExperimentalFeature, +} from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { cleanupSyntheticsWorkerValues, deleteEventBridgeStub, initEventBridgeStub, mockClock, + mockExperimentalFeatures, mockSyntheticsWorkerValues, } from '@datadog/browser-core/test' import type { HybridInitConfiguration, RumConfiguration, RumInitConfiguration } from '../domain/configuration' @@ -302,6 +311,17 @@ describe('preStartRum', () => { expect(startViewSpy).not.toHaveBeenCalled() }) + it('calling startView then init does not start rum if tracking consent is not granted', () => { + mockExperimentalFeatures([ExperimentalFeature.TRACKING_CONSENT]) + const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) + strategy.startView({ name: 'foo' }) + strategy.init({ + ...MANUAL_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartRumSpy).not.toHaveBeenCalled() + }) + it('calling startView twice before init should start rum and create a new view', () => { clock = mockClock() clock.tick(10) @@ -487,4 +507,29 @@ describe('preStartRum', () => { expect(addFeatureFlagEvaluationSpy).toHaveBeenCalledOnceWith(key, value) }) }) + + describe('tracking consent', () => { + describe('with tracking_consent enabled', () => { + it('does not start rum if tracking consent is not granted at init', () => { + mockExperimentalFeatures([ExperimentalFeature.TRACKING_CONSENT]) + const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartRumSpy).not.toHaveBeenCalled() + }) + }) + + describe('with tracking_consent disabled', () => { + it('ignores the trackingConsent init param', () => { + const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartRumSpy).toHaveBeenCalled() + }) + }) + }) }) diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 959bf46a0a..0e9fd77d44 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -9,6 +9,7 @@ import { timeStampNow, clocksNow, assign, + createTrackingConsentState, } from '@datadog/browser-core' import { validateAndBuildRumConfiguration, @@ -38,9 +39,11 @@ export function createPreStartStrategy( let cachedInitConfiguration: RumInitConfiguration | undefined let cachedConfiguration: RumConfiguration | undefined + const trackingConsentState = createTrackingConsentState() + trackingConsentState.observable.subscribe(tryStartRum) function tryStartRum() { - if (!cachedInitConfiguration || !cachedConfiguration) { + if (!cachedInitConfiguration || !cachedConfiguration || !trackingConsentState.isGranted()) { return } @@ -119,6 +122,7 @@ export function createPreStartStrategy( } cachedConfiguration = configuration + trackingConsentState.setIfNotDefined(configuration.trackingConsent) tryStartRum() }, From b435f09cdc2ffb4a7ccacb6389a180692c6d6413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Fri, 12 Jan 2024 14:30:14 +0100 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8=20[RUM-2445]=20implement=20setTra?= =?UTF-8?q?ckingConsent=20public=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logs/src/boot/logsPublicApi.ts | 1 + packages/logs/src/boot/preStartLogs.spec.ts | 32 ++++++++++++- packages/logs/src/boot/preStartLogs.ts | 2 + packages/logs/src/boot/startLogs.ts | 4 ++ .../rum-core/src/boot/preStartRum.spec.ts | 47 +++++++++++++++++-- packages/rum-core/src/boot/preStartRum.ts | 2 + .../rum-core/src/boot/rumPublicApi.spec.ts | 1 + packages/rum-core/src/boot/rumPublicApi.ts | 4 ++ packages/rum-core/src/boot/startRum.ts | 4 ++ 9 files changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index eaca555ecd..6da8789070 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -32,6 +32,7 @@ const LOGS_STORAGE_KEY = 'logs' export interface Strategy { init: (initConfiguration: LogsInitConfiguration) => void + setTrackingConsent: StartLogsResult['setTrackingConsent'] initConfiguration: LogsInitConfiguration | undefined getInternalContext: StartLogsResult['getInternalContext'] handleLog: StartLogsResult['handleLog'] diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 72e010c853..e91dc4c022 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -210,8 +210,11 @@ describe('preStartLogs', () => { describe('tracking consent', () => { describe('with tracking_consent enabled', () => { - it('does not start logs if tracking consent is not granted at init', () => { + beforeEach(() => { mockExperimentalFeatures([ExperimentalFeature.TRACKING_CONSENT]) + }) + + it('does not start logs if tracking consent is not granted at init', () => { const strategy = createPreStartStrategy(getCommonContextSpy, doStartLogsSpy) strategy.init({ ...DEFAULT_INIT_CONFIGURATION, @@ -219,6 +222,26 @@ describe('preStartLogs', () => { }) expect(doStartLogsSpy).not.toHaveBeenCalled() }) + + it('starts logs if tracking consent is granted before init', () => { + const strategy = createPreStartStrategy(getCommonContextSpy, doStartLogsSpy) + strategy.setTrackingConsent(TrackingConsent.GRANTED) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartLogsSpy).toHaveBeenCalledTimes(1) + }) + + it('does not start logs if tracking consent is not withdrawn before init', () => { + const strategy = createPreStartStrategy(getCommonContextSpy, doStartLogsSpy) + strategy.setTrackingConsent(TrackingConsent.NOT_GRANTED) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.GRANTED, + }) + expect(doStartLogsSpy).not.toHaveBeenCalled() + }) }) describe('with tracking_consent disabled', () => { @@ -230,6 +253,13 @@ describe('preStartLogs', () => { }) expect(doStartLogsSpy).toHaveBeenCalled() }) + + it('ignores setTrackingConsent', () => { + const strategy = createPreStartStrategy(getCommonContextSpy, doStartLogsSpy) + strategy.setTrackingConsent(TrackingConsent.NOT_GRANTED) + strategy.init(DEFAULT_INIT_CONFIGURATION) + expect(doStartLogsSpy).toHaveBeenCalledTimes(1) + }) }) }) }) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index 66ff2415cd..ea7bb0cbe9 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -66,6 +66,8 @@ export function createPreStartStrategy( tryStartLogs() }, + setTrackingConsent: trackingConsentState.set, + get initConfiguration() { return cachedInitConfiguration }, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 0135b5b8a5..3c46fdf887 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,8 +1,10 @@ import { + TrackingConsent, sendToExtension, createPageExitObservable, willSyntheticsInjectRum, canUseEventBridge, + createTrackingConsentState, } from '@datadog/browser-core' import { startLogsSessionManager, startLogsSessionManagerStub } from '../domain/logsSessionManager' import type { LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' @@ -35,6 +37,7 @@ export function startLogs( const reportError = startReportError(lifeCycle) const pageExitObservable = createPageExitObservable(configuration) + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) const session = configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum() @@ -70,6 +73,7 @@ export function startLogs( return { handleLog, getInternalContext: internalContext.get, + setTrackingConsent: trackingConsentState.set, stop: () => { cleanupTasks.forEach((task) => task()) }, diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index e4b4ed5fbb..a55d4a2f51 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -28,6 +28,8 @@ import { createPreStartStrategy } from './preStartRum' const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' } const INVALID_INIT_CONFIGURATION = { clientToken: 'yes' } as RumInitConfiguration +const AUTO_CONFIGURATION = { ...DEFAULT_INIT_CONFIGURATION } +const MANUAL_CONFIGURATION = { ...AUTO_CONFIGURATION, trackViewsManually: true } const FAKE_WORKER = {} as DeflateWorker describe('preStartRum', () => { @@ -241,9 +243,6 @@ describe('preStartRum', () => { }) describe('trackViews mode', () => { - const AUTO_CONFIGURATION = { ...DEFAULT_INIT_CONFIGURATION } - const MANUAL_CONFIGURATION = { ...AUTO_CONFIGURATION, trackViewsManually: true } - let clock: Clock | undefined let strategy: Strategy let startViewSpy: jasmine.Spy @@ -510,8 +509,11 @@ describe('preStartRum', () => { describe('tracking consent', () => { describe('with tracking_consent enabled', () => { - it('does not start rum if tracking consent is not granted at init', () => { + beforeEach(() => { mockExperimentalFeatures([ExperimentalFeature.TRACKING_CONSENT]) + }) + + it('does not start rum if tracking consent is not granted at init', () => { const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) strategy.init({ ...DEFAULT_INIT_CONFIGURATION, @@ -519,6 +521,36 @@ describe('preStartRum', () => { }) expect(doStartRumSpy).not.toHaveBeenCalled() }) + + it('starts rum if tracking consent is granted before init', () => { + const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) + strategy.setTrackingConsent(TrackingConsent.GRANTED) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartRumSpy).toHaveBeenCalledTimes(1) + }) + + it('does not start rum if tracking consent is withdrawn before init', () => { + const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) + strategy.setTrackingConsent(TrackingConsent.NOT_GRANTED) + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.GRANTED, + }) + expect(doStartRumSpy).not.toHaveBeenCalled() + }) + + it('does not start rum if no view is started', () => { + const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) + strategy.setTrackingConsent(TrackingConsent.GRANTED) + strategy.init({ + ...MANUAL_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + expect(doStartRumSpy).not.toHaveBeenCalled() + }) }) describe('with tracking_consent disabled', () => { @@ -530,6 +562,13 @@ describe('preStartRum', () => { }) expect(doStartRumSpy).toHaveBeenCalled() }) + + it('ignores setTrackingConsent', () => { + const strategy = createPreStartStrategy({}, getCommonContextSpy, doStartRumSpy) + strategy.setTrackingConsent(TrackingConsent.NOT_GRANTED) + strategy.init(DEFAULT_INIT_CONFIGURATION) + expect(doStartRumSpy).toHaveBeenCalledTimes(1) + }) }) }) }) diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 0e9fd77d44..6f043d5887 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -130,6 +130,8 @@ export function createPreStartStrategy( return cachedInitConfiguration }, + setTrackingConsent: trackingConsentState.set, + getInternalContext: noop as () => undefined, stopSession: noop, diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 77396190ff..eb0aded88d 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -21,6 +21,7 @@ const noopStartRum = (): ReturnType => ({ addFeatureFlagEvaluation: () => undefined, startView: () => undefined, getInternalContext: () => undefined, + setTrackingConsent: () => undefined, lifeCycle: {} as any, viewContexts: {} as any, session: {} as any, diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index b0c4932cd2..b5e012c3f5 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -6,6 +6,7 @@ import type { DeflateWorker, DeflateEncoderStreamId, DeflateEncoder, + TrackingConsent, } from '@datadog/browser-core' import { CustomerDataType, @@ -72,6 +73,7 @@ const RUM_STORAGE_KEY = 'rum' export interface Strategy { init: (initConfiguration: RumInitConfiguration) => void + setTrackingConsent: StartRumResult['setTrackingConsent'] initConfiguration: RumInitConfiguration | undefined getInternalContext: StartRumResult['getInternalContext'] stopSession: StartRumResult['stopSession'] @@ -144,6 +146,8 @@ export function makeRumPublicApi(startRumImpl: StartRum, recorderApi: RecorderAp const rumPublicApi = makePublicApi({ init: monitor((initConfiguration: RumInitConfiguration) => strategy.init(initConfiguration)), + setTrackingConsent: monitor((trackingConsent: TrackingConsent) => strategy.setTrackingConsent(trackingConsent)), + setGlobalContextProperty: monitor((key, value) => globalContextManager.setContextProperty(key, value)), removeGlobalContextProperty: monitor((key) => globalContextManager.removeContextProperty(key)), diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 779ce0e708..0a140b4ee6 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -16,6 +16,8 @@ import { getEventBridge, addTelemetryDebug, CustomerDataType, + createTrackingConsentState, + TrackingConsent, } from '@datadog/browser-core' import { createDOMMutationObservable } from '../browser/domMutationObservable' import { startPerformanceCollection } from '../browser/performanceCollection' @@ -88,6 +90,7 @@ export function startRum( customerDataTrackerManager.getOrCreateTracker(CustomerDataType.FeatureFlag) ) + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) const pageExitObservable = createPageExitObservable(configuration) const pageExitSubscription = pageExitObservable.subscribe((event) => { lifeCycle.notify(LifeCycleEventType.PAGE_EXITED, event) @@ -174,6 +177,7 @@ export function startRum( addError, addTiming, addFeatureFlagEvaluation: featureFlagContexts.addFeatureFlagEvaluation, + setTrackingConsent: trackingConsentState.set, startView, lifeCycle, viewContexts, From 8977a910a9695774ba877525f7e5e1bc1be9b3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Fri, 12 Jan 2024 11:39:55 +0100 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=E2=9C=85=20[RUM-2445]=20?= =?UTF-8?q?modify=20session=20manager=20specs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define "start(Rum|Logs)SessionManagerWithDefault" to avoid having to pass new parameters everywhere. --- .../src/domain/session/sessionManager.spec.ts | 97 +++++++++++-------- .../src/domain/logsSessionManager.spec.ts | 38 ++++---- .../src/domain/rumSessionManager.spec.ts | 79 +++++++-------- 3 files changed, 109 insertions(+), 105 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 6c47cf9527..cc50403dbd 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -34,7 +34,6 @@ describe('startSessionManager', () => { const SECOND_PRODUCT_KEY = 'second' const STORE_TYPE: SessionStoreStrategyType = { type: 'Cookie', cookieOptions: {} } let clock: Clock - let configuration: Configuration function expireSessionCookie() { setCookie(SESSION_STORE_KEY, '', DURATION) @@ -74,7 +73,6 @@ describe('startSessionManager', () => { if (isIE()) { pending('no full rum support') } - configuration = { sessionStoreStrategyType: STORE_TYPE } as Configuration clock = mockClock() }) @@ -88,14 +86,14 @@ describe('startSessionManager', () => { describe('cookie management', () => { it('when tracked, should store tracking type and session id', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() expectSessionIdToBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) it('when not tracked should store tracking type', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults({ computeSessionState: () => NOT_TRACKED_SESSION_STATE }) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -104,7 +102,7 @@ describe('startSessionManager', () => { it('when tracked should keep existing tracking type and session id', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&first=tracked', DURATION) - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() expectSessionIdToBe(sessionManager, 'abcdef') expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) @@ -113,7 +111,7 @@ describe('startSessionManager', () => { it('when not tracked should keep existing tracking type', () => { setCookie(SESSION_STORE_KEY, 'first=not-tracked', DURATION) - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults({ computeSessionState: () => NOT_TRACKED_SESSION_STATE }) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -128,32 +126,32 @@ describe('startSessionManager', () => { }) it('should be called with an empty value if the cookie is not defined', () => { - startSessionManager(configuration, FIRST_PRODUCT_KEY, spy) + startSessionManagerWithDefaults({ computeSessionState: spy }) expect(spy).toHaveBeenCalledWith(undefined) }) it('should be called with an invalid value if the cookie has an invalid value', () => { setCookie(SESSION_STORE_KEY, 'first=invalid', DURATION) - startSessionManager(configuration, FIRST_PRODUCT_KEY, spy) + startSessionManagerWithDefaults({ computeSessionState: spy }) expect(spy).toHaveBeenCalledWith('invalid') }) it('should be called with TRACKED', () => { setCookie(SESSION_STORE_KEY, 'first=tracked', DURATION) - startSessionManager(configuration, FIRST_PRODUCT_KEY, spy) + startSessionManagerWithDefaults({ computeSessionState: spy }) expect(spy).toHaveBeenCalledWith(FakeTrackingType.TRACKED) }) it('should be called with NOT_TRACKED', () => { setCookie(SESSION_STORE_KEY, 'first=not-tracked', DURATION) - startSessionManager(configuration, FIRST_PRODUCT_KEY, spy) + startSessionManagerWithDefaults({ computeSessionState: spy }) expect(spy).toHaveBeenCalledWith(FakeTrackingType.NOT_TRACKED) }) }) describe('session renewal', () => { it('should renew on activity after expiration', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -171,7 +169,7 @@ describe('startSessionManager', () => { }) it('should not renew on visibility after expiration', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -186,17 +184,17 @@ describe('startSessionManager', () => { describe('multiple startSessionManager calls', () => { it('should re-use the same session id', () => { - const firstSessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) const idA = firstSessionManager.findActiveSession()!.id - const secondSessionManager = startSessionManager(configuration, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const secondSessionManager = startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) const idB = secondSessionManager.findActiveSession()!.id expect(idA).toBe(idB) }) it('should not erase other session type', () => { - startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) // schedule an expandOrRenewSession document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) @@ -206,7 +204,7 @@ describe('startSessionManager', () => { // expand first session cookie cache document.dispatchEvent(createNewEvent(DOM_EVENT.VISIBILITY_CHANGE)) - startSessionManager(configuration, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) // cookie correctly set expect(getCookie(SESSION_STORE_KEY)).toContain('first') @@ -220,25 +218,27 @@ describe('startSessionManager', () => { }) it('should have independent tracking types', () => { - const firstSessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) - const secondSessionManager = startSessionManager( - configuration, - SECOND_PRODUCT_KEY, - () => NOT_TRACKED_SESSION_STATE - ) + const firstSessionManager = startSessionManagerWithDefaults({ + productKey: FIRST_PRODUCT_KEY, + computeSessionState: () => TRACKED_SESSION_STATE, + }) + const secondSessionManager = startSessionManagerWithDefaults({ + productKey: SECOND_PRODUCT_KEY, + computeSessionState: () => NOT_TRACKED_SESSION_STATE, + }) expect(firstSessionManager.findActiveSession()!.trackingType).toEqual(FakeTrackingType.TRACKED) expect(secondSessionManager.findActiveSession()!.trackingType).toEqual(FakeTrackingType.NOT_TRACKED) }) it('should notify each expire and renew observables', () => { - const firstSessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) const expireSessionASpy = jasmine.createSpy() firstSessionManager.expireObservable.subscribe(expireSessionASpy) const renewSessionASpy = jasmine.createSpy() firstSessionManager.renewObservable.subscribe(renewSessionASpy) - const secondSessionManager = startSessionManager(configuration, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const secondSessionManager = startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) const expireSessionBSpy = jasmine.createSpy() secondSessionManager.expireObservable.subscribe(expireSessionBSpy) const renewSessionBSpy = jasmine.createSpy() @@ -260,7 +260,7 @@ describe('startSessionManager', () => { describe('session timeout', () => { it('should expire the session when the time out delay is reached', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -276,7 +276,7 @@ describe('startSessionManager', () => { it('should renew an existing timed out session', () => { setCookie(SESSION_STORE_KEY, `id=abcde&first=tracked&created=${Date.now() - SESSION_TIME_OUT_DELAY}`, DURATION) - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -288,7 +288,7 @@ describe('startSessionManager', () => { it('should not add created date to an existing session from an older versions', () => { setCookie(SESSION_STORE_KEY, 'id=abcde&first=tracked', DURATION) - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() expect(sessionManager.findActiveSession()!.id).toBe('abcde') expect(getCookie(SESSION_STORE_KEY)).not.toContain('created=') @@ -305,7 +305,7 @@ describe('startSessionManager', () => { }) it('should expire the session after expiration delay', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -317,7 +317,7 @@ describe('startSessionManager', () => { }) it('should expand duration on activity', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -336,7 +336,7 @@ describe('startSessionManager', () => { }) it('should expand not tracked session duration on activity', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults({ computeSessionState: () => NOT_TRACKED_SESSION_STATE }) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -357,7 +357,7 @@ describe('startSessionManager', () => { it('should expand session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -378,7 +378,7 @@ describe('startSessionManager', () => { it('should expand not tracked session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults({ computeSessionState: () => NOT_TRACKED_SESSION_STATE }) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -399,7 +399,7 @@ describe('startSessionManager', () => { describe('manual session expiration', () => { it('expires the session when calling expire()', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -410,7 +410,7 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() multiple times', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -422,7 +422,7 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() after the session has been expired', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -434,7 +434,7 @@ describe('startSessionManager', () => { }) it('renew the session on user activity', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() clock.tick(STORAGE_POLL_DELAY) sessionManager.expire() @@ -447,21 +447,21 @@ describe('startSessionManager', () => { describe('session history', () => { it('should return undefined when there is no current session and no startTime', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() expireSessionCookie() expect(sessionManager.findActiveSession()).toBeUndefined() }) it('should return the current session context when there is no start time', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() expect(sessionManager.findActiveSession()!.id).toBeDefined() expect(sessionManager.findActiveSession()!.trackingType).toBeDefined() }) it('should return the session context corresponding to startTime', () => { - const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManagerWithDefaults() // 0s to 10s: first session clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY) @@ -489,4 +489,23 @@ describe('startSessionManager', () => { ) }) }) + + function startSessionManagerWithDefaults({ + configuration, + productKey = FIRST_PRODUCT_KEY, + computeSessionState = () => TRACKED_SESSION_STATE, + }: { + configuration?: Partial + productKey?: string + computeSessionState?: () => { trackingType: FakeTrackingType; isTracked: boolean } + } = {}) { + return startSessionManager( + { + sessionStoreStrategyType: STORE_TYPE, + ...configuration, + } as Configuration, + productKey, + computeSessionState + ) + } }) diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 47cd85ab21..49ee8f4706 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -21,16 +21,9 @@ import { describe('logs session manager', () => { const DURATION = 123456 - const configuration: Partial = { - sessionSampleRate: 0.5, - sessionStoreStrategyType: { type: 'Cookie', cookieOptions: {} }, - } let clock: Clock - let tracked: boolean beforeEach(() => { - tracked = true - spyOn(Math, 'random').and.callFake(() => (tracked ? 0 : 1)) clock = mockClock() }) @@ -43,18 +36,14 @@ describe('logs session manager', () => { }) it('when tracked should store tracking type and session id', () => { - tracked = true - - startLogsSessionManager(configuration as LogsConfiguration) + startLogsSessionManagerWithDefaults() expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]+/) }) it('when not tracked should store tracking type', () => { - tracked = false - - startLogsSessionManager(configuration as LogsConfiguration) + startLogsSessionManagerWithDefaults({ configuration: { sessionSampleRate: 0 } }) expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.NOT_TRACKED}`) expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=') @@ -63,7 +52,7 @@ describe('logs session manager', () => { it('when tracked should keep existing tracking type and session id', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) - startLogsSessionManager(configuration as LogsConfiguration) + startLogsSessionManagerWithDefaults() expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) expect(getCookie(SESSION_STORE_KEY)).toContain('id=abcdef') @@ -72,19 +61,18 @@ describe('logs session manager', () => { it('when not tracked should keep existing tracking type', () => { setCookie(SESSION_STORE_KEY, 'logs=0', DURATION) - startLogsSessionManager(configuration as LogsConfiguration) + startLogsSessionManagerWithDefaults() expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.NOT_TRACKED}`) }) it('should renew on activity after expiration', () => { - startLogsSessionManager(configuration as LogsConfiguration) + startLogsSessionManagerWithDefaults() setCookie(SESSION_STORE_KEY, '', DURATION) expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() clock.tick(STORAGE_POLL_DELAY) - tracked = true document.body.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]+/) @@ -94,18 +82,18 @@ describe('logs session manager', () => { describe('findSession', () => { it('should return the current session', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) - const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) + const logsSessionManager = startLogsSessionManagerWithDefaults() expect(logsSessionManager.findTrackedSession()!.id).toBe('abcdef') }) it('should return undefined if the session is not tracked', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=0', DURATION) - const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) + const logsSessionManager = startLogsSessionManagerWithDefaults() expect(logsSessionManager.findTrackedSession()).toBe(undefined) }) it('should return undefined if the session has expired', () => { - const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) + const logsSessionManager = startLogsSessionManagerWithDefaults() setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(STORAGE_POLL_DELAY) expect(logsSessionManager.findTrackedSession()).toBe(undefined) @@ -113,7 +101,7 @@ describe('logs session manager', () => { it('should return session corresponding to start time', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) - const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) + const logsSessionManager = startLogsSessionManagerWithDefaults() clock.tick(10 * ONE_SECOND) setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(STORAGE_POLL_DELAY) @@ -121,6 +109,14 @@ describe('logs session manager', () => { expect(logsSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') }) }) + + function startLogsSessionManagerWithDefaults({ configuration }: { configuration?: Partial } = {}) { + return startLogsSessionManager({ + sessionSampleRate: 100, + sessionStoreStrategyType: { type: 'Cookie', cookieOptions: {} }, + ...configuration, + } as LogsConfiguration) + } }) describe('logger session stub', () => { diff --git a/packages/rum-core/src/domain/rumSessionManager.spec.ts b/packages/rum-core/src/domain/rumSessionManager.spec.ts index 560c6e0b36..758e9da54d 100644 --- a/packages/rum-core/src/domain/rumSessionManager.spec.ts +++ b/packages/rum-core/src/domain/rumSessionManager.spec.ts @@ -19,34 +19,15 @@ import { RUM_SESSION_KEY, RumTrackingType, startRumSessionManager } from './rumS describe('rum session manager', () => { const DURATION = 123456 - let configuration: RumConfiguration let lifeCycle: LifeCycle let expireSessionSpy: jasmine.Spy let renewSessionSpy: jasmine.Spy let clock: Clock - function setupDraws({ - tracked, - trackedWithSessionReplay, - }: { - tracked?: boolean - trackedWithSessionReplay?: boolean - }) { - configuration.sessionSampleRate = tracked ? 100 : 0 - configuration.sessionReplaySampleRate = trackedWithSessionReplay ? 100 : 0 - } - beforeEach(() => { if (isIE()) { pending('no full rum support') } - configuration = { - ...validateAndBuildRumConfiguration({ clientToken: 'xxx', applicationId: 'xxx' })!, - sessionSampleRate: 50, - sessionReplaySampleRate: 50, - trackResources: true, - trackLongTasks: true, - } clock = mockClock() expireSessionSpy = jasmine.createSpy('expireSessionSpy') renewSessionSpy = jasmine.createSpy('renewSessionSpy') @@ -65,9 +46,7 @@ describe('rum session manager', () => { describe('cookie storage', () => { it('when tracked with session replay should store session type and id', () => { - setupDraws({ tracked: true, trackedWithSessionReplay: true }) - - startRumSessionManager(configuration, lifeCycle) + startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 100 } }) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -78,9 +57,7 @@ describe('rum session manager', () => { }) it('when tracked without session replay should store session type and id', () => { - setupDraws({ tracked: true, trackedWithSessionReplay: false }) - - startRumSessionManager(configuration, lifeCycle) + startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 0 } }) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -91,9 +68,7 @@ describe('rum session manager', () => { }) it('when not tracked should store session type', () => { - setupDraws({ tracked: false }) - - startRumSessionManager(configuration, lifeCycle) + startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 0 } }) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -104,7 +79,7 @@ describe('rum session manager', () => { it('when tracked should keep existing session type and id', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - startRumSessionManager(configuration, lifeCycle) + startRumSessionManagerWithDefaults() expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -117,7 +92,7 @@ describe('rum session manager', () => { it('when not tracked should keep existing session type', () => { setCookie(SESSION_STORE_KEY, 'rum=0', DURATION) - startRumSessionManager(configuration, lifeCycle) + startRumSessionManagerWithDefaults() expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -126,7 +101,8 @@ describe('rum session manager', () => { it('should renew on activity after expiration', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - startRumSessionManager(configuration, lifeCycle) + + startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 100 } }) setCookie(SESSION_STORE_KEY, '', DURATION) expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() @@ -134,7 +110,6 @@ describe('rum session manager', () => { expect(renewSessionSpy).not.toHaveBeenCalled() clock.tick(STORAGE_POLL_DELAY) - setupDraws({ tracked: true, trackedWithSessionReplay: true }) document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(expireSessionSpy).toHaveBeenCalled() @@ -149,18 +124,18 @@ describe('rum session manager', () => { describe('findSession', () => { it('should return the current session', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - const rumSessionManager = startRumSessionManager(configuration, lifeCycle) + const rumSessionManager = startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()!.id).toBe('abcdef') }) it('should return undefined if the session is not tracked', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=0', DURATION) - const rumSessionManager = startRumSessionManager(configuration, lifeCycle) + const rumSessionManager = startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()).toBe(undefined) }) it('should return undefined if the session has expired', () => { - const rumSessionManager = startRumSessionManager(configuration, lifeCycle) + const rumSessionManager = startRumSessionManagerWithDefaults() setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(STORAGE_POLL_DELAY) expect(rumSessionManager.findTrackedSession()).toBe(undefined) @@ -168,7 +143,7 @@ describe('rum session manager', () => { it('should return session corresponding to start time', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - const rumSessionManager = startRumSessionManager(configuration, lifeCycle) + const rumSessionManager = startRumSessionManagerWithDefaults() clock.tick(10 * ONE_SECOND) setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(STORAGE_POLL_DELAY) @@ -178,13 +153,13 @@ describe('rum session manager', () => { it('should return session TRACKED_WITH_SESSION_REPLAY', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - const rumSessionManager = startRumSessionManager(configuration, lifeCycle) + const rumSessionManager = startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()!.sessionReplayAllowed).toBe(true) }) it('should return session TRACKED_WITHOUT_SESSION_REPLAY', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=2', DURATION) - const rumSessionManager = startRumSessionManager(configuration, lifeCycle) + const rumSessionManager = startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()!.sessionReplayAllowed).toBe(false) }) }) @@ -193,31 +168,45 @@ describe('rum session manager', () => { ;[ { description: 'TRACKED_WITH_SESSION_REPLAY should have replay', - trackedWithSessionReplay: true, + sessionReplaySampleRate: 100, expectSessionReplay: true, }, { description: 'TRACKED_WITHOUT_SESSION_REPLAY should have no replay', - trackedWithSessionReplay: false, + sessionReplaySampleRate: 0, expectSessionReplay: false, }, ].forEach( ({ description, - trackedWithSessionReplay, + sessionReplaySampleRate, expectSessionReplay, }: { description: string - trackedWithSessionReplay: boolean + sessionReplaySampleRate: number expectSessionReplay: boolean }) => { it(description, () => { - setupDraws({ tracked: true, trackedWithSessionReplay }) - - const rumSessionManager = startRumSessionManager(configuration, lifeCycle) + const rumSessionManager = startRumSessionManagerWithDefaults({ + configuration: { sessionSampleRate: 100, sessionReplaySampleRate }, + }) expect(rumSessionManager.findTrackedSession()!.sessionReplayAllowed).toBe(expectSessionReplay) }) } ) }) + + function startRumSessionManagerWithDefaults({ configuration }: { configuration?: Partial } = {}) { + return startRumSessionManager( + { + ...validateAndBuildRumConfiguration({ clientToken: 'xxx', applicationId: 'xxx' })!, + sessionSampleRate: 50, + sessionReplaySampleRate: 50, + trackResources: true, + trackLongTasks: true, + ...configuration, + }, + lifeCycle + ) + } }) From 4c1e34ba46d1baef3ff0a113fec7a2f27a1a16ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Fri, 12 Jan 2024 11:54:07 +0100 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=A8=20[RUM-2445]=20expire=20and=20ren?= =?UTF-8?q?ew=20session=20based=20on=20tracking=20consent=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/sessionManager.spec.ts | 60 ++++++++++++++++++- .../core/src/domain/session/sessionManager.ts | 20 ++++++- packages/logs/src/boot/startLogs.ts | 2 +- .../src/domain/logsSessionManager.spec.ts | 15 +++-- .../logs/src/domain/logsSessionManager.ts | 14 +++-- packages/rum-core/src/boot/startRum.ts | 4 +- .../src/domain/rumSessionManager.spec.ts | 5 +- .../rum-core/src/domain/rumSessionManager.ts | 15 +++-- 8 files changed, 115 insertions(+), 20 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index cc50403dbd..69731c836a 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -1,4 +1,10 @@ -import { createNewEvent, mockClock, restorePageVisibility, setPageVisibility } from '../../../test' +import { + createNewEvent, + mockClock, + mockExperimentalFeatures, + restorePageVisibility, + setPageVisibility, +} from '../../../test' import type { Clock } from '../../../test' import { getCookie, setCookie } from '../../browser/cookie' import type { RelativeTime } from '../../tools/utils/timeUtils' @@ -6,6 +12,9 @@ import { isIE } from '../../tools/utils/browserDetection' import { DOM_EVENT } from '../../browser/addEventListener' import { ONE_HOUR, ONE_SECOND } from '../../tools/utils/timeUtils' import type { Configuration } from '../configuration' +import type { TrackingConsentState } from '../trackingConsent' +import { TrackingConsent, createTrackingConsentState } from '../trackingConsent' +import { ExperimentalFeature } from '../../tools/experimentalFeatures' import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' @@ -490,14 +499,60 @@ describe('startSessionManager', () => { }) }) + describe('tracking consent', () => { + beforeEach(() => { + mockExperimentalFeatures([ExperimentalFeature.TRACKING_CONSENT]) + }) + + it('expires the session when tracking consent is withdrawn', () => { + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) + const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) + + trackingConsentState.set(TrackingConsent.NOT_GRANTED) + + expectSessionIdToNotBeDefined(sessionManager) + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() + }) + + it('does not renew the session when tracking consent is withdrawn', () => { + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) + const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) + + trackingConsentState.set(TrackingConsent.NOT_GRANTED) + + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) + + expectSessionIdToNotBeDefined(sessionManager) + }) + + it('renews the session when tracking consent is granted', () => { + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) + const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) + const initialSessionId = sessionManager.findActiveSession()!.id + + trackingConsentState.set(TrackingConsent.NOT_GRANTED) + + expectSessionIdToNotBeDefined(sessionManager) + + trackingConsentState.set(TrackingConsent.GRANTED) + + clock.tick(STORAGE_POLL_DELAY) + + expectSessionIdToBeDefined(sessionManager) + expect(sessionManager.findActiveSession()!.id).not.toBe(initialSessionId) + }) + }) + function startSessionManagerWithDefaults({ configuration, productKey = FIRST_PRODUCT_KEY, computeSessionState = () => TRACKED_SESSION_STATE, + trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED), }: { configuration?: Partial productKey?: string computeSessionState?: () => { trackingType: FakeTrackingType; isTracked: boolean } + trackingConsentState?: TrackingConsentState } = {}) { return startSessionManager( { @@ -505,7 +560,8 @@ describe('startSessionManager', () => { ...configuration, } as Configuration, productKey, - computeSessionState + computeSessionState, + trackingConsentState ) } }) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 68af45ff1f..0a9a480cf3 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -6,6 +6,7 @@ import { relativeNow, clocksOrigin, ONE_MINUTE } from '../../tools/utils/timeUti import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/addEventListener' import { clearInterval, setInterval } from '../../tools/timer' import type { Configuration } from '../configuration' +import type { TrackingConsentState } from '../trackingConsent' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { startSessionStore } from './sessionStore' @@ -28,7 +29,8 @@ let stopCallbacks: Array<() => void> = [] export function startSessionManager( configuration: Configuration, productKey: string, - computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } + computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean }, + trackingConsentState: TrackingConsentState ): SessionManager { // TODO - Improve configuration type and remove assertion const sessionStore = startSessionStore(configuration.sessionStoreStrategyType!, productKey, computeSessionState) @@ -44,10 +46,24 @@ export function startSessionManager( sessionContextHistory.closeActive(relativeNow()) }) + // We expand/renew session unconditionally as tracking consent is always granted when the session + // manager is started. sessionStore.expandOrRenewSession() sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative) - trackActivity(configuration, () => sessionStore.expandOrRenewSession()) + trackingConsentState.observable.subscribe(() => { + if (trackingConsentState.isGranted()) { + sessionStore.expandOrRenewSession() + } else { + sessionStore.expire() + } + }) + + trackActivity(configuration, () => { + if (trackingConsentState.isGranted()) { + sessionStore.expandOrRenewSession() + } + }) trackVisibility(configuration, () => sessionStore.expandSession()) function buildSessionContext() { diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 3c46fdf887..81dba2577c 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -41,7 +41,7 @@ export function startLogs( const session = configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum() - ? startLogsSessionManager(configuration) + ? startLogsSessionManager(configuration, trackingConsentState) : startLogsSessionManagerStub(configuration) const { stop: stopLogsTelemetry } = startLogsTelemetry( diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 49ee8f4706..0f9c3c4604 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -7,6 +7,8 @@ import { stopSessionManager, ONE_SECOND, DOM_EVENT, + createTrackingConsentState, + TrackingConsent, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { createNewEvent, mockClock } from '@datadog/browser-core/test' @@ -111,11 +113,14 @@ describe('logs session manager', () => { }) function startLogsSessionManagerWithDefaults({ configuration }: { configuration?: Partial } = {}) { - return startLogsSessionManager({ - sessionSampleRate: 100, - sessionStoreStrategyType: { type: 'Cookie', cookieOptions: {} }, - ...configuration, - } as LogsConfiguration) + return startLogsSessionManager( + { + sessionSampleRate: 100, + sessionStoreStrategyType: { type: 'Cookie', cookieOptions: {} }, + ...configuration, + } as LogsConfiguration, + createTrackingConsentState(TrackingConsent.GRANTED) + ) } }) diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index acbb7d0abb..dcd426b0ef 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -1,4 +1,4 @@ -import type { RelativeTime } from '@datadog/browser-core' +import type { RelativeTime, TrackingConsentState } from '@datadog/browser-core' import { Observable, performDraw, startSessionManager } from '@datadog/browser-core' import type { LogsConfiguration } from './configuration' @@ -18,9 +18,15 @@ export const enum LoggerTrackingType { TRACKED = '1', } -export function startLogsSessionManager(configuration: LogsConfiguration): LogsSessionManager { - const sessionManager = startSessionManager(configuration, LOGS_SESSION_KEY, (rawTrackingType) => - computeSessionState(configuration, rawTrackingType) +export function startLogsSessionManager( + configuration: LogsConfiguration, + trackingConsentState: TrackingConsentState +): LogsSessionManager { + const sessionManager = startSessionManager( + configuration, + LOGS_SESSION_KEY, + (rawTrackingType) => computeSessionState(configuration, rawTrackingType), + trackingConsentState ) return { findTrackedSession: (startTime) => { diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 0a140b4ee6..e69f2f9b48 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -97,7 +97,9 @@ export function startRum( }) cleanupTasks.push(() => pageExitSubscription.unsubscribe()) - const session = !canUseEventBridge() ? startRumSessionManager(configuration, lifeCycle) : startRumSessionManagerStub() + const session = !canUseEventBridge() + ? startRumSessionManager(configuration, lifeCycle, trackingConsentState) + : startRumSessionManagerStub() if (!canUseEventBridge()) { const batch = startRumBatch( configuration, diff --git a/packages/rum-core/src/domain/rumSessionManager.spec.ts b/packages/rum-core/src/domain/rumSessionManager.spec.ts index 758e9da54d..1d7fbc8580 100644 --- a/packages/rum-core/src/domain/rumSessionManager.spec.ts +++ b/packages/rum-core/src/domain/rumSessionManager.spec.ts @@ -8,6 +8,8 @@ import { stopSessionManager, ONE_SECOND, DOM_EVENT, + createTrackingConsentState, + TrackingConsent, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { createNewEvent, mockClock } from '@datadog/browser-core/test' @@ -206,7 +208,8 @@ describe('rum session manager', () => { trackLongTasks: true, ...configuration, }, - lifeCycle + lifeCycle, + createTrackingConsentState(TrackingConsent.GRANTED) ) } }) diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index c717cdf425..8a619cff63 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -1,4 +1,4 @@ -import type { RelativeTime } from '@datadog/browser-core' +import type { RelativeTime, TrackingConsentState } from '@datadog/browser-core' import { Observable, noop, performDraw, startSessionManager } from '@datadog/browser-core' import type { RumConfiguration } from './configuration' import type { LifeCycle } from './lifeCycle' @@ -23,9 +23,16 @@ export const enum RumTrackingType { TRACKED_WITHOUT_SESSION_REPLAY = '2', } -export function startRumSessionManager(configuration: RumConfiguration, lifeCycle: LifeCycle): RumSessionManager { - const sessionManager = startSessionManager(configuration, RUM_SESSION_KEY, (rawTrackingType) => - computeSessionState(configuration, rawTrackingType) +export function startRumSessionManager( + configuration: RumConfiguration, + lifeCycle: LifeCycle, + trackingConsentState: TrackingConsentState +): RumSessionManager { + const sessionManager = startSessionManager( + configuration, + RUM_SESSION_KEY, + (rawTrackingType) => computeSessionState(configuration, rawTrackingType), + trackingConsentState ) sessionManager.expireObservable.subscribe(() => { From 2c328abf62e533ea5ff2a88df60c4823c3c57473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Zugmeyer?= Date: Mon, 5 Feb 2024 14:33:12 +0100 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=85=20[RUM-2445]=20add=20e2e=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/scenario/trackingConsent.scenario.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 test/e2e/scenario/trackingConsent.scenario.ts diff --git a/test/e2e/scenario/trackingConsent.scenario.ts b/test/e2e/scenario/trackingConsent.scenario.ts new file mode 100644 index 0000000000..1da5367d69 --- /dev/null +++ b/test/e2e/scenario/trackingConsent.scenario.ts @@ -0,0 +1,62 @@ +import { createTest, flushEvents } from '../lib/framework' +import { browserExecute } from '../lib/helpers/browser' +import { findSessionCookie } from '../lib/helpers/session' + +describe('tracking consent', () => { + createTest('does not start the SDK if tracking consent is not given at init') + .withRum({ enableExperimentalFeatures: ['tracking_consent'], trackingConsent: 'not-granted' }) + .run(async ({ intakeRegistry }) => { + await flushEvents() + + expect(intakeRegistry.isEmpty).toBe(true) + expect(await findSessionCookie()).toBeUndefined() + }) + + createTest('starts the SDK once tracking consent is granted') + .withRum({ enableExperimentalFeatures: ['tracking_consent'], trackingConsent: 'not-granted' }) + .run(async ({ intakeRegistry }) => { + await browserExecute(() => { + window.DD_RUM!.setTrackingConsent('granted') + }) + + await flushEvents() + + expect(intakeRegistry.isEmpty).toBe(false) + expect(await findSessionCookie()).toBeDefined() + }) + + createTest('stops sending events if tracking consent is revoked') + .withRum({ enableExperimentalFeatures: ['tracking_consent'], trackUserInteractions: true }) + .run(async ({ intakeRegistry }) => { + await browserExecute(() => { + window.DD_RUM!.setTrackingConsent('not-granted') + }) + + const htmlElement = await $('html') + await htmlElement.click() + + await flushEvents() + + expect(intakeRegistry.rumActionEvents).toEqual([]) + expect(await findSessionCookie()).toBeUndefined() + }) + + createTest('starts a new session when tracking consent is granted again') + .withRum({ enableExperimentalFeatures: ['tracking_consent'] }) + .run(async ({ intakeRegistry }) => { + const initialSessionId = await findSessionCookie() + + await browserExecute(() => { + window.DD_RUM!.setTrackingConsent('not-granted') + window.DD_RUM!.setTrackingConsent('granted') + }) + + await flushEvents() + + const firstView = intakeRegistry.rumViewEvents[0] + const lastView = intakeRegistry.rumViewEvents.at(-1)! + expect(firstView.session.id).not.toEqual(lastView.session.id) + expect(firstView.view.id).not.toEqual(lastView.view.id) + expect(await findSessionCookie()).not.toEqual(initialSessionId) + }) +})