diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0b109dcd70..dc77b1f1f7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,6 @@ variables: APP: 'browser-sdk' - CURRENT_CI_IMAGE: 24 + CURRENT_CI_IMAGE: 25 BUILD_STABLE_REGISTRY: '486234852809.dkr.ecr.us-east-1.amazonaws.com' CI_IMAGE: '$BUILD_STABLE_REGISTRY/ci/$APP:$CURRENT_CI_IMAGE' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c99073d84..f8f7c0aab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ --- +## v3.1.3 + +- ⚗✨[REPLAY-336] Privacy by Default ([#951](https://github.com/DataDog/browser-sdk/pull/951)) +- ⚗✨ [REPLAY-379] add replay stats on view (getter edition) ([#994](https://github.com/DataDog/browser-sdk/pull/994)) +- 📝 Update Readme for v3 cdn links ([#999](https://github.com/DataDog/browser-sdk/pull/999)) +- 🐛[RUMF-990] restore global check to detect synthetics sessions ([#997](https://github.com/DataDog/browser-sdk/pull/997)) + ## v3.1.2 - ✨[RUMF-970] enable buffered PerformanceObserver ([#995](https://github.com/DataDog/browser-sdk/pull/995)) diff --git a/Dockerfile b/Dockerfile index 21f293a753..79fc6c4d3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN apt-get update && apt-get install -y -q --no-install-recommends \ # Download and install Chrome # Debian taken from https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable -RUN curl --silent --show-error --fail http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_90.0.4430.85-1_amd64.deb --output google-chrome.deb \ +RUN curl --silent --show-error --fail http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_92.0.4515.107-1_amd64.deb --output google-chrome.deb \ && dpkg -i google-chrome.deb \ && rm google-chrome.deb diff --git a/developer-extension/package.json b/developer-extension/package.json index 0b11902427..7c42220d59 100644 --- a/developer-extension/package.json +++ b/developer-extension/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-sdk-developer-extension", - "version": "3.1.2", + "version": "3.1.3", "private": true, "scripts": { "build": "rm -rf dist && webpack --mode production", diff --git a/lerna.json b/lerna.json index 1b746baece..33655208be 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { "npmClient": "yarn", "useWorkspaces": true, - "version": "3.1.2", + "version": "3.1.3", "publishConfig": { "access": "public" } diff --git a/packages/core/package.json b/packages/core/package.json index 9fce7d8412..6e9378f2d9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-core", - "version": "3.1.2", + "version": "3.1.3", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/packages/logs/README.md b/packages/logs/README.md index a50bdb3548..1ee3240ab4 100644 --- a/packages/logs/README.md +++ b/packages/logs/README.md @@ -254,7 +254,9 @@ If your Browser logs contain sensitive information that needs redacting, configu The `beforeSend` callback function gives you access to each event collected by the Browser SDK before it is sent to Datadog, and lets you update commonly redacted properties. -For example, to redact email addresses from your web application URLs: +For examples of `beforeSend`, see [Enrich and control browser RUM data with beforeSend][5]. + +To redact email addresses from your web application URLs: #### NPM @@ -309,8 +311,7 @@ You can update the following event properties: | `message` | String | The content of the log. | | `error.stack` | String | The stack trace or complementary information about the error. | | `http.url` | String | The HTTP URL. | - -**Note**: The Browser SDK will ignore modifications made to event properties not listed above. For more information about event properties, see the [Browser SDK repository][5]. +| `context` | String | Extra contextual attributes added with the logger. | ### Define multiple loggers @@ -597,4 +598,4 @@ window.DD_LOGS && DD_LOGS.logger.setHandler(['', '']) [2]: /account_management/api-app-keys/#client-tokens [3]: https://www.npmjs.com/package/@datadog/browser-logs [4]: https://github.com/DataDog/browser-sdk/blob/main/packages/logs/BROWSER_SUPPORT.md -[5]: https://github.com/DataDog/browser-sdk/blob/main/packages/logs/src/logsEvent.types.ts +[5]: /real_user_monitoring/guide/enrich-and-control-rum-data/ diff --git a/packages/logs/package.json b/packages/logs/package.json index c3ba82b254..b5a891d2af 100644 --- a/packages/logs/package.json +++ b/packages/logs/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-logs", - "version": "3.1.2", + "version": "3.1.3", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", @@ -13,7 +13,7 @@ "replace-build-env": "node ../../scripts/replace-build-env.js" }, "dependencies": { - "@datadog/browser-core": "3.1.2", + "@datadog/browser-core": "3.1.3", "tslib": "^1.10.0" }, "devDependencies": { diff --git a/packages/rum-core/package.json b/packages/rum-core/package.json index fbe8ed891b..b06fc765a5 100644 --- a/packages/rum-core/package.json +++ b/packages/rum-core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-core", - "version": "3.1.2", + "version": "3.1.3", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", @@ -12,7 +12,7 @@ "replace-build-env": "node ../../scripts/replace-build-env.js" }, "dependencies": { - "@datadog/browser-core": "3.1.2", + "@datadog/browser-core": "3.1.3", "tslib": "^1.10.0" }, "devDependencies": { diff --git a/packages/rum-core/src/domain/assembly.spec.ts b/packages/rum-core/src/domain/assembly.spec.ts index 3659b8c39f..dd139740c6 100644 --- a/packages/rum-core/src/domain/assembly.spec.ts +++ b/packages/rum-core/src/domain/assembly.spec.ts @@ -51,6 +51,7 @@ describe('rum assembly', () => { afterEach(() => { setupBuilder.cleanup() + cleanupSyntheticsGlobals() }) describe('beforeSend', () => { @@ -516,7 +517,7 @@ describe('rum assembly', () => { }) it('should detect synthetics sessions from global', () => { - ;(window as BrowserWindow)._DATADOG_SYNTHETICS_BROWSER = true + setSyntheticsGlobals('foo', 'bar') const { lifeCycle } = setupBuilder.build() notifyRawRumEvent(lifeCycle, { @@ -524,8 +525,6 @@ describe('rum assembly', () => { }) expect(serverRumEvents[0].session.type).toEqual('synthetics') - - delete (window as BrowserWindow)._DATADOG_SYNTHETICS_BROWSER }) it('should set the session.has_replay attribute if it is defined in the common context', () => { @@ -549,6 +548,44 @@ describe('rum assembly', () => { }) }) + describe('synthetics context', () => { + it('sets the synthetics context defined by global variables', () => { + setSyntheticsGlobals('foo', 'bar') + + const { lifeCycle } = setupBuilder.build() + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.VIEW), + }) + + expect(serverRumEvents[0].synthetics).toEqual({ + test_id: 'foo', + result_id: 'bar', + }) + }) + + it('does not set synthetics context if one global variable is undefined', () => { + setSyntheticsGlobals('foo') + + const { lifeCycle } = setupBuilder.build() + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.VIEW), + }) + + expect(serverRumEvents[0].synthetics).toBeUndefined() + }) + + it('does not set synthetics context if global variables are not strings', () => { + setSyntheticsGlobals(1, 2) + + const { lifeCycle } = setupBuilder.build() + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.VIEW), + }) + + expect(serverRumEvents[0].synthetics).toBeUndefined() + }) + }) + describe('error events limitation', () => { const notifiedRawErrors: RawError[] = [] @@ -629,3 +666,13 @@ function notifyRawRumEvent( } lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, fullData) } + +function setSyntheticsGlobals(publicId: any, resultId?: any) { + ;(window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID = publicId + ;(window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID = resultId +} + +function cleanupSyntheticsGlobals() { + delete (window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID + delete (window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID +} diff --git a/packages/rum-core/src/domain/assembly.ts b/packages/rum-core/src/domain/assembly.ts index 49e21b631e..83b5313c7c 100644 --- a/packages/rum-core/src/domain/assembly.ts +++ b/packages/rum-core/src/domain/assembly.ts @@ -30,7 +30,8 @@ import { ParentContexts } from './parentContexts' import { RumSession, RumSessionPlan } from './rumSession' export interface BrowserWindow extends Window { - _DATADOG_SYNTHETICS_BROWSER?: unknown + _DATADOG_SYNTHETICS_PUBLIC_ID?: string + _DATADOG_SYNTHETICS_RESULT_ID?: string } enum SessionType { @@ -93,6 +94,7 @@ export function startRumAssembly( // must be computed on each event because synthetics instrumentation can be done after sdk execution type: getSessionType(), }, + synthetics: getSyntheticsContext(), } const serverRumEvent = (needToAssembleWithAction(rawRumEvent) ? combine(rumContext, viewContext, actionContext, rawRumEvent) @@ -162,8 +164,19 @@ function needToAssembleWithAction( } function getSessionType() { - return navigator.userAgent.indexOf('DatadogSynthetics') === -1 && - (window as BrowserWindow)._DATADOG_SYNTHETICS_BROWSER === undefined + return navigator.userAgent.indexOf('DatadogSynthetics') === -1 && !getSyntheticsContext() ? SessionType.USER : SessionType.SYNTHETICS } + +function getSyntheticsContext() { + const testId = (window as BrowserWindow)._DATADOG_SYNTHETICS_PUBLIC_ID + const resultId = (window as BrowserWindow)._DATADOG_SYNTHETICS_RESULT_ID + + if (typeof testId === 'string' && typeof resultId === 'string') { + return { + test_id: testId, + result_id: resultId, + } + } +} diff --git a/packages/rum-core/src/domain/lifeCycle.ts b/packages/rum-core/src/domain/lifeCycle.ts index f6b657c596..e5970ff7f8 100644 --- a/packages/rum-core/src/domain/lifeCycle.ts +++ b/packages/rum-core/src/domain/lifeCycle.ts @@ -17,6 +17,19 @@ export enum LifeCycleEventType { VIEW_ENDED, REQUEST_STARTED, REQUEST_COMPLETED, + + // The SESSION_EXPIRED lifecycle event has been introduced to represent when a session has expired + // and trigger cleanup tasks related to this, prior to renewing the session. Its implementation is + // slightly naive: it is not triggered as soon as the session is expired, but rather just before + // notifying that the session is renewed. Thus, the session id is already set to the newly renewed + // session. + // + // This implementation is "good enough" for our use-cases. Improving this is not trivial, + // primarily because multiple instances of the SDK may be managing the same session cookie at + // the same time, for example when using Logs and RUM on the same page, or opening multiple tabs + // on the same domain. + SESSION_EXPIRED, + SESSION_RENEWED, BEFORE_UNLOAD, RAW_RUM_EVENT_COLLECTED, @@ -41,6 +54,7 @@ export class LifeCycle { ): void notify( eventType: + | LifeCycleEventType.SESSION_EXPIRED | LifeCycleEventType.SESSION_RENEWED | LifeCycleEventType.BEFORE_UNLOAD | LifeCycleEventType.AUTO_ACTION_DISCARDED @@ -77,6 +91,7 @@ export class LifeCycle { ): Subscription subscribe( eventType: + | LifeCycleEventType.SESSION_EXPIRED | LifeCycleEventType.SESSION_RENEWED | LifeCycleEventType.BEFORE_UNLOAD | LifeCycleEventType.AUTO_ACTION_DISCARDED, diff --git a/packages/rum-core/src/domain/rumSession.spec.ts b/packages/rum-core/src/domain/rumSession.spec.ts index a6d78b41ef..523b82bdb0 100644 --- a/packages/rum-core/src/domain/rumSession.spec.ts +++ b/packages/rum-core/src/domain/rumSession.spec.ts @@ -25,6 +25,7 @@ describe('rum session', () => { replaySampleRate: 50, } let lifeCycle: LifeCycle + let expireSessionSpy: jasmine.Spy let renewSessionSpy: jasmine.Spy let clock: Clock @@ -33,8 +34,10 @@ describe('rum session', () => { pending('no full rum support') } clock = mockClock() + expireSessionSpy = jasmine.createSpy('expireSessionSpy') renewSessionSpy = jasmine.createSpy('renewSessionSpy') lifeCycle = new LifeCycle() + lifeCycle.subscribe(LifeCycleEventType.SESSION_EXPIRED, expireSessionSpy) lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, renewSessionSpy) }) @@ -52,6 +55,7 @@ describe('rum session', () => { startRumSession(configuration as Configuration, lifeCycle) + expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_REPLAY}`) expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]/) @@ -62,6 +66,7 @@ describe('rum session', () => { startRumSession(configuration as Configuration, lifeCycle) + expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_LITE}`) expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]/) @@ -72,6 +77,7 @@ describe('rum session', () => { startRumSession(configuration as Configuration, lifeCycle) + expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.NOT_TRACKED}`) expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') @@ -82,6 +88,7 @@ describe('rum session', () => { startRumSession(configuration as Configuration, lifeCycle) + expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_REPLAY}`) expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcdef') @@ -92,6 +99,7 @@ describe('rum session', () => { startRumSession(configuration as Configuration, lifeCycle) + expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.NOT_TRACKED}`) }) @@ -101,12 +109,14 @@ describe('rum session', () => { setCookie(SESSION_COOKIE_NAME, '', DURATION) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() clock.tick(COOKIE_ACCESS_DELAY) setupDraws({ tracked: true, trackedWithReplay: true }) document.dispatchEvent(new CustomEvent('click')) + expect(expireSessionSpy).toHaveBeenCalled() expect(renewSessionSpy).toHaveBeenCalled() expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_REPLAY}`) expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]/) diff --git a/packages/rum-core/src/domain/rumSession.ts b/packages/rum-core/src/domain/rumSession.ts index a09578a138..889f0998e0 100644 --- a/packages/rum-core/src/domain/rumSession.ts +++ b/packages/rum-core/src/domain/rumSession.ts @@ -30,6 +30,7 @@ export function startRumSession(configuration: Configuration, lifeCycle: LifeCyc ) session.renewObservable.subscribe(() => { + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) }) diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 531944c3db..104d2af171 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -179,6 +179,10 @@ export interface RumContext { type: string has_replay?: boolean } + synthetics?: { + test_id: string + result_id: string + } _dd: { format_version: 2 drift: number diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 02dd38523f..f5a35add65 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -14,7 +14,7 @@ export type RumActionEvent = CommonProperties & { /** * RUM event type */ - readonly type: string + readonly type: 'action' /** * Action properties */ @@ -102,7 +102,7 @@ export type RumErrorEvent = CommonProperties & { /** * RUM event type */ - readonly type: string + readonly type: 'error' /** * Error properties */ @@ -220,7 +220,7 @@ export type RumLongTaskEvent = CommonProperties & { /** * RUM event type */ - readonly type: string + readonly type: 'long_task' /** * Long Task properties */ @@ -233,6 +233,10 @@ export type RumLongTaskEvent = CommonProperties & { * Duration in ns of the long task */ readonly duration: number + /** + * Whether this long task is considered a frozen frame + */ + readonly is_frozen_frame?: boolean [k: string]: unknown } /** @@ -254,7 +258,7 @@ export type RumResourceEvent = CommonProperties & { /** * RUM event type */ - readonly type: string + readonly type: 'resource' /** * Resource properties */ @@ -438,7 +442,7 @@ export type RumViewEvent = CommonProperties & { /** * RUM event type */ - readonly type: string + readonly type: 'view' /** * View properties */ @@ -509,6 +513,10 @@ export type RumViewEvent = CommonProperties & { * Whether the View corresponding to this event is considered active */ readonly is_active?: boolean + /** + * Whether the View had a low average refresh rate + */ + readonly is_slow_rendered?: boolean /** * Properties of the actions of the view */ @@ -549,6 +557,16 @@ export type RumViewEvent = CommonProperties & { readonly count: number [k: string]: unknown } + /** + * Properties of the frozen frames of the view + */ + readonly frozen_frame?: { + /** + * Number of frozen frames that occurred on the view + */ + readonly count: number + [k: string]: unknown + } /** * Properties of the resources of the view */ @@ -730,6 +748,20 @@ export interface CommonProperties { } [k: string]: unknown } + /** + * Synthetics properties + */ + readonly synthetics?: { + /** + * The identifier of the current Synthetics test + */ + readonly test_id: string + /** + * The identifier of the current Synthetics test results + */ + readonly result_id: string + [k: string]: unknown + } /** * Internal properties */ @@ -737,7 +769,17 @@ export interface CommonProperties { /** * Version of the RUM event format */ - readonly format_version: number + readonly format_version: 2 + /** + * Session-related internal properties + */ + session?: { + /** + * Session plan: 1 is the 'lite' plan, 2 is the 'replay' plan + */ + plan: 1 | 2 + [k: string]: unknown + } [k: string]: unknown } /** diff --git a/packages/rum-slim/package.json b/packages/rum-slim/package.json index 397ae3085c..6013ed2b43 100644 --- a/packages/rum-slim/package.json +++ b/packages/rum-slim/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-slim", - "version": "3.1.2", + "version": "3.1.3", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", @@ -12,8 +12,8 @@ "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json" }, "dependencies": { - "@datadog/browser-core": "3.1.2", - "@datadog/browser-rum-core": "3.1.2", + "@datadog/browser-core": "3.1.3", + "@datadog/browser-rum-core": "3.1.3", "tslib": "^1.10.0" }, "repository": { diff --git a/packages/rum/package.json b/packages/rum/package.json index 0953b4f131..01d61fa4a1 100644 --- a/packages/rum/package.json +++ b/packages/rum/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum", - "version": "3.1.2", + "version": "3.1.3", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", @@ -12,8 +12,8 @@ "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json" }, "dependencies": { - "@datadog/browser-core": "3.1.2", - "@datadog/browser-rum-core": "3.1.2", + "@datadog/browser-core": "3.1.3", + "@datadog/browser-rum-core": "3.1.3", "tslib": "^1.10.0" }, "repository": { diff --git a/packages/rum/src/boot/recorderApi.spec.ts b/packages/rum/src/boot/recorderApi.spec.ts index 09929a595e..2df3b13a6d 100644 --- a/packages/rum/src/boot/recorderApi.spec.ts +++ b/packages/rum/src/boot/recorderApi.spec.ts @@ -155,6 +155,8 @@ describe('makeRecorderApi', () => { it('starts recording if startSessionReplayRecording was called', () => { recorderApi.start() session.setReplayPlan() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) + expect(startRecordingSpy).not.toHaveBeenCalled() lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).toHaveBeenCalled() expect(stopRecordingSpy).not.toHaveBeenCalled() @@ -164,6 +166,7 @@ describe('makeRecorderApi', () => { recorderApi.start() recorderApi.stop() session.setReplayPlan() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() }) @@ -177,6 +180,7 @@ describe('makeRecorderApi', () => { it('keeps not recording if startSessionReplayRecording was called', () => { recorderApi.start() session.setNotTracked() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() expect(stopRecordingSpy).not.toHaveBeenCalled() @@ -190,6 +194,7 @@ describe('makeRecorderApi', () => { it('keeps not recording if startSessionReplayRecording was called', () => { recorderApi.start() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() expect(stopRecordingSpy).not.toHaveBeenCalled() @@ -203,10 +208,24 @@ describe('makeRecorderApi', () => { it('stops recording if startSessionReplayRecording was called', () => { recorderApi.start() + expect(startRecordingSpy).toHaveBeenCalledTimes(1) session.setLitePlan() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) + expect(stopRecordingSpy).toHaveBeenCalled() lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).toHaveBeenCalledTimes(1) - expect(stopRecordingSpy).toHaveBeenCalled() + }) + + it('prevents session recording to start if the session is renewed before onload', () => { + setupBuilder.build() + const { triggerOnLoad } = mockDocumentReadyState() + rumInit(DEFAULT_INIT_CONFIGURATION) + recorderApi.start() + session.setLitePlan() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + triggerOnLoad() + expect(startRecordingSpy).not.toHaveBeenCalled() }) }) @@ -217,10 +236,12 @@ describe('makeRecorderApi', () => { it('stops recording if startSessionReplayRecording was called', () => { recorderApi.start() + expect(startRecordingSpy).toHaveBeenCalledTimes(1) session.setNotTracked() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) + expect(stopRecordingSpy).toHaveBeenCalled() lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).toHaveBeenCalledTimes(1) - expect(stopRecordingSpy).toHaveBeenCalled() }) }) @@ -231,14 +252,19 @@ describe('makeRecorderApi', () => { it('keeps recording if startSessionReplayRecording was called', () => { recorderApi.start() - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + expect(startRecordingSpy).toHaveBeenCalledTimes(1) + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) expect(stopRecordingSpy).toHaveBeenCalled() + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).toHaveBeenCalledTimes(2) }) it('does not starts recording if stopSessionReplayRecording was called', () => { recorderApi.start() + expect(startRecordingSpy).toHaveBeenCalledTimes(1) recorderApi.stop() + expect(stopRecordingSpy).toHaveBeenCalledTimes(1) + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).toHaveBeenCalledTimes(1) expect(stopRecordingSpy).toHaveBeenCalledTimes(1) @@ -253,6 +279,7 @@ describe('makeRecorderApi', () => { it('starts recording if startSessionReplayRecording was called', () => { recorderApi.start() session.setReplayPlan() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).toHaveBeenCalled() expect(stopRecordingSpy).not.toHaveBeenCalled() @@ -262,8 +289,10 @@ describe('makeRecorderApi', () => { recorderApi.start() recorderApi.stop() session.setReplayPlan() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() + expect(stopRecordingSpy).not.toHaveBeenCalled() }) }) @@ -275,8 +304,10 @@ describe('makeRecorderApi', () => { it('keeps not recording if startSessionReplayRecording was called', () => { recorderApi.start() session.setLitePlan() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() + expect(stopRecordingSpy).not.toHaveBeenCalled() }) }) @@ -287,8 +318,10 @@ describe('makeRecorderApi', () => { it('keeps not recording if startSessionReplayRecording was called', () => { recorderApi.start() + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() + expect(stopRecordingSpy).not.toHaveBeenCalled() }) }) }) diff --git a/packages/rum/src/boot/recorderApi.ts b/packages/rum/src/boot/recorderApi.ts index 361f74adb9..7f199a0f26 100644 --- a/packages/rum/src/boot/recorderApi.ts +++ b/packages/rum/src/boot/recorderApi.ts @@ -62,12 +62,14 @@ export function makeRecorderApi(startRecordingImpl: StartRecording): RecorderApi session: RumSession, parentContexts: ParentContexts ) => { - lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => { - if (state.status === RecorderStatus.Started) { + lifeCycle.subscribe(LifeCycleEventType.SESSION_EXPIRED, () => { + if (state.status === RecorderStatus.Starting || state.status === RecorderStatus.Started) { stopStrategy() state = { status: RecorderStatus.IntentToStart } } + }) + lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => { if (state.status === RecorderStatus.IntentToStart) { startStrategy() } diff --git a/packages/rum/src/boot/startRecording.spec.ts b/packages/rum/src/boot/startRecording.spec.ts index 01974f1f3d..97b1678ca0 100644 --- a/packages/rum/src/boot/startRecording.spec.ts +++ b/packages/rum/src/boot/startRecording.spec.ts @@ -22,6 +22,7 @@ describe('startRecording', () => { let sandbox: HTMLElement let textField: HTMLInputElement let expectNoExtraRequestSendCalls: (done: () => void) => void + let stopRecording: () => void beforeEach(() => { if (isIE()) { @@ -51,9 +52,10 @@ describe('startRecording', () => { }, }) .withSession(session) - .beforeBuild(({ lifeCycle, applicationId, configuration, parentContexts, session }) => - startRecording(lifeCycle, applicationId, configuration, session, parentContexts) - ) + .beforeBuild(({ lifeCycle, applicationId, configuration, parentContexts, session }) => { + ;({ stop: stopRecording } = startRecording(lifeCycle, applicationId, configuration, session, parentContexts)) + return { stop: stopRecording } + }) const requestSendSpy = spyOn(HttpRequest.prototype, 'send') ;({ @@ -206,6 +208,35 @@ describe('startRecording', () => { }) }) + describe('when calling stop()', () => { + it('stops collecting records', (done) => { + const { lifeCycle } = setupBuilder.build() + + document.body.dispatchEvent(createNewEvent('click')) + stopRecording() + document.body.dispatchEvent(createNewEvent('click')) + flushSegment(lifeCycle) + + waitRequestSendCalls(1, (calls) => { + expect(getRequestData(calls.first()).records_count).toBe('4') + expectNoExtraRequestSendCalls(done) + }) + }) + + it('stops taking full snapshots on view creation', (done) => { + const { lifeCycle } = setupBuilder.build() + + stopRecording() + changeView(lifeCycle) + flushSegment(lifeCycle) + + waitRequestSendCalls(1, (calls) => { + expect(getRequestData(calls.first()).records_count).toBe('3') + expectNoExtraRequestSendCalls(done) + }) + }) + }) + function changeView(lifeCycle: LifeCycle) { lifeCycle.notify(LifeCycleEventType.VIEW_ENDED, {} as any) viewId = 'view-id-2' diff --git a/packages/rum/src/boot/startRecording.ts b/packages/rum/src/boot/startRecording.ts index c48379fb76..368fe3f96c 100644 --- a/packages/rum/src/boot/startRecording.ts +++ b/packages/rum/src/boot/startRecording.ts @@ -30,22 +30,20 @@ export function startRecording( initialPrivacyLevel: configuration.initialPrivacyLevel, }) - lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, flushMutations) - lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, takeFullSnapshot) - trackViewEndRecord(lifeCycle, (record) => addRawRecord(record)) + const { unsubscribe: unsubscribeViewEnded } = lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, () => { + flushMutations() + addRawRecord({ + type: RecordType.ViewEnd, + }) + }) + const { unsubscribe: unsubscribeViewCreated } = lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, takeFullSnapshot) return { stop: () => { + unsubscribeViewEnded() + unsubscribeViewCreated() stopRecording() stopSegmentCollection() }, } } - -export function trackViewEndRecord(lifeCycle: LifeCycle, addRawRecord: (record: RawRecord) => void) { - lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, () => { - addRawRecord({ - type: RecordType.ViewEnd, - }) - }) -} diff --git a/performances/package.json b/performances/package.json index 31f0ff6bac..a045ea7428 100644 --- a/performances/package.json +++ b/performances/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "performances", - "version": "3.1.2", + "version": "3.1.3", "scripts": { "start": "ts-node ./src/main.ts" }, diff --git a/rum-events-format b/rum-events-format index fcd5662c40..2d27327153 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit fcd5662c4007a3225ce9abb443bbb898b513b8b1 +Subproject commit 2d27327153d8e109954c201f3bd2d213cb598bc2 diff --git a/test/app/yarn.lock b/test/app/yarn.lock index 17a760cf11..8f1eb7a039 100644 --- a/test/app/yarn.lock +++ b/test/app/yarn.lock @@ -2,28 +2,28 @@ # yarn lockfile v1 -"@datadog/browser-core@3.1.2", "@datadog/browser-core@file:../../packages/core": - version "3.1.2" +"@datadog/browser-core@3.1.3", "@datadog/browser-core@file:../../packages/core": + version "3.1.3" dependencies: tslib "^1.10.0" "@datadog/browser-logs@file:../../packages/logs": - version "3.1.2" + version "3.1.3" dependencies: - "@datadog/browser-core" "3.1.2" + "@datadog/browser-core" "3.1.3" tslib "^1.10.0" -"@datadog/browser-rum-core@3.1.2", "@datadog/browser-rum-core@file:../../packages/rum-core": - version "3.1.2" +"@datadog/browser-rum-core@3.1.3", "@datadog/browser-rum-core@file:../../packages/rum-core": + version "3.1.3" dependencies: - "@datadog/browser-core" "3.1.2" + "@datadog/browser-core" "3.1.3" tslib "^1.10.0" "@datadog/browser-rum@file:../../packages/rum": - version "3.1.2" + version "3.1.3" dependencies: - "@datadog/browser-core" "3.1.2" - "@datadog/browser-rum-core" "3.1.2" + "@datadog/browser-core" "3.1.3" + "@datadog/browser-rum-core" "3.1.3" tslib "^1.10.0" "@types/eslint-scope@^3.7.0": @@ -58,9 +58,9 @@ integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== "@types/node@*": - version "16.6.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.0.tgz#0d5685f85066f94e97f19e8a67fe003c5fadacc4" - integrity sha512-OyiZPohMMjZEYqcVo/UJ04GyAxXOJEZO/FpzyXxcH4r/ArrVoXHf4MbUrkLp0Tz7/p1mMKpo5zJ6ZHl8XBNthQ== + version "16.6.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50" + integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA== "@webassemblyjs/ast@1.11.0": version "1.11.0" @@ -233,22 +233,22 @@ braces@^3.0.1: fill-range "^7.0.1" browserslist@^4.14.5: - version "4.16.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.7.tgz#108b0d1ef33c4af1b587c54f390e7041178e4335" - integrity sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA== + version "4.16.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0" + integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ== dependencies: - caniuse-lite "^1.0.30001248" - colorette "^1.2.2" - electron-to-chromium "^1.3.793" + caniuse-lite "^1.0.30001251" + colorette "^1.3.0" + electron-to-chromium "^1.3.811" escalade "^3.1.1" - node-releases "^1.1.73" + node-releases "^1.1.75" buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -caniuse-lite@^1.0.30001248: +caniuse-lite@^1.0.30001251: version "1.0.30001251" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz#6853a606ec50893115db660f82c094d18f096d85" integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A== @@ -279,7 +279,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -colorette@^1.2.2: +colorette@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w== @@ -294,10 +294,10 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -electron-to-chromium@^1.3.793: - version "1.3.803" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.803.tgz#78993a991d096500f21a77e91cd2a44295fe3cbe" - integrity sha512-tmRK9qB8Zs8eLMtTBp+w2zVS9MUe62gQQQHjYdAc5Zljam3ZIokNb+vZLPRz9RCREp6EFRwyhOFwbt1fEriQ2Q== +electron-to-chromium@^1.3.811: + version "1.3.813" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.813.tgz#751a007d71c00faed8b5e9edaf3634c14b9c5a1f" + integrity sha512-YcSRImHt6JZZ2sSuQ4Bzajtk98igQ0iKkksqlzZLzbh4p0OIyJRSvUbsgqfcR8txdfsoYCc4ym306t4p2kP/aw== emojis-list@^3.0.0: version "3.0.0" @@ -508,10 +508,10 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -node-releases@^1.1.73: - version "1.1.74" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.74.tgz#e5866488080ebaa70a93b91144ccde06f3c3463e" - integrity sha512-caJBVempXZPepZoZAPCWRTNxYQ+xtG/KAi4ozTA5A+nJ7IU+kLQCbqaUjb5Rwy14M9upBWiQ4NutcmW04LJSRw== +node-releases@^1.1.75: + version "1.1.75" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe" + integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw== p-limit@^3.1.0: version "3.1.0" diff --git a/test/e2e/scenario/recorder.scenario.ts b/test/e2e/scenario/recorder.scenario.ts index 43d5bd5133..77dacf83bb 100644 --- a/test/e2e/scenario/recorder.scenario.ts +++ b/test/e2e/scenario/recorder.scenario.ts @@ -1,10 +1,10 @@ -import { CreationReason, IncrementalSource, Segment } from '@datadog/browser-rum/cjs/types' +import { CreationReason, IncrementalSource, RecordType, Segment } from '@datadog/browser-rum/cjs/types' import { InputData, StyleSheetRuleData, NodeType } from '@datadog/browser-rum/cjs/domain/record/types' import { RumInitConfiguration } from '@datadog/browser-rum-core' import { createTest, bundleSetup, html, EventRegistry } from '../lib/framework' import { browserExecute } from '../lib/helpers/browser' -import { flushEvents } from '../lib/helpers/sdk' +import { flushEvents, renewSession } from '../lib/helpers/sdk' import { findElement, findElementWithIdAttribute, @@ -628,12 +628,37 @@ describe('recorder', () => { expect(styleSheetRules[1].data.adds).toEqual([{ rule: '.added {}', index: 0 }]) }) }) + + describe('session renewal', () => { + createTest('a single fullSnapshot is taken when the session is renewed') + .withRum() + .withRumInit(initRumAndStartRecording) + .withSetup(bundleSetup) + .run(async ({ events }) => { + await renewSession() + + await flushEvents() + + expect(events.sessionReplay.length).toBe(2) + + const segment = getLastSegment(events) + expect(segment.creation_reason).toBe('init') + expect(segment.records[0].type).toBe(RecordType.Meta) + expect(segment.records[1].type).toBe(RecordType.Focus) + expect(segment.records[2].type).toBe(RecordType.FullSnapshot) + expect(segment.records.slice(3).every((record) => record.type !== RecordType.FullSnapshot)).toBe(true) + }) + }) }) function getFirstSegment(events: EventRegistry) { return events.sessionReplay[0].segment.data } +function getLastSegment(events: EventRegistry) { + return events.sessionReplay[events.sessionReplay.length - 1].segment.data +} + function initRumAndStartRecording(initConfiguration: RumInitConfiguration) { window.DD_RUM!.init(initConfiguration) window.DD_RUM!.startSessionReplayRecording() diff --git a/test/e2e/wdio.local.conf.js b/test/e2e/wdio.local.conf.js index c9dcffba68..c891222fd0 100644 --- a/test/e2e/wdio.local.conf.js +++ b/test/e2e/wdio.local.conf.js @@ -1,7 +1,7 @@ const baseConf = require('./wdio.base.conf') // https://sites.google.com/a/chromium.org/chromedriver/downloads -const CHROME_DRIVER_VERSION = '90.0.4430.24' +const CHROME_DRIVER_VERSION = '92.0.4515.107' exports.config = { ...baseConf,