diff --git a/CHANGELOG.md b/CHANGELOG.md index 779f68195a..7d5f4c01a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,29 @@ --- +## v5.0.0-alpha.0 + +- 💥 [RUMF-1577] Stop collecting foreground periods ([#2311](https://github.com/DataDog/browser-sdk/pull/2311)) +- 💥 [RUMF-1473] Ignore untrusted event ([#2308](https://github.com/DataDog/browser-sdk/pull/2308)) +- 💥 [RUMF-1564] remove intake subdomains ([#2309](https://github.com/DataDog/browser-sdk/pull/2309)) +- 💥 [RUMF-1557] beforeSend domain context: use PerformanceEntry ([#2300](https://github.com/DataDog/browser-sdk/pull/2300)) +- 💥 [RUMF-1556] Typings: consistent beforeSend return type ([#2303](https://github.com/DataDog/browser-sdk/pull/2303)) +- 💥 [RUMF-1230] Only apply main logger configuration to its own logs ([#2298](https://github.com/DataDog/browser-sdk/pull/2298)) +- 💥 [RUMF-1229] Logs: remove `error.origin` attribute ([#2294](https://github.com/DataDog/browser-sdk/pull/2294)) +- 💥 [RUMF-1228] Remove console error message prefix ([#2289](https://github.com/DataDog/browser-sdk/pull/2289)) +- 💥 [RUMF-1555] Rework logger context APIs ([#2285](https://github.com/DataDog/browser-sdk/pull/2285)) +- 💥 [RUMF-1152] sanitize resource method names ([#2288](https://github.com/DataDog/browser-sdk/pull/2288)) +- 💥 [RUMF-1555] Remove `event` in action domain context ([#2286](https://github.com/DataDog/browser-sdk/pull/2286)) +- 💥 [RUMF-1589] automatically start recording ([#2275](https://github.com/DataDog/browser-sdk/pull/2275)) +- 💥 [RUMF-1588] Update default session replay behaviour ([#2257](https://github.com/DataDog/browser-sdk/pull/2257)) +- 💥 [RUMF-1587] Remove `premiumSampleRate` and `replaySampleRate` ([#2256](https://github.com/DataDog/browser-sdk/pull/2256)) +- 💥 [RUMF-1554] Drop some deprecated public APIs ([#2241](https://github.com/DataDog/browser-sdk/pull/2241)) +- 💥 [RUMF-1554] Drop some deprecated config parameters ([#2238](https://github.com/DataDog/browser-sdk/pull/2238)) +- 💥 [RUMF-1578] Promote track frustration as default action behaviour ([#2232](https://github.com/DataDog/browser-sdk/pull/2232)) +- 🐛 [RUMF-1499] Don't send duration for resources crossing a page frozen state ([#2271](https://github.com/DataDog/browser-sdk/pull/2271)) +- 🔥 [RUMF-1555] Remove `startTime` in xhr start context ([#2287](https://github.com/DataDog/browser-sdk/pull/2287)) +- ♻️ [RUMF-1555] Remove deprecated context manager APIs ([#2284](https://github.com/DataDog/browser-sdk/pull/2284)) + ## v4.45.0 - ✨ [RUM-235] add sample rates fields ([#2323](https://github.com/DataDog/browser-sdk/pull/2323)) diff --git a/developer-extension/package.json b/developer-extension/package.json index 7a55adc7e2..830ce572dd 100644 --- a/developer-extension/package.json +++ b/developer-extension/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-sdk-developer-extension", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "private": true, "scripts": { "build": "rm -rf dist && webpack --mode production", diff --git a/developer-extension/src/panel/hooks/useSdkInfos.ts b/developer-extension/src/panel/hooks/useSdkInfos.ts index e75295af91..28734bb325 100644 --- a/developer-extension/src/panel/hooks/useSdkInfos.ts +++ b/developer-extension/src/panel/hooks/useSdkInfos.ts @@ -63,13 +63,13 @@ async function getInfos(): Promise { version: window.DD_RUM?.version, config: window.DD_RUM?.getInitConfiguration?.(), internalContext: window.DD_RUM?.getInternalContext?.(), - globalContext: window.DD_RUM?.getRumGlobalContext?.(), + globalContext: window.DD_RUM?.getGlobalContext?.(), user: window.DD_RUM?.getUser?.(), } const logs = window.DD_LOGS && { version: window.DD_LOGS?.version, config: window.DD_LOGS?.getInitConfiguration?.(), - globalContext: window.DD_LOGS?.getLoggerGlobalContext?.(), + globalContext: window.DD_LOGS?.getGlobalContext?.(), user: window.DD_LOGS?.getUser?.(), } return { rum, logs, cookie } diff --git a/lerna.json b/lerna.json index d971796b35..fdb97d7595 100644 --- a/lerna.json +++ b/lerna.json @@ -1,4 +1,4 @@ { "npmClient": "yarn", - "version": "4.45.0" + "version": "5.0.0-alpha.0" } diff --git a/packages/core/package.json b/packages/core/package.json index a8f29fe110..66572912e7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-core", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/packages/core/src/browser/addEventListener.spec.ts b/packages/core/src/browser/addEventListener.spec.ts index a79ef082f1..bda902127d 100644 --- a/packages/core/src/browser/addEventListener.spec.ts +++ b/packages/core/src/browser/addEventListener.spec.ts @@ -1,4 +1,4 @@ -import { stubZoneJs } from '../../test' +import { createNewEvent, stubZoneJs } from '../../test' import { noop } from '../tools/utils/functionUtils' import { addEventListener, DOM_EVENT } from './addEventListener' @@ -33,4 +33,37 @@ describe('addEventListener', () => { expect(zoneJsPatchedRemoveEventListener).not.toHaveBeenCalled() }) }) + + describe('Untrusted event', () => { + it('should be ignored if __ddIsTrusted is absent', () => { + const listener = jasmine.createSpy() + const eventTarget = document.createElement('div') + addEventListener(eventTarget, DOM_EVENT.CLICK, listener) + + const event = createNewEvent(DOM_EVENT.CLICK, { __ddIsTrusted: undefined }) + eventTarget.dispatchEvent(event) + expect(listener).not.toHaveBeenCalled() + }) + + it('should be ignored if __ddIsTrusted is false', () => { + const listener = jasmine.createSpy() + const eventTarget = document.createElement('div') + addEventListener(eventTarget, DOM_EVENT.CLICK, listener) + + const event = createNewEvent(DOM_EVENT.CLICK, { __ddIsTrusted: false }) + eventTarget.dispatchEvent(event) + expect(listener).not.toHaveBeenCalled() + }) + + it('should not be ignored if __ddIsTrusted is true', () => { + const listener = jasmine.createSpy() + const eventTarget = document.createElement('div') + addEventListener(eventTarget, DOM_EVENT.CLICK, listener) + + const event = createNewEvent(DOM_EVENT.CLICK, { __ddIsTrusted: true }) + eventTarget.dispatchEvent(event) + + expect(listener).toHaveBeenCalled() + }) + }) }) diff --git a/packages/core/src/browser/addEventListener.ts b/packages/core/src/browser/addEventListener.ts index c0bba5f2f3..e393e0b945 100644 --- a/packages/core/src/browser/addEventListener.ts +++ b/packages/core/src/browser/addEventListener.ts @@ -2,7 +2,11 @@ import { monitor } from '../tools/monitor' import { getZoneJsOriginalValue } from '../tools/getZoneJsOriginalValue' import type { VisualViewport, VisualViewportEventMap } from './types' -export const enum DOM_EVENT { +export type TrustableEvent = E & { __ddIsTrusted?: boolean } + +// We want to use a real enum (i.e. not a const enum) here, to be able to iterate over it to automatically add _ddIsTrusted in e2e tests +// eslint-disable-next-line no-restricted-syntax +export enum DOM_EVENT { BEFORE_UNLOAD = 'beforeunload', CLICK = 'click', DBL_CLICK = 'dblclick', @@ -108,14 +112,15 @@ export function addEventListeners[EventName]) => void, { once, capture, passive }: AddEventListenerOptions = {} ) { - const wrappedListener = monitor( - once - ? (event: Event) => { - stop() - listener(event as EventMapFor[EventName]) - } - : (listener as (event: Event) => void) - ) + const wrappedListener = monitor((event: TrustableEvent) => { + if (!event.isTrusted && !event.__ddIsTrusted) { + return + } + if (once) { + stop() + } + listener(event as EventMapFor[EventName]) + }) const options = passive ? { capture, passive } : capture diff --git a/packages/core/src/browser/fetchObservable.spec.ts b/packages/core/src/browser/fetchObservable.spec.ts index 9629b6d077..b233552725 100644 --- a/packages/core/src/browser/fetchObservable.spec.ts +++ b/packages/core/src/browser/fetchObservable.spec.ts @@ -113,6 +113,7 @@ describe('fetch proxy', () => { fetchStub(new Request(FAKE_URL, { method: 'PUT' }), { method: 'POST' }).resolveWith({ status: 500 }) fetchStub(new Request(FAKE_URL), { method: 'POST' }).resolveWith({ status: 500 }) fetchStub(FAKE_URL, { method: 'POST' }).resolveWith({ status: 500 }) + fetchStub(FAKE_URL, { method: 'post' }).resolveWith({ status: 500 }) fetchStub(null as any).resolveWith({ status: 500 }) fetchStub({ method: 'POST' } as any).resolveWith({ status: 500 }) @@ -123,8 +124,9 @@ describe('fetch proxy', () => { expect(requests[3].method).toEqual('POST') expect(requests[4].method).toEqual('POST') expect(requests[5].method).toEqual('POST') - expect(requests[6].method).toEqual('GET') + expect(requests[6].method).toEqual('POST') expect(requests[7].method).toEqual('GET') + expect(requests[8].method).toEqual('GET') done() }) }) diff --git a/packages/core/src/browser/fetchObservable.ts b/packages/core/src/browser/fetchObservable.ts index 02716d8b3c..7ede80e839 100644 --- a/packages/core/src/browser/fetchObservable.ts +++ b/packages/core/src/browser/fetchObservable.ts @@ -69,7 +69,8 @@ function createFetchObservable() { } function beforeSend(observable: Observable, input: unknown, init?: RequestInit) { - const method = (init && init.method) || (input instanceof Request && input.method) || 'GET' + const methodFromParams = (init && init.method) || (input instanceof Request && input.method) + const method = methodFromParams ? methodFromParams.toUpperCase() : 'GET' const url = input instanceof Request ? input.url : normalizeUrl(String(input)) const startClocks = clocksNow() diff --git a/packages/core/src/browser/xhrObservable.spec.ts b/packages/core/src/browser/xhrObservable.spec.ts index e0270e16ea..9aaf5fd74d 100644 --- a/packages/core/src/browser/xhrObservable.spec.ts +++ b/packages/core/src/browser/xhrObservable.spec.ts @@ -47,13 +47,27 @@ describe('xhr observable', () => { expect(request.url).toContain('/ok') expect(request.status).toBe(200) expect(request.isAborted).toBe(false) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) done() }, }) }) + it('should sanitize request method', (done) => { + withXhr({ + setup(xhr) { + xhr.open('get', '/ok') + xhr.send() + xhr.complete(200, 'ok') + }, + onComplete() { + const request = requests[0] + expect(request.method).toBe('GET') + done() + }, + }) + }) + it('should track client error', (done) => { withXhr({ setup(xhr) { @@ -67,7 +81,6 @@ describe('xhr observable', () => { expect(request.url).toContain('/expected-404') expect(request.status).toBe(404) expect(request.isAborted).toBe(false) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) done() }, @@ -87,7 +100,6 @@ describe('xhr observable', () => { expect(request.url).toContain('/throw') expect(request.status).toBe(500) expect(request.isAborted).toBe(false) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) done() }, @@ -107,7 +119,6 @@ describe('xhr observable', () => { expect(request.url).toBe('http://foo.bar/qux') expect(request.status).toBe(0) expect(request.isAborted).toBe(false) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) done() }, @@ -133,7 +144,6 @@ describe('xhr observable', () => { expect(request.method).toBe('GET') expect(request.url).toContain('/ok') expect(request.status).toBe(200) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) expect(request.isAborted).toBe(false) expect(xhr.status).toBe(0) @@ -155,7 +165,6 @@ describe('xhr observable', () => { expect(request.method).toBe('GET') expect(request.url).toContain('/ok') expect(request.status).toBe(0) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) expect(request.isAborted).toBe(true) expect(xhr.status).toBe(0) @@ -178,7 +187,6 @@ describe('xhr observable', () => { expect(request.url).toContain('/ok') expect(request.status).toBe(200) expect(request.isAborted).toBe(false) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) expect(xhr.onreadystatechange).toHaveBeenCalled() done() @@ -200,7 +208,6 @@ describe('xhr observable', () => { expect(request.url).toContain('/ok') expect(request.status).toBe(200) expect(request.isAborted).toBe(false) - expect(request.startTime).toEqual(jasmine.any(Number)) expect(request.duration).toEqual(jasmine.any(Number)) expect(xhr.onreadystatechange).toHaveBeenCalled() done() @@ -247,7 +254,7 @@ describe('xhr observable', () => { }) it('should track multiple requests with the same xhr instance', (done) => { - let listeners: { [k: string]: Array<() => void> } + let listeners: { [k: string]: Array<(event: Event) => void> } withXhr({ setup(xhr) { const secondOnload = () => { @@ -273,7 +280,6 @@ describe('xhr observable', () => { expect(firstRequest.url).toContain('/ok?request=1') expect(firstRequest.status).toBe(200) expect(firstRequest.isAborted).toBe(false) - expect(firstRequest.startTime).toEqual(jasmine.any(Number)) expect(firstRequest.duration).toEqual(jasmine.any(Number)) const secondRequest = requests[1] @@ -281,7 +287,6 @@ describe('xhr observable', () => { expect(secondRequest.url).toContain('/ok?request=2') expect(secondRequest.status).toBe(400) expect(secondRequest.isAborted).toBe(false) - expect(secondRequest.startTime).toEqual(jasmine.any(Number)) expect(secondRequest.duration).toEqual(jasmine.any(Number)) expect(xhr.onreadystatechange).toHaveBeenCalledTimes(2) diff --git a/packages/core/src/browser/xhrObservable.ts b/packages/core/src/browser/xhrObservable.ts index b0b7f73026..ee6be47548 100644 --- a/packages/core/src/browser/xhrObservable.ts +++ b/packages/core/src/browser/xhrObservable.ts @@ -1,7 +1,7 @@ import { instrumentMethodAndCallOriginal } from '../tools/instrumentMethod' import { Observable } from '../tools/observable' -import type { Duration, RelativeTime, ClocksState } from '../tools/utils/timeUtils' -import { elapsed, relativeNow, clocksNow, timeStampNow } from '../tools/utils/timeUtils' +import type { Duration, ClocksState } from '../tools/utils/timeUtils' +import { elapsed, clocksNow, timeStampNow } from '../tools/utils/timeUtils' import { normalizeUrl } from '../tools/utils/urlPolyfill' import { shallowClone } from '../tools/utils/objectUtils' import { addEventListener } from './addEventListener' @@ -14,7 +14,6 @@ export interface XhrOpenContext { export interface XhrStartContext extends Omit { state: 'start' - startTime: RelativeTime // deprecated startClocks: ClocksState isAborted: boolean xhr: XMLHttpRequest @@ -66,7 +65,7 @@ function createXhrObservable() { function openXhr(this: XMLHttpRequest, method: string, url: string | URL | undefined | null) { xhrContexts.set(this, { state: 'open', - method, + method: method.toUpperCase(), url: normalizeUrl(String(url)), }) } @@ -79,7 +78,6 @@ function sendXhr(this: XMLHttpRequest, observable: Observable) { const startContext = context as XhrStartContext startContext.state = 'start' - startContext.startTime = relativeNow() startContext.startClocks = clocksNow() startContext.isAborted = false startContext.xhr = this @@ -91,7 +89,7 @@ function sendXhr(this: XMLHttpRequest, observable: Observable) { if (this.readyState === XMLHttpRequest.DONE) { // Try to report the XHR as soon as possible, because the XHR may be mutated by the // application during a future event. For example, Angular is calling .abort() on - // completed requests during a onreadystatechange event, so the status becomes '0' + // completed requests during an onreadystatechange event, so the status becomes '0' // before the request is collected. onEnd() } diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index c8c71f9b3e..ce854f3604 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -65,23 +65,6 @@ describe('validateAndBuildConfiguration', () => { expect(displaySpy).not.toHaveBeenCalled() }) - it('requires deprecated sampleRate to be a percentage', () => { - expect( - validateAndBuildConfiguration({ clientToken, sampleRate: 'foo' } as unknown as InitConfiguration) - ).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Session Sample Rate should be a number between 0 and 100') - - displaySpy.calls.reset() - expect( - validateAndBuildConfiguration({ clientToken, sampleRate: 200 } as unknown as InitConfiguration) - ).toBeUndefined() - expect(displaySpy).toHaveBeenCalledOnceWith('Session Sample Rate should be a number between 0 and 100') - - displaySpy.calls.reset() - validateAndBuildConfiguration({ clientToken: 'yes', sampleRate: 1 }) - expect(displaySpy).not.toHaveBeenCalled() - }) - it('requires sessionSampleRate to be a percentage', () => { expect( validateAndBuildConfiguration({ clientToken, sessionSampleRate: 'foo' } as unknown as InitConfiguration) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 2be145377d..09e4a0db84 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -24,10 +24,6 @@ export interface InitConfiguration { // global options clientToken: string beforeSend?: GenericBeforeSendCallback | undefined - /** - * @deprecated use sessionSampleRate instead - */ - sampleRate?: number | undefined sessionSampleRate?: number | undefined telemetrySampleRate?: number | undefined silentMultipleInit?: boolean | undefined @@ -36,10 +32,6 @@ export interface InitConfiguration { // transport options proxy?: string | undefined - /** - * @deprecated use `proxy` instead - */ - proxyUrl?: string | undefined site?: string | undefined // tag and context options @@ -100,8 +92,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati return } - const sessionSampleRate = initConfiguration.sessionSampleRate ?? initConfiguration.sampleRate - if (sessionSampleRate !== undefined && !isPercentage(sessionSampleRate)) { + if (initConfiguration.sessionSampleRate !== undefined && !isPercentage(initConfiguration.sessionSampleRate)) { display.error('Session Sample Rate should be a number between 0 and 100') return } @@ -133,7 +124,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati beforeSend: initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), sessionStoreStrategyType: selectSessionStoreStrategyType(initConfiguration), - sessionSampleRate: sessionSampleRate ?? 100, + sessionSampleRate: initConfiguration.sessionSampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, service: initConfiguration.service, @@ -165,15 +156,14 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati } export function serializeConfiguration(initConfiguration: InitConfiguration): Partial { - const proxy = initConfiguration.proxy ?? initConfiguration.proxyUrl return { - session_sample_rate: initConfiguration.sessionSampleRate ?? initConfiguration.sampleRate, + session_sample_rate: initConfiguration.sessionSampleRate, telemetry_sample_rate: initConfiguration.telemetrySampleRate, telemetry_configuration_sample_rate: initConfiguration.telemetryConfigurationSampleRate, use_before_send: !!initConfiguration.beforeSend, use_cross_site_session_cookie: initConfiguration.useCrossSiteSessionCookie, use_secure_session_cookie: initConfiguration.useSecureSessionCookie, - use_proxy: proxy !== undefined ? !!proxy : undefined, + use_proxy: !!initConfiguration.proxy, silent_multiple_init: initConfiguration.silentMultipleInit, track_session_across_subdomains: initConfiguration.trackSessionAcrossSubdomains, track_resources: initConfiguration.trackResources, diff --git a/packages/core/src/domain/configuration/endpointBuilder.spec.ts b/packages/core/src/domain/configuration/endpointBuilder.spec.ts index 53ac1efbac..4a32fd3109 100644 --- a/packages/core/src/domain/configuration/endpointBuilder.spec.ts +++ b/packages/core/src/domain/configuration/endpointBuilder.spec.ts @@ -31,7 +31,7 @@ describe('endpointBuilder', () => { it('should not add batch_time for logs and replay endpoints', () => { expect(createEndpointBuilder(initConfiguration, 'logs', []).build('xhr')).not.toContain('&batch_time=') - expect(createEndpointBuilder(initConfiguration, 'sessionReplay', []).build('xhr')).not.toContain('&batch_time=') + expect(createEndpointBuilder(initConfiguration, 'replay', []).build('xhr')).not.toContain('&batch_time=') }) it('should not start with ddsource for internal analytics mode', () => { @@ -63,46 +63,6 @@ describe('endpointBuilder', () => { ) ).toBeTrue() }) - - it('uses `proxy` over `proxyUrl`', () => { - expect( - createEndpointBuilder( - { ...initConfiguration, proxy: 'https://proxy.io/path', proxyUrl: 'https://legacy-proxy.io/path' }, - 'rum', - [] - ).build('xhr') - ).toMatch(/^https:\/\/proxy.io\/path\?/) - - expect( - createEndpointBuilder( - { ...initConfiguration, proxy: false as any, proxyUrl: 'https://legacy-proxy.io/path' }, - 'rum', - [] - ).build('xhr') - ).toMatch(/^https:\/\/rum.browser-intake-datadoghq.com\//) - }) - }) - - describe('deprecated proxyUrl configuration', () => { - it('should replace the full intake endpoint by the proxyUrl and set it in the attribute ddforward', () => { - expect( - createEndpointBuilder({ ...initConfiguration, proxyUrl: 'https://proxy.io/path' }, 'rum', []).build('xhr') - ).toMatch( - `https://proxy.io/path\\?ddforward=${encodeURIComponent( - `https://rum.browser-intake-datadoghq.com/api/v2/rum?ddsource=(.*)&ddtags=(.*)&dd-api-key=${clientToken}` + - '&dd-evp-origin-version=(.*)&dd-evp-origin=browser&dd-request-id=(.*)&batch_time=(.*)' - )}` - ) - }) - - it('normalizes the proxy url', () => { - expect( - startsWith( - createEndpointBuilder({ ...initConfiguration, proxyUrl: '/path' }, 'rum', []).build('xhr'), - `${location.origin}/path?ddforward` - ) - ).toBeTrue() - }) }) describe('tags', () => { diff --git a/packages/core/src/domain/configuration/endpointBuilder.ts b/packages/core/src/domain/configuration/endpointBuilder.ts index f7676c77bf..df1bc8ff39 100644 --- a/packages/core/src/domain/configuration/endpointBuilder.ts +++ b/packages/core/src/domain/configuration/endpointBuilder.ts @@ -4,39 +4,27 @@ import { normalizeUrl } from '../../tools/utils/urlPolyfill' import { ExperimentalFeature, isExperimentalFeatureEnabled } from '../../tools/experimentalFeatures' import { generateUUID } from '../../tools/utils/stringUtils' import type { InitConfiguration } from './configuration' -import { INTAKE_SITE_AP1, INTAKE_SITE_US1 } from './intakeSites' +import { INTAKE_SITE_US1 } from './intakeSites' // replaced at build time declare const __BUILD_ENV__SDK_VERSION__: string -export const ENDPOINTS = { - logs: 'logs', - rum: 'rum', - sessionReplay: 'session-replay', -} as const - -const INTAKE_TRACKS = { - logs: 'logs', - rum: 'rum', - sessionReplay: 'replay', -} - -export type EndpointType = keyof typeof ENDPOINTS +export type TrackType = 'logs' | 'rum' | 'replay' export type EndpointBuilder = ReturnType export function createEndpointBuilder( initConfiguration: InitConfiguration, - endpointType: EndpointType, + trackType: TrackType, configurationTags: string[] ) { - const buildUrlWithParameters = createEndpointUrlWithParametersBuilder(initConfiguration, endpointType) + const buildUrlWithParameters = createEndpointUrlWithParametersBuilder(initConfiguration, trackType) return { build(api: 'xhr' | 'fetch' | 'beacon', flushReason?: FlushReason, retry?: RetryInfo) { const parameters = buildEndpointParameters( initConfiguration, - endpointType, + trackType, configurationTags, api, flushReason, @@ -45,7 +33,7 @@ export function createEndpointBuilder( return buildUrlWithParameters(parameters) }, urlPrefix: buildUrlWithParameters(''), - endpointType, + trackType, } } @@ -56,29 +44,19 @@ export function createEndpointBuilder( */ function createEndpointUrlWithParametersBuilder( initConfiguration: InitConfiguration, - endpointType: EndpointType + trackType: TrackType ): (parameters: string) => string { - const path = `/api/v2/${INTAKE_TRACKS[endpointType]}` - - const { proxy, proxyUrl } = initConfiguration + const path = `/api/v2/${trackType}` + const proxy = initConfiguration.proxy if (proxy) { const normalizedProxyUrl = normalizeUrl(proxy) return (parameters) => `${normalizedProxyUrl}?ddforward=${encodeURIComponent(`${path}?${parameters}`)}` } - - const host = buildEndpointHost(initConfiguration, endpointType) - - if (proxy === undefined && proxyUrl) { - // TODO: remove this in a future major. - const normalizedProxyUrl = normalizeUrl(proxyUrl) - return (parameters) => - `${normalizedProxyUrl}?ddforward=${encodeURIComponent(`https://${host}${path}?${parameters}`)}` - } - + const host = buildEndpointHost(initConfiguration) return (parameters) => `https://${host}${path}?${parameters}` } -function buildEndpointHost(initConfiguration: InitConfiguration, endpointType: EndpointType) { +function buildEndpointHost(initConfiguration: InitConfiguration) { const { site = INTAKE_SITE_US1, internalAnalyticsSubdomain } = initConfiguration if (internalAnalyticsSubdomain && site === INTAKE_SITE_US1) { @@ -87,8 +65,7 @@ function buildEndpointHost(initConfiguration: InitConfiguration, endpointType: E const domainParts = site.split('.') const extension = domainParts.pop() - const subdomain = site !== INTAKE_SITE_AP1 ? `${ENDPOINTS[endpointType]}.` : '' - return `${subdomain}browser-intake-${domainParts.join('-')}.${extension!}` + return `browser-intake-${domainParts.join('-')}.${extension!}` } /** @@ -97,7 +74,7 @@ function buildEndpointHost(initConfiguration: InitConfiguration, endpointType: E */ function buildEndpointParameters( { clientToken, internalAnalyticsSubdomain }: InitConfiguration, - endpointType: EndpointType, + trackType: TrackType, configurationTags: string[], api: 'xhr' | 'fetch' | 'beacon', flushReason: FlushReason | undefined, @@ -119,7 +96,7 @@ function buildEndpointParameters( `dd-request-id=${generateUUID()}`, ] - if (endpointType === 'rum') { + if (trackType === 'rum') { parameters.push(`batch_time=${timeStampNow()}`) } if (internalAnalyticsSubdomain) { diff --git a/packages/core/src/domain/configuration/index.ts b/packages/core/src/domain/configuration/index.ts index 3ebfe3d4cd..42f6c28387 100644 --- a/packages/core/src/domain/configuration/index.ts +++ b/packages/core/src/domain/configuration/index.ts @@ -5,5 +5,5 @@ export { validateAndBuildConfiguration, serializeConfiguration, } from './configuration' -export { createEndpointBuilder, EndpointBuilder, EndpointType } from './endpointBuilder' +export { createEndpointBuilder, EndpointBuilder, TrackType } from './endpointBuilder' export * from './intakeSites' diff --git a/packages/core/src/domain/configuration/intakeSites.ts b/packages/core/src/domain/configuration/intakeSites.ts index 96915a3331..71308ddb20 100644 --- a/packages/core/src/domain/configuration/intakeSites.ts +++ b/packages/core/src/domain/configuration/intakeSites.ts @@ -1,5 +1,4 @@ export const INTAKE_SITE_STAGING = 'datad0g.com' export const INTAKE_SITE_US1 = 'datadoghq.com' export const INTAKE_SITE_EU1 = 'datadoghq.eu' -export const INTAKE_SITE_AP1 = 'ap1.datadoghq.com' export const INTAKE_SITE_US1_FED = 'ddog-gov.com' diff --git a/packages/core/src/domain/configuration/transportConfiguration.spec.ts b/packages/core/src/domain/configuration/transportConfiguration.spec.ts index a33da4f3b6..3080ba721b 100644 --- a/packages/core/src/domain/configuration/transportConfiguration.spec.ts +++ b/packages/core/src/domain/configuration/transportConfiguration.spec.ts @@ -63,25 +63,19 @@ describe('transportConfiguration', () => { describe('isIntakeUrl', () => { ;[ - { expectSubdomain: true, site: 'datadoghq.eu', intakeDomain: 'browser-intake-datadoghq.eu' }, - { expectSubdomain: true, site: 'datadoghq.com', intakeDomain: 'browser-intake-datadoghq.com' }, - { expectSubdomain: true, site: 'us3.datadoghq.com', intakeDomain: 'browser-intake-us3-datadoghq.com' }, - { expectSubdomain: true, site: 'us5.datadoghq.com', intakeDomain: 'browser-intake-us5-datadoghq.com' }, - { expectSubdomain: true, site: 'ddog-gov.com', intakeDomain: 'browser-intake-ddog-gov.com' }, - { expectSubdomain: false, site: 'ap1.datadoghq.com', intakeDomain: 'browser-intake-ap1-datadoghq.com' }, - ].forEach(({ site, intakeDomain, expectSubdomain }) => { + { site: 'datadoghq.eu', intakeDomain: 'browser-intake-datadoghq.eu' }, + { site: 'datadoghq.com', intakeDomain: 'browser-intake-datadoghq.com' }, + { site: 'us3.datadoghq.com', intakeDomain: 'browser-intake-us3-datadoghq.com' }, + { site: 'us5.datadoghq.com', intakeDomain: 'browser-intake-us5-datadoghq.com' }, + { site: 'ddog-gov.com', intakeDomain: 'browser-intake-ddog-gov.com' }, + { site: 'ap1.datadoghq.com', intakeDomain: 'browser-intake-ap1-datadoghq.com' }, + ].forEach(({ site, intakeDomain }) => { it(`should detect intake request for ${site} site`, () => { const configuration = computeTransportConfiguration({ clientToken, site }) - expect(configuration.isIntakeUrl(`https://rum.${intakeDomain}/api/v2/rum?xxx`)).toBe(expectSubdomain) - expect(configuration.isIntakeUrl(`https://logs.${intakeDomain}/api/v2/logs?xxx`)).toBe(expectSubdomain) - expect(configuration.isIntakeUrl(`https://session-replay.${intakeDomain}/api/v2/replay?xxx`)).toBe( - expectSubdomain - ) - - expect(configuration.isIntakeUrl(`https://${intakeDomain}/api/v2/rum?xxx`)).toBe(!expectSubdomain) - expect(configuration.isIntakeUrl(`https://${intakeDomain}/api/v2/logs?xxx`)).toBe(!expectSubdomain) - expect(configuration.isIntakeUrl(`https://${intakeDomain}/api/v2/replay?xxx`)).toBe(!expectSubdomain) + expect(configuration.isIntakeUrl(`https://${intakeDomain}/api/v2/rum?xxx`)).toBe(true) + expect(configuration.isIntakeUrl(`https://${intakeDomain}/api/v2/logs?xxx`)).toBe(true) + expect(configuration.isIntakeUrl(`https://${intakeDomain}/api/v2/replay?xxx`)).toBe(true) }) }) @@ -97,44 +91,34 @@ describe('transportConfiguration', () => { const configuration = computeTransportConfiguration({ clientToken }) expect(configuration.isIntakeUrl('https://www.foo.com')).toBe(false) }) - ;[ - { - proxyConfigurationName: 'proxy' as const, - intakeUrl: '/api/v2/rum', - }, - { - proxyConfigurationName: 'proxyUrl' as const, - intakeUrl: 'https://rum.browser-intake-datadoghq.com/api/v2/rum', - }, - ].forEach(({ proxyConfigurationName, intakeUrl }) => { - describe(`${proxyConfigurationName} configuration`, () => { - it('should detect proxy intake request', () => { - let configuration = computeTransportConfiguration({ - clientToken, - [proxyConfigurationName]: 'https://www.proxy.com', - }) - expect( - configuration.isIntakeUrl(`https://www.proxy.com/?ddforward=${encodeURIComponent(`${intakeUrl}?foo=bar`)}`) - ).toBe(true) - configuration = computeTransportConfiguration({ - clientToken, - [proxyConfigurationName]: 'https://www.proxy.com/custom/path', - }) - expect( - configuration.isIntakeUrl( - `https://www.proxy.com/custom/path?ddforward=${encodeURIComponent(`${intakeUrl}?foo=bar`)}` - ) - ).toBe(true) + describe('proxy configuration', () => { + it('should detect proxy intake request', () => { + let configuration = computeTransportConfiguration({ + clientToken, + proxy: 'https://www.proxy.com', + }) + expect( + configuration.isIntakeUrl(`https://www.proxy.com/?ddforward=${encodeURIComponent('/api/v2/rum?foo=bar')}`) + ).toBe(true) + + configuration = computeTransportConfiguration({ + clientToken, + proxy: 'https://www.proxy.com/custom/path', }) + expect( + configuration.isIntakeUrl( + `https://www.proxy.com/custom/path?ddforward=${encodeURIComponent('/api/v2/rum?foo=bar')}` + ) + ).toBe(true) + }) - it('should not detect request done on the same host as the proxy', () => { - const configuration = computeTransportConfiguration({ - clientToken, - [proxyConfigurationName]: 'https://www.proxy.com', - }) - expect(configuration.isIntakeUrl('https://www.proxy.com/foo')).toBe(false) + it('should not detect request done on the same host as the proxy', () => { + const configuration = computeTransportConfiguration({ + clientToken, + proxy: 'https://www.proxy.com', }) + expect(configuration.isIntakeUrl('https://www.proxy.com/foo')).toBe(false) }) }) ;[ diff --git a/packages/core/src/domain/configuration/transportConfiguration.ts b/packages/core/src/domain/configuration/transportConfiguration.ts index 057fc356e8..929f0318f2 100644 --- a/packages/core/src/domain/configuration/transportConfiguration.ts +++ b/packages/core/src/domain/configuration/transportConfiguration.ts @@ -42,7 +42,7 @@ function computeEndpointBuilders(initConfiguration: InitConfiguration, tags: str return { logsEndpointBuilder: createEndpointBuilder(initConfiguration, 'logs', tags), rumEndpointBuilder: createEndpointBuilder(initConfiguration, 'rum', tags), - sessionReplayEndpointBuilder: createEndpointBuilder(initConfiguration, 'sessionReplay', tags), + sessionReplayEndpointBuilder: createEndpointBuilder(initConfiguration, 'replay', tags), } } diff --git a/packages/core/src/domain/console/consoleObservable.spec.ts b/packages/core/src/domain/console/consoleObservable.spec.ts index 8aeec39631..4064eeaab0 100644 --- a/packages/core/src/domain/console/consoleObservable.spec.ts +++ b/packages/core/src/domain/console/consoleObservable.spec.ts @@ -8,12 +8,12 @@ import { initConsoleObservable } from './consoleObservable' // prettier: avoid formatting issue // cf https://github.com/prettier/prettier/issues/12211 ;[ - { api: ConsoleApiName.log, prefix: '' }, - { api: ConsoleApiName.info, prefix: '' }, - { api: ConsoleApiName.warn, prefix: '' }, - { api: ConsoleApiName.debug, prefix: '' }, - { api: ConsoleApiName.error, prefix: 'console error: ' }, -].forEach(({ api, prefix }) => { + { api: ConsoleApiName.log }, + { api: ConsoleApiName.info }, + { api: ConsoleApiName.warn }, + { api: ConsoleApiName.debug }, + { api: ConsoleApiName.error }, +].forEach(({ api }) => { describe(`console ${api} observable`, () => { let consoleStub: jasmine.Spy let consoleSubscription: Subscription @@ -37,7 +37,7 @@ import { initConsoleObservable } from './consoleObservable' expect(consoleLog).toEqual( jasmine.objectContaining({ - message: `${prefix}foo bar`, + message: 'foo bar', api, }) ) @@ -52,13 +52,13 @@ import { initConsoleObservable } from './consoleObservable' it('should format error instance', () => { console[api](new TypeError('hello')) const consoleLog = notifyLog.calls.mostRecent().args[0] - expect(consoleLog.message).toBe(`${prefix}TypeError: hello`) + expect(consoleLog.message).toBe('TypeError: hello') }) it('should stringify object parameters', () => { console[api]('Hello', { foo: 'bar' }) const consoleLog = notifyLog.calls.mostRecent().args[0] - expect(consoleLog.message).toBe(`${prefix}Hello {\n "foo": "bar"\n}`) + expect(consoleLog.message).toBe('Hello {\n "foo": "bar"\n}') }) it('should allow multiple callers', () => { diff --git a/packages/core/src/domain/console/consoleObservable.ts b/packages/core/src/domain/console/consoleObservable.ts index bbf8f7c027..21c7ecab86 100644 --- a/packages/core/src/domain/console/consoleObservable.ts +++ b/packages/core/src/domain/console/consoleObservable.ts @@ -54,8 +54,7 @@ function createConsoleObservable(api: ConsoleApiName) { } function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack: string): ConsoleLog { - // Todo: remove console error prefix in the next major version - let message = params.map((param) => formatConsoleParameters(param)).join(' ') + const message = params.map((param) => formatConsoleParameters(param)).join(' ') let stack let fingerprint @@ -63,7 +62,6 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack: const firstErrorParam = find(params, (param: unknown): param is Error => param instanceof Error) stack = firstErrorParam ? toStackTraceString(computeStackTrace(firstErrorParam)) : undefined fingerprint = tryToGetFingerprint(firstErrorParam) - message = `console error: ${message}` } return { diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 36e48bebb0..cee406eb41 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -160,7 +160,7 @@ describe('startSessionManager', () => { expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToNotBeDefined(sessionManager, FIRST_PRODUCT_KEY) - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(renewSessionSpy).toHaveBeenCalled() expectSessionIdToBeDefined(sessionManager) @@ -196,7 +196,7 @@ describe('startSessionManager', () => { startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // schedule an expandOrRenewSession - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) clock.tick(STORAGE_POLL_DELAY / 2) @@ -244,7 +244,7 @@ describe('startSessionManager', () => { expect(renewSessionASpy).not.toHaveBeenCalled() expect(renewSessionBSpy).not.toHaveBeenCalled() - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(renewSessionASpy).toHaveBeenCalled() expect(renewSessionBSpy).toHaveBeenCalled() @@ -317,7 +317,7 @@ describe('startSessionManager', () => { expectSessionIdToBeDefined(sessionManager) clock.tick(SESSION_EXPIRATION_DELAY - 10) - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) clock.tick(10) expectSessionIdToBeDefined(sessionManager) @@ -336,7 +336,7 @@ describe('startSessionManager', () => { expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) clock.tick(SESSION_EXPIRATION_DELAY - 10) - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) clock.tick(10) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -432,7 +432,7 @@ describe('startSessionManager', () => { sessionManager.expire() - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expectSessionIdToBeDefined(sessionManager) }) @@ -466,7 +466,7 @@ describe('startSessionManager', () => { clock.tick(10 * ONE_SECOND) // 20s to end: second session - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) clock.tick(10 * ONE_SECOND) const secondSessionId = sessionManager.findActiveSession()!.id const secondSessionTrackingType = sessionManager.findActiveSession()!.trackingType diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4338cb3c6f..f35f23d374 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,7 +5,6 @@ export { DefaultPrivacyLevel, EndpointBuilder, serializeConfiguration, - INTAKE_SITE_AP1, INTAKE_SITE_STAGING, INTAKE_SITE_US1, INTAKE_SITE_US1_FED, diff --git a/packages/core/src/tools/experimentalFeatures.ts b/packages/core/src/tools/experimentalFeatures.ts index 63959816e3..e6c90f7a68 100644 --- a/packages/core/src/tools/experimentalFeatures.ts +++ b/packages/core/src/tools/experimentalFeatures.ts @@ -14,9 +14,7 @@ export enum ExperimentalFeature { PAGEHIDE = 'pagehide', FEATURE_FLAGS = 'feature_flags', RESOURCE_PAGE_STATES = 'resource_page_states', - PAGE_STATES = 'page_states', COLLECT_FLUSH_REASON = 'collect_flush_reason', - NO_RESOURCE_DURATION_FROZEN_STATE = 'no_resource_duration_frozen_state', SCROLLMAP = 'scrollmap', } diff --git a/packages/core/src/tools/getGlobalObject.ts b/packages/core/src/tools/getGlobalObject.ts index 755754c8dc..2b049e7949 100644 --- a/packages/core/src/tools/getGlobalObject.ts +++ b/packages/core/src/tools/getGlobalObject.ts @@ -2,7 +2,7 @@ * inspired by https://mathiasbynens.be/notes/globalthis */ -export function getGlobalObject(): T { +export function getGlobalObject(): T { if (typeof globalThis === 'object') { return globalThis as unknown as T } diff --git a/packages/core/src/tools/serialisation/contextManager.spec.ts b/packages/core/src/tools/serialisation/contextManager.spec.ts index 2b104faa52..60c20ec34d 100644 --- a/packages/core/src/tools/serialisation/contextManager.spec.ts +++ b/packages/core/src/tools/serialisation/contextManager.spec.ts @@ -20,42 +20,35 @@ describe('createContextManager', () => { it('starts with an empty context', () => { const manager = createContextManager(CustomerDataType.User) - expect(manager.get()).toEqual({}) + expect(manager.getContext()).toEqual({}) }) it('updates the context', () => { const manager = createContextManager(CustomerDataType.User) - manager.set({ bar: 'foo' }) - - expect(manager.get()).toEqual({ bar: 'foo' }) - }) + manager.setContext({ bar: 'foo' }) - it('updates the context without copy', () => { - const manager = createContextManager(CustomerDataType.User) - const context = {} - manager.set(context) - expect(manager.get()).toBe(context) + expect(manager.getContext()).toEqual({ bar: 'foo' }) }) it('completely replaces the context', () => { const manager = createContextManager(CustomerDataType.User) - manager.set({ a: 'foo' }) - expect(manager.get()).toEqual({ a: 'foo' }) - manager.set({ b: 'foo' }) - expect(manager.get()).toEqual({ b: 'foo' }) + manager.setContext({ a: 'foo' }) + expect(manager.getContext()).toEqual({ a: 'foo' }) + manager.setContext({ b: 'foo' }) + expect(manager.getContext()).toEqual({ b: 'foo' }) }) it('sets a context value', () => { const manager = createContextManager(CustomerDataType.User) - manager.add('foo', 'bar') - expect(manager.get()).toEqual({ foo: 'bar' }) + manager.setContextProperty('foo', 'bar') + expect(manager.getContext()).toEqual({ foo: 'bar' }) }) it('removes a context value', () => { const manager = createContextManager(CustomerDataType.User) - manager.set({ a: 'foo', b: 'bar' }) - manager.remove('a') - expect(manager.get()).toEqual({ b: 'bar' }) + manager.setContext({ a: 'foo', b: 'bar' }) + manager.removeContextProperty('a') + expect(manager.getContext()).toEqual({ b: 'bar' }) manager.removeContextProperty('b') expect(manager.getContext()).toEqual({}) }) @@ -107,13 +100,13 @@ describe('createContextManager', () => { const computeBytesCountStub = jasmine.createSpy('computeBytesCountStub').and.returnValue(1) const manager = createContextManager(CustomerDataType.User, computeBytesCountStub) - manager.add('foo', 'bar') + manager.setContextProperty('foo', 'bar') clock.tick(BYTES_COMPUTATION_THROTTLING_DELAY) - manager.remove('foo') + manager.removeContextProperty('foo') clock.tick(BYTES_COMPUTATION_THROTTLING_DELAY) - manager.set({ foo: 'bar' }) + manager.setContext({ foo: 'bar' }) clock.tick(BYTES_COMPUTATION_THROTTLING_DELAY) manager.setContextProperty('foo', 'bar') diff --git a/packages/core/src/tools/serialisation/contextManager.ts b/packages/core/src/tools/serialisation/contextManager.ts index d9355db041..dee5782b07 100644 --- a/packages/core/src/tools/serialisation/contextManager.ts +++ b/packages/core/src/tools/serialisation/contextManager.ts @@ -6,7 +6,7 @@ import { jsonStringify } from './jsonStringify' import { sanitize } from './sanitize' import { warnIfCustomerDataLimitReached } from './heavyCustomerDataWarning' import type { CustomerDataType } from './heavyCustomerDataWarning' -import type { Context, ContextValue } from './context' +import type { Context } from './context' export const BYTES_COMPUTATION_THROTTLING_DELAY = 200 @@ -28,26 +28,6 @@ export function createContextManager(customerDataType: CustomerDataType, compute const contextManager = { getBytesCount: () => bytesCountCache, - /** @deprecated use getContext instead */ - get: () => context, - - /** @deprecated use setContextProperty instead */ - add: (key: string, value: any) => { - context[key] = value as ContextValue - computeBytesCountThrottled(context) - }, - - /** @deprecated renamed to removeContextProperty */ - remove: (key: string) => { - delete context[key] - computeBytesCountThrottled(context) - }, - - /** @deprecated use setContext instead */ - set: (newContext: object) => { - context = newContext as Context - computeBytesCountThrottled(context) - }, getContext: () => deepClone(context), diff --git a/packages/core/src/tools/serialisation/sanitize.spec.ts b/packages/core/src/tools/serialisation/sanitize.spec.ts index 53b85df228..22ba57ec51 100644 --- a/packages/core/src/tools/serialisation/sanitize.spec.ts +++ b/packages/core/src/tools/serialisation/sanitize.spec.ts @@ -1,5 +1,6 @@ import { isIE } from '../utils/browserDetection' import { display } from '../display' +import { createNewEvent } from '../../../test' import { sanitize } from './sanitize' describe('sanitize', () => { @@ -72,13 +73,7 @@ describe('sanitize', () => { }) it('should serialize events', () => { - let event: CustomEvent - if (isIE()) { - event = document.createEvent('CustomEvent') - event.initCustomEvent('MyEvent', false, false, {}) - } else { - event = new CustomEvent('MyEvent') - } + const event = createNewEvent('click') expect(sanitize(event)).toEqual({ isTrusted: false, diff --git a/packages/core/src/tools/timer.ts b/packages/core/src/tools/timer.ts index fec65efff7..73183f3dce 100644 --- a/packages/core/src/tools/timer.ts +++ b/packages/core/src/tools/timer.ts @@ -2,20 +2,20 @@ import { getZoneJsOriginalValue } from './getZoneJsOriginalValue' import { monitor } from './monitor' import { getGlobalObject } from './getGlobalObject' -export type TimeoutId = ReturnType +export type TimeoutId = ReturnType export function setTimeout(callback: () => void, delay?: number): TimeoutId { - return getZoneJsOriginalValue(getGlobalObject(), 'setTimeout')(monitor(callback), delay) + return getZoneJsOriginalValue(getGlobalObject(), 'setTimeout')(monitor(callback), delay) } export function clearTimeout(timeoutId: TimeoutId | undefined) { - getZoneJsOriginalValue(getGlobalObject(), 'clearTimeout')(timeoutId) + getZoneJsOriginalValue(getGlobalObject(), 'clearTimeout')(timeoutId) } export function setInterval(callback: () => void, delay?: number): TimeoutId { - return getZoneJsOriginalValue(window, 'setInterval')(monitor(callback), delay) + return getZoneJsOriginalValue(getGlobalObject(), 'setInterval')(monitor(callback), delay) } export function clearInterval(timeoutId: TimeoutId | undefined) { - getZoneJsOriginalValue(window, 'clearInterval')(timeoutId) + getZoneJsOriginalValue(getGlobalObject(), 'clearInterval')(timeoutId) } diff --git a/packages/core/src/transport/httpRequest.ts b/packages/core/src/transport/httpRequest.ts index f796f49121..f9693f80f5 100644 --- a/packages/core/src/transport/httpRequest.ts +++ b/packages/core/src/transport/httpRequest.ts @@ -46,7 +46,7 @@ export function createHttpRequest( return { send: (payload: Payload) => { - sendWithRetryStrategy(payload, retryState, sendStrategyForRetry, endpointBuilder.endpointType, reportError) + sendWithRetryStrategy(payload, retryState, sendStrategyForRetry, endpointBuilder.trackType, reportError) }, /** * Since fetch keepalive behaves like regular fetch on Firefox, diff --git a/packages/core/src/transport/sendWithRetryStrategy.ts b/packages/core/src/transport/sendWithRetryStrategy.ts index c294b0421e..2560fe2f3a 100644 --- a/packages/core/src/transport/sendWithRetryStrategy.ts +++ b/packages/core/src/transport/sendWithRetryStrategy.ts @@ -1,4 +1,4 @@ -import type { EndpointType } from '../domain/configuration' +import type { TrackType } from '../domain/configuration' import { setTimeout } from '../tools/timer' import { clocksNow, ONE_MINUTE, ONE_SECOND } from '../tools/utils/timeUtils' import { ONE_MEBI_BYTE, ONE_KIBI_BYTE } from '../tools/utils/byteUtils' @@ -38,7 +38,7 @@ export function sendWithRetryStrategy( payload: Payload, state: RetryState, sendStrategy: SendStrategy, - endpointType: EndpointType, + trackType: TrackType, reportError: (error: RawError) => void ) { if ( @@ -47,10 +47,10 @@ export function sendWithRetryStrategy( state.bandwidthMonitor.canHandle(payload) ) { send(payload, state, sendStrategy, { - onSuccess: () => retryQueuedPayloads(RetryReason.AFTER_SUCCESS, state, sendStrategy, endpointType, reportError), + onSuccess: () => retryQueuedPayloads(RetryReason.AFTER_SUCCESS, state, sendStrategy, trackType, reportError), onFailure: () => { state.queuedPayloads.enqueue(payload) - scheduleRetry(state, sendStrategy, endpointType, reportError) + scheduleRetry(state, sendStrategy, trackType, reportError) }, }) } else { @@ -61,7 +61,7 @@ export function sendWithRetryStrategy( function scheduleRetry( state: RetryState, sendStrategy: SendStrategy, - endpointType: EndpointType, + trackType: TrackType, reportError: (error: RawError) => void ) { if (state.transportStatus !== TransportStatus.DOWN) { @@ -73,11 +73,11 @@ function scheduleRetry( onSuccess: () => { state.queuedPayloads.dequeue() state.currentBackoffTime = INITIAL_BACKOFF_TIME - retryQueuedPayloads(RetryReason.AFTER_RESUME, state, sendStrategy, endpointType, reportError) + retryQueuedPayloads(RetryReason.AFTER_RESUME, state, sendStrategy, trackType, reportError) }, onFailure: () => { state.currentBackoffTime = Math.min(MAX_BACKOFF_TIME, state.currentBackoffTime * 2) - scheduleRetry(state, sendStrategy, endpointType, reportError) + scheduleRetry(state, sendStrategy, trackType, reportError) }, }) }, state.currentBackoffTime) @@ -112,12 +112,12 @@ function retryQueuedPayloads( reason: RetryReason, state: RetryState, sendStrategy: SendStrategy, - endpointType: EndpointType, + trackType: TrackType, reportError: (error: RawError) => void ) { if (reason === RetryReason.AFTER_SUCCESS && state.queuedPayloads.isFull() && !state.queueFullReported) { reportError({ - message: `Reached max ${endpointType} events size queued for upload: ${MAX_QUEUE_BYTES_COUNT / ONE_MEBI_BYTE}MiB`, + message: `Reached max ${trackType} events size queued for upload: ${MAX_QUEUE_BYTES_COUNT / ONE_MEBI_BYTE}MiB`, source: ErrorSource.AGENT, startClocks: clocksNow(), }) @@ -126,7 +126,7 @@ function retryQueuedPayloads( const previousQueue = state.queuedPayloads state.queuedPayloads = newPayloadQueue() while (previousQueue.size() > 0) { - sendWithRetryStrategy(previousQueue.dequeue()!, state, sendStrategy, endpointType, reportError) + sendWithRetryStrategy(previousQueue.dequeue()!, state, sendStrategy, trackType, reportError) } } diff --git a/packages/core/test/emulate/createNewEvent.ts b/packages/core/test/emulate/createNewEvent.ts index e5a9089000..4d9d298802 100644 --- a/packages/core/test/emulate/createNewEvent.ts +++ b/packages/core/test/emulate/createNewEvent.ts @@ -1,19 +1,26 @@ +import type { TrustableEvent } from '../../src' import { objectEntries } from '../../src' -export function createNewEvent

>(eventName: 'click', properties?: P): MouseEvent & P -export function createNewEvent

>( +export function createNewEvent(eventName: 'click', properties?: Partial): MouseEvent +export function createNewEvent( eventName: 'pointerup', - properties?: P -): PointerEvent & P + properties?: Partial +): PointerEvent & { target: Element } +export function createNewEvent(eventName: 'message', properties?: Partial): MessageEvent +export function createNewEvent( + eventName: 'securitypolicyviolation', + properties?: Partial +): SecurityPolicyViolationEvent export function createNewEvent(eventName: string, properties?: { [name: string]: unknown }): Event export function createNewEvent(eventName: string, properties: { [name: string]: unknown } = {}) { - let event: Event + let event: TrustableEvent if (typeof Event === 'function') { event = new Event(eventName) } else { event = document.createEvent('Event') event.initEvent(eventName, true, true) } + event.__ddIsTrusted = true objectEntries(properties).forEach(([name, value]) => { // Setting values directly or with a `value` descriptor seems unsupported in IE11 Object.defineProperty(event, name, { diff --git a/packages/core/test/emulate/stubReportApis.ts b/packages/core/test/emulate/stubReportApis.ts index 72be8c8260..61249b43dc 100644 --- a/packages/core/test/emulate/stubReportApis.ts +++ b/packages/core/test/emulate/stubReportApis.ts @@ -1,5 +1,6 @@ import type { InterventionReport, ReportType } from '../../src/domain/report/browser.types' import { noop } from '../../src/tools/utils/functionUtils' +import { createNewEvent } from './createNewEvent' export function stubReportingObserver() { const originalReportingObserver = window.ReportingObserver @@ -56,7 +57,7 @@ export function stubCspEventListener() { } } -export const FAKE_CSP_VIOLATION_EVENT = { +export const FAKE_CSP_VIOLATION_EVENT = createNewEvent('securitypolicyviolation', { blockedURI: 'blob', columnNumber: 8, documentURI: 'blob', @@ -67,7 +68,7 @@ export const FAKE_CSP_VIOLATION_EVENT = { sourceFile: 'http://foo.bar/index.js', statusCode: 200, violatedDirective: 'worker-src', -} as SecurityPolicyViolationEvent +}) export const FAKE_REPORT: InterventionReport = { type: 'intervention', diff --git a/packages/core/test/requests.ts b/packages/core/test/requests.ts index 33c5a60c0e..e72ab147c1 100644 --- a/packages/core/test/requests.ts +++ b/packages/core/test/requests.ts @@ -1,5 +1,6 @@ import type { EndpointBuilder } from '../src' import { noop, isServerError } from '../src' +import { createNewEvent } from './emulate/createNewEvent' export const SPEC_ENDPOINTS = { logsEndpointBuilder: stubEndpointBuilder('https://logs-intake.com/v1/input/abcde?foo=bar'), @@ -249,7 +250,7 @@ export interface FetchStubPromise extends Promise { } class StubEventEmitter { - public listeners: { [k: string]: Array<() => void> } = {} + public listeners: { [k: string]: Array<(event: Event) => void> } = {} addEventListener(name: string, callback: () => void) { if (!this.listeners[name]) { @@ -271,7 +272,7 @@ class StubEventEmitter { if (!this.listeners[name]) { return } - this.listeners[name].forEach((listener) => listener.call(this)) + this.listeners[name].forEach((listener) => listener.apply(this, [createNewEvent(name)])) } } diff --git a/packages/logs/package.json b/packages/logs/package.json index 0623822ae5..9b8ae68a75 100644 --- a/packages/logs/package.json +++ b/packages/logs/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-logs", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "license": "Apache-2.0", "main": "cjs/entries/main.js", "module": "esm/entries/main.js", @@ -13,10 +13,10 @@ "replace-build-env": "node ../../scripts/build/replace-build-env.js" }, "dependencies": { - "@datadog/browser-core": "4.45.0" + "@datadog/browser-core": "5.0.0-alpha.0" }, "peerDependencies": { - "@datadog/browser-rum": "4.45.0" + "@datadog/browser-rum": "5.0.0-alpha.0" }, "peerDependenciesMeta": { "@datadog/browser-rum": { diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index b383e68f98..0c34049d3a 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -213,9 +213,9 @@ describe('logs entry', () => { }) it('stores a deep copy of the global context', () => { - LOGS.addLoggerGlobalContext('foo', 'bar') + LOGS.setGlobalContextProperty('foo', 'bar') LOGS.logger.log('message') - LOGS.addLoggerGlobalContext('foo', 'baz') + LOGS.setGlobalContextProperty('foo', 'baz') LOGS.init(DEFAULT_INIT_CONFIGURATION) @@ -322,7 +322,7 @@ describe('logs entry', () => { LOGS = makeLogsPublicApi(startLogs) }) - it('should return undefined if not initalized', () => { + it('should return undefined if not initialized', () => { expect(LOGS.getInternalContext()).toBeUndefined() }) diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index 67144dd5d1..533377e473 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -90,8 +90,7 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs) { ;({ handleLog: handleLogStrategy, getInternalContext: getInternalContextStrategy } = startLogsImpl( initConfiguration, configuration, - buildCommonContext, - mainLogger + buildCommonContext )) beforeInitLoggerLog.drain() @@ -99,20 +98,12 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs) { isAlreadyInitialized = true }), - /** @deprecated: use getGlobalContext instead */ - getLoggerGlobalContext: monitor(globalContextManager.get), getGlobalContext: monitor(globalContextManager.getContext), - /** @deprecated: use setGlobalContext instead */ - setLoggerGlobalContext: monitor(globalContextManager.set), setGlobalContext: monitor(globalContextManager.setContext), - /** @deprecated: use setGlobalContextProperty instead */ - addLoggerGlobalContext: monitor(globalContextManager.add), setGlobalContextProperty: monitor(globalContextManager.setContextProperty), - /** @deprecated: use removeGlobalContextProperty instead */ - removeLoggerGlobalContext: monitor(globalContextManager.remove), removeGlobalContextProperty: monitor(globalContextManager.removeContextProperty), clearGlobalContext: monitor(globalContextManager.clearContext), diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index cca957bc5c..5cc2eafaf7 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -69,7 +69,7 @@ describe('logs', () => { describe('request', () => { it('should send the needed data', () => { - ;({ handleLog: handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog: handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT)) handleLog({ message: 'message', status: StatusType.warn, context: { foo: 'bar' } }, logger, COMMON_CONTEXT) @@ -94,8 +94,7 @@ describe('logs', () => { ;({ handleLog } = startLogs( initConfiguration, { ...baseConfiguration, batchMessagesLimit: 3 }, - () => COMMON_CONTEXT, - logger + () => COMMON_CONTEXT )) handleLog(DEFAULT_MESSAGE, logger) @@ -107,7 +106,7 @@ describe('logs', () => { it('should send bridge event when bridge is present', () => { const sendSpy = spyOn(initEventBridgeStub(), 'send') - ;({ handleLog: handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog: handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT)) handleLog(DEFAULT_MESSAGE, logger) @@ -126,13 +125,13 @@ describe('logs', () => { const sendSpy = spyOn(initEventBridgeStub(), 'send') let configuration = { ...baseConfiguration, sessionSampleRate: 0 } - ;({ handleLog } = startLogs(initConfiguration, configuration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, configuration, () => COMMON_CONTEXT)) handleLog(DEFAULT_MESSAGE, logger) expect(sendSpy).not.toHaveBeenCalled() configuration = { ...baseConfiguration, sessionSampleRate: 100 } - ;({ handleLog } = startLogs(initConfiguration, configuration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, configuration, () => COMMON_CONTEXT)) handleLog(DEFAULT_MESSAGE, logger) expect(sendSpy).toHaveBeenCalled() @@ -144,8 +143,7 @@ describe('logs', () => { ;({ handleLog } = startLogs( initConfiguration, { ...baseConfiguration, forwardConsoleLogs: ['log'] }, - () => COMMON_CONTEXT, - logger + () => COMMON_CONTEXT )) /* eslint-disable-next-line no-console */ @@ -161,21 +159,21 @@ describe('logs', () => { }) it('creates a session on normal conditions', () => { - ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT)) expect(getCookie(SESSION_STORE_KEY)).not.toBeUndefined() }) it('does not create a session if event bridge is present', () => { initEventBridgeStub() - ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT)) expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) it('does not create a session if synthetics worker will inject RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) - ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT)) expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 09b7983ec0..9946cff850 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -26,15 +26,13 @@ import { startLoggerCollection } from '../domain/logsCollection/logger/loggerCol import type { CommonContext } from '../rawLogsEvent.types' import { startLogsBatch } from '../transport/startLogsBatch' import { startLogsBridge } from '../transport/startLogsBridge' -import type { Logger } from '../domain/logger' import { StatusType } from '../domain/logger' import { startInternalContext } from '../domain/internalContext' export function startLogs( initConfiguration: LogsInitConfiguration, configuration: LogsConfiguration, - buildCommonContext: () => CommonContext, - mainLogger: Logger + buildCommonContext: () => CommonContext ) { const lifeCycle = new LifeCycle() @@ -45,9 +43,6 @@ export function startLogs( rawLogsEvent: { message: error.message, date: error.startClocks.timeStamp, - error: { - origin: ErrorSource.AGENT, // Todo: Remove in the next major release - }, origin: ErrorSource.AGENT, status: StatusType.error, }, @@ -83,7 +78,7 @@ export function startLogs( startReportCollection(configuration, lifeCycle) const { handleLog } = startLoggerCollection(lifeCycle) - startLogsAssembly(session, configuration, lifeCycle, buildCommonContext, mainLogger, reportError) + startLogsAssembly(session, configuration, lifeCycle, buildCommonContext, reportError) if (!canUseEventBridge()) { startLogsBatch(configuration, lifeCycle, reportError, pageExitObservable, session.expireObservable) diff --git a/packages/logs/src/domain/assembly.spec.ts b/packages/logs/src/domain/assembly.spec.ts index baef7c4c37..d0c2108f91 100644 --- a/packages/logs/src/domain/assembly.spec.ts +++ b/packages/logs/src/domain/assembly.spec.ts @@ -62,7 +62,7 @@ describe('startLogsAssembly', () => { } beforeSend = noop mainLogger = new Logger(() => noop) - startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT, mainLogger, noop) + startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT, noop) window.DD_RUM = { getInternalContext: noop, } @@ -73,6 +73,22 @@ describe('startLogsAssembly', () => { serverLogs = [] }) + it('should send if beforeSend returned true', () => { + beforeSend = () => true + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(1) + }) + + it('should send if beforeSend returned undefined', () => { + beforeSend = () => undefined + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(1) + }) + it('should not send if beforeSend returned false', () => { beforeSend = () => false lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { @@ -153,22 +169,11 @@ describe('startLogsAssembly', () => { expect(serverLogs[0].common_context_key).toBeUndefined() }) - it('should include main logger context', () => { + it('should not include main logger context', () => { mainLogger.setContext({ foo: 'from-main-logger' }) lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) - expect(serverLogs[0].foo).toEqual('from-main-logger') - }) - - it('should include logger context instead of main logger context when present', () => { - const logger = new Logger(() => noop) - mainLogger.setContext({ foo: 'from-main-logger', bar: 'from-main-logger' }) - logger.setContext({ foo: 'from-logger' }) - - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE, logger }) - - expect(serverLogs[0].foo).toEqual('from-logger') - expect(serverLogs[0].bar).toBeUndefined() + expect(serverLogs[0].foo).toBeUndefined() }) it('should include rum internal context related to the error time', () => { @@ -238,15 +243,7 @@ describe('startLogsAssembly', () => { expect(serverLogs[0].message).toEqual('message') }) - it('logger context should take precedence over raw log', () => { - mainLogger.setContext({ message: 'from-main-logger' }) - - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) - - expect(serverLogs[0].message).toEqual('from-main-logger') - }) - - it('message context should take precedence over logger context', () => { + it('message context should take precedence over raw log', () => { lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE, messageContext: { message: 'from-message-context' }, @@ -296,7 +293,6 @@ describe('user management', () => { let serverLogs: Array = [] const beforeSend: (event: LogsEvent) => void | boolean = noop - const mainLogger = new Logger(() => noop) const configuration = { ...validateAndBuildLogsConfiguration(initConfiguration)!, beforeSend: (x: LogsEvent) => beforeSend(x), @@ -314,14 +310,14 @@ describe('user management', () => { }) it('should not output usr key if user is not set', () => { - startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT, mainLogger, noop) + startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT, noop) lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) expect(serverLogs[0].usr).toBeUndefined() }) it('should include user data when user has been set', () => { - startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT_WITH_USER, mainLogger, noop) + startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT_WITH_USER, noop) lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) expect(serverLogs[0].usr).toEqual({ @@ -342,7 +338,7 @@ describe('user management', () => { }, }, } - startLogsAssembly(sessionManager, configuration, lifeCycle, () => globalContextWithUser, mainLogger, noop) + startLogsAssembly(sessionManager, configuration, lifeCycle, () => globalContextWithUser, noop) lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { rawLogsEvent: DEFAULT_MESSAGE }) expect(serverLogs[0].usr).toEqual({ @@ -363,7 +359,6 @@ describe('logs limitation', () => { let beforeSend: (event: LogsEvent) => void | boolean let lifeCycle: LifeCycle let serverLogs: Array = [] - let mainLogger: Logger let reportErrorSpy: jasmine.Spy beforeEach(() => { @@ -376,9 +371,8 @@ describe('logs limitation', () => { beforeSend: (x: LogsEvent) => beforeSend(x), } beforeSend = noop - mainLogger = new Logger(() => noop) reportErrorSpy = jasmine.createSpy('reportError') - startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT, mainLogger, reportErrorSpy) + startLogsAssembly(sessionManager, configuration, lifeCycle, () => COMMON_CONTEXT, reportErrorSpy) clock = mockClock() }) @@ -386,6 +380,19 @@ describe('logs limitation', () => { clock.cleanup() serverLogs = [] }) + it('should not apply to agent logs', () => { + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: { ...DEFAULT_MESSAGE, origin: ErrorSource.AGENT, status: 'error', message: 'foo' }, + }) + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: { ...DEFAULT_MESSAGE, origin: ErrorSource.AGENT, status: 'error', message: 'bar' }, + }) + + expect(serverLogs.length).toEqual(2) + expect(reportErrorSpy).not.toHaveBeenCalled() + expect(serverLogs[0].message).toBe('foo') + expect(serverLogs[1].message).toBe('bar') + }) ;[ { status: StatusType.error, messageContext: {}, message: 'Reached max number of errors by minute: 1' }, { status: StatusType.warn, messageContext: {}, message: 'Reached max number of warns by minute: 1' }, diff --git a/packages/logs/src/domain/assembly.ts b/packages/logs/src/domain/assembly.ts index 16f7ff21a1..6e6143fe6d 100644 --- a/packages/logs/src/domain/assembly.ts +++ b/packages/logs/src/domain/assembly.ts @@ -14,9 +14,7 @@ import type { CommonContext } from '../rawLogsEvent.types' import type { LogsConfiguration } from './configuration' import type { LifeCycle } from './lifeCycle' import { LifeCycleEventType } from './lifeCycle' -import type { Logger } from './logger' -import { STATUSES, HandlerType } from './logger' -import { isAuthorized } from './logsCollection/logger/loggerCollection' +import { STATUSES } from './logger' import type { LogsSessionManager } from './logsSessionManager' export function startLogsAssembly( @@ -24,7 +22,6 @@ export function startLogsAssembly( configuration: LogsConfiguration, lifeCycle: LifeCycle, buildCommonContext: () => CommonContext, - mainLogger: Logger, // Todo: [RUMF-1230] Remove this parameter in the next major release reportError: (error: RawError) => void ) { const statusWithCustom = (STATUSES as string[]).concat(['custom']) @@ -35,7 +32,7 @@ export function startLogsAssembly( lifeCycle.subscribe( LifeCycleEventType.RAW_LOG_COLLECTED, - ({ rawLogsEvent, messageContext = undefined, savedCommonContext = undefined, logger = mainLogger }) => { + ({ rawLogsEvent, messageContext = undefined, savedCommonContext = undefined }) => { const startTime = getRelativeTime(rawLogsEvent.date) const session = sessionManager.findTrackedSession(startTime) @@ -55,15 +52,12 @@ export function startLogsAssembly( commonContext.context, getRUMInternalContext(startTime), rawLogsEvent, - logger.getContext(), messageContext ) if ( - // Todo: [RUMF-1230] Move this check to the logger collection in the next major release - !isAuthorized(rawLogsEvent.status, HandlerType.http, logger) || configuration.beforeSend?.(log) === false || - (log.error?.origin !== ErrorSource.AGENT && + (log.origin !== ErrorSource.AGENT && (logRateLimiters[log.status] ?? logRateLimiters['custom']).isLimitReached()) ) { return diff --git a/packages/logs/src/domain/configuration.ts b/packages/logs/src/domain/configuration.ts index f554913101..f2c8959f71 100644 --- a/packages/logs/src/domain/configuration.ts +++ b/packages/logs/src/domain/configuration.ts @@ -14,7 +14,7 @@ import { import type { LogsEvent } from '../logsEvent.types' export interface LogsInitConfiguration extends InitConfiguration { - beforeSend?: ((event: LogsEvent) => void | boolean) | undefined + beforeSend?: ((event: LogsEvent) => boolean) | undefined forwardErrorsToLogs?: boolean | undefined forwardConsoleLogs?: ConsoleApiName[] | 'all' | undefined forwardReports?: RawReportType[] | 'all' | undefined diff --git a/packages/logs/src/domain/lifeCycle.ts b/packages/logs/src/domain/lifeCycle.ts index 312056cb32..116d89d796 100644 --- a/packages/logs/src/domain/lifeCycle.ts +++ b/packages/logs/src/domain/lifeCycle.ts @@ -2,7 +2,6 @@ import { AbstractLifeCycle } from '@datadog/browser-core' import type { Context } from '@datadog/browser-core' import type { LogsEvent } from '../logsEvent.types' import type { CommonContext, RawLogsEvent } from '../rawLogsEvent.types' -import type { Logger } from './logger' export const enum LifeCycleEventType { RAW_LOG_COLLECTED, @@ -21,5 +20,4 @@ export interface RawLogsEventCollectedData { expect(getLoggedMessage(0).context).toEqual({ error: { - origin: 'logger', kind: 'SyntaxError', message: 'My Error', stack: jasmine.stringMatching(/^SyntaxError: My Error/), @@ -69,7 +68,6 @@ describe('Logger', () => { message: 'message', context: { error: { - origin: 'logger', kind: undefined, message: 'Provided "My Error"', stack: NO_ERROR_STACK_PRESENT_MESSAGE, @@ -79,21 +77,48 @@ describe('Logger', () => { }) }) - it("'logger.error' should populate an error context with origin even if no Error object is provided", () => { + it("'logger.error' should have an empty context if no Error object is provided", () => { logger.error('message') expect(getLoggedMessage(0)).toEqual({ message: 'message', - context: { - error: { - origin: 'logger', - }, - }, status: 'error', + context: undefined, }) }) }) + describe('context methods', () => { + beforeEach(() => { + const loggerContext = { foo: 'bar' } + logger = new Logger(handleLogSpy, undefined, HandlerType.http, StatusType.debug, loggerContext) + }) + + it('getContext should return the context', () => { + expect(logger.getContext()).toEqual({ foo: 'bar' }) + }) + + it('setContext should overwrite the whole context', () => { + logger.setContext({ qux: 'qix' }) + expect(logger.getContext()).toEqual({ qux: 'qix' }) + }) + + it('setContextProperty should set a context value', () => { + logger.setContextProperty('qux', 'qix') + expect(logger.getContext()).toEqual({ foo: 'bar', qux: 'qix' }) + }) + + it('removeContextProperty should remove a context value', () => { + logger.removeContextProperty('foo') + expect(logger.getContext()).toEqual({}) + }) + + it('clearContext should clear the context', () => { + logger.clearContext() + expect(logger.getContext()).toEqual({}) + }) + }) + describe('contexts', () => { it('logger context should be deep copied', () => { const loggerContext = { foo: 'bar' } diff --git a/packages/logs/src/domain/logger.ts b/packages/logs/src/domain/logger.ts index de626a879f..2629ae8f38 100644 --- a/packages/logs/src/domain/logger.ts +++ b/packages/logs/src/domain/logger.ts @@ -5,7 +5,6 @@ import { ErrorHandling, computeStackTrace, CustomerDataType, - assign, combine, createContextManager, ErrorSource, @@ -50,18 +49,16 @@ export class Logger { private level: StatusType = StatusType.debug, loggerContext: object = {} ) { - this.contextManager.set(assign({}, loggerContext, name ? { logger: { name } } : undefined)) + this.contextManager.setContext(loggerContext as Context) + if (name) { + this.contextManager.setContextProperty('logger', { name }) + } } @monitored log(message: string, messageContext?: object, status: StatusType = StatusType.info, error?: Error) { let errorContext: LogsEvent['error'] - if (status === StatusType.error) { - // Always add origin if status is error (backward compatibility - Remove in next major) - errorContext = { origin: ErrorSource.LOGGER } - } - if (error !== undefined && error !== null) { const stackTrace = error instanceof Error ? computeStackTrace(error) : undefined const rawError = computeRawError({ @@ -74,7 +71,6 @@ export class Logger { }) errorContext = { - origin: ErrorSource.LOGGER, // Remove in next major stack: rawError.stack, kind: rawError.type, message: rawError.message, @@ -114,19 +110,23 @@ export class Logger { } setContext(context: object) { - this.contextManager.set(context) + this.contextManager.setContext(context as Context) } getContext() { - return this.contextManager.get() + return this.contextManager.getContext() + } + + setContextProperty(key: string, value: any) { + this.contextManager.setContextProperty(key, value) } - addContext(key: string, value: any) { - this.contextManager.add(key, value) + removeContextProperty(key: string) { + this.contextManager.removeContextProperty(key) } - removeContext(key: string) { - this.contextManager.remove(key) + clearContext() { + this.contextManager.clearContext() } setHandler(handler: HandlerType | HandlerType[]) { diff --git a/packages/logs/src/domain/logsCollection/console/consoleCollection.spec.ts b/packages/logs/src/domain/logsCollection/console/consoleCollection.spec.ts index 38ea5f667f..7cc72636cb 100644 --- a/packages/logs/src/domain/logsCollection/console/consoleCollection.spec.ts +++ b/packages/logs/src/domain/logsCollection/console/consoleCollection.spec.ts @@ -1,14 +1,13 @@ -import { ErrorSource, noop } from '@datadog/browser-core' +import { ErrorSource, noop, objectEntries } from '@datadog/browser-core' import type { RawConsoleLogsEvent } from '../../../rawLogsEvent.types' import { validateAndBuildLogsConfiguration } from '../../configuration' import type { RawLogsEventCollectedData } from '../../lifeCycle' import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' -import { StatusType } from '../../logger' -import { startConsoleCollection } from './consoleCollection' +import { startConsoleCollection, LogStatusForApi } from './consoleCollection' describe('console collection', () => { const initConfiguration = { clientToken: 'xxx', service: 'service' } - let consoleLogSpy: jasmine.Spy + let consoleSpies: { [key: string]: jasmine.Spy } let stopConsoleCollection: () => void let lifeCycle: LifeCycle let rawLogsEvents: Array> @@ -20,32 +19,39 @@ describe('console collection', () => { rawLogsEvents.push(rawLogsEvent as RawLogsEventCollectedData) ) stopConsoleCollection = noop - consoleLogSpy = spyOn(console, 'log').and.callFake(() => true) - spyOn(console, 'error').and.callFake(() => true) + consoleSpies = { + log: spyOn(console, 'log').and.callFake(() => true), + debug: spyOn(console, 'debug').and.callFake(() => true), + info: spyOn(console, 'info').and.callFake(() => true), + warn: spyOn(console, 'warn').and.callFake(() => true), + error: spyOn(console, 'error').and.callFake(() => true), + } }) afterEach(() => { stopConsoleCollection() }) - it('should send console logs', () => { - ;({ stop: stopConsoleCollection } = startConsoleCollection( - validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: ['log'] })!, - lifeCycle - )) + objectEntries(LogStatusForApi).forEach(([api, status]) => { + it(`should collect ${status} logs from console.${api}`, () => { + ;({ stop: stopConsoleCollection } = startConsoleCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: 'all' })!, + lifeCycle + )) - /* eslint-disable-next-line no-console */ - console.log('foo', 'bar') + /* eslint-disable-next-line no-console */ + console[api as keyof typeof LogStatusForApi]('foo', 'bar') - expect(rawLogsEvents[0].rawLogsEvent).toEqual({ - date: jasmine.any(Number), - message: 'foo bar', - status: StatusType.info, - origin: ErrorSource.CONSOLE, - error: undefined, - }) + expect(rawLogsEvents[0].rawLogsEvent).toEqual({ + date: jasmine.any(Number), + message: 'foo bar', + status, + origin: ErrorSource.CONSOLE, + error: whatever(), + }) - expect(consoleLogSpy).toHaveBeenCalled() + expect(consoleSpies[api]).toHaveBeenCalled() + }) }) it('console error should have an error object defined', () => { @@ -58,7 +64,6 @@ describe('console collection', () => { console.error('foo', 'bar') expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({ - origin: ErrorSource.CONSOLE, stack: undefined, fingerprint: undefined, }) @@ -79,9 +84,15 @@ describe('console collection', () => { console.error(error) expect(rawLogsEvents[0].rawLogsEvent.error).toEqual({ - origin: ErrorSource.CONSOLE, stack: jasmine.any(String), fingerprint: 'my-fingerprint', }) }) }) + +function whatever() { + return { + asymmetricMatch: () => true, + jasmineToString: () => '', + } +} diff --git a/packages/logs/src/domain/logsCollection/console/consoleCollection.ts b/packages/logs/src/domain/logsCollection/console/consoleCollection.ts index 8df499b3a9..b83fb5fc7c 100644 --- a/packages/logs/src/domain/logsCollection/console/consoleCollection.ts +++ b/packages/logs/src/domain/logsCollection/console/consoleCollection.ts @@ -12,7 +12,7 @@ export interface ProvidedError { handlingStack: string } -const LogStatusForApi = { +export const LogStatusForApi = { [ConsoleApiName.log]: StatusType.info, [ConsoleApiName.debug]: StatusType.debug, [ConsoleApiName.info]: StatusType.info, @@ -29,7 +29,6 @@ export function startConsoleCollection(configuration: LogsConfiguration, lifeCyc error: log.api === ConsoleApiName.error ? { - origin: ErrorSource.CONSOLE, // Todo: Remove in the next major release stack: log.stack, fingerprint: log.fingerprint, } diff --git a/packages/logs/src/domain/logsCollection/logger/loggerCollection.spec.ts b/packages/logs/src/domain/logsCollection/logger/loggerCollection.spec.ts index b59d1f37c0..e9f0d178b6 100644 --- a/packages/logs/src/domain/logsCollection/logger/loggerCollection.spec.ts +++ b/packages/logs/src/domain/logsCollection/logger/loggerCollection.spec.ts @@ -34,23 +34,6 @@ describe('logger collection', () => { clock.cleanup() }) - it('should send logger logs', () => { - handleLog({ message: 'message', status: StatusType.error }, logger, COMMON_CONTEXT) - - expect(rawLogsEvents[0].rawLogsEvent).toEqual({ - date: timeStampNow(), - origin: ErrorSource.LOGGER, - message: 'message', - status: StatusType.error, - }) - }) - - it('should send the saved date when present', () => { - handleLog({ message: 'message', status: StatusType.error }, logger, COMMON_CONTEXT, FAKE_DATE) - - expect(rawLogsEvents[0].rawLogsEvent.date).toEqual(FAKE_DATE) - }) - describe('when handle type is set to "console"', () => { beforeEach(() => { logger.setHandler(HandlerType.console) @@ -62,19 +45,18 @@ describe('logger collection', () => { }) it('should print the log message and context to the console', () => { - logger.setContext({ 'logger-context': 'foo' }) + logger.setContext({ foo: 'from-logger', bar: 'from-logger' }) handleLog( - { message: 'message', status: StatusType.error, context: { 'log-context': 'bar' } }, + { message: 'message', status: StatusType.error, context: { bar: 'from-message' } }, logger, COMMON_CONTEXT ) expect(display.error).toHaveBeenCalledOnceWith('message', { - 'logger-context': 'foo', - 'log-context': 'bar', + foo: 'from-logger', + bar: 'from-message', }) - expect(rawLogsEvents.length).toEqual(1) }) for (const { status, api } of [ @@ -107,4 +89,53 @@ describe('logger collection', () => { expect(display.debug).not.toHaveBeenCalled() }) }) + + describe('when handle type is set to "http"', () => { + beforeEach(() => { + logger.setHandler(HandlerType.http) + }) + + it('should send the log message and context', () => { + logger.setContext({ foo: 'from-logger', bar: 'from-logger' }) + + handleLog( + { message: 'message', status: StatusType.error, context: { bar: 'from-message' } }, + logger, + COMMON_CONTEXT + ) + + expect(rawLogsEvents[0]).toEqual({ + rawLogsEvent: { + date: timeStampNow(), + origin: ErrorSource.LOGGER, + message: 'message', + status: StatusType.error, + }, + messageContext: { + foo: 'from-logger', + bar: 'from-message', + }, + savedCommonContext: COMMON_CONTEXT, + }) + }) + + it('should send the saved date when present', () => { + handleLog({ message: 'message', status: StatusType.error }, logger, COMMON_CONTEXT, FAKE_DATE) + + expect(rawLogsEvents[0].rawLogsEvent.date).toEqual(FAKE_DATE) + }) + + it('does not send the log if its status is below the logger level', () => { + logger.setLevel(StatusType.warn) + handleLog({ message: 'message', status: StatusType.info }, logger, COMMON_CONTEXT) + + expect(rawLogsEvents.length).toBe(0) + }) + + it('does not send the log and does not crash if its status is unknown', () => { + handleLog({ message: 'message', status: 'unknown' as StatusType }, logger, COMMON_CONTEXT) + + expect(rawLogsEvents.length).toBe(0) + }) + }) }) diff --git a/packages/logs/src/domain/logsCollection/logger/loggerCollection.ts b/packages/logs/src/domain/logsCollection/logger/loggerCollection.ts index 798faeeaf6..09748eb542 100644 --- a/packages/logs/src/domain/logsCollection/logger/loggerCollection.ts +++ b/packages/logs/src/domain/logsCollection/logger/loggerCollection.ts @@ -20,23 +20,24 @@ export function startLoggerCollection(lifeCycle: LifeCycle) { savedCommonContext?: CommonContext, savedDate?: TimeStamp ) { - const messageContext = logsMessage.context + const messageContext = combine(logger.getContext(), logsMessage.context) if (isAuthorized(logsMessage.status, HandlerType.console, logger)) { - display(logsMessage.status, logsMessage.message, combine(logger.getContext(), messageContext)) + display(logsMessage.status, logsMessage.message, messageContext) } - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { - rawLogsEvent: { - date: savedDate || timeStampNow(), - message: logsMessage.message, - status: logsMessage.status, - origin: ErrorSource.LOGGER, - }, - messageContext, - savedCommonContext, - logger, - }) + if (isAuthorized(logsMessage.status, HandlerType.http, logger)) { + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: { + date: savedDate || timeStampNow(), + message: logsMessage.message, + status: logsMessage.status, + origin: ErrorSource.LOGGER, + }, + messageContext, + savedCommonContext, + }) + } } return { diff --git a/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.spec.ts b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.spec.ts index 44cfa550d5..1183068415 100644 --- a/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.spec.ts +++ b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.spec.ts @@ -72,7 +72,6 @@ describe('network error collection', () => { status: StatusType.error, origin: ErrorSource.NETWORK, error: { - origin: ErrorSource.NETWORK, stack: 'Server error', }, http: { diff --git a/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.ts b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.ts index c99cb56a2d..06789fc60e 100644 --- a/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.ts +++ b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.ts @@ -50,7 +50,6 @@ export function startNetworkErrorCollection(configuration: LogsConfiguration, li message: `${format(type)} error ${request.method} ${request.url}`, date: request.startClocks.timeStamp, error: { - origin: ErrorSource.NETWORK, // Todo: Remove in the next major release stack: (responseData as string) || 'Failed to load', }, http: { diff --git a/packages/logs/src/domain/logsCollection/report/reportCollection.spec.ts b/packages/logs/src/domain/logsCollection/report/reportCollection.spec.ts index 5b5a4e0610..47f617be20 100644 --- a/packages/logs/src/domain/logsCollection/report/reportCollection.spec.ts +++ b/packages/logs/src/domain/logsCollection/report/reportCollection.spec.ts @@ -39,7 +39,6 @@ describe('reports', () => { expect(rawLogsEvents[0].rawLogsEvent).toEqual({ error: { kind: 'NavigatorVibrate', - origin: ErrorSource.REPORT, stack: jasmine.any(String), }, date: jasmine.any(Number), diff --git a/packages/logs/src/domain/logsCollection/report/reportCollection.ts b/packages/logs/src/domain/logsCollection/report/reportCollection.ts index b48fb9e7f1..937869a2ce 100644 --- a/packages/logs/src/domain/logsCollection/report/reportCollection.ts +++ b/packages/logs/src/domain/logsCollection/report/reportCollection.ts @@ -32,7 +32,6 @@ export function startReportCollection(configuration: LogsConfiguration, lifeCycl if (status === StatusType.error) { error = { kind: report.subtype, - origin: ErrorSource.REPORT, // Todo: Remove in the next major release stack: report.stack, } } else if (report.stack) { diff --git a/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.spec.ts b/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.spec.ts index f2c5ce4190..90b0034db5 100644 --- a/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.spec.ts +++ b/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.spec.ts @@ -39,7 +39,7 @@ describe('runtime error collection', () => { setTimeout(() => { expect(rawLogsEvents[0].rawLogsEvent).toEqual({ date: jasmine.any(Number), - error: { origin: ErrorSource.SOURCE, kind: 'Error', stack: jasmine.any(String) }, + error: { kind: 'Error', stack: jasmine.any(String) }, message: 'error!', status: StatusType.error, origin: ErrorSource.SOURCE, diff --git a/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.ts b/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.ts index 9d5fbac792..8b1bb4ef03 100644 --- a/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.ts +++ b/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.ts @@ -28,7 +28,6 @@ export function startRuntimeErrorCollection(configuration: LogsConfiguration, li date: rawError.startClocks.timeStamp, error: { kind: rawError.type, - origin: ErrorSource.SOURCE, // Todo: Remove in the next major release stack: rawError.stack, }, origin: ErrorSource.SOURCE, diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 62f7aea34d..47cd85ab21 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -6,9 +6,10 @@ import { setCookie, stopSessionManager, ONE_SECOND, + DOM_EVENT, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { mockClock } from '@datadog/browser-core/test' +import { createNewEvent, mockClock } from '@datadog/browser-core/test' import type { LogsConfiguration } from './configuration' import { @@ -84,7 +85,7 @@ describe('logs session manager', () => { clock.tick(STORAGE_POLL_DELAY) tracked = true - document.body.click() + document.body.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]+/) expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) diff --git a/packages/logs/src/logsEvent.types.ts b/packages/logs/src/logsEvent.types.ts index e90a110475..d1286f6dda 100644 --- a/packages/logs/src/logsEvent.types.ts +++ b/packages/logs/src/logsEvent.types.ts @@ -14,7 +14,7 @@ export interface LogsEvent { /** * Origin of the log */ - origin?: 'network' | 'source' | 'console' | 'logger' | 'agent' | 'report' | 'custom' + origin: 'network' | 'source' | 'console' | 'logger' | 'agent' | 'report' /** * UUID of the application */ @@ -65,10 +65,6 @@ export interface LogsEvent { * Kind of the error */ kind?: string - /** - * Origin of the error - */ - origin: 'network' | 'source' | 'console' | 'logger' | 'agent' | 'report' | 'custom' /** * Stacktrace of the error */ diff --git a/packages/logs/src/rawLogsEvent.types.ts b/packages/logs/src/rawLogsEvent.types.ts index 276078c5ae..0d0ee3eaf9 100644 --- a/packages/logs/src/rawLogsEvent.types.ts +++ b/packages/logs/src/rawLogsEvent.types.ts @@ -11,7 +11,6 @@ export type RawLogsEvent = type Error = { kind?: string - origin: ErrorSource // Todo: Remove in the next major release stack?: string fingerprint?: string [k: string]: unknown @@ -22,6 +21,7 @@ interface CommonRawLogsEvent { message: string status: StatusType error?: Error + origin: 'network' | 'source' | 'console' | 'logger' | 'agent' | 'report' } export interface RawConsoleLogsEvent extends CommonRawLogsEvent { @@ -57,7 +57,6 @@ export interface RawRuntimeLogsEvent extends CommonRawLogsEvent { export interface RawAgentLogsEvent extends CommonRawLogsEvent { origin: typeof ErrorSource.AGENT status: typeof StatusType.error - error: Error } export interface CommonContext { diff --git a/packages/rum-core/package.json b/packages/rum-core/package.json index 8909721df8..1063223a18 100644 --- a/packages/rum-core/package.json +++ b/packages/rum-core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-core", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", @@ -12,7 +12,7 @@ "replace-build-env": "node ../../scripts/build/replace-build-env.js" }, "dependencies": { - "@datadog/browser-core": "4.45.0" + "@datadog/browser-core": "5.0.0-alpha.0" }, "devDependencies": { "ajv": "6.12.6" diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 4cb83c8e88..bbdcd2fd00 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -279,9 +279,9 @@ describe('rum public api', () => { }) it('stores a deep copy of the global context', () => { - rumPublicApi.addRumGlobalContext('foo', 'bar') + rumPublicApi.setGlobalContextProperty('foo', 'bar') rumPublicApi.addAction('message') - rumPublicApi.addRumGlobalContext('foo', 'baz') + rumPublicApi.setGlobalContextProperty('foo', 'baz') rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) @@ -381,9 +381,9 @@ describe('rum public api', () => { }) it('stores a deep copy of the global context', () => { - rumPublicApi.addRumGlobalContext('foo', 'bar') + rumPublicApi.setGlobalContextProperty('foo', 'bar') rumPublicApi.addError(new Error('message')) - rumPublicApi.addRumGlobalContext('foo', 'baz') + rumPublicApi.setGlobalContextProperty('foo', 'baz') rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) @@ -476,7 +476,7 @@ describe('rum public api', () => { it('should remove the user', () => { const user = { id: 'foo', name: 'bar', email: 'qux' } rumPublicApi.setUser(user) - rumPublicApi.removeUser() + rumPublicApi.clearUser() rumPublicApi.addAction('message') rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) @@ -845,10 +845,12 @@ describe('rum public api', () => { let recorderApiOnRumStartSpy: jasmine.Spy let setupBuilder: TestSetupBuilder let rumPublicApi: RumPublicApi + let recorderApi: RecorderApi beforeEach(() => { recorderApiOnRumStartSpy = jasmine.createSpy('recorderApiOnRumStart') - rumPublicApi = makeRumPublicApi(noopStartRum, { ...noopRecorderApi, onRumStart: recorderApiOnRumStartSpy }) + recorderApi = { ...noopRecorderApi, onRumStart: recorderApiOnRumStartSpy } + rumPublicApi = makeRumPublicApi(noopStartRum, recorderApi) setupBuilder = setup() }) @@ -856,19 +858,46 @@ describe('rum public api', () => { setupBuilder.cleanup() }) - it('recording is started with the default defaultPrivacyLevel', () => { + it('is started with the default defaultPrivacyLevel', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK) + }) + + it('is started with the configured defaultPrivacyLevel', () => { + rumPublicApi.init({ + ...DEFAULT_INIT_CONFIGURATION, + defaultPrivacyLevel: DefaultPrivacyLevel.MASK_USER_INPUT, + }) expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].defaultPrivacyLevel).toBe( DefaultPrivacyLevel.MASK_USER_INPUT ) }) - it('recording is started with the configured defaultPrivacyLevel', () => { + it('api calls before init are performed after onRumStart', () => { + // in order to let recording initial state to be defined by init configuration + const callOrders: string[] = [] + spyOn(recorderApi, 'start').and.callFake(() => callOrders.push('start')) + spyOn(recorderApi, 'stop').and.callFake(() => callOrders.push('stop')) + recorderApiOnRumStartSpy.and.callFake(() => callOrders.push('onRumStart')) + + rumPublicApi.startSessionReplayRecording() + rumPublicApi.stopSessionReplayRecording() + rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + + expect(callOrders).toEqual(['onRumStart', 'start', 'stop']) + }) + + it('is started with the default startSessionReplayRecordingManually', () => { + rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(false) + }) + + it('is started with the configured startSessionReplayRecordingManually', () => { rumPublicApi.init({ ...DEFAULT_INIT_CONFIGURATION, - defaultPrivacyLevel: DefaultPrivacyLevel.MASK, + startSessionReplayRecordingManually: true, }) - expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK) + expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(true) }) }) diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index c0fe8d8aea..afd53a6494 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -94,6 +94,14 @@ export function makeRumPublicApi( bufferApiCalls.add(() => addErrorStrategy(providedError, commonContext)) } + let recorderStartStrategy: typeof recorderApi.start = () => { + bufferApiCalls.add(() => recorderStartStrategy()) + } + + let recorderStopStrategy: typeof recorderApi.stop = () => { + bufferApiCalls.add(() => recorderStopStrategy()) + } + let addFeatureFlagEvaluationStrategy: StartRumResult['addFeatureFlagEvaluation'] = (key: string, value: any) => { bufferApiCalls.add(() => addFeatureFlagEvaluationStrategy(key, value)) } @@ -162,6 +170,8 @@ export function makeRumPublicApi( ) getSessionReplayLinkStrategy = () => recorderApi.getSessionReplayLink(configuration, startRumResults.session, startRumResults.viewContexts) + recorderStartStrategy = recorderApi.start + recorderStopStrategy = recorderApi.stop ;({ startView: startViewStrategy, addAction: addActionStrategy, @@ -171,7 +181,6 @@ export function makeRumPublicApi( getInternalContext: getInternalContextStrategy, stopSession: stopSessionStrategy, } = startRumResults) - bufferApiCalls.drain() recorderApi.onRumStart( startRumResults.lifeCycle, @@ -179,6 +188,7 @@ export function makeRumPublicApi( startRumResults.session, startRumResults.viewContexts ) + bufferApiCalls.drain() } const startView: { @@ -192,20 +202,12 @@ export function makeRumPublicApi( const rumPublicApi = makePublicApi({ init: monitor(initRum), - /** @deprecated: use setGlobalContextProperty instead */ - addRumGlobalContext: monitor(globalContextManager.add), setGlobalContextProperty: monitor(globalContextManager.setContextProperty), - /** @deprecated: use removeGlobalContextProperty instead */ - removeRumGlobalContext: monitor(globalContextManager.remove), removeGlobalContextProperty: monitor(globalContextManager.removeContextProperty), - /** @deprecated: use getGlobalContext instead */ - getRumGlobalContext: monitor(globalContextManager.get), getGlobalContext: monitor(globalContextManager.getContext), - /** @deprecated: use setGlobalContext instead */ - setRumGlobalContext: monitor(globalContextManager.set), setGlobalContext: monitor(globalContextManager.setContext), clearGlobalContext: monitor(globalContextManager.clearContext), @@ -253,8 +255,6 @@ export function makeRumPublicApi( removeUserProperty: monitor(userContextManager.removeContextProperty), - /** @deprecated: renamed to clearUser */ - removeUser: monitor(userContextManager.clearContext), clearUser: monitor(userContextManager.clearContext), startView, @@ -263,8 +263,8 @@ export function makeRumPublicApi( stopSessionStrategy() }), - startSessionReplayRecording: monitor(recorderApi.start), - stopSessionReplayRecording: monitor(recorderApi.stop), + startSessionReplayRecording: monitor(() => recorderStartStrategy()), + stopSessionReplayRecording: monitor(() => recorderStopStrategy()), /** * This feature is currently in beta. For more information see the full [feature flag tracking guide](https://docs.datadoghq.com/real_user_monitoring/feature_flag_tracking/). diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index 94cbb21148..1da1ddf8da 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -76,7 +76,7 @@ function startRumStub( noopRecorderApi ) - startLongTaskCollection(lifeCycle, sessionManager) + startLongTaskCollection(lifeCycle, configuration, sessionManager) return { stop: () => { rumEventCollectionStop() diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 709991a181..e2fc4567fc 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -116,7 +116,7 @@ export function startRum( addTelemetryConfiguration(serializeRumConfiguration(initConfiguration)) - startLongTaskCollection(lifeCycle, session) + startLongTaskCollection(lifeCycle, configuration, session) startResourceCollection(lifeCycle, configuration, session, pageStateHistory) const { addTiming, startView } = startViewCollection( lifeCycle, diff --git a/packages/rum-core/src/browser/performanceCollection.spec.ts b/packages/rum-core/src/browser/performanceCollection.spec.ts index af56a4029f..df34050635 100644 --- a/packages/rum-core/src/browser/performanceCollection.spec.ts +++ b/packages/rum-core/src/browser/performanceCollection.spec.ts @@ -18,6 +18,12 @@ describe('rum initial document resource', () => { retrieveInitialDocumentResourceTiming((timing) => { expect(timing.entryType).toBe('resource') expect(timing.duration).toBeGreaterThan(0) + + // generate a performance entry like structure + const toJsonTiming = timing.toJSON() + expect(toJsonTiming.entryType).toEqual(timing.entryType) + expect(toJsonTiming.duration).toEqual(timing.duration) + expect((toJsonTiming as any).toJSON).toBeUndefined() done() }) }) diff --git a/packages/rum-core/src/browser/performanceCollection.ts b/packages/rum-core/src/browser/performanceCollection.ts index 0f54daacc5..95fee5cba1 100644 --- a/packages/rum-core/src/browser/performanceCollection.ts +++ b/packages/rum-core/src/browser/performanceCollection.ts @@ -19,7 +19,6 @@ import { LifeCycleEventType } from '../domain/lifeCycle' import { FAKE_INITIAL_DOCUMENT, isAllowedRequestUrl } from '../domain/rumEventsCollection/resource/resourceUtils' import { getDocumentTraceId } from '../domain/tracing/getDocumentTraceId' -import type { PerformanceEntryRepresentation } from '../domainContext.types' export interface RumPerformanceResourceTiming { entryType: 'resource' @@ -40,13 +39,15 @@ export interface RumPerformanceResourceTiming { redirectEnd: RelativeTime decodedBodySize: number traceId?: string + toJSON(): Omit } export interface RumPerformanceLongTaskTiming { + name: string entryType: 'longtask' startTime: RelativeTime duration: Duration - toJSON(): PerformanceEntryRepresentation + toJSON(): Omit } export interface RumPerformancePaintTiming { @@ -172,10 +173,11 @@ export function retrieveInitialDocumentResourceTiming(callback: (timing: RumPerf entryType: 'resource' as const, initiatorType: FAKE_INITIAL_DOCUMENT, traceId: getDocumentTraceId(document), + toJSON: () => assign({}, timing, { toJSON: undefined }), } if (supportPerformanceTimingEvent('navigation') && performance.getEntriesByType('navigation').length > 0) { const navigationEntry = performance.getEntriesByType('navigation')[0] - timing = assign(navigationEntry.toJSON(), forcedAttributes) + timing = assign(navigationEntry.toJSON() as RumPerformanceResourceTiming, forcedAttributes) } else { const relativePerformanceTiming = computeRelativePerformanceTiming() timing = assign( diff --git a/packages/rum-core/src/domain/assembly.spec.ts b/packages/rum-core/src/domain/assembly.spec.ts index 9e0cd39b19..87d8babebb 100644 --- a/packages/rum-core/src/domain/assembly.spec.ts +++ b/packages/rum-core/src/domain/assembly.spec.ts @@ -329,6 +329,38 @@ describe('rum assembly', () => { expect(displaySpy).toHaveBeenCalledWith("Can't dismiss view events using beforeSend!") }) }) + + it('should not dismiss when true is returned', () => { + const { lifeCycle } = setupBuilder + .withConfiguration({ + beforeSend: () => true, + }) + .build() + + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.ACTION, { + view: { id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' }, + }), + }) + + expect(serverRumEvents.length).toBe(1) + }) + + it('should not dismiss when undefined is returned', () => { + const { lifeCycle } = setupBuilder + .withConfiguration({ + beforeSend: () => undefined, + }) + .build() + + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.ACTION, { + view: { id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' }, + }), + }) + + expect(serverRumEvents.length).toBe(1) + }) }) describe('rum context', () => { @@ -635,7 +667,7 @@ describe('rum assembly', () => { rawRumEvent: createRawRumEvent(RumEventType.VIEW), }) expect(serverRumEvents[0]._dd.configuration).toEqual({ - session_replay_sample_rate: 100, + session_replay_sample_rate: 0, session_sample_rate: 100, }) }) diff --git a/packages/rum-core/src/domain/configuration.spec.ts b/packages/rum-core/src/domain/configuration.spec.ts index 99947ee892..b3dbd9489a 100644 --- a/packages/rum-core/src/domain/configuration.spec.ts +++ b/packages/rum-core/src/domain/configuration.spec.ts @@ -25,8 +25,8 @@ describe('validateAndBuildRumConfiguration', () => { }) describe('sessionReplaySampleRate', () => { - it('defaults to 100 if the option is not provided', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.sessionReplaySampleRate).toBe(100) + it('defaults to 0 if the option is not provided', () => { + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.sessionReplaySampleRate).toBe(0) }) it('is set to `sessionReplaySampleRate` provided value', () => { @@ -36,38 +36,6 @@ describe('validateAndBuildRumConfiguration', () => { ).toBe(50) }) - it('is set to `premiumSampleRate` provided value', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, premiumSampleRate: 50 })! - .sessionReplaySampleRate - ).toBe(50) - }) - - it('is set to `replaySampleRate` provided value', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, replaySampleRate: 50 })! - .sessionReplaySampleRate - ).toBe(50) - }) - - it('is set with precedence `sessionReplaySampleRate` > `premiumSampleRate` > `replaySampleRate`', () => { - expect( - validateAndBuildRumConfiguration({ - ...DEFAULT_INIT_CONFIGURATION, - replaySampleRate: 25, - premiumSampleRate: 50, - sessionReplaySampleRate: 75, - })!.sessionReplaySampleRate - ).toBe(75) - expect( - validateAndBuildRumConfiguration({ - ...DEFAULT_INIT_CONFIGURATION, - replaySampleRate: 25, - premiumSampleRate: 50, - })!.sessionReplaySampleRate - ).toBe(50) - }) - it('does not validate the configuration if an incorrect value is provided', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionReplaySampleRate: 'foo' as any }) @@ -84,85 +52,6 @@ describe('validateAndBuildRumConfiguration', () => { expect(displayErrorSpy).toHaveBeenCalledOnceWith( 'Session Replay Sample Rate should be a number between 0 and 100' ) - - displayErrorSpy.calls.reset() - - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, premiumSampleRate: 'foo' as any }) - ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Premium Sample Rate should be a number between 0 and 100') - - displayErrorSpy.calls.reset() - - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, premiumSampleRate: 200 }) - ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Premium Sample Rate should be a number between 0 and 100') - - displayErrorSpy.calls.reset() - - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, replaySampleRate: 'foo' as any }) - ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Premium Sample Rate should be a number between 0 and 100') - - displayErrorSpy.calls.reset() - - expect(validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, replaySampleRate: 200 })).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Premium Sample Rate should be a number between 0 and 100') - - displayErrorSpy.calls.reset() - }) - - it('should validate and display a warn if both `sessionReplaySampleRate` and `premiumSampleRate` are set', () => { - expect( - validateAndBuildRumConfiguration({ - ...DEFAULT_INIT_CONFIGURATION, - sessionReplaySampleRate: 100, - premiumSampleRate: 100, - }) - ).toBeDefined() - expect(displayWarnSpy).toHaveBeenCalledOnceWith( - 'Ignoring Premium Sample Rate because Session Replay Sample Rate is set' - ) - }) - }) - - describe('oldPlansBehavior', () => { - it('should be true by default', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.oldPlansBehavior).toBeTrue() - }) - - it('should be false if `sessionReplaySampleRate` is set', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, sessionReplaySampleRate: 100 })! - .oldPlansBehavior - ).toBeFalse() - }) - }) - - describe('deprecated tracingSampleRate', () => { - it('defaults to undefined if the option is not provided', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.traceSampleRate).toBeUndefined() - }) - - it('is set to provided value', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, tracingSampleRate: 50 })!.traceSampleRate - ).toBe(50) - }) - - it('does not validate the configuration if an incorrect value is provided', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, tracingSampleRate: 'foo' as any }) - ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Trace Sample Rate should be a number between 0 and 100') - - displayErrorSpy.calls.reset() - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, tracingSampleRate: 200 }) - ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Trace Sample Rate should be a number between 0 and 100') }) }) @@ -189,47 +78,6 @@ describe('validateAndBuildRumConfiguration', () => { }) }) - describe('allowedTracingOrigins', () => { - it('is set to provided value', () => { - expect( - validateAndBuildRumConfiguration({ - ...DEFAULT_INIT_CONFIGURATION, - allowedTracingOrigins: ['foo'], - service: 'bar', - })!.allowedTracingUrls - ).toEqual([{ match: 'foo', propagatorTypes: ['datadog'] }]) - }) - - it('accepts functions', () => { - const originMatchSpy = jasmine.createSpy<(origin: string) => boolean>() - - const tracingUrlOptionMatch = validateAndBuildRumConfiguration({ - ...DEFAULT_INIT_CONFIGURATION, - allowedTracingOrigins: [originMatchSpy], - service: 'bar', - })!.allowedTracingUrls[0].match as (url: string) => boolean - - expect(typeof tracingUrlOptionMatch).toBe('function') - // Replicating behavior from allowedTracingOrigins, new function will treat the origin part of the URL - tracingUrlOptionMatch('https://my.origin.com/api') - expect(originMatchSpy).toHaveBeenCalledWith('https://my.origin.com') - }) - - it('does not validate the configuration if a value is provided and service is undefined', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingOrigins: ['foo'] }) - ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Service needs to be configured when tracing is enabled') - }) - - it('does not validate the configuration if an incorrect value is provided', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, allowedTracingOrigins: 'foo' as any }) - ).toBeUndefined() - expect(displayErrorSpy).toHaveBeenCalledOnceWith('Allowed Tracing Origins should be an array') - }) - }) - describe('allowedTracingUrls', () => { it('defaults to an empty array', () => { expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.allowedTracingUrls).toEqual([]) @@ -344,30 +192,6 @@ describe('validateAndBuildRumConfiguration', () => { }) }) - describe('deprecated trackInteractions', () => { - it('defaults to false', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackUserInteractions).toBeFalse() - }) - - it('is set to provided value', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackInteractions: true })! - .trackUserInteractions - ).toBeTrue() - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackInteractions: false })! - .trackUserInteractions - ).toBeFalse() - }) - - it('the provided value is cast to boolean', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackInteractions: 'foo' as any })! - .trackUserInteractions - ).toBeTrue() - }) - }) - describe('trackUserInteractions', () => { it('defaults to false', () => { expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackUserInteractions).toBeFalse() @@ -392,55 +216,54 @@ describe('validateAndBuildRumConfiguration', () => { }) }) - describe('trackFrustrations', () => { + describe('trackViewsManually', () => { it('defaults to false', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackFrustrations).toBeFalse() + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackViewsManually).toBeFalse() }) - it('the initialization parameter is set to provided value', () => { + it('is set to provided value', () => { expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackFrustrations: true })!.trackFrustrations + validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: true })! + .trackViewsManually ).toBeTrue() expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackFrustrations: false })!.trackFrustrations + validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: false })! + .trackViewsManually ).toBeFalse() }) - it('the initialization parameter the provided value is cast to boolean', () => { - expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackFrustrations: 'foo' as any })! - .trackFrustrations - ).toBeTrue() - }) - - it('implies "trackUserInteractions"', () => { + it('the provided value is cast to boolean', () => { expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackFrustrations: true })! - .trackUserInteractions + validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: 'foo' as any })! + .trackViewsManually ).toBeTrue() }) }) - describe('trackViewsManually', () => { + describe('startSessionReplayRecordingManually', () => { it('defaults to false', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackViewsManually).toBeFalse() + expect( + validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.startSessionReplayRecordingManually + ).toBeFalse() }) it('is set to provided value', () => { expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: true })! - .trackViewsManually + validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: true })! + .startSessionReplayRecordingManually ).toBeTrue() expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: false })! - .trackViewsManually + validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: false })! + .startSessionReplayRecordingManually ).toBeFalse() }) it('the provided value is cast to boolean', () => { expect( - validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, trackViewsManually: 'foo' as any })! - .trackViewsManually + validateAndBuildRumConfiguration({ + ...DEFAULT_INIT_CONFIGURATION, + startSessionReplayRecordingManually: 'foo' as any, + })!.startSessionReplayRecordingManually ).toBeTrue() }) }) @@ -459,9 +282,9 @@ describe('validateAndBuildRumConfiguration', () => { }) describe('defaultPrivacyLevel', () => { - it('defaults to MASK_USER_INPUT', () => { + it('defaults to MASK', () => { expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.defaultPrivacyLevel).toBe( - DefaultPrivacyLevel.MASK_USER_INPUT + DefaultPrivacyLevel.MASK ) }) @@ -478,13 +301,13 @@ describe('validateAndBuildRumConfiguration', () => { expect( validateAndBuildRumConfiguration({ ...DEFAULT_INIT_CONFIGURATION, defaultPrivacyLevel: 'foo' as any })! .defaultPrivacyLevel - ).toBe(DefaultPrivacyLevel.MASK_USER_INPUT) + ).toBe(DefaultPrivacyLevel.MASK) }) }) describe('trackResources', () => { - it('defaults to undefined', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackResources).toBeUndefined() + it('defaults to false', () => { + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackResources).toBeFalse() }) it('is set to provided value', () => { @@ -498,8 +321,8 @@ describe('validateAndBuildRumConfiguration', () => { }) describe('trackLongTasks', () => { - it('defaults to undefined', () => { - expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackLongTasks).toBeUndefined() + it('defaults to false', () => { + expect(validateAndBuildRumConfiguration(DEFAULT_INIT_CONFIGURATION)!.trackLongTasks).toBeFalse() }) it('is set to provided value', () => { diff --git a/packages/rum-core/src/domain/configuration.ts b/packages/rum-core/src/domain/configuration.ts index 823d6c2b64..413e1b049c 100644 --- a/packages/rum-core/src/domain/configuration.ts +++ b/packages/rum-core/src/domain/configuration.ts @@ -2,7 +2,6 @@ import type { Configuration, InitConfiguration, MatchOption, RawTelemetryConfigu import { getType, arrayFrom, - getOrigin, isMatchOption, serializeConfiguration, assign, @@ -20,41 +19,21 @@ import type { PropagatorType, TracingOption } from './tracing/tracer.types' export interface RumInitConfiguration extends InitConfiguration { // global options applicationId: string - beforeSend?: ((event: RumEvent, context: RumEventDomainContext) => void | boolean) | undefined - /** - * @deprecated use sessionReplaySampleRate instead - */ - premiumSampleRate?: number | undefined + beforeSend?: ((event: RumEvent, context: RumEventDomainContext) => boolean) | undefined excludedActivityUrls?: MatchOption[] | undefined // tracing options - /** - * @deprecated use allowedTracingUrls instead - */ - allowedTracingOrigins?: MatchOption[] | undefined allowedTracingUrls?: Array | undefined - /** - * @deprecated use traceSampleRate instead - */ - tracingSampleRate?: number | undefined traceSampleRate?: number | undefined // replay options defaultPrivacyLevel?: DefaultPrivacyLevel | undefined subdomain?: string - /** - * @deprecated use sessionReplaySampleRate instead - */ - replaySampleRate?: number | undefined sessionReplaySampleRate?: number | undefined + startSessionReplayRecordingManually?: boolean | undefined // action options - /** - * @deprecated use trackUserInteractions instead - */ - trackInteractions?: boolean | undefined trackUserInteractions?: boolean | undefined - trackFrustrations?: boolean | undefined actionNameAttribute?: string | undefined // view options @@ -74,13 +53,12 @@ export interface RumConfiguration extends Configuration { excludedActivityUrls: MatchOption[] applicationId: string defaultPrivacyLevel: DefaultPrivacyLevel - oldPlansBehavior: boolean sessionReplaySampleRate: number + startSessionReplayRecordingManually: boolean trackUserInteractions: boolean - trackFrustrations: boolean trackViewsManually: boolean - trackResources: boolean | undefined - trackLongTasks: boolean | undefined + trackResources: boolean + trackLongTasks: boolean version?: string subdomain?: string customerDataTelemetrySampleRate: number @@ -102,20 +80,7 @@ export function validateAndBuildRumConfiguration( return } - // TODO remove fallback in next major - let premiumSampleRate = initConfiguration.premiumSampleRate ?? initConfiguration.replaySampleRate - if (premiumSampleRate !== undefined && initConfiguration.sessionReplaySampleRate !== undefined) { - display.warn('Ignoring Premium Sample Rate because Session Replay Sample Rate is set') - premiumSampleRate = undefined - } - - if (premiumSampleRate !== undefined && !isPercentage(premiumSampleRate)) { - display.error('Premium Sample Rate should be a number between 0 and 100') - return - } - - const traceSampleRate = initConfiguration.traceSampleRate ?? initConfiguration.tracingSampleRate - if (traceSampleRate !== undefined && !isPercentage(traceSampleRate)) { + if (initConfiguration.traceSampleRate !== undefined && !isPercentage(initConfiguration.traceSampleRate)) { display.error('Trace Sample Rate should be a number between 0 and 100') return } @@ -135,28 +100,24 @@ export function validateAndBuildRumConfiguration( return } - const trackUserInteractions = !!(initConfiguration.trackUserInteractions ?? initConfiguration.trackInteractions) - const trackFrustrations = !!initConfiguration.trackFrustrations - return assign( { applicationId: initConfiguration.applicationId, version: initConfiguration.version, actionNameAttribute: initConfiguration.actionNameAttribute, - sessionReplaySampleRate: initConfiguration.sessionReplaySampleRate ?? premiumSampleRate ?? 100, - oldPlansBehavior: initConfiguration.sessionReplaySampleRate === undefined, - traceSampleRate, + sessionReplaySampleRate: initConfiguration.sessionReplaySampleRate ?? 0, + startSessionReplayRecordingManually: !!initConfiguration.startSessionReplayRecordingManually, + traceSampleRate: initConfiguration.traceSampleRate, allowedTracingUrls, excludedActivityUrls: initConfiguration.excludedActivityUrls ?? [], - trackUserInteractions: trackUserInteractions || trackFrustrations, - trackFrustrations, + trackUserInteractions: !!initConfiguration.trackUserInteractions, trackViewsManually: !!initConfiguration.trackViewsManually, - trackResources: initConfiguration.trackResources, - trackLongTasks: initConfiguration.trackLongTasks, + trackResources: !!initConfiguration.trackResources, + trackLongTasks: !!initConfiguration.trackLongTasks, subdomain: initConfiguration.subdomain, defaultPrivacyLevel: objectHasValue(DefaultPrivacyLevel, initConfiguration.defaultPrivacyLevel) ? initConfiguration.defaultPrivacyLevel - : DefaultPrivacyLevel.MASK_USER_INPUT, + : DefaultPrivacyLevel.MASK, customerDataTelemetrySampleRate: 1, }, baseConfiguration @@ -164,16 +125,9 @@ export function validateAndBuildRumConfiguration( } /** - * Handles allowedTracingUrls and processes legacy allowedTracingOrigins + * Validates allowedTracingUrls and converts match options to tracing options */ function validateAndBuildTracingOptions(initConfiguration: RumInitConfiguration): TracingOption[] | undefined { - // Advise about parameters precedence. - if (initConfiguration.allowedTracingUrls !== undefined && initConfiguration.allowedTracingOrigins !== undefined) { - display.warn( - 'Both allowedTracingUrls and allowedTracingOrigins (deprecated) have been defined. The parameter allowedTracingUrls will override allowedTracingOrigins.' - ) - } - // Handle allowedTracingUrls first if (initConfiguration.allowedTracingUrls !== undefined) { if (!Array.isArray(initConfiguration.allowedTracingUrls)) { display.error('Allowed Tracing URLs should be an array') @@ -201,55 +155,11 @@ function validateAndBuildTracingOptions(initConfiguration: RumInitConfiguration) return tracingOptions } - // Handle conversion of allowedTracingOrigins to allowedTracingUrls - if (initConfiguration.allowedTracingOrigins !== undefined) { - if (!Array.isArray(initConfiguration.allowedTracingOrigins)) { - display.error('Allowed Tracing Origins should be an array') - return - } - if (initConfiguration.allowedTracingOrigins.length !== 0 && initConfiguration.service === undefined) { - display.error('Service needs to be configured when tracing is enabled') - return - } - - const tracingOptions: TracingOption[] = [] - initConfiguration.allowedTracingOrigins.forEach((legacyMatchOption) => { - const tracingOption = convertLegacyMatchOptionToTracingOption(legacyMatchOption) - if (tracingOption) { - tracingOptions.push(tracingOption) - } - }) - return tracingOptions - } - return [] } /** - * Converts parameters from the deprecated allowedTracingOrigins - * to allowedTracingUrls. Handles the change from origin to full URLs. - */ -function convertLegacyMatchOptionToTracingOption(item: MatchOption): TracingOption | undefined { - let match: MatchOption | undefined - if (typeof item === 'string') { - match = item - } else if (item instanceof RegExp) { - match = (url) => item.test(getOrigin(url)) - } else if (typeof item === 'function') { - match = (url) => item(getOrigin(url)) - } - - if (match === undefined) { - display.warn('Allowed Tracing Origins parameters should be a string, RegExp or function. Ignoring parameter', item) - return undefined - } - - return { match, propagatorTypes: ['datadog'] } -} - -/** - * Combines the selected tracing propagators from the different options in allowedTracingUrls, - * and assumes 'datadog' has been selected when using allowedTracingOrigins + * Combines the selected tracing propagators from the different options in allowedTracingUrls */ function getSelectedTracingPropagators(configuration: RumInitConfiguration): PropagatorType[] { const usedTracingPropagators = new Set() @@ -265,10 +175,6 @@ function getSelectedTracingPropagators(configuration: RumInitConfiguration): Pro }) } - if (Array.isArray(configuration.allowedTracingOrigins) && configuration.allowedTracingOrigins.length > 0) { - usedTracingPropagators.add('datadog') - } - return arrayFrom(usedTracingPropagators) } @@ -277,22 +183,18 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration): return assign( { - premium_sample_rate: configuration.premiumSampleRate, - replay_sample_rate: configuration.replaySampleRate, session_replay_sample_rate: configuration.sessionReplaySampleRate, - trace_sample_rate: configuration.traceSampleRate ?? configuration.tracingSampleRate, + start_session_replay_recording_manually: configuration.startSessionReplayRecordingManually, + trace_sample_rate: configuration.traceSampleRate, action_name_attribute: configuration.actionNameAttribute, - use_allowed_tracing_origins: - Array.isArray(configuration.allowedTracingOrigins) && configuration.allowedTracingOrigins.length > 0, use_allowed_tracing_urls: Array.isArray(configuration.allowedTracingUrls) && configuration.allowedTracingUrls.length > 0, selected_tracing_propagators: getSelectedTracingPropagators(configuration), default_privacy_level: configuration.defaultPrivacyLevel, use_excluded_activity_urls: Array.isArray(configuration.excludedActivityUrls) && configuration.excludedActivityUrls.length > 0, - track_frustrations: configuration.trackFrustrations, track_views_manually: configuration.trackViewsManually, - track_user_interactions: configuration.trackUserInteractions ?? configuration.trackInteractions, + track_user_interactions: configuration.trackUserInteractions, }, baseSerializedConfiguration ) diff --git a/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts b/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts deleted file mode 100644 index 140372ad84..0000000000 --- a/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { RelativeTime, Duration, ServerDuration } from '@datadog/browser-core' -import { relativeNow } from '@datadog/browser-core' -import type { TestSetupBuilder } from '../../../test' -import { setup } from '../../../test' -import { mapToForegroundPeriods } from './foregroundContexts' -import type { PageStateHistory } from './pageStateHistory' -import { PageState, startPageStateHistory } from './pageStateHistory' - -const FOCUS_PERIOD_LENGTH = 10 as Duration -const BLUR_PERIOD_LENGTH = 5 as Duration - -describe('foreground context', () => { - let setupBuilder: TestSetupBuilder - let pageStateHistory: PageStateHistory - - function addNewForegroundPeriod() { - pageStateHistory.addPageState(PageState.ACTIVE) - } - - function closeForegroundPeriod() { - pageStateHistory.addPageState(PageState.PASSIVE) - } - - function selectInForegroundPeriodsFor(startTime: RelativeTime, duration: Duration) { - const pageStates = pageStateHistory.findAll(startTime, duration) - return mapToForegroundPeriods(pageStates || [], duration) - } - - function isInForegroundAt(startTime: RelativeTime) { - return pageStateHistory.isInActivePageStateAt(startTime) - } - - beforeEach(() => { - setupBuilder = setup() - .withFakeClock() - .beforeBuild(() => { - pageStateHistory = startPageStateHistory() - return pageStateHistory - }) - }) - - afterEach(() => { - setupBuilder.cleanup() - }) - - describe('when the page do not have the focus when starting', () => { - beforeEach(() => { - spyOn(Document.prototype, 'hasFocus').and.callFake(() => false) - pageStateHistory = startPageStateHistory() - }) - describe('without any focus nor blur event', () => { - describe('isInForegroundAt', () => { - it('should return false', () => { - const { clock } = setupBuilder.build() - - clock.tick(1_000) - - expect(isInForegroundAt(relativeNow())).toEqual(false) - }) - }) - - describe('selectInForegroundPeriodsFor', () => { - it('should an empty array', () => { - const { clock } = setupBuilder.build() - - clock.tick(1_000) - - expect(selectInForegroundPeriodsFor(relativeNow(), 0 as Duration)).toEqual([]) - }) - }) - }) - - describe('with two closed focus period & one active one', () => { - /* - events F B F B F - periods <------> <-------> <---- - - - - time 0 5 10 15 20 25 30 35 40 45 - */ - beforeEach(() => { - const { clock } = setupBuilder.build() - clock.tick(BLUR_PERIOD_LENGTH) - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - closeForegroundPeriod() - clock.tick(BLUR_PERIOD_LENGTH) - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - closeForegroundPeriod() - clock.tick(BLUR_PERIOD_LENGTH) - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - }) - - it('isInForegroundAt should match the focused/burred period', () => { - // first blurred period - expect(isInForegroundAt(2 as RelativeTime)).toEqual(false) - - // first focused period - expect(isInForegroundAt(10 as RelativeTime)).toEqual(true) - - // second blurred period - expect(isInForegroundAt(17 as RelativeTime)).toEqual(false) - - // second focused period - expect(isInForegroundAt(25 as RelativeTime)).toEqual(true) - - // third blurred period - expect(isInForegroundAt(32 as RelativeTime)).toEqual(false) - - // current focused periods - expect(isInForegroundAt(42 as RelativeTime)).toEqual(true) - }) - - describe('selectInForegroundPeriodsFor', () => { - it('should have 3 in foreground periods for the whole period', () => { - const periods = selectInForegroundPeriodsFor(0 as RelativeTime, 50 as Duration) - - expect(periods).toHaveSize(3) - expect(periods[0]).toEqual({ - start: (5 * 1e6) as ServerDuration, - duration: (10 * 1e6) as ServerDuration, - }) - expect(periods[1]).toEqual({ - start: (20 * 1e6) as ServerDuration, - duration: (10 * 1e6) as ServerDuration, - }) - expect(periods[2]).toEqual({ - start: (35 * 1e6) as ServerDuration, - duration: (15 * 1e6) as ServerDuration, - }) - }) - - it('should have 2 in foreground periods when in between the two full periods', () => { - const periods = selectInForegroundPeriodsFor(10 as RelativeTime, 15 as Duration) - - expect(periods).toHaveSize(2) - expect(periods[0]).toEqual({ - start: 0 as ServerDuration, - duration: (5 * 1e6) as ServerDuration, - }) - expect(periods[1]).toEqual({ - start: (10 * 1e6) as ServerDuration, - duration: (5 * 1e6) as ServerDuration, - }) - }) - - it('should have 2 periods, when in between the the full period and ongoing periods', () => { - const periods = selectInForegroundPeriodsFor(25 as RelativeTime, 20 as Duration) - - expect(periods).toHaveSize(2) - expect(periods[0]).toEqual({ - start: 0 as ServerDuration, - duration: (5 * 1e6) as ServerDuration, - }) - expect(periods[1]).toEqual({ - start: (10 * 1e6) as ServerDuration, - duration: (10 * 1e6) as ServerDuration, - }) - }) - }) - }) - - describe('with one missing blur event. with two closed focus period every 5 seconds lasting 10 seconds', () => { - /* - events F F B - periods <------><-------> - time 0 5 10 15 20 25 - */ - beforeEach(() => { - const { clock } = setupBuilder.build() - clock.tick(BLUR_PERIOD_LENGTH) - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - closeForegroundPeriod() - clock.tick(BLUR_PERIOD_LENGTH) - }) - it('isInForegroundAt should match the focused/burred period', () => { - expect(isInForegroundAt(2 as RelativeTime)).toEqual(false) - expect(isInForegroundAt(10 as RelativeTime)).toEqual(true) - expect(isInForegroundAt(20 as RelativeTime)).toEqual(true) - expect(isInForegroundAt(30 as RelativeTime)).toEqual(false) - }) - }) - - it('should not be in foreground, when the periods is closed twice', () => { - const { clock } = setupBuilder.build() - addNewForegroundPeriod() - clock.tick(BLUR_PERIOD_LENGTH) - pageStateHistory.addPageState(PageState.PASSIVE) - - expect(isInForegroundAt(relativeNow())).toEqual(false) - }) - - it('after starting with a blur even, should not be in foreground', () => { - pageStateHistory.addPageState(PageState.PASSIVE) - - expect(isInForegroundAt(relativeNow())).toEqual(false) - }) - }) - - describe('when the page has focus when starting', () => { - beforeEach(() => { - spyOn(Document.prototype, 'hasFocus').and.callFake(() => true) - pageStateHistory = startPageStateHistory() - }) - - describe('when there is no focus event', () => { - it('should return true during the focused period', () => { - const { clock } = setupBuilder.build() - clock.tick(FOCUS_PERIOD_LENGTH) - closeForegroundPeriod() - - expect(isInForegroundAt(2 as RelativeTime)).toEqual(true) - }) - - it('should return false after the first focused period', () => { - const { clock } = setupBuilder.build() - clock.tick(FOCUS_PERIOD_LENGTH) - closeForegroundPeriod() - - expect(isInForegroundAt(12 as RelativeTime)).toEqual(false) - }) - }) - - describe('when still getting the first focus event and closing the first periods after 10 seconds', () => { - it('should return true during the focused period', () => { - const { clock } = setupBuilder.build() - clock.tick(FOCUS_PERIOD_LENGTH / 2) - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH / 2) - closeForegroundPeriod() - - expect(isInForegroundAt(2 as RelativeTime)).toEqual(true) - }) - - it('should return false after the focused period', () => { - const { clock } = setupBuilder.build() - clock.tick(FOCUS_PERIOD_LENGTH / 2) - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH / 2) - closeForegroundPeriod() - - expect(isInForegroundAt(12 as RelativeTime)).toEqual(false) - }) - }) - }) -}) diff --git a/packages/rum-core/src/domain/contexts/foregroundContexts.ts b/packages/rum-core/src/domain/contexts/foregroundContexts.ts deleted file mode 100644 index ce21728ea5..0000000000 --- a/packages/rum-core/src/domain/contexts/foregroundContexts.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { RelativeTime, Duration, ServerDuration } from '@datadog/browser-core' -import { toServerDuration } from '@datadog/browser-core' -import type { InForegroundPeriod, PageStateServerEntry } from '../../rawRumEvent.types' -import { PageState } from './pageStateHistory' - -export interface ForegroundPeriod { - start: RelativeTime - end?: RelativeTime -} - -// Todo: Remove in the next major release -export function mapToForegroundPeriods( - pageStateServerEntries: PageStateServerEntry[], - duration: Duration -): InForegroundPeriod[] { - const foregroundPeriods: InForegroundPeriod[] = [] - for (let i = 0; i < pageStateServerEntries.length; i++) { - const current = pageStateServerEntries[i] - const next = pageStateServerEntries[i + 1] - - if (current.state === PageState.ACTIVE) { - const start = current.start >= 0 ? current.start : (0 as ServerDuration) - const end = next ? next.start : toServerDuration(duration) - foregroundPeriods.push({ - start, - duration: (end - start) as ServerDuration, - }) - } - } - - return foregroundPeriods -} diff --git a/packages/rum-core/src/domain/contexts/pageStateHistory.ts b/packages/rum-core/src/domain/contexts/pageStateHistory.ts index 61e2fc517d..bc8ef7e69f 100644 --- a/packages/rum-core/src/domain/contexts/pageStateHistory.ts +++ b/packages/rum-core/src/domain/contexts/pageStateHistory.ts @@ -54,11 +54,7 @@ export function startPageStateHistory( DOM_EVENT.PAGE_HIDE, ], (event) => { - // Only get events fired by the browser to avoid false currentPageState changes done with custom events - // cf: developer extension auto flush: https://github.com/DataDog/browser-sdk/blob/2f72bf05a672794c9e33965351964382a94c72ba/developer-extension/src/panel/flushEvents.ts#L11-L12 - if (event.isTrusted) { - addPageState(computePageState(event), event.timeStamp as RelativeTime) - } + addPageState(computePageState(event), event.timeStamp as RelativeTime) }, { capture: true } ) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts index 9471c11ed8..b3a96df3a2 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts @@ -91,7 +91,6 @@ describe('actionCollection', () => { }, }) expect(rawRumEvents[0].domainContext).toEqual({ - event, events: [event], }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts index 06ddc3d3b1..7f7bb3dd74 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts @@ -104,7 +104,7 @@ function processAction( customerContext, rawRumEvent: actionEvent, startTime: action.startClocks.relative, - domainContext: isAutoAction(action) ? { event: action.event, events: action.events } : {}, + domainContext: isAutoAction(action) ? { events: action.events } : {}, } } diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts index 198d89a428..3d70e81dee 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts @@ -167,268 +167,195 @@ describe('trackClickActions', () => { expect(events[0].name).toBe('test-1') }) - describe('without tracking frustrations', () => { - it('discards any click action with a negative duration', () => { - const { clock } = setupBuilder.build() - emulateClick({ activity: { delay: -1 } }) - expect(findActionId()).not.toBeUndefined() - clock.tick(EXPIRE_DELAY) + it('discards any click action with a negative duration', () => { + const { clock } = setupBuilder.build() + emulateClick({ activity: { delay: -1 } }) + expect(findActionId()!.length).toEqual(2) + clock.tick(EXPIRE_DELAY) - expect(events).toEqual([]) - expect(findActionId()).toBeUndefined() - }) + expect(events).toEqual([]) + expect(findActionId()).toEqual([]) + }) - it('discards ongoing click action on view ended', () => { - const { lifeCycle, clock } = setupBuilder.build() - emulateClick({ activity: {} }) - expect(findActionId()).not.toBeUndefined() + it('ongoing click action is stopped on view end', () => { + const { lifeCycle, clock } = setupBuilder.build() + emulateClick({ activity: { delay: BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY } }) - lifeCycle.notify(LifeCycleEventType.VIEW_ENDED, { - endClocks: clocksNow(), - }) - clock.tick(EXPIRE_DELAY) + clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) - expect(events).toEqual([]) - expect(findActionId()).toBeUndefined() + lifeCycle.notify(LifeCycleEventType.VIEW_ENDED, { + endClocks: clocksNow(), }) - it('ignores any starting click action while another one is ongoing', () => { - const { clock } = setupBuilder.build() + expect(events.length).toBe(1) + expect(events[0].duration).toBe((2 * BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) as Duration) + }) + it('collect click actions even if another one is ongoing', () => { + const { clock } = setupBuilder.build() + + const firstPointerDownTimeStamp = timeStampNow() + emulateClick({ activity: {} }) + const secondPointerDownTimeStamp = timeStampNow() + emulateClick({ activity: {} }) + + clock.tick(EXPIRE_DELAY) + expect(events.length).toBe(2) + expect(events[0].startClocks.timeStamp).toBe(addDuration(firstPointerDownTimeStamp, EMULATED_CLICK_DURATION)) + expect(events[1].startClocks.timeStamp).toBe(addDuration(secondPointerDownTimeStamp, EMULATED_CLICK_DURATION)) + }) + + it('collect click actions even if nothing happens after a click (dead click)', () => { + const { clock } = setupBuilder.build() + emulateClick() + + clock.tick(EXPIRE_DELAY) + expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toEqual([FrustrationType.DEAD_CLICK]) + expect(findActionId()).toEqual([]) + }) + + it('does not set a duration for dead clicks', () => { + const { clock } = setupBuilder.build() + emulateClick() + + clock.tick(EXPIRE_DELAY) + expect(events.length).toBe(1) + expect(events[0].duration).toBeUndefined() + }) + + it('collect click actions even if it fails to find a name', () => { + const { clock } = setupBuilder.build() + emulateClick({ activity: {}, target: emptyElement }) + expect(findActionId()!.length).toBeGreaterThan(0) + clock.tick(EXPIRE_DELAY) + + expect(events.length).toBe(1) + }) + + describe('rage clicks', () => { + it('considers a chain of three clicks or more as a single action with "rage" frustration type', () => { + const { clock } = setupBuilder.build() const firstPointerDownTimeStamp = timeStampNow() - emulateClick({ activity: {} }) - emulateClick({ activity: {} }) + const activityDelay = 5 + emulateClick({ activity: { delay: activityDelay } }) + emulateClick({ activity: { delay: activityDelay } }) + emulateClick({ activity: { delay: activityDelay } }) clock.tick(EXPIRE_DELAY) expect(events.length).toBe(1) expect(events[0].startClocks.timeStamp).toBe(addDuration(firstPointerDownTimeStamp, EMULATED_CLICK_DURATION)) + expect(events[0].frustrationTypes).toEqual([FrustrationType.RAGE_CLICK]) + expect(events[0].duration).toBe( + (MAX_DURATION_BETWEEN_CLICKS + 2 * activityDelay + 2 * EMULATED_CLICK_DURATION) as Duration + ) }) - it('discards a click action when nothing happens after a click', () => { + it('should contain original events from of rage sequence', () => { const { clock } = setupBuilder.build() - emulateClick() + const activityDelay = 5 + emulateClick({ activity: { delay: activityDelay } }) + emulateClick({ activity: { delay: activityDelay } }) + emulateClick({ activity: { delay: activityDelay } }) clock.tick(EXPIRE_DELAY) - expect(events).toEqual([]) - expect(findActionId()).toBeUndefined() - }) - - it('ignores a click action if it fails to find a name', () => { - const { clock } = setupBuilder.build() - emulateClick({ activity: {}, target: emptyElement }) - expect(findActionId()).toBeUndefined() - clock.tick(EXPIRE_DELAY) - - expect(events).toEqual([]) + expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toEqual([FrustrationType.RAGE_CLICK]) + expect(events[0].events?.length).toBe(3) }) - it('does not populate the frustrationTypes array', () => { + it('aggregates frustrationTypes from all clicks', () => { const { lifeCycle, clock } = setupBuilder.build() + // Dead + emulateClick() + clock.tick(PAGE_ACTIVITY_VALIDATION_DELAY) + + // Error emulateClick({ activity: {} }) lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeErrorEvent()) + clock.tick(PAGE_ACTIVITY_VALIDATION_DELAY) + + // Third click to make a rage click + emulateClick({ activity: {} }) clock.tick(EXPIRE_DELAY) expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual([]) + expect(events[0].frustrationTypes).toEqual( + jasmine.arrayWithExactContents([ + FrustrationType.DEAD_CLICK, + FrustrationType.ERROR_CLICK, + FrustrationType.RAGE_CLICK, + ]) + ) }) }) - describe('when tracking frustrations', () => { - beforeEach(() => { - setupBuilder.withConfiguration({ trackFrustrations: true }) - }) - - it('discards any click action with a negative duration', () => { - const { clock } = setupBuilder.build() - emulateClick({ activity: { delay: -1 } }) - expect(findActionId()!.length).toEqual(2) - clock.tick(EXPIRE_DELAY) - - expect(events).toEqual([]) - expect(findActionId()).toEqual([]) - }) - - it('ongoing click action is stopped on view end', () => { + describe('error clicks', () => { + it('considers a "click with activity" followed by an error as a click action with "error" frustration type', () => { const { lifeCycle, clock } = setupBuilder.build() - emulateClick({ activity: { delay: BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY } }) - clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) - - lifeCycle.notify(LifeCycleEventType.VIEW_ENDED, { - endClocks: clocksNow(), - }) + emulateClick({ activity: {} }) + lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeErrorEvent()) + clock.tick(EXPIRE_DELAY) expect(events.length).toBe(1) - expect(events[0].duration).toBe((2 * BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) as Duration) + expect(events[0].frustrationTypes).toEqual([FrustrationType.ERROR_CLICK]) }) - it('collect click actions even if another one is ongoing', () => { - const { clock } = setupBuilder.build() + it('considers a "click without activity" followed by an error as a click action with "error" (and "dead") frustration type', () => { + const { lifeCycle, clock } = setupBuilder.build() - const firstPointerDownTimeStamp = timeStampNow() - emulateClick({ activity: {} }) - const secondPointerDownTimeStamp = timeStampNow() - emulateClick({ activity: {} }) + emulateClick() + lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeErrorEvent()) clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(2) - expect(events[0].startClocks.timeStamp).toBe(addDuration(firstPointerDownTimeStamp, EMULATED_CLICK_DURATION)) - expect(events[1].startClocks.timeStamp).toBe(addDuration(secondPointerDownTimeStamp, EMULATED_CLICK_DURATION)) + expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toEqual( + jasmine.arrayWithExactContents([FrustrationType.ERROR_CLICK, FrustrationType.DEAD_CLICK]) + ) }) + }) - it('collect click actions even if nothing happens after a click (dead click)', () => { + describe('dead clicks', () => { + it('considers a "click without activity" as a dead click', () => { const { clock } = setupBuilder.build() + emulateClick() clock.tick(EXPIRE_DELAY) expect(events.length).toBe(1) expect(events[0].frustrationTypes).toEqual([FrustrationType.DEAD_CLICK]) - expect(findActionId()).toEqual([]) }) - it('does not set a duration for dead clicks', () => { + it('does not consider a click with activity happening on pointerdown as a dead click', () => { const { clock } = setupBuilder.build() - emulateClick() - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].duration).toBeUndefined() - }) + emulateClick({ activity: { on: 'pointerdown' } }) - it('collect click actions even if it fails to find a name', () => { - const { clock } = setupBuilder.build() - emulateClick({ activity: {}, target: emptyElement }) - expect(findActionId()!.length).toBeGreaterThan(0) clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toEqual([]) }) - describe('rage clicks', () => { - it('considers a chain of three clicks or more as a single action with "rage" frustration type', () => { - const { clock } = setupBuilder.build() - const firstPointerDownTimeStamp = timeStampNow() - const activityDelay = 5 - emulateClick({ activity: { delay: activityDelay } }) - emulateClick({ activity: { delay: activityDelay } }) - emulateClick({ activity: { delay: activityDelay } }) - - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].startClocks.timeStamp).toBe(addDuration(firstPointerDownTimeStamp, EMULATED_CLICK_DURATION)) - expect(events[0].frustrationTypes).toEqual([FrustrationType.RAGE_CLICK]) - expect(events[0].duration).toBe( - (MAX_DURATION_BETWEEN_CLICKS + 2 * activityDelay + 2 * EMULATED_CLICK_DURATION) as Duration - ) - }) - - it('should contain original events from of rage sequence', () => { - const { clock } = setupBuilder.build() - const activityDelay = 5 - emulateClick({ activity: { delay: activityDelay } }) - emulateClick({ activity: { delay: activityDelay } }) - emulateClick({ activity: { delay: activityDelay } }) - - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual([FrustrationType.RAGE_CLICK]) - expect(events[0].events?.length).toBe(3) - }) - - it('aggregates frustrationTypes from all clicks', () => { - const { lifeCycle, clock } = setupBuilder.build() - - // Dead - emulateClick() - clock.tick(PAGE_ACTIVITY_VALIDATION_DELAY) - - // Error - emulateClick({ activity: {} }) - lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeErrorEvent()) - clock.tick(PAGE_ACTIVITY_VALIDATION_DELAY) - - // Third click to make a rage click - emulateClick({ activity: {} }) - - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual( - jasmine.arrayWithExactContents([ - FrustrationType.DEAD_CLICK, - FrustrationType.ERROR_CLICK, - FrustrationType.RAGE_CLICK, - ]) - ) - }) - }) - - describe('error clicks', () => { - it('considers a "click with activity" followed by an error as a click action with "error" frustration type', () => { - const { lifeCycle, clock } = setupBuilder.build() - - emulateClick({ activity: {} }) - lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeErrorEvent()) - - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual([FrustrationType.ERROR_CLICK]) - }) - - it('considers a "click without activity" followed by an error as a click action with "error" (and "dead") frustration type', () => { - const { lifeCycle, clock } = setupBuilder.build() + it('activity happening on pointerdown is not taken into account for the action duration', () => { + const { clock } = setupBuilder.build() - emulateClick() - lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, createFakeErrorEvent()) + emulateClick({ activity: { on: 'pointerdown' } }) - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual( - jasmine.arrayWithExactContents([FrustrationType.ERROR_CLICK, FrustrationType.DEAD_CLICK]) - ) - }) + clock.tick(EXPIRE_DELAY) + expect(events.length).toBe(1) + expect(events[0].duration).toBe(0 as Duration) }) - describe('dead clicks', () => { - it('considers a "click without activity" as a dead click', () => { - const { clock } = setupBuilder.build() - - emulateClick() - - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual([FrustrationType.DEAD_CLICK]) - }) - - it('does not consider a click with activity happening on pointerdown as a dead click', () => { - const { clock } = setupBuilder.build() - - emulateClick({ activity: { on: 'pointerdown' } }) - - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual([]) - }) - - it('activity happening on pointerdown is not taken into account for the action duration', () => { - const { clock } = setupBuilder.build() - - emulateClick({ activity: { on: 'pointerdown' } }) - - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].duration).toBe(0 as Duration) - }) - - it('does not consider a click with activity happening on pointerup as a dead click', () => { - const { clock } = setupBuilder.build() + it('does not consider a click with activity happening on pointerup as a dead click', () => { + const { clock } = setupBuilder.build() - emulateClick({ activity: { on: 'pointerup' } }) + emulateClick({ activity: { on: 'pointerup' } }) - clock.tick(EXPIRE_DELAY) - expect(events.length).toBe(1) - expect(events[0].frustrationTypes).toEqual([]) - }) + clock.tick(EXPIRE_DELAY) + expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toEqual([]) }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts index 357c8d6b4e..4b5a58fd76 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts @@ -82,7 +82,7 @@ export function trackClickActions( hadActivityOnPointerDown: () => boolean }>({ onPointerDown: (pointerDownEvent) => - processPointerDown(configuration, lifeCycle, domMutationObservable, history, pointerDownEvent), + processPointerDown(configuration, lifeCycle, domMutationObservable, pointerDownEvent), onPointerUp: ({ clickActionBase, hadActivityOnPointerDown }, startEvent, getUserActivity) => startClickAction( configuration, @@ -99,8 +99,7 @@ export function trackClickActions( }) const actionContexts: ActionContexts = { - findActionId: (startTime?: RelativeTime) => - configuration.trackFrustrations ? history.findAll(startTime) : history.find(startTime), + findActionId: (startTime?: RelativeTime) => history.findAll(startTime), } return { @@ -132,21 +131,9 @@ function processPointerDown( configuration: RumConfiguration, lifeCycle: LifeCycle, domMutationObservable: Observable, - history: ClickActionIdHistory, pointerDownEvent: MouseEventOnElement ) { - if (!configuration.trackFrustrations && history.find()) { - // TODO: remove this in a future major version. To keep retrocompatibility, ignore any new - // action if another one is already occurring. - return - } - const clickActionBase = computeClickActionBase(pointerDownEvent, configuration.actionNameAttribute) - if (!configuration.trackFrustrations && !clickActionBase.name) { - // TODO: remove this in a future major version. To keep retrocompatibility, ignore any action - // with a blank name - return - } let hadActivityOnPointerDown = false @@ -178,10 +165,7 @@ function startClickAction( hadActivityOnPointerDown: () => boolean ) { const click = newClick(lifeCycle, history, getUserActivity, clickActionBase, startEvent) - - if (configuration.trackFrustrations) { - appendClickToClickChain(click) - } + appendClickToClickChain(click) const { stop: stopWaitPageActivityEnd } = waitPageActivityEnd( lifeCycle, @@ -203,18 +187,6 @@ function startClickAction( } else { click.stop() } - - // Validate or discard the click only if we don't track frustrations. It'll be done when - // the click chain is finalized. - if (!configuration.trackFrustrations) { - if (!pageActivityEndEvent.hadActivity) { - // If we are not tracking frustrations, we should discard the click to keep backward - // compatibility. - click.discard() - } else { - click.validate() - } - } } }, CLICK_ACTION_MAX_DURATION diff --git a/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.spec.ts index c700d934e7..4d1643b5c8 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.spec.ts @@ -7,6 +7,7 @@ import { LifeCycleEventType } from '../../lifeCycle' import { startLongTaskCollection } from './longTaskCollection' const LONG_TASK: RumPerformanceLongTaskTiming = { + name: 'self', duration: 100 as Duration, entryType: 'longtask', startTime: 1234 as RelativeTime, @@ -18,12 +19,15 @@ const LONG_TASK: RumPerformanceLongTaskTiming = { describe('long task collection', () => { let setupBuilder: TestSetupBuilder let sessionManager: RumSessionManagerMock + let trackLongTasks: boolean + beforeEach(() => { + trackLongTasks = true sessionManager = createRumSessionManagerMock() setupBuilder = setup() .withSessionManager(sessionManager) - .beforeBuild(({ lifeCycle, sessionManager }) => { - startLongTaskCollection(lifeCycle, sessionManager) + .beforeBuild(({ lifeCycle, sessionManager, configuration }) => { + startLongTaskCollection(lifeCycle, { ...configuration, trackLongTasks }, sessionManager) }) }) @@ -43,16 +47,20 @@ describe('long task collection', () => { expect(rawRumEvents.length).toBe(1) }) - it('should only collect when session allows long tasks', () => { + it('should collect when trackLongTasks=true', () => { + trackLongTasks = true const { lifeCycle, rawRumEvents } = setupBuilder.build() - sessionManager.setLongTaskAllowed(true) lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK]) expect(rawRumEvents.length).toBe(1) + }) + + it('should not collect when trackLongTasks=false', () => { + trackLongTasks = false + const { lifeCycle, rawRumEvents } = setupBuilder.build() - sessionManager.setLongTaskAllowed(false) lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK]) - expect(rawRumEvents.length).toBe(1) + expect(rawRumEvents.length).toBe(0) }) it('should create raw rum event from performance entry', () => { @@ -72,7 +80,13 @@ describe('long task collection', () => { }, }) expect(rawRumEvents[0].domainContext).toEqual({ - performanceEntry: { name: 'self', duration: 100, entryType: 'longtask', startTime: 1234 }, + performanceEntry: { + name: 'self', + duration: 100, + entryType: 'longtask', + startTime: 1234, + toJSON: jasmine.any(Function), + }, }) }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.ts index a9cba29c9a..f1916c4ea3 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/longTask/longTaskCollection.ts @@ -4,15 +4,20 @@ import { RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' import type { RumSessionManager } from '../../rumSessionManager' +import type { RumConfiguration } from '../../configuration' -export function startLongTaskCollection(lifeCycle: LifeCycle, sessionManager: RumSessionManager) { +export function startLongTaskCollection( + lifeCycle: LifeCycle, + configuration: RumConfiguration, + sessionManager: RumSessionManager +) { lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => { for (const entry of entries) { if (entry.entryType !== 'longtask') { break } const session = sessionManager.findTrackedSession(entry.startTime) - if (!session || !session.longTaskAllowed) { + if (!session || !configuration.trackLongTasks) { break } const startClocks = relativeToClocks(entry.startTime) @@ -30,7 +35,7 @@ export function startLongTaskCollection(lifeCycle: LifeCycle, sessionManager: Ru lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, { rawRumEvent, startTime: startClocks.relative, - domainContext: { performanceEntry: entry.toJSON() }, + domainContext: { performanceEntry: entry }, }) } }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.spec.ts index 35c895e6b0..1ec2b64976 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.spec.ts @@ -21,17 +21,14 @@ import { startResourceCollection } from './resourceCollection' describe('resourceCollection', () => { let setupBuilder: TestSetupBuilder + let trackResources: boolean let pageStateHistorySpy: jasmine.Spy beforeEach(() => { - setupBuilder = setup().beforeBuild(({ lifeCycle, sessionManager, pageStateHistory }) => { + trackResources = true + setupBuilder = setup().beforeBuild(({ lifeCycle, sessionManager, pageStateHistory, configuration }) => { pageStateHistorySpy = spyOn(pageStateHistory, 'findAll') - startResourceCollection( - lifeCycle, - validateAndBuildRumConfiguration({ clientToken: 'xxx', applicationId: 'xxx' })!, - sessionManager, - pageStateHistory - ) + startResourceCollection(lifeCycle, { ...configuration, trackResources }, sessionManager, pageStateHistory) }) }) @@ -135,8 +132,7 @@ describe('resourceCollection', () => { expect(rawRumResourceEventEntry._dd.page_states).toEqual(jasmine.objectContaining(mockPageStates)) }) - it('should not have a duration if a frozen state happens during the request and no performance entry matches when NO_RESOURCE_DURATION_FROZEN_STATE enabled', () => { - addExperimentalFeatures([ExperimentalFeature.NO_RESOURCE_DURATION_FROZEN_STATE]) + it('should not have a duration if a frozen state happens during the request and no performance entry matches', () => { const { lifeCycle, rawRumEvents } = setupBuilder.build() const mockPageStates = [{ state: PageState.FROZEN, startTime: 0 as RelativeTime }] const mockXHR = createCompletedRequest() @@ -149,19 +145,6 @@ describe('resourceCollection', () => { expect(rawRumResourceEventFetch.resource.duration).toBeUndefined() }) - it('should have a duration if a frozen state happens during the request and no performance entry matches when NO_RESOURCE_DURATION_FROZEN_STATE disabled', () => { - const { lifeCycle, rawRumEvents } = setupBuilder.build() - const mockPageStates = [{ state: PageState.FROZEN, startTime: 0 as RelativeTime }] - const mockXHR = createCompletedRequest() - - pageStateHistorySpy.and.returnValue(mockPageStates) - - lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, mockXHR) - - const rawRumResourceEventFetch = rawRumEvents[0].rawRumEvent as RawRumResourceEvent - expect(rawRumResourceEventFetch.resource.duration).toBeDefined() - }) - it('should not collect page states on resources when ff resource_page_states disabled', () => { const { lifeCycle, rawRumEvents } = setupBuilder.build() const mockPageStates = [{ state: PageState.ACTIVE, startTime: 0 as RelativeTime }] @@ -176,7 +159,6 @@ describe('resourceCollection', () => { const rawRumResourceEventFetch = rawRumEvents[0].rawRumEvent as RawRumResourceEvent const rawRumResourceEventEntry = rawRumEvents[1].rawRumEvent as RawRumResourceEvent - expect(pageStateHistorySpy).not.toHaveBeenCalled() expect(rawRumResourceEventFetch._dd.page_states).not.toBeDefined() expect(rawRumResourceEventEntry._dd.page_states).not.toBeDefined() }) @@ -394,8 +376,8 @@ describe('resourceCollection', () => { expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd.discarded).toBeTrue() }) - it('should be discarded=true if session does not allow resources', () => { - setupBuilder.withSessionManager(createRumSessionManagerMock().setResourceAllowed(false)) + it('should be discarded=true when trackResources is disabled', () => { + trackResources = false const { lifeCycle, rawRumEvents } = setupBuilder.build() lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [createResourceEntry()]) @@ -403,8 +385,8 @@ describe('resourceCollection', () => { expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd.discarded).toBeTrue() }) - it('should be discarded=false if session allows resources', () => { - setupBuilder.withSessionManager(createRumSessionManagerMock().setResourceAllowed(true)) + it('should be discarded=false when trackResources is enabled', () => { + trackResources = true const { lifeCycle, rawRumEvents } = setupBuilder.build() lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [createResourceEntry()]) diff --git a/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.ts index a62a4f801e..b79916913d 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/resource/resourceCollection.ts @@ -12,12 +12,8 @@ import { } from '@datadog/browser-core' import type { ClocksState, Duration } from '@datadog/browser-core' import type { RumConfiguration } from '../../configuration' -import type { RumPerformanceEntry, RumPerformanceResourceTiming } from '../../../browser/performanceCollection' -import type { - PerformanceEntryRepresentation, - RumXhrResourceEventDomainContext, - RumFetchResourceEventDomainContext, -} from '../../../domainContext.types' +import type { RumPerformanceResourceTiming } from '../../../browser/performanceCollection' +import type { RumXhrResourceEventDomainContext, RumFetchResourceEventDomainContext } from '../../../domainContext.types' import type { RawRumResourceEvent } from '../../../rawRumEvent.types' import { RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle, RawRumEventCollectedData } from '../../lifeCycle' @@ -73,7 +69,7 @@ function processRequest( const correspondingTimingOverrides = matchingTiming ? computePerformanceEntryMetrics(matchingTiming) : undefined const tracingInfo = computeRequestTracingInfo(request, configuration) - const indexingInfo = computeIndexingInfo(sessionManager, startClocks) + const indexingInfo = computeIndexingInfo(configuration, sessionManager, startClocks) const duration = computeRequestDuration(pageStateHistory, startClocks, request.duration) const pageStateInfo = computePageStateInfo( @@ -105,7 +101,7 @@ function processRequest( startTime: startClocks.relative, rawRumEvent: resourceEvent, domainContext: { - performanceEntry: matchingTiming && toPerformanceEntryRepresentation(matchingTiming), + performanceEntry: matchingTiming, xhr: request.xhr, response: request.response, requestInput: request.input, @@ -126,7 +122,7 @@ function processResourceEntry( const startClocks = relativeToClocks(entry.startTime) const tracingInfo = computeEntryTracingInfo(entry, configuration) - const indexingInfo = computeIndexingInfo(sessionManager, startClocks) + const indexingInfo = computeIndexingInfo(configuration, sessionManager, startClocks) const pageStateInfo = computePageStateInfo(pageStateHistory, startClocks, entry.duration) const resourceEvent = combine( @@ -148,7 +144,7 @@ function processResourceEntry( startTime: startClocks.relative, rawRumEvent: resourceEvent, domainContext: { - performanceEntry: toPerformanceEntryRepresentation(entry), + performanceEntry: entry, }, } } @@ -192,11 +188,6 @@ function computeEntryTracingInfo(entry: RumPerformanceResourceTiming, configurat } } -// TODO next major: use directly PerformanceEntry type in domain context -function toPerformanceEntryRepresentation(entry: RumPerformanceEntry): PerformanceEntryRepresentation { - return entry as PerformanceEntryRepresentation -} - /** * @returns number between 0 and 1 which represents trace sample rate */ @@ -204,11 +195,15 @@ function getRulePsr(configuration: RumConfiguration) { return isNumber(configuration.traceSampleRate) ? configuration.traceSampleRate / 100 : undefined } -function computeIndexingInfo(sessionManager: RumSessionManager, resourceStart: ClocksState) { +function computeIndexingInfo( + configuration: RumConfiguration, + sessionManager: RumSessionManager, + resourceStart: ClocksState +) { const session = sessionManager.findTrackedSession(resourceStart.relative) return { _dd: { - discarded: !session || !session.resourceAllowed, + discarded: !session || !configuration.trackResources, }, } } @@ -227,11 +222,6 @@ function computePageStateInfo(pageStateHistory: PageStateHistory, startClocks: C } function computeRequestDuration(pageStateHistory: PageStateHistory, startClocks: ClocksState, duration: Duration) { - // TODO remove FF in next major - if (!isExperimentalFeatureEnabled(ExperimentalFeature.NO_RESOURCE_DURATION_FROZEN_STATE)) { - return toServerDuration(duration) - } - const requestCrossedFrozenState = pageStateHistory .findAll(startClocks.relative, duration) ?.some((pageState) => pageState.state === PageState.FROZEN) 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 998c22d7ec..56e605a89a 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts @@ -1,5 +1,5 @@ import type { Duration, RelativeTime, ServerDuration, TimeStamp } from '@datadog/browser-core' -import { resetExperimentalFeatures, ExperimentalFeature, addExperimentalFeatures } from '@datadog/browser-core' +import { resetExperimentalFeatures } from '@datadog/browser-core' import type { RecorderApi } from '../../../boot/rumPublicApi' import type { TestSetupBuilder } from '../../../../test' import { setup, noopRecorderApi } from '../../../../test' @@ -105,7 +105,10 @@ describe('viewCollection', () => { _dd: { document_version: 3, replay_stats: undefined, - page_states: undefined, + page_states: [ + { start: 0 as ServerDuration, state: PageState.ACTIVE }, + { start: 10 as ServerDuration, state: PageState.PASSIVE }, + ], }, date: jasmine.any(Number), type: RumEventType.VIEW, @@ -144,7 +147,6 @@ describe('viewCollection', () => { count: 10, }, time_spent: (100 * 1e6) as ServerDuration, - in_foreground_periods: [{ start: 0 as ServerDuration, duration: 10 as ServerDuration }], }, session: { has_replay: undefined, @@ -159,7 +161,7 @@ describe('viewCollection', () => { max_depth_scroll_top: 1000, }, }, - privacy: { replay_level: 'mask-user-input' }, + privacy: { replay_level: 'mask' }, }) }) @@ -207,25 +209,6 @@ describe('viewCollection', () => { expect(rawRumViewEvent.view.loading_time).toBeUndefined() }) - it('should include page_states but not in_foreground_periods when PAGE_STATES ff is enabled', () => { - addExperimentalFeatures([ExperimentalFeature.PAGE_STATES]) - const { lifeCycle, rawRumEvents } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, VIEW) - const rawRumViewEvent = rawRumEvents[rawRumEvents.length - 1].rawRumEvent as RawRumViewEvent - - expect(rawRumViewEvent._dd.page_states).toBeDefined() - expect(rawRumViewEvent.view.in_foreground_periods).toBeUndefined() - }) - - it('should include in_foreground_periods but not page_states when PAGE_STATES ff is disabled', () => { - const { lifeCycle, rawRumEvents } = setupBuilder.build() - lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, VIEW) - const rawRumViewEvent = rawRumEvents[rawRumEvents.length - 1].rawRumEvent as RawRumViewEvent - - expect(rawRumViewEvent._dd.page_states).toBeUndefined() - expect(rawRumViewEvent.view.in_foreground_periods).toBeDefined() - }) - it('should not include scroll metrics when there are not scroll metrics in the raw event', () => { const { lifeCycle, rawRumEvents } = setupBuilder.build() lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, { ...VIEW, scrollMetrics: undefined }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index ee65a41084..40416fcf61 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -1,18 +1,10 @@ import type { Duration, ServerDuration, Observable } from '@datadog/browser-core' -import { - isExperimentalFeatureEnabled, - ExperimentalFeature, - isEmptyObject, - mapValues, - toServerDuration, - isNumber, -} from '@datadog/browser-core' +import { isEmptyObject, mapValues, toServerDuration, isNumber } from '@datadog/browser-core' import type { RecorderApi } from '../../../boot/rumPublicApi' import type { RawRumViewEvent } from '../../../rawRumEvent.types' import { RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle, RawRumEventCollectedData } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' -import { mapToForegroundPeriods } from '../../contexts/foregroundContexts' import type { LocationChange } from '../../../browser/locationChangeObservable' import type { RumConfiguration } from '../../configuration' import type { FeatureFlagContexts } from '../../contexts/featureFlagContext' @@ -60,13 +52,12 @@ function processViewUpdate( ): RawRumEventCollectedData { const replayStats = recorderApi.getReplayStats(view.id) const featureFlagContext = featureFlagContexts.findFeatureFlagEvaluations(view.startClocks.relative) - const pageStatesEnabled = isExperimentalFeatureEnabled(ExperimentalFeature.PAGE_STATES) const pageStates = pageStateHistory.findAll(view.startClocks.relative, view.duration) const viewEvent: RawRumViewEvent = { _dd: { document_version: view.documentVersion, replay_stats: replayStats, - page_states: pageStatesEnabled ? pageStates : undefined, + page_states: pageStates, }, date: view.startClocks.timeStamp, type: RumEventType.VIEW, @@ -101,8 +92,6 @@ function processViewUpdate( count: view.eventCounts.resourceCount, }, time_spent: toServerDuration(view.duration), - in_foreground_periods: - !pageStatesEnabled && pageStates ? mapToForegroundPeriods(pageStates, view.duration) : undefined, // Todo: Remove in the next major release }, feature_flags: featureFlagContext && !isEmptyObject(featureFlagContext) ? featureFlagContext : undefined, display: view.scrollMetrics diff --git a/packages/rum-core/src/domain/rumSessionManager.spec.ts b/packages/rum-core/src/domain/rumSessionManager.spec.ts index 7199060a2e..e53baff3ab 100644 --- a/packages/rum-core/src/domain/rumSessionManager.spec.ts +++ b/packages/rum-core/src/domain/rumSessionManager.spec.ts @@ -7,9 +7,10 @@ import { setCookie, stopSessionManager, ONE_SECOND, + DOM_EVENT, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { mockClock } from '@datadog/browser-core/test' +import { createNewEvent, mockClock } from '@datadog/browser-core/test' import type { RumConfiguration } from './configuration' import { validateAndBuildRumConfiguration } from './configuration' @@ -134,7 +135,7 @@ describe('rum session manager', () => { clock.tick(STORAGE_POLL_DELAY) setupDraws({ tracked: true, trackedWithSessionReplay: true }) - document.dispatchEvent(new CustomEvent('click')) + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(expireSessionSpy).toHaveBeenCalled() expect(renewSessionSpy).toHaveBeenCalled() @@ -190,172 +191,33 @@ describe('rum session manager', () => { describe('session behaviors', () => { ;[ - { - description: - 'WITH_SESSION_REPLAY plan without trackResources/LongTasks should have replay, no resources and no long tasks', - trackedWithSessionReplay: true, - oldPlansBehavior: false, - trackResources: undefined, - trackLongTasks: undefined, - expectSessionReplay: true, - expectResources: false, - expectLongTasks: false, - }, - { - description: - 'WITHOUT_SESSION_REPLAY plan without trackResources/LongTasks should have no replay, no resources and no long tasks', - trackedWithSessionReplay: false, - oldPlansBehavior: false, - trackResources: undefined, - trackLongTasks: undefined, - expectSessionReplay: false, - expectResources: false, - expectLongTasks: false, - }, { description: 'WITH_SESSION_REPLAY plan with trackResources/LongTasks=false should have replay, no resources and no long tasks', trackedWithSessionReplay: true, - oldPlansBehavior: false, - trackResources: false, - trackLongTasks: false, expectSessionReplay: true, - expectResources: false, - expectLongTasks: false, }, { description: 'WITHOUT_SESSION_REPLAY plan with trackResources/LongTasks=false should have no replay, no resources and no long tasks', trackedWithSessionReplay: false, - oldPlansBehavior: false, - trackResources: false, - trackLongTasks: false, - expectSessionReplay: false, - expectResources: false, - expectLongTasks: false, - }, - { - description: - 'WITH_SESSION_REPLAY plan with trackResources/LongTasks=true should have replay, resources and long tasks', - trackedWithSessionReplay: true, - oldPlansBehavior: true, - trackResources: true, - trackLongTasks: true, - expectSessionReplay: true, - expectResources: true, - expectLongTasks: true, - }, - { - description: - 'WITHOUT_SESSION_REPLAY plan with trackResources/LongTasks=true should have no replay, resources and long tasks', - trackedWithSessionReplay: false, - oldPlansBehavior: false, - trackResources: true, - trackLongTasks: true, expectSessionReplay: false, - expectResources: true, - expectLongTasks: true, - }, - { - description: - 'old WITH_SESSION_REPLAY plan without trackResources/LongTasks should have replay, resources and long tasks', - trackedWithSessionReplay: true, - oldPlansBehavior: true, - trackResources: undefined, - trackLongTasks: undefined, - expectSessionReplay: true, - expectResources: true, - expectLongTasks: true, - }, - { - description: - 'old WITHOUT_SESSION_REPLAY plan without trackResources/LongTasks should have no replay, no resources and no long tasks', - trackedWithSessionReplay: false, - oldPlansBehavior: true, - trackResources: undefined, - trackLongTasks: undefined, - expectSessionReplay: false, - expectResources: false, - expectLongTasks: false, - }, - { - description: - 'old WITH_SESSION_REPLAY plan with trackResources/LongTasks=false should have replay, no resources and no long tasks', - trackedWithSessionReplay: true, - oldPlansBehavior: true, - trackResources: false, - trackLongTasks: false, - expectSessionReplay: true, - expectResources: false, - expectLongTasks: false, - }, - { - description: - 'old WITHOUT_SESSION_REPLAY plan with trackResources/LongTasks=false should have no replay, no resources and no long tasks', - trackedWithSessionReplay: false, - oldPlansBehavior: true, - trackResources: false, - trackLongTasks: false, - expectSessionReplay: false, - expectResources: false, - expectLongTasks: false, - }, - { - description: - 'old WITH_SESSION_REPLAY plan with trackResources/LongTasks=true should have replay, resources and long tasks', - trackedWithSessionReplay: true, - oldPlansBehavior: true, - trackResources: true, - trackLongTasks: true, - expectSessionReplay: true, - expectResources: true, - expectLongTasks: true, - }, - { - description: - 'old WITHOUT_SESSION_REPLAY plan with trackResources/LongTasks=true should have no replay, resources and long tasks', - trackedWithSessionReplay: false, - oldPlansBehavior: true, - trackResources: true, - trackLongTasks: true, - expectSessionReplay: false, - expectResources: true, - expectLongTasks: true, }, ].forEach( ({ description, trackedWithSessionReplay, - oldPlansBehavior, - trackResources, - trackLongTasks, expectSessionReplay, - expectResources, - expectLongTasks, }: { description: string trackedWithSessionReplay: boolean - oldPlansBehavior: boolean - trackResources: boolean | undefined - trackLongTasks: boolean | undefined expectSessionReplay: boolean - expectResources: boolean - expectLongTasks: boolean }) => { it(description, () => { - configuration = { - ...configuration, - trackResources, - trackLongTasks, - oldPlansBehavior, - } - setupDraws({ tracked: true, trackedWithSessionReplay }) const rumSessionManager = startRumSessionManager(configuration, lifeCycle) expect(rumSessionManager.findTrackedSession()!.sessionReplayAllowed).toBe(expectSessionReplay) - expect(rumSessionManager.findTrackedSession()!.resourceAllowed).toBe(expectResources) - expect(rumSessionManager.findTrackedSession()!.longTaskAllowed).toBe(expectLongTasks) }) } ) diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index b232475695..e3e2009f49 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -16,8 +16,6 @@ export type RumSession = { id: string plan: RumSessionPlan sessionReplayAllowed: boolean - longTaskAllowed: boolean - resourceAllowed: boolean } export const enum RumSessionPlan { @@ -28,7 +26,7 @@ export const enum RumSessionPlan { export const enum RumTrackingType { NOT_TRACKED = '0', // Note: the "tracking type" value (stored in the session cookie) does not match the "session - // plan" value (sent in RUM events). This is expected, and was done to keep retrocompatibility + // plan" value (sent in RUM events). This is expected, and was done to keep backward-compatibility // with active sessions when upgrading the SDK. TRACKED_WITH_SESSION_REPLAY = '1', TRACKED_WITHOUT_SESSION_REPLAY = '2', @@ -64,14 +62,6 @@ export function startRumSessionManager(configuration: RumConfiguration, lifeCycl id: session.id, plan, sessionReplayAllowed: plan === RumSessionPlan.WITH_SESSION_REPLAY, - longTaskAllowed: - configuration.trackLongTasks !== undefined - ? configuration.trackLongTasks - : configuration.oldPlansBehavior && plan === RumSessionPlan.WITH_SESSION_REPLAY, - resourceAllowed: - configuration.trackResources !== undefined - ? configuration.trackResources - : configuration.oldPlansBehavior && plan === RumSessionPlan.WITH_SESSION_REPLAY, } }, expire: sessionManager.expire, @@ -87,8 +77,6 @@ export function startRumSessionManagerStub(): RumSessionManager { id: '00000000-aaaa-0000-aaaa-000000000000', plan: RumSessionPlan.WITHOUT_SESSION_REPLAY, // plan value should not be taken into account for mobile sessionReplayAllowed: false, - longTaskAllowed: true, - resourceAllowed: true, } return { findTrackedSession: () => session, diff --git a/packages/rum-core/src/domain/startCustomerDataTelemetry.spec.ts b/packages/rum-core/src/domain/startCustomerDataTelemetry.spec.ts index 37e43b21fa..55b96a857c 100644 --- a/packages/rum-core/src/domain/startCustomerDataTelemetry.spec.ts +++ b/packages/rum-core/src/domain/startCustomerDataTelemetry.spec.ts @@ -43,7 +43,7 @@ describe('customerDataTelemetry', () => { } function spyOnContextManager(contextManager: ContextManager) { - spyOn(contextManager, 'get').and.callFake(() => fakeContext) + spyOn(contextManager, 'getContext').and.callFake(() => fakeContext) spyOn(contextManager, 'getBytesCount').and.callFake(() => fakeContextBytesCount) } diff --git a/packages/rum-core/src/domain/startCustomerDataTelemetry.ts b/packages/rum-core/src/domain/startCustomerDataTelemetry.ts index 3158019e1e..a0c58aa842 100644 --- a/packages/rum-core/src/domain/startCustomerDataTelemetry.ts +++ b/packages/rum-core/src/domain/startCustomerDataTelemetry.ts @@ -57,12 +57,12 @@ export function startCustomerDataTelemetry( batchHasRumEvent = true updateMeasure( currentBatchMeasures.globalContextBytes, - !isEmptyObject(globalContextManager.get()) ? globalContextManager.getBytesCount() : 0 + !isEmptyObject(globalContextManager.getContext()) ? globalContextManager.getBytesCount() : 0 ) updateMeasure( currentBatchMeasures.userContextBytes, - !isEmptyObject(userContextManager.get()) ? userContextManager.getBytesCount() : 0 + !isEmptyObject(userContextManager.getContext()) ? userContextManager.getBytesCount() : 0 ) const featureFlagContext = featureFlagContexts.findFeatureFlagEvaluations() diff --git a/packages/rum-core/src/domainContext.types.ts b/packages/rum-core/src/domainContext.types.ts index 3b8bd2a2df..c4d390291a 100644 --- a/packages/rum-core/src/domainContext.types.ts +++ b/packages/rum-core/src/domainContext.types.ts @@ -21,10 +21,6 @@ export interface RumViewEventDomainContext { } export interface RumActionEventDomainContext { - /** - * @deprecated use events array instead - */ - event?: Event events?: Event[] } @@ -33,16 +29,16 @@ export interface RumFetchResourceEventDomainContext { requestInput: RequestInfo response?: Response error?: Error - performanceEntry?: PerformanceEntryRepresentation + performanceEntry?: PerformanceEntry } export interface RumXhrResourceEventDomainContext { xhr: XMLHttpRequest - performanceEntry?: PerformanceEntryRepresentation + performanceEntry?: PerformanceEntry } export interface RumOtherResourceEventDomainContext { - performanceEntry: PerformanceEntryRepresentation + performanceEntry: PerformanceEntry } export interface RumErrorEventDomainContext { @@ -50,12 +46,5 @@ export interface RumErrorEventDomainContext { } export interface RumLongTaskEventDomainContext { - performanceEntry: PerformanceEntryRepresentation + performanceEntry: PerformanceEntry } - -/** - * Symbolizes the type of the value returned by performanceEntry.toJSON(). Can also be built - * manually to represent other kind of performance entries (ex: initial_document) or polyfilled - * based on `performance.timing`. - */ -export type PerformanceEntryRepresentation = Omit diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 81951e31d9..f59baf51de 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -101,7 +101,6 @@ export interface RawRumViewEvent { long_task: Count resource: Count frustration: Count - in_foreground_periods?: InForegroundPeriod[] } session: { has_replay: true | undefined @@ -128,11 +127,6 @@ interface ViewDisplay { } } -export interface InForegroundPeriod { - start: ServerDuration - duration: ServerDuration -} - export type PageStateServerEntry = { state: PageState; start: ServerDuration } export const enum ViewLoadingType { diff --git a/packages/rum-core/test/fixtures.ts b/packages/rum-core/test/fixtures.ts index deb22f9417..f186b71297 100644 --- a/packages/rum-core/test/fixtures.ts +++ b/packages/rum-core/test/fixtures.ts @@ -120,6 +120,6 @@ export function createResourceEntry( startTime: 200 as RelativeTime, ...overrides, } - entry.toJSON = () => entry + entry.toJSON = () => entry as RumPerformanceResourceTiming & PerformanceResourceTiming return entry as RumPerformanceResourceTiming & PerformanceResourceTiming } diff --git a/packages/rum-core/test/mockRumSessionManager.ts b/packages/rum-core/test/mockRumSessionManager.ts index eb8c78df0c..9966b5b17e 100644 --- a/packages/rum-core/test/mockRumSessionManager.ts +++ b/packages/rum-core/test/mockRumSessionManager.ts @@ -7,8 +7,6 @@ export interface RumSessionManagerMock extends RumSessionManager { setNotTracked(): RumSessionManagerMock setPlanWithoutSessionReplay(): RumSessionManagerMock setPlanWithSessionReplay(): RumSessionManagerMock - setLongTaskAllowed(longTaskAllowed: boolean): RumSessionManagerMock - setResourceAllowed(resourceAllowed: boolean): RumSessionManagerMock } const DEFAULT_ID = 'session-id' @@ -22,8 +20,6 @@ const enum SessionStatus { export function createRumSessionManagerMock(): RumSessionManagerMock { let id = DEFAULT_ID let sessionStatus: SessionStatus = SessionStatus.TRACKED_WITH_SESSION_REPLAY - let resourceAllowed = true - let longTaskAllowed = true return { findTrackedSession() { if ( @@ -39,8 +35,6 @@ export function createRumSessionManagerMock(): RumSessionManagerMock { ? RumSessionPlan.WITH_SESSION_REPLAY : RumSessionPlan.WITHOUT_SESSION_REPLAY, sessionReplayAllowed: sessionStatus === SessionStatus.TRACKED_WITH_SESSION_REPLAY, - longTaskAllowed, - resourceAllowed, } }, expire() { @@ -64,13 +58,5 @@ export function createRumSessionManagerMock(): RumSessionManagerMock { sessionStatus = SessionStatus.TRACKED_WITH_SESSION_REPLAY return this }, - setLongTaskAllowed(isAllowed: boolean) { - longTaskAllowed = isAllowed - return this - }, - setResourceAllowed(isAllowed: boolean) { - resourceAllowed = isAllowed - return this - }, } } diff --git a/packages/rum-core/test/testSetupBuilder.ts b/packages/rum-core/test/testSetupBuilder.ts index 48e360522a..ef5a2518f4 100644 --- a/packages/rum-core/test/testSetupBuilder.ts +++ b/packages/rum-core/test/testSetupBuilder.ts @@ -103,7 +103,12 @@ export function setup(): TestSetupBuilder { } const FAKE_APP_ID = 'appId' const configuration: RumConfiguration = { - ...validateAndBuildRumConfiguration({ clientToken: 'xxx', applicationId: FAKE_APP_ID })!, + ...validateAndBuildRumConfiguration({ + clientToken: 'xxx', + applicationId: FAKE_APP_ID, + trackResources: true, + trackLongTasks: true, + })!, ...SPEC_ENDPOINTS, } diff --git a/packages/rum-slim/package.json b/packages/rum-slim/package.json index af8192b51d..6dab862c78 100644 --- a/packages/rum-slim/package.json +++ b/packages/rum-slim/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-slim", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "license": "Apache-2.0", "main": "cjs/entries/main.js", "module": "esm/entries/main.js", @@ -12,11 +12,11 @@ "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json" }, "dependencies": { - "@datadog/browser-core": "4.45.0", - "@datadog/browser-rum-core": "4.45.0" + "@datadog/browser-core": "5.0.0-alpha.0", + "@datadog/browser-rum-core": "5.0.0-alpha.0" }, "peerDependencies": { - "@datadog/browser-logs": "4.45.0" + "@datadog/browser-logs": "5.0.0-alpha.0" }, "peerDependenciesMeta": { "@datadog/browser-logs": { diff --git a/packages/rum/package.json b/packages/rum/package.json index fdf224600a..e05ab8e61a 100644 --- a/packages/rum/package.json +++ b/packages/rum/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "license": "Apache-2.0", "main": "cjs/entries/main.js", "module": "esm/entries/main.js", @@ -12,12 +12,12 @@ "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json" }, "dependencies": { - "@datadog/browser-core": "4.45.0", - "@datadog/browser-rum-core": "4.45.0", - "@datadog/browser-worker": "4.45.0" + "@datadog/browser-core": "5.0.0-alpha.0", + "@datadog/browser-rum-core": "5.0.0-alpha.0", + "@datadog/browser-worker": "5.0.0-alpha.0" }, "peerDependencies": { - "@datadog/browser-logs": "4.45.0" + "@datadog/browser-logs": "5.0.0-alpha.0" }, "peerDependenciesMeta": { "@datadog/browser-logs": { diff --git a/packages/rum/src/boot/recorderApi.spec.ts b/packages/rum/src/boot/recorderApi.spec.ts index 77598cff30..3be3b0a583 100644 --- a/packages/rum/src/boot/recorderApi.spec.ts +++ b/packages/rum/src/boot/recorderApi.spec.ts @@ -32,11 +32,13 @@ describe('makeRecorderApi', () => { } let rumInit: () => void + let startSessionReplayRecordingManually: boolean beforeEach(() => { if (isIE()) { pending('IE not supported') } + startSessionReplayRecordingManually = false setupBuilder = setup().beforeBuild(({ lifeCycle, sessionManager }) => { stopRecordingSpy = jasmine.createSpy('stopRecording') startRecordingSpy = jasmine.createSpy('startRecording').and.callFake(() => ({ @@ -46,7 +48,12 @@ describe('makeRecorderApi', () => { startDeflateWorkerWith(FAKE_WORKER) recorderApi = makeRecorderApi(startRecordingSpy, startDeflateWorkerSpy) rumInit = () => { - recorderApi.onRumStart(lifeCycle, {} as RumConfiguration, sessionManager, {} as ViewContexts) + recorderApi.onRumStart( + lifeCycle, + { startSessionReplayRecordingManually } as RumConfiguration, + sessionManager, + {} as ViewContexts + ) } }) }) @@ -55,26 +62,48 @@ describe('makeRecorderApi', () => { setupBuilder.cleanup() }) - describe('boot', () => { - it('does not start recording when init() is called', () => { - setupBuilder.build() - expect(startRecordingSpy).not.toHaveBeenCalled() - rumInit() - expect(startRecordingSpy).not.toHaveBeenCalled() + describe('recorder boot', () => { + describe('with automatic start', () => { + it('starts recording when init() is called', () => { + setupBuilder.build() + expect(startRecordingSpy).not.toHaveBeenCalled() + rumInit() + expect(startRecordingSpy).toHaveBeenCalled() + }) + + it('starts recording after the DOM is loaded', () => { + setupBuilder.build() + const { triggerOnDomLoaded } = mockDocumentReadyState() + rumInit() + expect(startRecordingSpy).not.toHaveBeenCalled() + triggerOnDomLoaded() + expect(startRecordingSpy).toHaveBeenCalled() + }) }) - it('does not start recording after the DOM is loaded', () => { - setupBuilder.build() - const { triggerOnDomLoaded } = mockDocumentReadyState() - rumInit() - expect(startRecordingSpy).not.toHaveBeenCalled() - triggerOnDomLoaded() - expect(startRecordingSpy).not.toHaveBeenCalled() + describe('with manual start', () => { + it('does not start recording when init() is called', () => { + startSessionReplayRecordingManually = true + setupBuilder.build() + expect(startRecordingSpy).not.toHaveBeenCalled() + rumInit() + expect(startRecordingSpy).not.toHaveBeenCalled() + }) + + it('does not start recording after the DOM is loaded', () => { + startSessionReplayRecordingManually = true + setupBuilder.build() + const { triggerOnDomLoaded } = mockDocumentReadyState() + rumInit() + expect(startRecordingSpy).not.toHaveBeenCalled() + triggerOnDomLoaded() + expect(startRecordingSpy).not.toHaveBeenCalled() + }) }) }) - describe('startSessionReplayRecording()', () => { - it('ignores calls while recording is already started', () => { + describe('recorder start', () => { + it('ignores additional start calls while recording is already started', () => { setupBuilder.build() rumInit() recorderApi.start() @@ -83,32 +112,24 @@ describe('makeRecorderApi', () => { expect(startRecordingSpy).toHaveBeenCalledTimes(1) }) - it('starts recording if called before init()', () => { - setupBuilder.build() - recorderApi.start() - rumInit() - expect(startRecordingSpy).toHaveBeenCalled() - }) - - it('does not start recording multiple times if restarted before the DOM is loaded', () => { + it('ignores restart before the DOM is loaded', () => { setupBuilder.build() const { triggerOnDomLoaded } = mockDocumentReadyState() rumInit() - recorderApi.start() recorderApi.stop() recorderApi.start() triggerOnDomLoaded() expect(startRecordingSpy).toHaveBeenCalledTimes(1) }) - it('ignores calls if the session is not tracked', () => { + it('ignores start calls if the session is not tracked', () => { setupBuilder.withSessionManager(createRumSessionManagerMock().setNotTracked()).build() rumInit() recorderApi.start() expect(startRecordingSpy).not.toHaveBeenCalled() }) - it('ignores calls if the session plan is WITHOUT_REPLAY', () => { + it('ignores start calls if the session plan is WITHOUT_REPLAY', () => { setupBuilder.withSessionManager(createRumSessionManagerMock().setPlanWithoutSessionReplay()).build() rumInit() recorderApi.start() @@ -117,17 +138,15 @@ describe('makeRecorderApi', () => { it('do not start recording if worker fails to be instantiated', () => { setupBuilder.build() - rumInit() startDeflateWorkerWith(undefined) - recorderApi.start() + rumInit() expect(startRecordingSpy).not.toHaveBeenCalled() }) it('does not start recording multiple times if restarted before worker is initialized', () => { setupBuilder.build() - rumInit() stopDeflateWorker() - recorderApi.start() + rumInit() recorderApi.stop() callLastRegisteredInitialisationCallback() @@ -174,30 +193,20 @@ describe('makeRecorderApi', () => { }) }) - describe('stopSessionReplayRecording()', () => { + describe('recorder stop', () => { it('ignores calls while recording is already stopped', () => { setupBuilder.build() rumInit() - recorderApi.start() recorderApi.stop() recorderApi.stop() recorderApi.stop() expect(stopRecordingSpy).toHaveBeenCalledTimes(1) }) - it('does not start recording if called before init()', () => { - setupBuilder.build() - recorderApi.start() - recorderApi.stop() - rumInit() - expect(startRecordingSpy).not.toHaveBeenCalled() - }) - it('prevents recording to start when the DOM is loaded', () => { setupBuilder.build() const { triggerOnDomLoaded } = mockDocumentReadyState() rumInit() - recorderApi.start() recorderApi.stop() triggerOnDomLoaded() expect(startRecordingSpy).not.toHaveBeenCalled() @@ -211,7 +220,6 @@ describe('makeRecorderApi', () => { sessionManager = createRumSessionManagerMock() setupBuilder.withSessionManager(sessionManager) ;({ lifeCycle } = setupBuilder.build()) - rumInit() }) describe('from WITHOUT_REPLAY to WITH_REPLAY', () => { @@ -220,7 +228,7 @@ describe('makeRecorderApi', () => { }) it('starts recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() sessionManager.setPlanWithSessionReplay() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) expect(startRecordingSpy).not.toHaveBeenCalled() @@ -230,7 +238,7 @@ describe('makeRecorderApi', () => { }) it('does not starts recording if stopSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() recorderApi.stop() sessionManager.setPlanWithSessionReplay() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) @@ -245,7 +253,7 @@ describe('makeRecorderApi', () => { }) it('keeps not recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() sessionManager.setNotTracked() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) @@ -260,7 +268,7 @@ describe('makeRecorderApi', () => { }) it('keeps not recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() @@ -274,7 +282,7 @@ describe('makeRecorderApi', () => { }) it('stops recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() expect(startRecordingSpy).toHaveBeenCalledTimes(1) sessionManager.setPlanWithoutSessionReplay() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) @@ -284,10 +292,8 @@ describe('makeRecorderApi', () => { }) it('prevents session recording to start if the session is renewed before the DOM is loaded', () => { - setupBuilder.build() const { triggerOnDomLoaded } = mockDocumentReadyState() rumInit() - recorderApi.start() sessionManager.setPlanWithoutSessionReplay() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) @@ -302,7 +308,7 @@ describe('makeRecorderApi', () => { }) it('stops recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() expect(startRecordingSpy).toHaveBeenCalledTimes(1) sessionManager.setNotTracked() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) @@ -318,7 +324,7 @@ describe('makeRecorderApi', () => { }) it('keeps recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() expect(startRecordingSpy).toHaveBeenCalledTimes(1) lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) expect(stopRecordingSpy).toHaveBeenCalled() @@ -327,7 +333,7 @@ describe('makeRecorderApi', () => { }) it('does not starts recording if stopSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() expect(startRecordingSpy).toHaveBeenCalledTimes(1) recorderApi.stop() expect(stopRecordingSpy).toHaveBeenCalledTimes(1) @@ -344,7 +350,7 @@ describe('makeRecorderApi', () => { }) it('starts recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() sessionManager.setPlanWithSessionReplay() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) @@ -353,7 +359,7 @@ describe('makeRecorderApi', () => { }) it('does not starts recording if stopSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() recorderApi.stop() sessionManager.setPlanWithSessionReplay() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) @@ -369,7 +375,7 @@ describe('makeRecorderApi', () => { }) it('keeps not recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() sessionManager.setPlanWithoutSessionReplay() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) @@ -384,7 +390,7 @@ describe('makeRecorderApi', () => { }) it('keeps not recording if startSessionReplayRecording was called', () => { - recorderApi.start() + rumInit() lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) expect(startRecordingSpy).not.toHaveBeenCalled() @@ -397,11 +403,11 @@ describe('makeRecorderApi', () => { it('is true only if recording', () => { setupBuilder.build() rumInit() - expect(recorderApi.isRecording()).toBeFalse() - recorderApi.start() expect(recorderApi.isRecording()).toBeTrue() recorderApi.stop() expect(recorderApi.isRecording()).toBeFalse() + recorderApi.start() + expect(recorderApi.isRecording()).toBeTrue() }) it('is false before the DOM is loaded', () => { @@ -409,8 +415,6 @@ describe('makeRecorderApi', () => { const { triggerOnDomLoaded } = mockDocumentReadyState() rumInit() expect(recorderApi.isRecording()).toBeFalse() - recorderApi.start() - expect(recorderApi.isRecording()).toBeFalse() triggerOnDomLoaded() expect(recorderApi.isRecording()).toBeTrue() }) diff --git a/packages/rum/src/boot/recorderApi.ts b/packages/rum/src/boot/recorderApi.ts index fb3bf6a21c..322c6c1752 100644 --- a/packages/rum/src/boot/recorderApi.ts +++ b/packages/rum/src/boot/recorderApi.ts @@ -64,7 +64,7 @@ export function makeRecorderApi( } let state: RecorderState = { - status: RecorderStatus.Stopped, + status: RecorderStatus.IntentToStart, } let startStrategy = () => { @@ -87,6 +87,9 @@ export function makeRecorderApi( sessionManager: RumSessionManager, viewContexts: ViewContexts ) => { + if (configuration.startSessionReplayRecordingManually) { + state = { status: RecorderStatus.Stopped } + } lifeCycle.subscribe(LifeCycleEventType.SESSION_EXPIRED, () => { if (state.status === RecorderStatus.Starting || state.status === RecorderStatus.Started) { stopStrategy() diff --git a/packages/rum/src/domain/record/observers/frustrationObserver.spec.ts b/packages/rum/src/domain/record/observers/frustrationObserver.spec.ts index 50cdc267cd..9c1e8ebd7c 100644 --- a/packages/rum/src/domain/record/observers/frustrationObserver.spec.ts +++ b/packages/rum/src/domain/record/observers/frustrationObserver.spec.ts @@ -40,7 +40,7 @@ describe('initFrustrationObserver', () => { }, }, }, - domainContext: { event: mouseEvent, events: [mouseEvent] }, + domainContext: { events: [mouseEvent] }, } }) diff --git a/packages/rum/src/domain/record/observers/mouseInteractionObserver.spec.ts b/packages/rum/src/domain/record/observers/mouseInteractionObserver.spec.ts index 22cca63a57..b70b7aefce 100644 --- a/packages/rum/src/domain/record/observers/mouseInteractionObserver.spec.ts +++ b/packages/rum/src/domain/record/observers/mouseInteractionObserver.spec.ts @@ -1,4 +1,4 @@ -import { DefaultPrivacyLevel, isIE } from '@datadog/browser-core' +import { DOM_EVENT, DefaultPrivacyLevel, isIE } from '@datadog/browser-core' import { createNewEvent } from '@datadog/browser-core/test' import { IncrementalSource, MouseInteractionType, RecordType } from '../../../types' import { serializeDocument, SerializationContextStatus } from '../serialization' @@ -26,7 +26,7 @@ describe('initMouseInteractionObserver', () => { a.setAttribute('tabindex', '0') // make the element focusable sandbox.appendChild(a) document.body.appendChild(sandbox) - a.focus() + a.dispatchEvent(createNewEvent(DOM_EVENT.FOCUS)) serializeDocument(document, DEFAULT_CONFIGURATION, { shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, @@ -45,7 +45,7 @@ describe('initMouseInteractionObserver', () => { }) it('should generate click record', () => { - a.click() + a.dispatchEvent(createNewEvent(DOM_EVENT.CLICK, { clientX: 0, clientY: 0 })) expect(mouseInteractionCallbackSpy).toHaveBeenCalledWith({ id: jasmine.any(Number), @@ -62,7 +62,7 @@ describe('initMouseInteractionObserver', () => { }) it('should generate mouseup record on pointerup DOM event', () => { - const pointerupEvent = createNewEvent('pointerup', { clientX: 1, clientY: 2 }) + const pointerupEvent = createNewEvent(DOM_EVENT.POINTER_UP, { clientX: 1, clientY: 2 }) a.dispatchEvent(pointerupEvent) expect(mouseInteractionCallbackSpy).toHaveBeenCalledWith({ @@ -80,14 +80,14 @@ describe('initMouseInteractionObserver', () => { }) it('should not generate click record if x/y are missing', () => { - const clickEvent = createNewEvent('click') + const clickEvent = createNewEvent(DOM_EVENT.CLICK) a.dispatchEvent(clickEvent) expect(mouseInteractionCallbackSpy).not.toHaveBeenCalled() }) it('should generate blur record', () => { - a.blur() + a.dispatchEvent(createNewEvent(DOM_EVENT.BLUR)) expect(mouseInteractionCallbackSpy).toHaveBeenCalledWith({ id: jasmine.any(Number), @@ -125,12 +125,12 @@ describe('initMouseInteractionObserver', () => { }) it('should compute x/y coordinates for click record', () => { - a.click() + a.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(coordinatesComputed).toBeTrue() }) it('should not compute x/y coordinates for blur record', () => { - a.blur() + a.dispatchEvent(createNewEvent(DOM_EVENT.BLUR)) expect(coordinatesComputed).toBeFalse() }) }) diff --git a/packages/rum/test/mockWorker.ts b/packages/rum/test/mockWorker.ts index 1bf324fabb..038a4ba63f 100644 --- a/packages/rum/test/mockWorker.ts +++ b/packages/rum/test/mockWorker.ts @@ -1,4 +1,5 @@ import type { DeflateWorkerAction, DeflateWorkerResponse } from '@datadog/browser-worker' +import { createNewEvent } from '../../core/test' import type { DeflateWorker } from '../src/domain/segmentCollection' type DeflateWorkerListener = (event: { data: DeflateWorkerResponse }) => void @@ -67,25 +68,29 @@ export class MockWorker implements DeflateWorker { switch (message.action) { case 'init': this.listeners.message.forEach((listener) => - listener({ - data: { - type: 'initialized', - }, - }) + listener( + createNewEvent('message', { + data: { + type: 'initialized', + }, + }) + ) ) break case 'write': { const additionalBytesCount = this.pushData(message.data) this.listeners.message.forEach((listener) => - listener({ - data: { - type: 'wrote', - id: message.id, - compressedBytesCount: uint8ArraysSize(this.deflatedData), - additionalBytesCount, - }, - }) + listener( + createNewEvent('message', { + data: { + type: 'wrote', + id: message.id, + compressedBytesCount: uint8ArraysSize(this.deflatedData), + additionalBytesCount, + }, + }) + ) ) } break @@ -93,15 +98,17 @@ export class MockWorker implements DeflateWorker { { const additionalBytesCount = this.pushData(message.data) this.listeners.message.forEach((listener) => - listener({ - data: { - type: 'flushed', - id: message.id, - result: mergeUint8Arrays(this.deflatedData), - rawBytesCount: this.rawBytesCount, - additionalBytesCount, - }, - }) + listener( + createNewEvent('message', { + data: { + type: 'flushed', + id: message.id, + result: mergeUint8Arrays(this.deflatedData), + rawBytesCount: this.rawBytesCount, + additionalBytesCount, + }, + }) + ) ) this.deflatedData.length = 0 this.rawBytesCount = 0 @@ -112,12 +119,14 @@ export class MockWorker implements DeflateWorker { } dispatchErrorEvent() { - const error = new ErrorEvent('worker') + const error = createNewEvent('worker') this.listeners.error.forEach((listener) => listener(error)) } dispatchErrorMessage(error: Error | string) { - this.listeners.message.forEach((listener) => listener({ data: { type: 'errored', error } })) + this.listeners.message.forEach((listener) => + listener(createNewEvent('message', { data: { type: 'errored', error } })) + ) } private pushData(data?: string) { diff --git a/packages/worker/package.json b/packages/worker/package.json index 6dff7f43cf..80ce8f3e06 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-worker", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "license": "Apache-2.0", "main": "bundle/worker.js", "types": "src/types.ts", diff --git a/performances/package.json b/performances/package.json index 8c0d307192..df9df13b18 100644 --- a/performances/package.json +++ b/performances/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "performances", - "version": "4.45.0", + "version": "5.0.0-alpha.0", "scripts": { "start": "ts-node ./src/main.ts" }, diff --git a/performances/src/main.ts b/performances/src/main.ts index a17ec9f738..51834c77b8 100644 --- a/performances/src/main.ts +++ b/performances/src/main.ts @@ -81,8 +81,8 @@ async function setupSDK(page: Page, options: ProfilingOptions) { clientToken: 'xxx', applicationId: 'xxx', site: 'datadoghq.com', - trackInteractions: true, - proxyUrl: ${JSON.stringify(options.proxy.origin)} + trackUserInteractions: true, + proxy: ${JSON.stringify(options.proxy.origin)} }) ${options.startRecording ? 'window.DD_RUM.startSessionReplayRecording()' : ''} }) diff --git a/test/app/yarn.lock b/test/app/yarn.lock index 0fb877d8b8..61c8948daa 100644 --- a/test/app/yarn.lock +++ b/test/app/yarn.lock @@ -15,9 +15,9 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/browser-logs@portal:../../packages/logs::locator=app%40workspace%3A." dependencies: - "@datadog/browser-core": 4.45.0 + "@datadog/browser-core": 5.0.0-alpha.0 peerDependencies: - "@datadog/browser-rum": 4.45.0 + "@datadog/browser-rum": 5.0.0-alpha.0 peerDependenciesMeta: "@datadog/browser-rum": optional: true @@ -28,7 +28,7 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/browser-rum-core@portal:../../packages/rum-core::locator=app%40workspace%3A." dependencies: - "@datadog/browser-core": 4.45.0 + "@datadog/browser-core": 5.0.0-alpha.0 languageName: node linkType: soft @@ -36,11 +36,11 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/browser-rum@portal:../../packages/rum::locator=app%40workspace%3A." dependencies: - "@datadog/browser-core": 4.45.0 - "@datadog/browser-rum-core": 4.45.0 - "@datadog/browser-worker": 4.45.0 + "@datadog/browser-core": 5.0.0-alpha.0 + "@datadog/browser-rum-core": 5.0.0-alpha.0 + "@datadog/browser-worker": 5.0.0-alpha.0 peerDependencies: - "@datadog/browser-logs": 4.45.0 + "@datadog/browser-logs": 5.0.0-alpha.0 peerDependenciesMeta: "@datadog/browser-logs": optional: true diff --git a/test/e2e/lib/framework/createTest.ts b/test/e2e/lib/framework/createTest.ts index 0d8c35ab97..cd87f9e054 100644 --- a/test/e2e/lib/framework/createTest.ts +++ b/test/e2e/lib/framework/createTest.ts @@ -1,5 +1,6 @@ import type { LogsInitConfiguration } from '@datadog/browser-logs' import type { RumInitConfiguration } from '@datadog/browser-rum-core' +import { DefaultPrivacyLevel } from '@datadog/browser-rum' import { getRunId } from '../../../envUtils' import { deleteAllCookies, getBrowserName, withBrowserLogs } from '../helpers/browser' import { APPLICATION_ID, CLIENT_TOKEN } from '../helpers/constants' @@ -18,6 +19,7 @@ const DEFAULT_RUM_CONFIGURATION = { applicationId: APPLICATION_ID, clientToken: CLIENT_TOKEN, sessionReplaySampleRate: 100, + defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, trackResources: true, trackLongTasks: true, telemetrySampleRate: 100, diff --git a/test/e2e/lib/framework/pageSetups.ts b/test/e2e/lib/framework/pageSetups.ts index 23e347b654..65cd4ef5d6 100644 --- a/test/e2e/lib/framework/pageSetups.ts +++ b/test/e2e/lib/framework/pageSetups.ts @@ -1,5 +1,6 @@ import type { LogsInitConfiguration } from '@datadog/browser-logs' import type { RumInitConfiguration } from '@datadog/browser-rum-core' +import { DOM_EVENT } from '@datadog/browser-core/src/browser/addEventListener' import type { Servers } from './httpServers' export interface SetupOptions { @@ -161,7 +162,7 @@ export function basePage({ header, body }: { header?: string; body?: string }) { - ${header || ''} + ${setupTrustedEvents()} ${header || ''} ${body || ''} @@ -170,6 +171,21 @@ export function basePage({ header, body }: { header?: string; body?: string }) { ` } +// Adds __ddIsTrusted: true to all events generated by e2e tests so we can track untrusted events generated by wdio +export function setupTrustedEvents() { + return html` ` +} + // html is a simple template string tag to allow prettier to format various setups as HTML export function html(parts: readonly string[], ...vars: string[]) { return parts.reduce((full, part, index) => full + vars[index - 1] + part) diff --git a/test/e2e/lib/helpers/replay.ts b/test/e2e/lib/helpers/replay.ts index 3ce6ef93f4..1a0331ef1f 100644 --- a/test/e2e/lib/helpers/replay.ts +++ b/test/e2e/lib/helpers/replay.ts @@ -1,4 +1,3 @@ -import type { RumInitConfiguration } from '@datadog/browser-rum-core' import type { EventRegistry } from '../framework' export function getFirstSegment(events: EventRegistry) { @@ -8,8 +7,3 @@ export function getFirstSegment(events: EventRegistry) { export function getLastSegment(events: EventRegistry) { return events.sessionReplay[events.sessionReplay.length - 1].segment.data } - -export function initRumAndStartRecording(initConfiguration: RumInitConfiguration) { - window.DD_RUM!.init(initConfiguration) - window.DD_RUM!.startSessionReplayRecording() -} diff --git a/test/e2e/scenario/logs.scenario.ts b/test/e2e/scenario/logs.scenario.ts index 171db742dc..fdf9e7716a 100644 --- a/test/e2e/scenario/logs.scenario.ts +++ b/test/e2e/scenario/logs.scenario.ts @@ -23,7 +23,7 @@ describe('logs', () => { }) await flushEvents() expect(serverEvents.logs.length).toBe(1) - expect(serverEvents.logs[0].message).toBe('console error: oh snap') + expect(serverEvents.logs[0].message).toBe('oh snap') await withBrowserLogs((browserLogs) => { expect(browserLogs.length).toEqual(1) }) @@ -42,7 +42,7 @@ describe('logs', () => { await flushEvents() expect(serverEvents.logs.length).toBe(1) expect(serverEvents.logs[0].message).toBe(`XHR error GET ${UNREACHABLE_URL}`) - expect(serverEvents.logs[0].error?.origin).toBe('network') + expect(serverEvents.logs[0].origin).toBe('network') await withBrowserLogs((browserLogs) => { // Some browser report two errors: @@ -64,7 +64,7 @@ describe('logs', () => { await flushEvents() expect(serverEvents.logs.length).toBe(1) expect(serverEvents.logs[0].message).toBe(`Fetch error GET ${UNREACHABLE_URL}`) - expect(serverEvents.logs[0].error?.origin).toBe('network') + expect(serverEvents.logs[0].origin).toBe('network') await withBrowserLogs((browserLogs) => { // Some browser report two errors: @@ -84,7 +84,7 @@ describe('logs', () => { await flushEvents() expect(serverEvents.logs.length).toBe(1) expect(serverEvents.logs[0].message).toBe(`Fetch error GET ${baseUrl}/throw-large-response`) - expect(serverEvents.logs[0].error?.origin).toBe('network') + expect(serverEvents.logs[0].origin).toBe('network') const ellipsisSize = 3 expect(serverEvents.logs[0].error?.stack?.length).toBe(DEFAULT_REQUEST_ERROR_RESPONSE_LENGTH_LIMIT + ellipsisSize) @@ -159,6 +159,7 @@ describe('logs', () => { .withLogs({ beforeSend: (event) => { event.foo = 'bar' + return true }, }) .run(async ({ serverEvents }) => { diff --git a/test/e2e/scenario/recorder/recorder.scenario.ts b/test/e2e/scenario/recorder/recorder.scenario.ts index 89f4c9ba1f..42da537afa 100644 --- a/test/e2e/scenario/recorder/recorder.scenario.ts +++ b/test/e2e/scenario/recorder/recorder.scenario.ts @@ -20,7 +20,7 @@ import { } from '@datadog/browser-rum/test' import { flushEvents, createTest, bundleSetup, html } from '../../lib/framework' import { browserExecute, browserExecuteAsync } from '../../lib/helpers/browser' -import { getFirstSegment, getLastSegment, initRumAndStartRecording } from '../../lib/helpers/replay' +import { getFirstSegment, getLastSegment } from '../../lib/helpers/replay' const TIMESTAMP_RE = /^\d{13}$/ const UUID_RE = /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/ @@ -28,7 +28,6 @@ const UUID_RE = /^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/ describe('recorder', () => { createTest('record mouse move') .withRum() - .withRumInit(initRumAndStartRecording) .run(async ({ serverEvents }) => { await browserExecute(() => document.documentElement.outerHTML) const html = await $('html') @@ -79,14 +78,13 @@ describe('recorder', () => { describe('full snapshot', () => { createTest('obfuscate elements') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html` -

foo
-

bar

- baz - - +
displayed
+

hidden

+ hidden + + `) .run(async ({ serverEvents }) => { await flushEvents() @@ -97,7 +95,7 @@ describe('recorder', () => { const node = findElementWithIdAttribute(fullSnapshot.data.node, 'not-obfuscated') expect(node).toBeTruthy() - expect(findTextContent(node!)).toBe('foo') + expect(findTextContent(node!)).toBe('displayed') const hiddenNodeByAttribute = findElement(fullSnapshot.data.node, (node) => node.tagName === 'p') expect(hiddenNodeByAttribute).toBeTruthy() @@ -105,15 +103,14 @@ describe('recorder', () => { expect(hiddenNodeByAttribute!.childNodes.length).toBe(0) const hiddenNodeByClassName = findElement(fullSnapshot.data.node, (node) => node.tagName === 'span') - expect(hiddenNodeByClassName).toBeTruthy() expect(hiddenNodeByClassName!.attributes.class).toBeUndefined() expect(hiddenNodeByClassName!.attributes['data-dd-privacy']).toBe('hidden') expect(hiddenNodeByClassName!.childNodes.length).toBe(0) - const inputIgnored = findElementWithIdAttribute(fullSnapshot.data.node, 'input-ignored') + const inputIgnored = findElementWithIdAttribute(fullSnapshot.data.node, 'input-not-obfuscated') expect(inputIgnored).toBeTruthy() - expect(inputIgnored!.attributes.value).toBe('***') + expect(inputIgnored!.attributes.value).toBe('displayed') const inputMasked = findElementWithIdAttribute(fullSnapshot.data.node, 'input-masked') expect(inputMasked).toBeTruthy() @@ -124,7 +121,6 @@ describe('recorder', () => { describe('mutations observer', () => { createTest('record mutations') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html`

mutation observer

@@ -169,7 +165,6 @@ describe('recorder', () => { createTest('record character data mutations') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html`

mutation observer

@@ -220,7 +215,6 @@ describe('recorder', () => { createTest('record attributes mutations') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html`

mutation observer

@@ -263,7 +257,6 @@ describe('recorder', () => { createTest("don't record hidden elements mutations") .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html`
@@ -289,7 +282,6 @@ describe('recorder', () => { createTest('record DOM node movement 1') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody( // prettier-ignore @@ -338,7 +330,6 @@ describe('recorder', () => { createTest('record DOM node movement 2') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody( // prettier-ignore @@ -390,7 +381,6 @@ describe('recorder', () => { createTest('serialize node before record') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody( // prettier-ignore @@ -446,7 +436,6 @@ describe('recorder', () => { .withRum({ defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, }) - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html`
@@ -526,7 +515,6 @@ describe('recorder', () => { .withRum({ defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, }) - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html` @@ -561,11 +549,10 @@ describe('recorder', () => { createTest('replace masked values by asterisks') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html` - - + + `) .run(async ({ serverEvents }) => { const firstInput = await $('#by-data-attribute') @@ -593,7 +580,6 @@ describe('recorder', () => { describe('stylesheet rules observer', () => { createTest('record dynamic CSS changes') .withRum() - .withRumInit(initRumAndStartRecording) .withSetup(bundleSetup) .withBody(html`