From 2e481e0d7b64e9f39a086e82f7ebf726d15c3b57 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Thu, 18 Feb 2021 16:15:42 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=8A=20Add=20clock=20drift=20monito?= =?UTF-8?q?ring=20(#736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/domain/internalMonitoring.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/core/src/domain/internalMonitoring.ts b/packages/core/src/domain/internalMonitoring.ts index 4c96bb293b..ff857e9e19 100644 --- a/packages/core/src/domain/internalMonitoring.ts +++ b/packages/core/src/domain/internalMonitoring.ts @@ -41,6 +41,8 @@ export function startInternalMonitoring(configuration: Configuration): InternalM maxMessagesPerPage: configuration.maxInternalMonitoringMessagesPerPage, sentMessageCount: 0, }) + + startMonitoringClockDrift() } return { setExternalContextProvider: (provider: () => Context) => { @@ -95,6 +97,26 @@ export function resetInternalMonitoring() { monitoringConfiguration.batch = undefined } +function startMonitoringClockDrift() { + const interval = setInterval( + monitor(() => { + const drift = Date.now() - utils.getTimestamp(performance.now()) + if (Math.abs(drift) > utils.ONE_SECOND) { + clearInterval(interval) + const navigationStart = utils.getTimestamp(0) + addMonitoringMessage('clock drift detected', { + debug: { + navigationStartUTC: new Date(navigationStart).toUTCString(), + timeSpent: Date.now() - navigationStart, + drift, + }, + }) + } + }), + utils.ONE_MINUTE + ) +} + export function monitored unknown>( _: any, __: string, From 7b7a68d168085b31396ed49b64404ab56001d117 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Thu, 18 Feb 2021 18:15:07 +0100 Subject: [PATCH 2/4] Readme: add missing rum-core package (#737) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ebaebd4066..60cf064c32 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This repository contains several packages: | browser-logs | [![npm version][01]][02] | [![bundle size][03]][04] | [datadog-logs][05] | [![API][1]][07] [![product][2]][08] | | browser-rum | [![npm version][11]][12] | [![bundle size][13]][14] | [datadog-rum][15] | [![API][1]][17] [![product][2]][18] | | browser-rum-recorder | [![npm version][21]][22] | [![bundle size][23]][24] | [datadog-rum-recorder][25] | [![API][1]][27] [![product][2]][28] | +| browser-rum-core | [![npm version][41]][42] | [![bundle size][43]][44] | | | browser-core | [![npm version][31]][32] | [![bundle size][33]][34] | | [1]: https://github.githubassets.com/favicons/favicon.png @@ -40,3 +41,7 @@ This repository contains several packages: [32]: https://badge.fury.io/js/%40datadog%2Fbrowser-core [33]: https://badgen.net/bundlephobia/minzip/@datadog/browser-core [34]: https://bundlephobia.com/result?p=@datadog/browser-core +[41]: https://badge.fury.io/js/%40datadog%2Fbrowser-rum-core.svg +[42]: https://badge.fury.io/js/%40datadog%2Fbrowser-rum-core +[43]: https://badgen.net/bundlephobia/minzip/@datadog/browser-rum-core +[44]: https://bundlephobia.com/result?p=@datadog/browser-rum-core From 3fdee5ccf5a616b357ad2b26705eb046d30f8a1d Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Fri, 19 Feb 2021 11:48:58 +0100 Subject: [PATCH 3/4] v2.5.4 (#740) --- CHANGELOG.md | 5 +++++ developer-extension/package.json | 2 +- lerna.json | 2 +- packages/core/package.json | 2 +- packages/logs/package.json | 4 ++-- packages/rum-core/package.json | 4 ++-- packages/rum-recorder/package.json | 6 +++--- packages/rum/package.json | 6 +++--- test/app/yarn.lock | 26 +++++++++++++------------- 9 files changed, 31 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ff2a3e401..77cab2a049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ --- +## v2.5.4 + +- ๐Ÿ”Š Add clock drift monitoring ([#736](https://github.com/DataDog/browser-sdk/pull/736)) +- โœจ Implement a developer extension ([#686](https://github.com/DataDog/browser-sdk/pull/686)) + ## v2.5.3 - โš— Remove mutation buffer global instance ([#728](https://github.com/DataDog/browser-sdk/pull/728)) diff --git a/developer-extension/package.json b/developer-extension/package.json index e250ba879c..64450e3146 100644 --- a/developer-extension/package.json +++ b/developer-extension/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-sdk-developer-extension", - "version": "1.0.0", + "version": "2.5.4", "private": true, "scripts": { "build": "rm -rf dist && webpack --mode production", diff --git a/lerna.json b/lerna.json index ed6fdc2056..850e9c217f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { "npmClient": "yarn", "useWorkspaces": true, - "version": "2.5.3", + "version": "2.5.4", "publishConfig": { "access": "public" } diff --git a/packages/core/package.json b/packages/core/package.json index d96f9ff7ec..60cee42496 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-core", - "version": "2.5.3", + "version": "2.5.4", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/packages/logs/package.json b/packages/logs/package.json index 08ec4b1263..ed5a8be00b 100644 --- a/packages/logs/package.json +++ b/packages/logs/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-logs", - "version": "2.5.3", + "version": "2.5.4", "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": "2.5.3", + "@datadog/browser-core": "2.5.4", "tslib": "^1.10.0" }, "devDependencies": { diff --git a/packages/rum-core/package.json b/packages/rum-core/package.json index 255db32712..1891afb88d 100644 --- a/packages/rum-core/package.json +++ b/packages/rum-core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-core", - "version": "2.5.3", + "version": "2.5.4", "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": "2.5.3", + "@datadog/browser-core": "2.5.4", "tslib": "^1.10.0" }, "devDependencies": { diff --git a/packages/rum-recorder/package.json b/packages/rum-recorder/package.json index 7d85dfaba7..e09825fdad 100644 --- a/packages/rum-recorder/package.json +++ b/packages/rum-recorder/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-recorder", - "version": "2.5.3", + "version": "2.5.4", "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": "2.5.3", - "@datadog/browser-rum-core": "2.5.3", + "@datadog/browser-core": "2.5.4", + "@datadog/browser-rum-core": "2.5.4", "tslib": "^1.10.0" }, "repository": { diff --git a/packages/rum/package.json b/packages/rum/package.json index faf4b9c1ce..1720094486 100644 --- a/packages/rum/package.json +++ b/packages/rum/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum", - "version": "2.5.3", + "version": "2.5.4", "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": "2.5.3", - "@datadog/browser-rum-core": "2.5.3", + "@datadog/browser-core": "2.5.4", + "@datadog/browser-rum-core": "2.5.4", "tslib": "^1.10.0" }, "repository": { diff --git a/test/app/yarn.lock b/test/app/yarn.lock index 1d77b70636..32b5790e1e 100644 --- a/test/app/yarn.lock +++ b/test/app/yarn.lock @@ -2,35 +2,35 @@ # yarn lockfile v1 -"@datadog/browser-core@2.5.3", "@datadog/browser-core@file:../../packages/core": - version "2.5.3" +"@datadog/browser-core@2.5.4", "@datadog/browser-core@file:../../packages/core": + version "2.5.4" dependencies: tslib "^1.10.0" "@datadog/browser-logs@file:../../packages/logs": - version "2.5.3" + version "2.5.4" dependencies: - "@datadog/browser-core" "2.5.3" + "@datadog/browser-core" "2.5.4" tslib "^1.10.0" -"@datadog/browser-rum-core@2.5.3", "@datadog/browser-rum-core@file:../../packages/rum-core": - version "2.5.3" +"@datadog/browser-rum-core@2.5.4", "@datadog/browser-rum-core@file:../../packages/rum-core": + version "2.5.4" dependencies: - "@datadog/browser-core" "2.5.3" + "@datadog/browser-core" "2.5.4" tslib "^1.10.0" "@datadog/browser-rum-recorder@file:../../packages/rum-recorder": - version "2.5.3" + version "2.5.4" dependencies: - "@datadog/browser-core" "2.5.3" - "@datadog/browser-rum-core" "2.5.3" + "@datadog/browser-core" "2.5.4" + "@datadog/browser-rum-core" "2.5.4" tslib "^1.10.0" "@datadog/browser-rum@file:../../packages/rum": - version "2.5.3" + version "2.5.4" dependencies: - "@datadog/browser-core" "2.5.3" - "@datadog/browser-rum-core" "2.5.3" + "@datadog/browser-core" "2.5.4" + "@datadog/browser-rum-core" "2.5.4" tslib "^1.10.0" "@webassemblyjs/ast@1.8.5": From 7480da586e2f874dec452ff9a8e4c94580e41c24 Mon Sep 17 00:00:00 2001 From: Amine Ben hammou Date: Mon, 22 Feb 2021 15:24:49 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=A8=20[RUMF-847]=20Add=20onNewLocatio?= =?UTF-8?q?n=20to=20configuration=20(#724)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * โœจ Add onNewLocation to configuration * Stop view tracking after test Co-authored-by: Benoรฎt * Make line readable Co-authored-by: Benoรฎt * ๐Ÿ‘Œ Remove uneeded variable * ๐Ÿ‘Œ Catch error thrown by onNewLocation * ๐Ÿ‘Œ Use isDifferentView as a fallback to shouldCreateView * ๐Ÿ‘Œ Add tests * Refactor try/catch * ๐Ÿ‘Œ Add feature flag * Stop history and hash tracking when stoping view tracking * ๐Ÿ‘Œ Refactor tests * ๐Ÿ‘Œ Do not expose onNewLocation types publicly * Add view.name to other events * ๐Ÿ‘Œ Add test for parent view name Co-authored-by: Benoรฎt --- packages/rum-core/src/boot/rum.ts | 21 ++- .../src/domain/parentContexts.spec.ts | 7 + .../rum-core/src/domain/parentContexts.ts | 1 + .../view/trackViews.spec.ts | 145 ++++++++++++++++++ .../rumEventsCollection/view/trackViews.ts | 65 ++++++-- .../view/viewCollection.spec.ts | 2 + .../view/viewCollection.ts | 6 +- packages/rum-core/src/rawRumEvent.types.ts | 2 + packages/rum-core/src/rumEvent.types.ts | 10 +- 9 files changed, 240 insertions(+), 19 deletions(-) diff --git a/packages/rum-core/src/boot/rum.ts b/packages/rum-core/src/boot/rum.ts index cee6b66396..7049067c6e 100644 --- a/packages/rum-core/src/boot/rum.ts +++ b/packages/rum-core/src/boot/rum.ts @@ -18,11 +18,22 @@ import { startRumBatch } from '../transport/batch' import { buildEnv } from './buildEnv' import { RumUserConfiguration } from './rumPublicApi' -export function startRum(userConfiguration: RumUserConfiguration, getCommonContext: () => CommonContext) { +export type NewLocationListener = ( + newLocation: Location, + oldLocation?: Location +) => undefined | { shouldCreateView?: boolean; viewName?: string } + +export function startRum( + userConfiguration: RumUserConfiguration & { onNewLocation?: NewLocationListener }, + getCommonContext: () => CommonContext +) { const lifeCycle = new LifeCycle() const { configuration, internalMonitoring } = commonInit(userConfiguration, buildEnv) const session = startRumSession(configuration, lifeCycle) + if (!configuration.isEnabled('onNewLocation')) { + userConfiguration.onNewLocation = undefined + } internalMonitoring.setExternalContextProvider(() => combine( @@ -40,7 +51,8 @@ export function startRum(userConfiguration: RumUserConfiguration, getCommonConte lifeCycle, configuration, session, - getCommonContext + getCommonContext, + userConfiguration.onNewLocation ) startRequestCollection(lifeCycle, configuration) @@ -67,14 +79,15 @@ export function startRumEventCollection( lifeCycle: LifeCycle, configuration: Configuration, session: RumSession, - getCommonContext: () => CommonContext + getCommonContext: () => CommonContext, + onNewLocation?: NewLocationListener ) { const parentContexts = startParentContexts(lifeCycle, session) const batch = startRumBatch(configuration, lifeCycle) startRumAssembly(applicationId, configuration, lifeCycle, session, parentContexts, getCommonContext) startLongTaskCollection(lifeCycle) startResourceCollection(lifeCycle, session) - const { addTiming } = startViewCollection(lifeCycle, location) + const { addTiming } = startViewCollection(lifeCycle, location, onNewLocation) const { addError } = startErrorCollection(lifeCycle, configuration) const { addAction } = startActionCollection(lifeCycle, configuration) diff --git a/packages/rum-core/src/domain/parentContexts.spec.ts b/packages/rum-core/src/domain/parentContexts.spec.ts index 8831d5f7d2..029094b91f 100644 --- a/packages/rum-core/src/domain/parentContexts.spec.ts +++ b/packages/rum-core/src/domain/parentContexts.spec.ts @@ -104,6 +104,13 @@ describe('parentContexts', () => { expect(parentContexts.findView()!.view.url).toBe('http://fake-url.com/foo') }) + it('should return the view name with the view', () => { + const { lifeCycle } = setupBuilder.build() + + lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, buildViewCreatedEvent({ name: 'Fake name' })) + expect(parentContexts.findView()!.view.name).toBe('Fake name') + }) + it('should update session id only on VIEW_CREATED', () => { const { lifeCycle } = setupBuilder.build() diff --git a/packages/rum-core/src/domain/parentContexts.ts b/packages/rum-core/src/domain/parentContexts.ts index d81f7a5eb1..624b2a594a 100644 --- a/packages/rum-core/src/domain/parentContexts.ts +++ b/packages/rum-core/src/domain/parentContexts.ts @@ -97,6 +97,7 @@ export function startParentContexts(lifeCycle: LifeCycle, session: RumSession): }, view: { id: currentView!.id, + name: currentView!.name, referrer: currentView!.referrer, url: currentView!.location.href, }, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.spec.ts index e649b8593f..e97e2bd421 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.spec.ts @@ -1,5 +1,6 @@ import { createRawRumEvent } from '../../../../test/fixtures' import { setup, TestSetupBuilder } from '../../../../test/specHelper' +import { NewLocationListener } from '../../../boot/rum' import { RumLargestContentfulPaintTiming, RumPerformanceNavigationTiming, @@ -208,6 +209,150 @@ describe('rum track url change', () => { }) }) +describe('rum use onNewLocation callback to rename/ignore views', () => { + let setupBuilder: TestSetupBuilder + let handler: jasmine.Spy + let getViewEvent: (index: number) => View + let onNewLocation: NewLocationListener + + beforeEach(() => { + ;({ handler, getViewEvent } = spyOnViews()) + + setupBuilder = setup() + .withFakeLocation('/foo') + .beforeBuild(({ location, lifeCycle }) => { + lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, handler) + return trackViews(location, lifeCycle, onNewLocation) + }) + }) + + afterEach(() => { + setupBuilder.cleanup() + }) + + it('should set the view name to the returned viewName', () => { + onNewLocation = (location) => { + switch (location.pathname) { + case '/foo': + return { viewName: 'Foo' } + case '/bar': + return { viewName: 'Bar' } + } + } + setupBuilder.build() + history.pushState({}, '', '/bar') + history.pushState({}, '', '/baz') + + expect(getViewEvent(0).name).toBe('Foo') + expect(getViewEvent(2).name).toBe('Bar') + expect(getViewEvent(4).name).toBeUndefined() + }) + + it('should allow customer to consider other location changes as new views', () => { + onNewLocation = (location) => ({ viewName: `Foo ${location.search}`, shouldCreateView: true }) + setupBuilder.build() + history.pushState({}, '', '/foo?view=bar') + history.pushState({}, '', '/foo?view=baz') + + expect(getViewEvent(0).name).toBe('Foo ') + expect(getViewEvent(2).name).toBe('Foo ?view=bar') + expect(getViewEvent(4).name).toBe('Foo ?view=baz') + }) + + it('pass current and old locations to onNewLocation', () => { + onNewLocation = (location, oldLocation) => ({ + viewName: `old: ${oldLocation?.pathname || 'undefined'}, new: ${location.pathname}`, + }) + setupBuilder.build() + history.pushState({}, '', '/bar') + + expect(getViewEvent(0).name).toBe('old: undefined, new: /foo') + expect(getViewEvent(2).name).toBe('old: /foo, new: /bar') + }) + + it('should use our own new view detection rules when shouldCreateView is undefined', () => { + onNewLocation = (location) => { + switch (location.pathname) { + case '/foo': + return { viewName: 'Foo' } + case '/bar': + return { viewName: 'Bar' } + } + } + setupBuilder.build() + history.pushState({}, '', '/foo') + history.pushState({}, '', '/bar') + history.pushState({}, '', '/bar') + history.pushState({}, '', '/foo') + + expect(getViewEvent(0).name).toBe('Foo') + expect(getViewEvent(2).id).toBe(getViewEvent(0).id) + expect(getViewEvent(3).name).toBe('Bar') + expect(getViewEvent(5).id).toBe(getViewEvent(3).id) + expect(getViewEvent(6).name).toBe('Foo') + }) + + it('should ignore the view when shouldCreateView is false', () => { + onNewLocation = (location) => { + switch (location.pathname) { + case '/foo': + return { viewName: 'Foo', shouldCreateView: true } + case '/bar': + return { shouldCreateView: false } + case '/baz': + return { viewName: 'Baz', shouldCreateView: true } + } + } + setupBuilder.build() + history.pushState({}, '', '/bar') + history.pushState({}, '', '/baz') + + const initialViewId = getViewEvent(0).id + expect(getViewEvent(0).name).toBe('Foo') + expect(getViewEvent(2).name).toBe('Foo') + expect(getViewEvent(2).id).toBe(initialViewId) + expect(getViewEvent(3).name).toBe('Baz') + expect(getViewEvent(3).id).not.toBe(initialViewId) + }) + + it('should create the initial view even when shouldCreateView is false', () => { + onNewLocation = (location) => { + if (location.pathname === '/foo') { + return { shouldCreateView: false } + } + if (location.pathname === '/bar') { + return { shouldCreateView: true } + } + } + setupBuilder.build() + history.pushState({}, '', '/bar') + history.pushState({}, '', '/foo') + + expect(getViewEvent(0).location.pathname).toBe('/foo') + expect(getViewEvent(2).location.pathname).toBe('/bar') + expect(getViewEvent(4)).toBeUndefined() + }) + + it('should catch thrown errors', () => { + const fooError = 'Error on /foo path' + const barError = 'Error on /bar path' + onNewLocation = (location) => { + if (location.pathname === '/foo') { + throw fooError + } + if (location.pathname === '/bar') { + throw barError + } + return undefined + } + const consoleErrorSpy = spyOn(console, 'error') + setupBuilder.build() + expect(consoleErrorSpy).toHaveBeenCalledWith('onNewLocation threw an error:', fooError) + history.pushState({}, '', '/bar') + expect(consoleErrorSpy).toHaveBeenCalledWith('onNewLocation threw an error:', barError) + }) +}) + describe('rum view referrer', () => { let setupBuilder: TestSetupBuilder let initialViewCreatedEvent: ViewCreatedEvent diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.ts b/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.ts index 67c540c56e..a29228f595 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/trackViews.ts @@ -1,4 +1,5 @@ import { addEventListener, DOM_EVENT, generateUUID, monitor, noop, ONE_MINUTE, throttle } from '@datadog/browser-core' +import { NewLocationListener } from '../../../boot/rum' import { supportPerformanceTimingEvent } from '../../../browser/performanceCollection' import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' @@ -9,6 +10,7 @@ import { Timings, trackTimings } from './trackTimings' export interface View { id: string + name?: string location: Location referrer: string timings: Timings @@ -25,6 +27,7 @@ export interface View { export interface ViewCreatedEvent { id: string + name?: string location: Location referrer: string startTime: number @@ -33,9 +36,21 @@ export interface ViewCreatedEvent { export const THROTTLE_VIEW_UPDATE_PERIOD = 3000 export const SESSION_KEEP_ALIVE_INTERVAL = 5 * ONE_MINUTE -export function trackViews(location: Location, lifeCycle: LifeCycle) { +export function trackViews( + location: Location, + lifeCycle: LifeCycle, + onNewLocation: NewLocationListener = () => undefined +) { + onNewLocation = wrapOnNewLocation(onNewLocation) const startOrigin = 0 - const initialView = newView(lifeCycle, location, ViewLoadingType.INITIAL_LOAD, document.referrer, startOrigin) + const initialView = newView( + lifeCycle, + location, + ViewLoadingType.INITIAL_LOAD, + document.referrer, + startOrigin, + onNewLocation(location)?.viewName + ) let currentView = initialView const { stop: stopTimingsTracking } = trackTimings(lifeCycle, (timings) => { @@ -43,19 +58,20 @@ export function trackViews(location: Location, lifeCycle: LifeCycle) { initialView.scheduleUpdate() }) - trackHistory(onLocationChange) - trackHash(onLocationChange) + const { stop: stopHistoryTracking } = trackHistory(onLocationChange) + const { stop: stopHashTracking } = trackHash(onLocationChange) function onLocationChange() { - if (currentView.isDifferentView(location)) { + const { viewName, shouldCreateView } = onNewLocation(location, currentView.getLocation()) || {} + if (shouldCreateView || (shouldCreateView === undefined && currentView.isDifferentView(location))) { // Renew view on location changes currentView.end() currentView.triggerUpdate() - currentView = newView(lifeCycle, location, ViewLoadingType.ROUTE_CHANGE, currentView.url) - } else { - currentView.updateLocation(location) - currentView.triggerUpdate() + currentView = newView(lifeCycle, location, ViewLoadingType.ROUTE_CHANGE, currentView.url, undefined, viewName) + return } + currentView.updateLocation(location) + currentView.triggerUpdate() } // Renew view on session renewal @@ -85,6 +101,8 @@ export function trackViews(location: Location, lifeCycle: LifeCycle) { currentView.triggerUpdate() }, stop: () => { + stopHistoryTracking() + stopHashTracking() stopTimingsTracking() currentView.end() clearInterval(keepAliveInterval) @@ -97,7 +115,8 @@ function newView( initialLocation: Location, loadingType: ViewLoadingType, referrer: string, - startTime: number = performance.now() + startTime: number = performance.now(), + name?: string ) { // Setup initial values const id = generateUUID() @@ -160,6 +179,7 @@ function newView( documentVersion, eventCounts, id, + name, loadingTime, loadingType, location, @@ -186,6 +206,9 @@ function newView( (!isHashAnAnchor(otherLocation.hash) && otherLocation.hash !== location.hash) ) }, + getLocation() { + return location + }, triggerUpdate() { // cancel any pending view updates execution cancelScheduleViewUpdate() @@ -227,11 +250,17 @@ function trackHistory(onHistoryChange: () => void) { originalReplaceState.apply(this, arguments as any) onHistoryChange() }) - addEventListener(window, DOM_EVENT.POP_STATE, onHistoryChange) + const { stop: removeListener } = addEventListener(window, DOM_EVENT.POP_STATE, onHistoryChange) + const stop = () => { + removeListener() + history.pushState = originalPushState + history.replaceState = originalReplaceState + } + return { stop } } function trackHash(onHashChange: () => void) { - addEventListener(window, DOM_EVENT.HASH_CHANGE, onHashChange) + return addEventListener(window, DOM_EVENT.HASH_CHANGE, onHashChange) } function trackLoadingTime(loadType: ViewLoadingType, callback: (loadingTime: number) => void) { @@ -316,3 +345,15 @@ function sanitizeTiming(name: string) { } return sanitized } + +function wrapOnNewLocation(onNewLocation: NewLocationListener): NewLocationListener { + return (newLocation, oldLocation) => { + let result + try { + result = onNewLocation(newLocation, oldLocation) + } catch (err) { + console.error('onNewLocation threw an error:', err) + } + return result + } +} diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts index effa07d4f2..e1d191ea10 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts @@ -38,6 +38,7 @@ describe('viewCollection', () => { userActionCount: 10, }, id: 'xxx', + name: undefined, isActive: false, loadingTime: 20, loadingType: ViewLoadingType.INITIAL_LOAD, @@ -83,6 +84,7 @@ describe('viewCollection', () => { first_input_delay: 12 * 1e6, first_input_time: 10 * 1e6, is_active: false, + name: undefined, largest_contentful_paint: 10 * 1e6, load_event: 10 * 1e6, loading_time: 20 * 1e6, diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index 75ae1600f7..b278857fa3 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -1,14 +1,15 @@ import { getTimestamp, isEmptyObject, mapValues, msToNs } from '@datadog/browser-core' +import { NewLocationListener } from '../../../boot/rum' import { RawRumViewEvent, RumEventType } from '../../../rawRumEvent.types' import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' import { trackViews, View } from './trackViews' -export function startViewCollection(lifeCycle: LifeCycle, location: Location) { +export function startViewCollection(lifeCycle: LifeCycle, location: Location, onNewLocation?: NewLocationListener) { lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, (view) => lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processViewUpdate(view)) ) - return trackViews(location, lifeCycle) + return trackViews(location, lifeCycle, onNewLocation) } function processViewUpdate(view: View) { @@ -33,6 +34,7 @@ function processViewUpdate(view: View) { first_input_delay: msToNs(view.timings.firstInputDelay), first_input_time: msToNs(view.timings.firstInputTime), is_active: view.isActive, + name: view.name, largest_contentful_paint: msToNs(view.timings.largestContentfulPaint), load_event: msToNs(view.timings.loadEvent), loading_time: msToNs(view.loadingTime), diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index b88d7a5ac7..094ed3012f 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -71,6 +71,7 @@ export interface RawRumViewEvent { loading_time?: number time_spent: number is_active: boolean + name?: string error: Count action: Count long_task: Count @@ -151,6 +152,7 @@ export interface ViewContext extends Context { } view: { id: string + name?: string url: string referrer: string } diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 2a4f8089d1..1e64d5933a 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -113,6 +113,10 @@ export type RumErrorEvent = CommonProperties & { * Whether this error crashed the host application */ readonly is_crash?: boolean + /** + * The type of the error + */ + readonly type?: string /** * Resource properties of the error */ @@ -460,7 +464,7 @@ export type RumViewEvent = CommonProperties & { */ readonly load_event?: number /** - * User custom timings of the view + * User custom timings of the view. As timing name is used as facet path, it must contain only letters, digits, or the characters - _ . @ $ */ readonly custom_timings?: { [k: string]: number @@ -590,6 +594,10 @@ export interface CommonProperties { * URL of the view */ url: string + /** + * User defined name of the view + */ + name?: string [k: string]: unknown } /**