diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f4a6ad0c57..f501bf2b44 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -32,3 +32,5 @@ updates: # update node-fetch: RUMF-1167 - dependency-name: 'node-fetch' update-types: ['version-update:semver-major'] + - dependency-name: '*' + update-types: ['version-update:semver-minor', 'version-update:semver-patch'] diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b0d7a874be..0bae2d1e96 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - CURRENT_STAGING: staging-15 + CURRENT_STAGING: staging-18 APP: 'browser-sdk' CURRENT_CI_IMAGE: 31 BUILD_STABLE_REGISTRY: '486234852809.dkr.ecr.us-east-1.amazonaws.com' diff --git a/CHANGELOG.md b/CHANGELOG.md index bbab38eb77..1a11c759b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,24 @@ --- +## v4.8.0 + +- ✨ [RUMF-1192] forward Reports to Datadog ([#1506](https://github.com/DataDog/browser-sdk/pull/1506)) +- ✨ [RUMF-1192] forward `console.*` logs to Datadog ([#1505](https://github.com/DataDog/browser-sdk/pull/1505)) +- 📝 fix documentation for `proxyUrl` documentation ([#1503](https://github.com/DataDog/browser-sdk/pull/1503)) +- ✨ [RUMF-1237] The event bridge allowed hosts should also match subdomains ([#1499](https://github.com/DataDog/browser-sdk/pull/1499)) +- 📝 add `replaySampleRate` to README examples ([#1370](https://github.com/DataDog/browser-sdk/pull/1370)) + +## v4.7.1 + +- 🐛 Adjust records generated during view change so their date matches the view date ([#1486](https://github.com/DataDog/browser-sdk/pull/1486)) +- ⚗✨ [RUMF-1224] remove console APIs prefix ([#1479](https://github.com/DataDog/browser-sdk/pull/1479)) +- ♻️ [RUMF-1178] improve logs assembly part 2 ([#1463](https://github.com/DataDog/browser-sdk/pull/1463)) +- ⚗✨ Allow update service version with start view ([#1448](https://github.com/DataDog/browser-sdk/pull/1448)) +- ⚗✨ [RUMF-1208] don't discard automatic action on view creation ([#1451](https://github.com/DataDog/browser-sdk/pull/1451)) +- ⚗✨ [RUMF-1207] collect concurrent actions ([#1434](https://github.com/DataDog/browser-sdk/pull/1434)) +- ♻️ [RUMF-1207] collect concurrent actions groundwork - move action history closer to action collection ([#1432](https://github.com/DataDog/browser-sdk/pull/1432)) + ## v4.7.0 Note: The Logs Browser SDK 3.10.1 (released on December 21th, 2021) unexpectedly changed the initialization parameter `forwardErrorsToLogs` default value from `true` to `false`. This release restores the default value to `true`, so Logs Browser SDK users who don't specify this parameter will have errors forwarded as logs. diff --git a/developer-extension/package.json b/developer-extension/package.json index 0f07c643c7..3030fde8b5 100644 --- a/developer-extension/package.json +++ b/developer-extension/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-sdk-developer-extension", - "version": "4.7.0", + "version": "4.8.0", "private": true, "scripts": { "build": "rm -rf dist && webpack --mode production", @@ -8,8 +8,8 @@ }, "devDependencies": { "@types/chrome": "0.0.180", - "@types/react": "17.0.43", - "@types/react-dom": "17.0.14", + "@types/react": "18.0.1", + "@types/react-dom": "18.0.0", "copy-webpack-plugin": "8.1.0", "html-webpack-plugin": "5.3.1", "webpack": "5.28.0", @@ -18,8 +18,8 @@ "dependencies": { "@mantine/core": "4.1.2", "@mantine/hooks": "4.1.2", - "react": "17.0.2", - "react-dom": "17.0.2", + "react": "18.0.0", + "react-dom": "18.0.0", "react-json-view": "1.21.3" } } diff --git a/developer-extension/src/panel/components/eventsTab.tsx b/developer-extension/src/panel/components/eventsTab.tsx index e963d7d117..053b4f81a6 100644 --- a/developer-extension/src/panel/components/eventsTab.tsx +++ b/developer-extension/src/panel/components/eventsTab.tsx @@ -1,4 +1,5 @@ import { Badge, Group, SegmentedControl, Space, Table, TextInput } from '@mantine/core' +import { useColorScheme } from '@mantine/hooks' import React from 'react' import ReactJson from 'react-json-view' import type { RumEvent } from '../../../../packages/rum-core/src/rumEvent.types' @@ -11,6 +12,7 @@ const RUM_EVENT_TYPE_COLOR = { long_task: 'yellow', view: 'blue', resource: 'cyan', + telemetry: 'teal', } const LOG_STATUS_COLOR = { @@ -27,6 +29,7 @@ interface EventTabProps { } export function EventTab({ events, filters, onFiltered }: EventTabProps) { + const colorScheme = useColorScheme() return ( events && ( <> @@ -40,7 +43,7 @@ export function EventTab({ events, filters, onFiltered }: EventTabProps) { ]} /> onFiltered({ ...filters, query: event.currentTarget.value })} @@ -67,7 +70,7 @@ export function EventTab({ events, filters, onFiltered }: EventTabProps) { diff --git a/lerna.json b/lerna.json index 537848a388..60ae890316 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { "npmClient": "yarn", "useWorkspaces": true, - "version": "4.7.0", + "version": "4.8.0", "publishConfig": { "access": "public" } diff --git a/package.json b/package.json index 4fb3df8038..6d72c5ccc4 100644 --- a/package.json +++ b/package.json @@ -56,12 +56,12 @@ "eslint-module-utils": "2.7.3", "eslint-plugin-import": "2.25.4", "eslint-plugin-jasmine": "4.1.3", - "eslint-plugin-jsdoc": "38.1.6", + "eslint-plugin-jsdoc": "39.1.1", "eslint-plugin-local-rules": "1.1.0", "eslint-plugin-prefer-arrow": "1.2.3", "eslint-plugin-unicorn": "42.0.0", "express": "4.17.3", - "glob": "7.2.0", + "glob": "8.0.1", "jasmine-core": "3.6.0", "js-polyfills": "0.1.43", "json-schema-to-typescript": "bcaudan/json-schema-to-typescript#bcaudan/add-readonly-support", diff --git a/packages/core/package.json b/packages/core/package.json index ef7662d084..139a7d44b0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-core", - "version": "4.7.0", + "version": "4.8.0", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index e9f57c201e..c5487e9c6b 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -33,6 +33,11 @@ describe('validateAndBuildConfiguration', () => { expect(displaySpy).toHaveBeenCalledOnceWith('Client Token is not configured, we will not send any data.') }) + it("shouldn't display any error if the configuration is correct", () => { + validateAndBuildConfiguration({ clientToken: 'yes' }) + expect(displaySpy).not.toHaveBeenCalled() + }) + it('requires sampleRate to be a percentage', () => { expect( validateAndBuildConfiguration({ clientToken, sampleRate: 'foo' } as unknown as InitConfiguration) @@ -44,12 +49,28 @@ describe('validateAndBuildConfiguration', () => { validateAndBuildConfiguration({ clientToken, sampleRate: 200 } as unknown as InitConfiguration) ).toBeUndefined() expect(displaySpy).toHaveBeenCalledOnceWith('Sample Rate should be a number between 0 and 100') - }) - it("shouldn't display any error if the configuration is correct", () => { + displaySpy.calls.reset() validateAndBuildConfiguration({ clientToken: 'yes', sampleRate: 1 }) expect(displaySpy).not.toHaveBeenCalled() }) + + it('requires telemetrySampleRate to be a percentage', () => { + expect( + validateAndBuildConfiguration({ clientToken, telemetrySampleRate: 'foo' } as unknown as InitConfiguration) + ).toBeUndefined() + expect(displaySpy).toHaveBeenCalledOnceWith('Telemetry Sample Rate should be a number between 0 and 100') + + displaySpy.calls.reset() + expect( + validateAndBuildConfiguration({ clientToken, telemetrySampleRate: 200 } as unknown as InitConfiguration) + ).toBeUndefined() + expect(displaySpy).toHaveBeenCalledOnceWith('Telemetry Sample Rate should be a number between 0 and 100') + + displaySpy.calls.reset() + validateAndBuildConfiguration({ clientToken: 'yes', telemetrySampleRate: 1 }) + expect(displaySpy).not.toHaveBeenCalled() + }) }) describe('cookie options', () => { diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index b3d286a3fe..cd218c9e1e 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -19,6 +19,7 @@ export interface InitConfiguration { clientToken: string beforeSend?: GenericBeforeSendCallback | undefined sampleRate?: number | undefined + telemetrySampleRate?: number | undefined silentMultipleInit?: boolean | undefined // transport options @@ -56,6 +57,7 @@ export interface Configuration extends TransportConfiguration { beforeSend: GenericBeforeSendCallback | undefined cookieOptions: CookieOptions sampleRate: number + telemetrySampleRate: number service: string | undefined silentMultipleInit: boolean @@ -81,6 +83,11 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati return } + if (initConfiguration.telemetrySampleRate !== undefined && !isPercentage(initConfiguration.telemetrySampleRate)) { + display.error('Telemetry Sample Rate should be a number between 0 and 100') + return + } + // Set the experimental feature flags as early as possible, so we can use them in most places updateExperimentalFeatures(initConfiguration.enableExperimentalFeatures) @@ -90,6 +97,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), cookieOptions: buildCookieOptions(initConfiguration), sampleRate: initConfiguration.sampleRate ?? 100, + telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, service: initConfiguration.service, silentMultipleInit: !!initConfiguration.silentMultipleInit, diff --git a/packages/core/src/domain/console/consoleObservable.spec.ts b/packages/core/src/domain/console/consoleObservable.spec.ts index f65912a8ae..b42f6fba19 100644 --- a/packages/core/src/domain/console/consoleObservable.spec.ts +++ b/packages/core/src/domain/console/consoleObservable.spec.ts @@ -6,71 +6,75 @@ import { ConsoleApiName, initConsoleObservable } from './consoleObservable' // prettier: avoid formatting issue // cf https://github.com/prettier/prettier/issues/12211 -;[ConsoleApiName.log, ConsoleApiName.info, ConsoleApiName.warn, ConsoleApiName.debug, ConsoleApiName.error].forEach( - (api) => { - describe(`console ${api} observable`, () => { - let consoleStub: jasmine.Spy - let consoleSubscription: Subscription - let notifyLog: jasmine.Spy - - beforeEach(() => { - consoleStub = spyOn(console, api) - notifyLog = jasmine.createSpy('notifyLog') - - consoleSubscription = initConsoleObservable([api]).subscribe(notifyLog) - }) - - afterEach(() => { - consoleSubscription.unsubscribe() - }) - - it(`should notify ${api}`, () => { - console[api]('foo', 'bar') - - const consoleLog = notifyLog.calls.mostRecent().args[0] - - expect(consoleLog).toEqual( - jasmine.objectContaining({ - message: `console ${api}: foo bar`, - api, - }) - ) - }) - - it('should keep original behavior', () => { - console[api]('foo', 'bar') - - expect(consoleStub).toHaveBeenCalledWith('foo', 'bar') - }) - - it('should format error instance', () => { - console[api](new TypeError('hello')) - const consoleLog = notifyLog.calls.mostRecent().args[0] - expect(consoleLog.message).toBe(`console ${api}: TypeError: hello`) - }) - - it('should stringify object parameters', () => { - console[api]('Hello', { foo: 'bar' }) - const consoleLog = notifyLog.calls.mostRecent().args[0] - expect(consoleLog.message).toBe(`console ${api}: Hello {\n "foo": "bar"\n}`) - }) - - it('should allow multiple callers', () => { - const notifyOtherCaller = jasmine.createSpy('notifyOtherCaller') - const instrumentedConsoleApi = console[api] - const otherConsoleSubscription = initConsoleObservable([api]).subscribe(notifyOtherCaller) - - console[api]('foo', 'bar') - - expect(instrumentedConsoleApi).toEqual(console[api]) - expect(notifyLog).toHaveBeenCalledTimes(1) - expect(notifyOtherCaller).toHaveBeenCalledTimes(1) - - otherConsoleSubscription.unsubscribe() - }) +;[ + { 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 }) => { + describe(`console ${api} observable`, () => { + let consoleStub: jasmine.Spy + let consoleSubscription: Subscription + let notifyLog: jasmine.Spy + + beforeEach(() => { + consoleStub = spyOn(console, api) + notifyLog = jasmine.createSpy('notifyLog') + + consoleSubscription = initConsoleObservable([api]).subscribe(notifyLog) }) - } -) + + afterEach(() => { + consoleSubscription.unsubscribe() + }) + + it(`should notify ${api}`, () => { + console[api]('foo', 'bar') + + const consoleLog = notifyLog.calls.mostRecent().args[0] + + expect(consoleLog).toEqual( + jasmine.objectContaining({ + message: `${prefix}foo bar`, + api, + }) + ) + }) + + it('should keep original behavior', () => { + console[api]('foo', 'bar') + + expect(consoleStub).toHaveBeenCalledWith('foo', 'bar') + }) + + it('should format error instance', () => { + console[api](new TypeError('hello')) + const consoleLog = notifyLog.calls.mostRecent().args[0] + expect(consoleLog.message).toBe(`${prefix}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}`) + }) + + it('should allow multiple callers', () => { + const notifyOtherCaller = jasmine.createSpy('notifyOtherCaller') + const instrumentedConsoleApi = console[api] + const otherConsoleSubscription = initConsoleObservable([api]).subscribe(notifyOtherCaller) + + console[api]('foo', 'bar') + + expect(instrumentedConsoleApi).toEqual(console[api]) + expect(notifyLog).toHaveBeenCalledTimes(1) + expect(notifyOtherCaller).toHaveBeenCalledTimes(1) + + otherConsoleSubscription.unsubscribe() + }) + }) +}) describe('console error observable', () => { let consoleSubscription: Subscription diff --git a/packages/core/src/domain/console/consoleObservable.ts b/packages/core/src/domain/console/consoleObservable.ts index 51239fb898..6d69ef1275 100644 --- a/packages/core/src/domain/console/consoleObservable.ts +++ b/packages/core/src/domain/console/consoleObservable.ts @@ -57,21 +57,22 @@ function createConsoleObservable(api: ConsoleApiName) { } function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack: string): ConsoleLog { - const log: ConsoleLog = { - message: [`console ${api}:` as unknown] - .concat(params) - .map((param) => formatConsoleParameters(param)) - .join(' '), - api, - } + // Todo: remove console error prefix in the next major version + let message = params.map((param) => formatConsoleParameters(param)).join(' ') + let stack if (api === ConsoleApiName.error) { const firstErrorParam = find(params, (param: unknown): param is Error => param instanceof Error) - log.stack = firstErrorParam ? toStackTraceString(computeStackTrace(firstErrorParam)) : undefined - log.handlingStack = handlingStack + stack = firstErrorParam ? toStackTraceString(computeStackTrace(firstErrorParam)) : undefined + message = `console error: ${message}` } - return log + return { + api, + message, + stack, + handlingStack, + } } function formatConsoleParameters(param: unknown) { diff --git a/packages/core/src/domain/internalMonitoring/internalMonitoring.spec.ts b/packages/core/src/domain/internalMonitoring/internalMonitoring.spec.ts index 33890fac54..2beceea2a6 100644 --- a/packages/core/src/domain/internalMonitoring/internalMonitoring.spec.ts +++ b/packages/core/src/domain/internalMonitoring/internalMonitoring.spec.ts @@ -1,4 +1,5 @@ import type { Context } from '../../tools/context' +import { display } from '../../tools/display' import type { Configuration } from '../configuration' import { updateExperimentalFeatures, resetExperimentalFeatures } from '../configuration' import type { InternalMonitoring, MonitoringMessage } from './internalMonitoring' @@ -8,11 +9,13 @@ import { resetInternalMonitoring, startInternalMonitoring, callMonitored, + setDebugMode, } from './internalMonitoring' import type { TelemetryEvent } from './telemetryEvent.types' const configuration: Partial = { maxInternalMonitoringMessagesPerPage: 7, + telemetrySampleRate: 100, } describe('internal monitoring', () => { @@ -189,6 +192,52 @@ describe('internal monitoring', () => { }) }) + describe('setDebug', () => { + let displaySpy: jasmine.Spy + + beforeEach(() => { + displaySpy = spyOn(display, 'error') + }) + + afterEach(() => { + resetInternalMonitoring() + resetExperimentalFeatures() + }) + + it('when not called, should not display error', () => { + startInternalMonitoring(configuration as Configuration) + + callMonitored(() => { + throw new Error('message') + }) + + expect(displaySpy).not.toHaveBeenCalled() + }) + + it('when called, should display error', () => { + startInternalMonitoring(configuration as Configuration) + setDebugMode(true) + + callMonitored(() => { + throw new Error('message') + }) + + expect(displaySpy).toHaveBeenCalled() + }) + + it('when called and telemetry not sampled, should display error', () => { + updateExperimentalFeatures(['telemetry']) + startInternalMonitoring({ ...configuration, telemetrySampleRate: 0 } as Configuration) + setDebugMode(true) + + callMonitored(() => { + throw new Error('message') + }) + + expect(displaySpy).toHaveBeenCalled() + }) + }) + describe('new telemetry', () => { let internalMonitoring: InternalMonitoring let notifySpy: jasmine.Spy<(event: TelemetryEvent & Context) => void> @@ -246,6 +295,30 @@ describe('internal monitoring', () => { expect(oldNotifySpy.calls.mostRecent().args[0].foo).toEqual('bar') }) + + it('should notify when sampled', () => { + spyOn(Math, 'random').and.callFake(() => 0) + internalMonitoring = startInternalMonitoring({ ...configuration, telemetrySampleRate: 50 } as Configuration) + internalMonitoring.telemetryEventObservable.subscribe(notifySpy) + + callMonitored(() => { + throw new Error('message') + }) + + expect(notifySpy).toHaveBeenCalled() + }) + + it('should not notify when not sampled', () => { + spyOn(Math, 'random').and.callFake(() => 1) + internalMonitoring = startInternalMonitoring({ ...configuration, telemetrySampleRate: 50 } as Configuration) + internalMonitoring.telemetryEventObservable.subscribe(notifySpy) + + callMonitored(() => { + throw new Error('message') + }) + + expect(notifySpy).not.toHaveBeenCalled() + }) }) describe('when disabled', () => { diff --git a/packages/core/src/domain/internalMonitoring/internalMonitoring.ts b/packages/core/src/domain/internalMonitoring/internalMonitoring.ts index 6b216b7dcd..3cabc172e3 100644 --- a/packages/core/src/domain/internalMonitoring/internalMonitoring.ts +++ b/packages/core/src/domain/internalMonitoring/internalMonitoring.ts @@ -1,7 +1,7 @@ import type { Context } from '../../tools/context' import { display } from '../../tools/display' import { toStackTraceString } from '../../tools/error' -import { assign, combine, jsonStringify } from '../../tools/utils' +import { assign, combine, jsonStringify, performDraw } from '../../tools/utils' import type { Configuration } from '../configuration' import { computeStackTrace } from '../tracekit' import { Observable } from '../../tools/observable' @@ -38,7 +38,8 @@ const monitoringConfiguration: { debugMode?: boolean maxMessagesPerPage: number sentMessageCount: number -} = { maxMessagesPerPage: 0, sentMessageCount: 0 } + telemetryEnabled: boolean +} = { maxMessagesPerPage: 0, sentMessageCount: 0, telemetryEnabled: false } let onInternalMonitoringMessageCollected: ((message: MonitoringMessage) => void) | undefined @@ -48,9 +49,11 @@ export function startInternalMonitoring(configuration: Configuration): InternalM const monitoringMessageObservable = new Observable() const telemetryEventObservable = new Observable() + monitoringConfiguration.telemetryEnabled = performDraw(configuration.telemetrySampleRate) + onInternalMonitoringMessageCollected = (message: MonitoringMessage) => { monitoringMessageObservable.notify(withContext(message)) - if (isExperimentalFeatureEnabled('telemetry')) { + if (isExperimentalFeatureEnabled('telemetry') && monitoringConfiguration.telemetryEnabled) { telemetryEventObservable.notify(toTelemetryEvent(message)) } } @@ -113,6 +116,7 @@ export function startFakeInternalMonitoring() { export function resetInternalMonitoring() { onInternalMonitoringMessageCollected = undefined + monitoringConfiguration.debugMode = undefined } export function monitored unknown>( diff --git a/packages/core/src/tools/error.ts b/packages/core/src/tools/error.ts index 6ad1bcc11c..8371fab46d 100644 --- a/packages/core/src/tools/error.ts +++ b/packages/core/src/tools/error.ts @@ -10,11 +10,6 @@ export interface RawError { type?: string stack?: string source: ErrorSource - resource?: { - url: string - statusCode: number - method: string - } originalError?: unknown handling?: ErrorHandling handlingStack?: string diff --git a/packages/core/src/transport/eventBridge.spec.ts b/packages/core/src/transport/eventBridge.spec.ts index a119b2dbd5..13e6260a3f 100644 --- a/packages/core/src/transport/eventBridge.spec.ts +++ b/packages/core/src/transport/eventBridge.spec.ts @@ -9,16 +9,26 @@ describe('canUseEventBridge', () => { }) it('should detect when the bridge is present and the webView host is allowed', () => { - initEventBridgeStub() - expect(canUseEventBridge()).toBeTrue() - }) - - it('should not detect when the bridge is absent', () => { - expect(canUseEventBridge()).toBeFalse() + initEventBridgeStub(allowedWebViewHosts) + expect(canUseEventBridge('foo.bar')).toBeTrue() + expect(canUseEventBridge('baz.foo.bar')).toBeTrue() + expect(canUseEventBridge('www.foo.bar')).toBeTrue() + expect(canUseEventBridge('www.qux.foo.bar')).toBeTrue() }) it('should not detect when the bridge is present and the webView host is not allowed', () => { initEventBridgeStub(allowedWebViewHosts) + expect(canUseEventBridge('foo.com')).toBeFalse() + expect(canUseEventBridge('foo.bar.baz')).toBeFalse() + expect(canUseEventBridge('bazfoo.bar')).toBeFalse() + }) + + it('should not detect when the bridge on the parent domain if only the subdomain is allowed', () => { + initEventBridgeStub(['baz.foo.bar']) + expect(canUseEventBridge('foo.bar')).toBeFalse() + }) + + it('should not detect when the bridge is absent', () => { expect(canUseEventBridge()).toBeFalse() }) }) diff --git a/packages/core/src/transport/eventBridge.ts b/packages/core/src/transport/eventBridge.ts index 0a41e80d67..25da490bb4 100644 --- a/packages/core/src/transport/eventBridge.ts +++ b/packages/core/src/transport/eventBridge.ts @@ -1,4 +1,4 @@ -import { getGlobalObject, includes } from '..' +import { getGlobalObject } from '..' export interface BrowserWindowWithEventBridge extends Window { DatadogEventBridge?: DatadogEventBridge @@ -26,10 +26,16 @@ export function getEventBridge() { } } -export function canUseEventBridge(): boolean { +export function canUseEventBridge(hostname = getGlobalObject().location?.hostname): boolean { const bridge = getEventBridge() - - return !!bridge && includes(bridge.getAllowedWebViewHosts(), window.location.hostname) + return ( + !!bridge && + bridge.getAllowedWebViewHosts().some((host) => { + const escapedHost = host.replace(/\./g, '\\.') + const isDomainOrSubDomain = new RegExp(`^(.+\\.)*${escapedHost}$`) + return isDomainOrSubDomain.test(hostname) + }) + ) } function getEventBridgeGlobal() { diff --git a/packages/logs/README.md b/packages/logs/README.md index 70f91c73ab..c5665b5da5 100644 --- a/packages/logs/README.md +++ b/packages/logs/README.md @@ -117,17 +117,19 @@ window.DD_LOGS.init({ The following parameters are available to configure the Datadog browser logs SDK to send logs to Datadog: -| Parameter | Type | Required | Default | Description | -| --------------------- | ------- | -------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `clientToken` | String | Yes | | A [Datadog client token][2]. | -| `site` | String | Yes | `datadoghq.com` | The Datadog site of your organization. US: `datadoghq.com`, EU: `datadoghq.eu` | -| `service` | String | No | | The service name for your application. It should follow the [tag syntax requirements][7]. | -| `env` | String | No | | The application’s environment, for example: prod, pre-prod, staging, etc. It should follow the [tag syntax requirements][7]. | -| `version` | String | No | | The application’s version, for example: 1.2.3, 6c44da20, 2020.02.13, etc. It should follow the [tag syntax requirements][7]. | -| `forwardErrorsToLogs` | Boolean | No | `true` | Set to `false` to stop forwarding console.error logs, uncaught exceptions and network errors to Datadog. | -| `sampleRate` | Number | No | `100` | The percentage of sessions to track: `100` for all, `0` for none. Only tracked sessions send logs. | -| `silentMultipleInit` | Boolean | No | | Prevent logging errors while having multiple init. | -| `proxyUrl` | Boolean | No | | Optional proxy URL (ex: https://www.proxy.com/path), see the full [proxy setup guide][6] for more information. | +| Parameter | Type | Required | Default | Description | +| --------------------- | ------------------------------------------------------------------------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `clientToken` | String | Yes | | A [Datadog client token][2]. | +| `site` | String | Yes | `datadoghq.com` | The Datadog site of your organization. US: `datadoghq.com`, EU: `datadoghq.eu` | +| `service` | String | No | | The service name for your application. It should follow the [tag syntax requirements][7]. | +| `env` | String | No | | The application’s environment, for example: prod, pre-prod, staging, etc. It should follow the [tag syntax requirements][7]. | +| `version` | String | No | | The application’s version, for example: 1.2.3, 6c44da20, 2020.02.13, etc. It should follow the [tag syntax requirements][7]. | +| `forwardErrorsToLogs` | Boolean | No | `true` | Set to `false` to stop forwarding console.error logs, uncaught exceptions and network errors to Datadog. | +| `forwardConsoleLogs` | `"all"` or an Array of `"log"` `"debug"` `"info"` `"warn"` `"error"` | No | `[]` | Forward logs from `console.*` to Datadog. Use `"all"` to forward everything or an array of console API names to forward only a subset. | +| `forwardReports` | `"all"` or an Array of `"intervention"` `"deprecation"` `"csp_violation"` | No | `[]` | Forward reports from the [Reporting API][8] to Datadog. Use `"all"` to forward everything or an array of report types to forward only a subset. | +| `sampleRate` | Number | No | `100` | The percentage of sessions to track: `100` for all, `0` for none. Only tracked sessions send logs. | +| `silentMultipleInit` | Boolean | No | | Prevent logging errors while having multiple init. | +| `proxyUrl` | String | No | | Optional proxy URL (ex: https://www.proxy.com/path), see the full [proxy setup guide][6] for more information. | Options that must have a matching configuration when using the `RUM` SDK: @@ -185,6 +187,7 @@ The results are the same when using NPM, CDN async or CDN sync: "id": 123, "message": "Button clicked", "date": 1234567890000, + "origin": "logger", "http": { "useragent": "Mozilla/5.0 ...", }, @@ -663,10 +666,13 @@ window.DD_LOGS && DD_LOGS.logger.setHandler(['', '']) **Note**: The `window.DD_LOGS` check is used to prevent issues if a loading failure occurs with the SDK. -[1]: /account_management/api-app-keys/#api-keys -[2]: /account_management/api-app-keys/#client-tokens + + +[1]: https://docs.datadoghq.com/account_management/api-app-keys/#api-keys +[2]: https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens [3]: https://www.npmjs.com/package/@datadog/browser-logs [4]: https://github.com/DataDog/browser-sdk/blob/main/packages/logs/BROWSER_SUPPORT.md -[5]: /real_user_monitoring/guide/enrich-and-control-rum-data/ +[5]: https://docs.datadoghq.com/real_user_monitoring/guide/enrich-and-control-rum-data/ [6]: https://docs.datadoghq.com/real_user_monitoring/faq/proxy_rum_data/ [7]: https://docs.datadoghq.com/getting_started/tagging/#defining-tags +[8]: https://developer.mozilla.org/en-US/docs/Web/API/Reporting_API diff --git a/packages/logs/package.json b/packages/logs/package.json index e8bdf5400a..0c9eae0a76 100644 --- a/packages/logs/package.json +++ b/packages/logs/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-logs", - "version": "4.7.0", + "version": "4.8.0", "license": "Apache-2.0", "main": "cjs/entries/main.js", "module": "esm/entries/main.js", @@ -13,7 +13,7 @@ "replace-build-env": "node ../../scripts/replace-build-env.js" }, "dependencies": { - "@datadog/browser-core": "4.7.0" + "@datadog/browser-core": "4.8.0" }, "devDependencies": { "@types/sinon": "9.0.10", diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 6829900bd1..6ec80a4ce5 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,5 +1,5 @@ import type { Context } from '@datadog/browser-core' -import { monitor, ONE_SECOND, display } from '@datadog/browser-core' +import { monitor, ONE_SECOND, display, ErrorSource } from '@datadog/browser-core' import type { Clock } from '../../../core/test/specHelper' import { deleteEventBridgeStub, initEventBridgeStub, mockClock } from '../../../core/test/specHelper' import type { HybridInitConfiguration, LogsInitConfiguration } from '../domain/configuration' @@ -223,6 +223,7 @@ describe('logs entry', () => { message: { message: 'message', status: StatusType.info, + origin: ErrorSource.LOGGER, }, }) }) @@ -278,6 +279,7 @@ describe('logs entry', () => { expect(sendLogsSpy).not.toHaveBeenCalled() expect(display.log).toHaveBeenCalledWith('error: message', { + origin: 'logger', error: { origin: 'logger' }, logger: { name: 'foo' }, }) @@ -292,7 +294,7 @@ describe('logs entry', () => { logger.debug('message') expect(sendLogsSpy).toHaveBeenCalled() - expect(display.log).toHaveBeenCalledWith('debug: message', { logger: { name: 'foo' } }) + expect(display.log).toHaveBeenCalledWith('debug: message', { origin: 'logger', logger: { name: 'foo' } }) }) it('should have their name in their context', () => { diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 3b1ddac7b0..2e4a3a3bc8 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -1,30 +1,12 @@ -import type { ConsoleLog, Context, RawError, RelativeTime, RawReport, TimeStamp } from '@datadog/browser-core' -import { - ErrorSource, - noop, - Observable, - ONE_MINUTE, - resetExperimentalFeatures, - updateExperimentalFeatures, - getTimeStamp, - stopSessionManager, - display, - initConsoleObservable, -} from '@datadog/browser-core' +import type { Context, TimeStamp } from '@datadog/browser-core' +import { noop, stopSessionManager } from '@datadog/browser-core' import sinon from 'sinon' -import type { Clock } from '../../../core/test/specHelper' -import { - deleteEventBridgeStub, - initEventBridgeStub, - mockClock, - stubEndpointBuilder, -} from '../../../core/test/specHelper' -import { stubReportingObserver } from '../../../core/test/stubReportApis' +import { deleteEventBridgeStub, initEventBridgeStub, stubEndpointBuilder } from '../../../core/test/specHelper' import type { LogsConfiguration } from '../domain/configuration' import { validateAndBuildLogsConfiguration } from '../domain/configuration' import type { LogsMessage } from '../domain/logger' -import { StatusType, HandlerType } from '../domain/logger' +import { StatusType } from '../domain/logger' import type { LogsSessionManager } from '../domain/logsSessionManager' import type { Sender } from '../domain/sender' import { createSender } from '../domain/sender' @@ -61,28 +43,15 @@ describe('logs', () => { let baseConfiguration: LogsConfiguration let sessionIsTracked: boolean let server: sinon.SinonFakeServer - let rawErrorObservable: Observable - let reportObservable: Observable - let consoleObservable: Observable - let consoleLogSpy: jasmine.Spy + const sessionManager: LogsSessionManager = { findTrackedSession: () => (sessionIsTracked ? { id: SESSION_ID } : undefined), } - let stopLogs = noop const startLogs = ({ - sender = createSender(noop), configuration: configurationOverrides, }: { sender?: Sender; configuration?: Partial } = {}) => { const configuration = { ...baseConfiguration, ...configurationOverrides } - const startLogs = doStartLogs( - configuration, - rawErrorObservable, - consoleObservable, - reportObservable, - sessionManager, - sender - ) - stopLogs = startLogs.stop + const startLogs = doStartLogs(configuration, sessionManager, createSender(noop)) return startLogs.send } @@ -93,11 +62,7 @@ describe('logs', () => { maxBatchSize: 1, } sessionIsTracked = true - rawErrorObservable = new Observable() - consoleObservable = new Observable() - reportObservable = new Observable() server = sinon.fakeServer.create() - consoleLogSpy = spyOn(console, 'log').and.callFake(() => true) }) afterEach(() => { @@ -105,7 +70,6 @@ describe('logs', () => { delete window.DD_RUM deleteEventBridgeStub() stopSessionManager() - stopLogs() }) describe('request', () => { @@ -135,45 +99,6 @@ describe('logs', () => { }) }) - it('should include RUM context', () => { - window.DD_RUM = { - getInternalContext() { - return { view: { url: 'http://from-rum-context.com', id: 'view-id' } } - }, - } - const sendLog = startLogs() - sendLog(DEFAULT_MESSAGE, {}) - - expect(getLoggedMessage(server, 0).view).toEqual({ - id: 'view-id', - url: 'http://from-rum-context.com', - }) - }) - - it('should use the rum internal context related to the error time', () => { - window.DD_RUM = { - getInternalContext(startTime) { - return { - foo: startTime === 1234 ? 'b' : 'a', - } - }, - } - let sendLogStrategy: (message: LogsMessage, currentContext: Context) => void = noop - const sendLog = (message: LogsMessage) => { - sendLogStrategy(message, {}) - } - sendLogStrategy = startLogs({ sender: createSender(sendLog) }) - - rawErrorObservable.notify({ - message: 'error!', - source: ErrorSource.SOURCE, - startClocks: { relative: 1234 as RelativeTime, timeStamp: getTimeStamp(1234 as RelativeTime) }, - type: 'Error', - }) - - expect(getLoggedMessage(server, 0).foo).toBe('b') - }) - it('should all use the same batch', () => { const sendLog = startLogs({ configuration: { maxBatchSize: 3 } }) sendLog(DEFAULT_MESSAGE, {}) @@ -197,130 +122,6 @@ describe('logs', () => { event: jasmine.objectContaining({ message: 'message' }), }) }) - - it('should not print the log twice when console handler is enabled', () => { - const sender = createSender(noop) - const logErrorSpy = spyOn(sender, 'sendToHttp') - const displaySpy = spyOn(display, 'log') - - consoleObservable = initConsoleObservable(['log']) - startLogs({ sender }) - sender.setHandler([HandlerType.console]) - /* eslint-disable-next-line no-console */ - console.log('foo', 'bar') - - expect(logErrorSpy).toHaveBeenCalled() - expect(consoleLogSpy).toHaveBeenCalledTimes(1) - expect(displaySpy).not.toHaveBeenCalled() - - resetExperimentalFeatures() - }) - - it('should send console logs when ff forward-logs is enabled', () => { - const sender = createSender(noop) - const logErrorSpy = spyOn(sender, 'sendToHttp') - - updateExperimentalFeatures(['forward-logs']) - const { stop } = originalStartLogs( - validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: ['log'] })!, - sender - ) - - /* eslint-disable-next-line no-console */ - console.log('foo', 'bar') - - expect(logErrorSpy).toHaveBeenCalled() - expect(consoleLogSpy).toHaveBeenCalled() - - resetExperimentalFeatures() - stop() - }) - - it('should not send console logs when ff forward-logs is disabled', () => { - const sender = createSender(noop) - const logErrorSpy = spyOn(sender, 'sendToHttp') - - const { stop } = originalStartLogs( - validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: ['log'] })!, - sender - ) - - /* eslint-disable-next-line no-console */ - console.log('foo', 'bar') - - expect(logErrorSpy).not.toHaveBeenCalled() - expect(consoleLogSpy).toHaveBeenCalled() - stop() - }) - }) - - describe('reports', () => { - let sender: Sender - let logErrorSpy: jasmine.Spy - let reportingObserverStub: ReturnType - - beforeEach(() => { - sender = createSender(noop) - logErrorSpy = spyOn(sender, 'sendToHttp') - reportingObserverStub = stubReportingObserver() - }) - - afterEach(() => { - reportingObserverStub.reset() - }) - - it('should send reports when ff forward-reports is enabled', () => { - updateExperimentalFeatures(['forward-reports']) - const { stop } = originalStartLogs( - validateAndBuildLogsConfiguration({ ...initConfiguration, forwardReports: ['intervention'] })!, - sender - ) - - reportingObserverStub.raiseReport('intervention') - - expect(logErrorSpy).toHaveBeenCalled() - - resetExperimentalFeatures() - stop() - }) - - it('should not send reports when ff forward-reports is disabled', () => { - const { stop } = originalStartLogs( - validateAndBuildLogsConfiguration({ ...initConfiguration, forwardReports: ['intervention'] })!, - sender - ) - reportingObserverStub.raiseReport('intervention') - - expect(logErrorSpy).not.toHaveBeenCalled() - stop() - }) - - it('should not send reports when forwardReports init option not specified', () => { - const { stop } = originalStartLogs(validateAndBuildLogsConfiguration({ ...initConfiguration })!, sender) - reportingObserverStub.raiseReport('intervention') - - expect(logErrorSpy).not.toHaveBeenCalled() - stop() - }) - - it('should add the source file information to the message for non error reports', () => { - updateExperimentalFeatures(['forward-reports']) - const { stop } = originalStartLogs( - validateAndBuildLogsConfiguration({ ...initConfiguration, forwardReports: ['deprecation'] })!, - sender - ) - - reportingObserverStub.raiseReport('deprecation') - - expect(logErrorSpy).toHaveBeenCalledOnceWith( - 'deprecation: foo bar Found in http://foo.bar/index.js:20:10', - undefined, - 'warn' - ) - - resetExperimentalFeatures() - stop() - }) }) describe('sampling', () => { @@ -328,18 +129,16 @@ describe('logs', () => { const sendSpy = spyOn(initEventBridgeStub(), 'send') let configuration = { ...baseConfiguration, sampleRate: 0 } - let { send, stop } = originalStartLogs(configuration, createSender(noop)) + let { send } = originalStartLogs(configuration, createSender(noop)) send(DEFAULT_MESSAGE, {}) expect(sendSpy).not.toHaveBeenCalled() - stop() configuration = { ...baseConfiguration, sampleRate: 100 } - ;({ send, stop } = originalStartLogs(configuration, createSender(noop))) + ;({ send } = originalStartLogs(configuration, createSender(noop))) send(DEFAULT_MESSAGE, {}) expect(sendSpy).toHaveBeenCalled() - stop() }) }) @@ -374,123 +173,4 @@ describe('logs', () => { expect(server.requests.length).toEqual(2) }) }) - - describe('error collection', () => { - it('should send log errors', () => { - const sendLogSpy = jasmine.createSpy() - startLogs({ sender: createSender(sendLogSpy) }) - - rawErrorObservable.notify({ - message: 'error!', - source: ErrorSource.SOURCE, - startClocks: { relative: 1234 as RelativeTime, timeStamp: 123456789 as TimeStamp }, - type: 'Error', - }) - - expect(sendLogSpy).toHaveBeenCalled() - expect(sendLogSpy.calls.first().args).toEqual([ - { - date: 123456789 as TimeStamp, - error: { origin: ErrorSource.SOURCE, kind: 'Error', stack: undefined }, - message: 'error!', - status: StatusType.error, - }, - ]) - }) - }) - - describe('logs limitation', () => { - let clock: Clock - const configuration = { eventRateLimiterThreshold: 1 } - beforeEach(() => { - clock = mockClock() - }) - - afterEach(() => { - clock.cleanup() - }) - ;[ - { status: StatusType.error, message: 'Reached max number of errors by minute: 1' }, - { status: StatusType.warn, message: 'Reached max number of warns by minute: 1' }, - { status: StatusType.info, message: 'Reached max number of infos by minute: 1' }, - { status: StatusType.debug, message: 'Reached max number of debugs by minute: 1' }, - { status: 'unknown' as StatusType, message: 'Reached max number of customs by minute: 1' }, - ].forEach(({ status, message }) => { - it(`stops sending ${status} logs when reaching the limit`, () => { - const sendLogSpy = jasmine.createSpy<(message: LogsMessage & { foo?: string }) => void>() - const sendLog = startLogs({ sender: createSender(sendLogSpy), configuration }) - sendLog({ message: 'foo', status }, {}) - sendLog({ message: 'bar', status }, {}) - - expect(server.requests.length).toEqual(1) - expect(getLoggedMessage(server, 0).message).toBe('foo') - expect(sendLogSpy).toHaveBeenCalledOnceWith({ - message, - status: StatusType.error, - error: { - origin: ErrorSource.AGENT, - kind: undefined, - stack: undefined, - }, - date: Date.now(), - }) - }) - - it(`does not take discarded ${status} logs into account`, () => { - const sendLogSpy = jasmine.createSpy<(message: LogsMessage & { foo?: string }) => void>() - const sendLog = startLogs({ - sender: createSender(sendLogSpy), - configuration: { - ...configuration, - beforeSend(event) { - if (event.message === 'discard me') { - return false - } - }, - }, - }) - sendLog({ message: 'discard me', status }, {}) - sendLog({ message: 'discard me', status }, {}) - sendLog({ message: 'discard me', status }, {}) - sendLog({ message: 'foo', status }, {}) - - expect(server.requests.length).toEqual(1) - expect(getLoggedMessage(server, 0).message).toBe('foo') - expect(sendLogSpy).not.toHaveBeenCalled() - }) - - it(`allows to send new ${status}s after a minute`, () => { - const sendLog = startLogs({ configuration }) - sendLog({ message: 'foo', status }, {}) - sendLog({ message: 'bar', status }, {}) - clock.tick(ONE_MINUTE) - sendLog({ message: 'baz', status }, {}) - - expect(server.requests.length).toEqual(2) - expect(getLoggedMessage(server, 0).message).toBe('foo') - expect(getLoggedMessage(server, 1).message).toBe('baz') - }) - - it('allows to send logs with a different status when reaching the limit', () => { - const otherLogStatus = status === StatusType.error ? 'other' : StatusType.error - const sendLog = startLogs({ configuration }) - sendLog({ message: 'foo', status }, {}) - sendLog({ message: 'bar', status }, {}) - sendLog({ message: 'baz', status: otherLogStatus as StatusType }, {}) - - expect(server.requests.length).toEqual(2) - expect(getLoggedMessage(server, 0).message).toBe('foo') - expect(getLoggedMessage(server, 1).message).toBe('baz') - }) - }) - - it('two different custom statuses are accounted by the same limit', () => { - const sendLog = startLogs({ configuration }) - sendLog({ message: 'foo', status: 'foo' as StatusType }, {}) - sendLog({ message: 'bar', status: 'bar' as StatusType }, {}) - - expect(server.requests.length).toEqual(1) - expect(getLoggedMessage(server, 0).message).toBe('foo') - }) - }) }) diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index a31bdce726..0eb97e11db 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,43 +1,22 @@ -import type { ConsoleLog, Context, RawError, MonitoringMessage, TelemetryEvent, RawReport } from '@datadog/browser-core' +import type { Context, MonitoringMessage, TelemetryEvent } from '@datadog/browser-core' import { areCookiesAuthorized, combine, - Observable, - trackRuntimeError, canUseEventBridge, getEventBridge, startInternalMonitoring, - RawReportType, - initReportObservable, - initConsoleObservable, - ConsoleApiName, - ErrorSource, - getFileFromStackTraceString, startBatchWithReplica, } from '@datadog/browser-core' -import { trackNetworkError } from '../domain/trackNetworkError' import type { LogsMessage } from '../domain/logger' -import { StatusType } from '../domain/logger' import type { LogsSessionManager } from '../domain/logsSessionManager' import { startLogsSessionManager, startLogsSessionManagerStub } from '../domain/logsSessionManager' import type { LogsConfiguration } from '../domain/configuration' -import type { LogsEvent } from '../logsEvent.types' import { buildAssemble, getRUMInternalContext } from '../domain/assemble' import type { Sender } from '../domain/sender' - -const LogStatusForApi = { - [ConsoleApiName.log]: StatusType.info, - [ConsoleApiName.debug]: StatusType.debug, - [ConsoleApiName.info]: StatusType.info, - [ConsoleApiName.warn]: StatusType.warn, - [ConsoleApiName.error]: StatusType.error, -} - -const LogStatusForReport = { - [RawReportType.cspViolation]: StatusType.error, - [RawReportType.intervention]: StatusType.error, - [RawReportType.deprecation]: StatusType.warn, -} +import { startConsoleCollection } from '../domain/logsCollection/console/consoleCollection' +import { startReportCollection } from '../domain/logsCollection/report/reportCollection' +import { startNetworkErrorCollection } from '../domain/logsCollection/networkError/networkErrorCollection' +import { startRuntimeErrorCollection } from '../domain/logsCollection/runtimeError/runtimeErrorCollection' export function startLogs(configuration: LogsConfiguration, sender: Sender) { const internalMonitoring = startLogsInternalMonitoring(configuration) @@ -61,21 +40,17 @@ export function startLogs(configuration: LogsConfiguration, sender: Sender) { }, })) - const rawErrorObservable = new Observable() - - if (configuration.forwardErrorsToLogs) { - trackRuntimeError(rawErrorObservable) - trackNetworkError(configuration, rawErrorObservable) - } - const consoleObservable = initConsoleObservable(configuration.forwardConsoleLogs) - const reportObservable = initReportObservable(configuration.forwardReports) + startNetworkErrorCollection(configuration, sender) + startRuntimeErrorCollection(configuration, sender) + startConsoleCollection(configuration, sender) + startReportCollection(configuration, sender) const session = areCookiesAuthorized(configuration.cookieOptions) && !canUseEventBridge() ? startLogsSessionManager(configuration) : startLogsSessionManagerStub(configuration) - return doStartLogs(configuration, rawErrorObservable, consoleObservable, reportObservable, session, sender) + return doStartLogs(configuration, session, sender) } function startLogsInternalMonitoring(configuration: LogsConfiguration) { @@ -103,15 +78,8 @@ function startLogsInternalMonitoring(configuration: LogsConfiguration) { return internalMonitoring } -export function doStartLogs( - configuration: LogsConfiguration, - rawErrorObservable: Observable, - consoleObservable: Observable, - reportObservable: Observable, - sessionManager: LogsSessionManager, - sender: Sender -) { - const assemble = buildAssemble(sessionManager, configuration, reportRawError) +export function doStartLogs(configuration: LogsConfiguration, sessionManager: LogsSessionManager, sender: Sender) { + const assemble = buildAssemble(sessionManager, configuration, sender) let onLogEventCollected: (message: Context) => void if (canUseEventBridge()) { @@ -126,67 +94,7 @@ export function doStartLogs( onLogEventCollected = (message) => batch.add(message) } - function reportRawError(error: RawError) { - const messageContext: Partial = { - date: error.startClocks.timeStamp, - error: { - kind: error.type, - origin: error.source, - stack: error.stack, - }, - } - if (error.resource) { - messageContext.http = { - method: error.resource.method as any, // Cast resource method because of case mismatch cf issue RUMF-1152 - status_code: error.resource.statusCode, - url: error.resource.url, - } - } - sender.sendToHttp(error.message, messageContext, StatusType.error) - } - - function reportConsoleLog(log: ConsoleLog) { - let messageContext: Partial | undefined - if (log.api === ConsoleApiName.error) { - messageContext = { - error: { - origin: ErrorSource.CONSOLE, - stack: log.stack, - }, - } - } - sender.sendToHttp(log.message, messageContext, LogStatusForApi[log.api]) - } - - function logReport(report: RawReport) { - let message = report.message - let messageContext: Partial | undefined - const logStatus = LogStatusForReport[report.type] - if (logStatus === StatusType.error) { - messageContext = { - error: { - kind: report.subtype, - origin: ErrorSource.REPORT, - stack: report.stack, - }, - } - } else if (report.stack) { - message += ` Found in ${getFileFromStackTraceString(report.stack)!}` - } - - sender.sendToHttp(message, messageContext, logStatus) - } - - const rawErrorSubscription = rawErrorObservable.subscribe(reportRawError) - const consoleSubscription = consoleObservable.subscribe(reportConsoleLog) - const reportSubscription = reportObservable.subscribe(logReport) - return { - stop: () => { - rawErrorSubscription.unsubscribe() - consoleSubscription.unsubscribe() - reportSubscription.unsubscribe() - }, send: (message: LogsMessage, currentContext: Context) => { const contextualizedMessage = assemble(message, currentContext) if (contextualizedMessage) { diff --git a/packages/logs/src/domain/assemble.spec.ts b/packages/logs/src/domain/assemble.spec.ts index 6c33d202bb..73ca02fafa 100644 --- a/packages/logs/src/domain/assemble.spec.ts +++ b/packages/logs/src/domain/assemble.spec.ts @@ -1,13 +1,15 @@ -import type { Context } from '@datadog/browser-core' -import { noop } from '@datadog/browser-core' +import type { Context, RelativeTime } from '@datadog/browser-core' +import { ErrorSource, ONE_MINUTE, getTimeStamp, noop, clocksNow } from '@datadog/browser-core' import type { LogsEvent } from '../logsEvent.types' -import { stubEndpointBuilder } from '../../../core/test/specHelper' +import type { Clock } from '../../../core/test/specHelper' +import { mockClock } from '../../../core/test/specHelper' import { buildAssemble } from './assemble' import type { LogsConfiguration } from './configuration' import { validateAndBuildLogsConfiguration } from './configuration' import type { LogsMessage } from './logger' import { StatusType } from './logger' import type { LogsSessionManager } from './logsSessionManager' +import { createSender } from './sender' describe('assemble', () => { const initConfiguration = { clientToken: 'xxx', service: 'service' } @@ -20,22 +22,19 @@ describe('assemble', () => { let assemble: (message: LogsMessage, currentContext: Context) => Context | undefined let beforeSend: (event: LogsEvent) => void | boolean - let baseConfiguration: LogsConfiguration let sessionIsTracked: boolean + let sendLogSpy: jasmine.Spy beforeEach(() => { sessionIsTracked = true - baseConfiguration = { + sendLogSpy = jasmine.createSpy() + const configuration = { ...validateAndBuildLogsConfiguration(initConfiguration)!, - logsEndpointBuilder: stubEndpointBuilder('https://localhost/v1/input/log'), maxBatchSize: 1, + beforeSend: (x: LogsEvent) => beforeSend(x), } beforeSend = noop - assemble = buildAssemble( - sessionManager, - { ...baseConfiguration, beforeSend: (x: LogsEvent) => beforeSend(x) }, - noop - ) + assemble = buildAssemble(sessionManager, configuration, createSender(sendLogSpy)) window.DD_RUM = { getInternalContext: noop, } @@ -116,4 +115,130 @@ describe('assemble', () => { expect(assembledMessage!.foo).toBe('bar') }) + + it('should use the rum internal context related to the error time', () => { + window.DD_RUM = { + getInternalContext(startTime) { + return { + foo: startTime === 1234 ? 'b' : 'a', + } + }, + } + + const message = { ...DEFAULT_MESSAGE, date: getTimeStamp(1234 as RelativeTime) } + + const assembledMessage = assemble(message, {}) + + expect(assembledMessage!.foo).toBe('b') + }) + + it('should include RUM context', () => { + window.DD_RUM = { + getInternalContext() { + return { view: { url: 'http://from-rum-context.com', id: 'view-id' } } + }, + } + + const message = { ...DEFAULT_MESSAGE } + + const assembledMessage = assemble(message, {}) + + expect(assembledMessage!.view).toEqual({ + id: 'view-id', + url: 'http://from-rum-context.com', + }) + }) + + describe('logs limitation', () => { + let clock: Clock + beforeEach(() => { + clock = mockClock() + assemble = buildAssemble( + sessionManager, + { eventRateLimiterThreshold: 1, beforeSend: (x: LogsEvent) => beforeSend(x) } as LogsConfiguration, + createSender(sendLogSpy) + ) + }) + + afterEach(() => { + clock.cleanup() + }) + ;[ + { status: StatusType.error, message: 'Reached max number of errors by minute: 1' }, + { status: StatusType.warn, message: 'Reached max number of warns by minute: 1' }, + { status: StatusType.info, message: 'Reached max number of infos by minute: 1' }, + { status: StatusType.debug, message: 'Reached max number of debugs by minute: 1' }, + { status: 'unknown' as StatusType, message: 'Reached max number of customs by minute: 1' }, + ].forEach(({ status, message }) => { + it(`stops sending ${status} logs when reaching the limit`, () => { + const assembledMessage1 = assemble({ message: 'foo', status }, {}) + const assembledMessage2 = assemble({ message: 'bar', status }, {}) + + expect(assembledMessage1!.message).toBe('foo') + expect(assembledMessage2).toBeUndefined() + + expect(sendLogSpy).toHaveBeenCalledOnceWith({ + message, + status: StatusType.error, + date: clocksNow().timeStamp, + origin: ErrorSource.AGENT, + error: { + kind: undefined, + origin: ErrorSource.AGENT, + stack: undefined, + }, + }) + }) + + it(`does not take discarded ${status} logs into account`, () => { + const sendLogSpy = jasmine.createSpy<(message: LogsMessage & { foo?: string }) => void>() + beforeSend = (event) => { + if (event.message === 'discard me') { + return false + } + } + + const assembledMessage1 = assemble({ message: 'discard me', status }, {}) + const assembledMessage2 = assemble({ message: 'discard me', status }, {}) + const assembledMessage3 = assemble({ message: 'discard me', status }, {}) + const assembledMessage4 = assemble({ message: 'foo', status }, {}) + + expect(assembledMessage1).toBeUndefined() + expect(assembledMessage2).toBeUndefined() + expect(assembledMessage3).toBeUndefined() + expect(assembledMessage4!.message).toBe('foo') + expect(sendLogSpy).not.toHaveBeenCalled() + }) + + it(`allows to send new ${status}s after a minute`, () => { + const assembledMessage1 = assemble({ message: 'foo', status }, {}) + const assembledMessage2 = assemble({ message: 'bar', status }, {}) + clock.tick(ONE_MINUTE) + const assembledMessage3 = assemble({ message: 'baz', status }, {}) + + expect(assembledMessage2).toBeUndefined() + expect(assembledMessage1!.message).toBe('foo') + expect(assembledMessage3!.message).toBe('baz') + }) + + it('allows to send logs with a different status when reaching the limit', () => { + const otherLogStatus = status === StatusType.error ? 'other' : StatusType.error + const assembledMessage1 = assemble({ message: 'foo', status }, {}) + const assembledMessage2 = assemble({ message: 'bar', status }, {}) + const assembledMessage3 = assemble({ message: 'baz', status: otherLogStatus as StatusType }, {}) + + expect(assembledMessage2).toBeUndefined() + expect(assembledMessage1!.message).toBe('foo') + expect(assembledMessage3!.message).toBe('baz') + }) + }) + + it('two different custom statuses are accounted by the same limit', () => { + const assembledMessage1 = assemble({ message: 'foo', status: 'foo' as StatusType }, {}) + const assembledMessage2 = assemble({ message: 'bar', status: 'bar' as StatusType }, {}) + + expect(assembledMessage2).toBeUndefined() + expect(assembledMessage1!.message).toBe('foo') + }) + }) }) diff --git a/packages/logs/src/domain/assemble.ts b/packages/logs/src/domain/assemble.ts index 53700a20e5..6752aca548 100644 --- a/packages/logs/src/domain/assemble.ts +++ b/packages/logs/src/domain/assemble.ts @@ -4,26 +4,34 @@ import type { LogsConfiguration } from './configuration' import type { LogsMessage } from './logger' import { StatusType } from './logger' import type { LogsSessionManager } from './logsSessionManager' +import { reportRawError } from './reportRawError' +import type { Sender } from './sender' + +export function buildAssemble(sessionManager: LogsSessionManager, configuration: LogsConfiguration, sender: Sender) { + const reportAgentError = (error: RawError) => reportRawError(error, sender) -export function buildAssemble( - sessionManager: LogsSessionManager, - configuration: LogsConfiguration, - reportRawError: (error: RawError) => void -) { const logRateLimiters = { [StatusType.error]: createEventRateLimiter( StatusType.error, configuration.eventRateLimiterThreshold, - reportRawError + reportAgentError + ), + [StatusType.warn]: createEventRateLimiter( + StatusType.warn, + configuration.eventRateLimiterThreshold, + reportAgentError + ), + [StatusType.info]: createEventRateLimiter( + StatusType.info, + configuration.eventRateLimiterThreshold, + reportAgentError ), - [StatusType.warn]: createEventRateLimiter(StatusType.warn, configuration.eventRateLimiterThreshold, reportRawError), - [StatusType.info]: createEventRateLimiter(StatusType.info, configuration.eventRateLimiterThreshold, reportRawError), [StatusType.debug]: createEventRateLimiter( StatusType.debug, configuration.eventRateLimiterThreshold, - reportRawError + reportAgentError ), - ['custom']: createEventRateLimiter('custom', configuration.eventRateLimiterThreshold, reportRawError), + ['custom']: createEventRateLimiter('custom', configuration.eventRateLimiterThreshold, reportAgentError), } return (message: LogsMessage, currentContext: Context) => { diff --git a/packages/logs/src/domain/configuration.spec.ts b/packages/logs/src/domain/configuration.spec.ts index 326e7da550..f10b709073 100644 --- a/packages/logs/src/domain/configuration.spec.ts +++ b/packages/logs/src/domain/configuration.spec.ts @@ -1,4 +1,4 @@ -import { display, resetExperimentalFeatures, updateExperimentalFeatures } from '@datadog/browser-core' +import { display } from '@datadog/browser-core' import { validateAndBuildForwardOption, validateAndBuildLogsConfiguration } from './configuration' const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx' } @@ -67,47 +67,33 @@ describe('validateAndBuildForwardOption', () => { let displaySpy: jasmine.Spy const allowedValues = ['foo', 'bar'] const label = 'Label' - const ff = 'flag' const errorMessage = 'Label should be "all" or an array with allowed values "foo", "bar"' - describe('if ff enabled', () => { - beforeEach(() => { - displaySpy = spyOn(display, 'error') - updateExperimentalFeatures([ff]) - }) - - afterEach(() => { - resetExperimentalFeatures() - }) - - it('does not validate the configuration if an incorrect string is provided', () => { - validateAndBuildForwardOption('foo' as any, allowedValues, label, ff) + beforeEach(() => { + displaySpy = spyOn(display, 'error') + }) - expect(displaySpy).toHaveBeenCalledOnceWith(errorMessage) - }) + it('does not validate the configuration if an incorrect string is provided', () => { + validateAndBuildForwardOption('foo' as any, allowedValues, label) - it('does not validate the configuration if an incorrect api is provided', () => { - validateAndBuildForwardOption(['dir'], allowedValues, label, ff) + expect(displaySpy).toHaveBeenCalledOnceWith(errorMessage) + }) - expect(displaySpy).toHaveBeenCalledOnceWith(errorMessage) - }) + it('does not validate the configuration if an incorrect api is provided', () => { + validateAndBuildForwardOption(['dir'], allowedValues, label) - it('defaults to an empty array', () => { - expect(validateAndBuildForwardOption(undefined, allowedValues, label, ff)).toEqual([]) - }) + expect(displaySpy).toHaveBeenCalledOnceWith(errorMessage) + }) - it('is set to provided value', () => { - expect(validateAndBuildForwardOption(['foo'], allowedValues, label, ff)).toEqual(['foo']) - }) + it('defaults to an empty array', () => { + expect(validateAndBuildForwardOption(undefined, allowedValues, label)).toEqual([]) + }) - it('contains all options when "all" is provided', () => { - expect(validateAndBuildForwardOption('all', allowedValues, label, ff)).toEqual(allowedValues) - }) + it('is set to provided value', () => { + expect(validateAndBuildForwardOption(['foo'], allowedValues, label)).toEqual(['foo']) }) - describe('if ff disabled', () => { - it('should be set to empty array', () => { - expect(validateAndBuildForwardOption(['log'], allowedValues, label, ff)).toEqual([]) - }) + it('contains all options when "all" is provided', () => { + expect(validateAndBuildForwardOption('all', allowedValues, label)).toEqual(allowedValues) }) }) diff --git a/packages/logs/src/domain/configuration.ts b/packages/logs/src/domain/configuration.ts index 1795cb6850..84c2b52d8c 100644 --- a/packages/logs/src/domain/configuration.ts +++ b/packages/logs/src/domain/configuration.ts @@ -4,7 +4,6 @@ import { ONE_KILO_BYTE, validateAndBuildConfiguration, display, - isExperimentalFeatureEnabled, removeDuplicates, ConsoleApiName, RawReportType, @@ -42,15 +41,13 @@ export function validateAndBuildLogsConfiguration( const forwardConsoleLogs = validateAndBuildForwardOption( initConfiguration.forwardConsoleLogs, objectValues(ConsoleApiName), - 'Forward Console Logs', - 'forward-logs' + 'Forward Console Logs' ) const forwardReports = validateAndBuildForwardOption( initConfiguration.forwardReports, objectValues(RawReportType), - 'Forward Reports', - 'forward-reports' + 'Forward Reports' ) if (!baseConfiguration || !forwardConsoleLogs || !forwardReports) { @@ -75,10 +72,9 @@ export function validateAndBuildLogsConfiguration( export function validateAndBuildForwardOption( option: readonly T[] | 'all' | undefined, allowedValues: T[], - label: string, - featureFlag: string + label: string ): T[] | undefined { - if (!isExperimentalFeatureEnabled(featureFlag) || option === undefined) { + if (option === undefined) { return [] } diff --git a/packages/logs/src/domain/logger.spec.ts b/packages/logs/src/domain/logger.spec.ts index 690b135dfd..6b284ee092 100644 --- a/packages/logs/src/domain/logger.spec.ts +++ b/packages/logs/src/domain/logger.spec.ts @@ -1,4 +1,4 @@ -import { display } from '@datadog/browser-core' +import { display, ErrorSource } from '@datadog/browser-core' import type { LogsMessage } from './logger' import { HandlerType, Logger, STATUSES, StatusType } from './logger' import type { Sender } from './sender' @@ -26,6 +26,18 @@ describe('Logger', () => { expect(getLoggedMessage(0).status).toEqual(StatusType.info) }) + it("'logger.log' should set 'logger' origin", () => { + logger.log('message') + + expect(getLoggedMessage(0).origin).toEqual(ErrorSource.LOGGER) + }) + + it("'logger.log' message context can override the 'logger' origin", () => { + logger.log('message', { origin: 'foo' }) + + expect(getLoggedMessage(0).origin).toEqual('foo') + }) + STATUSES.forEach((status) => { it(`'logger.${status}' should have ${status} status`, () => { logger[status]('message') @@ -95,6 +107,7 @@ describe('Logger', () => { expect(sendLogSpy).not.toHaveBeenCalled() expect(display.log).toHaveBeenCalledWith('error: message', { error: { origin: 'logger' }, + origin: 'logger', foo: 'bar', lorem: 'ipsum', }) diff --git a/packages/logs/src/domain/logger.ts b/packages/logs/src/domain/logger.ts index d05ca0122c..3cb706b703 100644 --- a/packages/logs/src/domain/logger.ts +++ b/packages/logs/src/domain/logger.ts @@ -32,7 +32,16 @@ export class Logger { @monitored log(message: string, messageContext?: object, status: StatusType = StatusType.info) { - this.sender.sendLog(message, messageContext, status) + this.sender.sendLog( + message, + combine( + { + origin: ErrorSource.LOGGER, + }, + messageContext + ), + status + ) } debug(message: string, messageContext?: object) { @@ -50,6 +59,7 @@ export class Logger { error(message: string, messageContext?: object) { const errorOrigin = { error: { + // Todo: remove error origin in the next major version origin: ErrorSource.LOGGER, }, } diff --git a/packages/logs/src/domain/logsCollection/console/consoleCollection.spec.ts b/packages/logs/src/domain/logsCollection/console/consoleCollection.spec.ts new file mode 100644 index 0000000000..649b150025 --- /dev/null +++ b/packages/logs/src/domain/logsCollection/console/consoleCollection.spec.ts @@ -0,0 +1,72 @@ +import { ErrorSource, display, noop } from '@datadog/browser-core' +import { validateAndBuildLogsConfiguration } from '../../configuration' +import { HandlerType, StatusType } from '../../logger' +import { createSender } from '../../sender' +import { startConsoleCollection } from './consoleCollection' + +describe('console collection', () => { + const initConfiguration = { clientToken: 'xxx', service: 'service' } + let sendLogSpy: jasmine.Spy + let consoleLogSpy: jasmine.Spy + let stopConsolCollection: () => void + + beforeEach(() => { + stopConsolCollection = noop + sendLogSpy = jasmine.createSpy('sendLogSpy') + consoleLogSpy = spyOn(console, 'log').and.callFake(() => true) + spyOn(console, 'error').and.callFake(() => true) + }) + + afterEach(() => { + stopConsolCollection() + }) + + it('should send console logs', () => { + ;({ stop: stopConsolCollection } = startConsoleCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: ['log'] })!, + createSender(sendLogSpy) + )) + + /* eslint-disable-next-line no-console */ + console.log('foo', 'bar') + + expect(sendLogSpy).toHaveBeenCalledWith({ + message: 'foo bar', + status: StatusType.info, + origin: ErrorSource.CONSOLE, + }) + + expect(consoleLogSpy).toHaveBeenCalled() + }) + + it('console error should have an error object defined', () => { + ;({ stop: stopConsolCollection } = startConsoleCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardErrorsToLogs: true })!, + createSender(sendLogSpy) + )) + + /* eslint-disable-next-line no-console */ + console.error('foo', 'bar') + + expect(sendLogSpy.calls.mostRecent().args[0].error).toEqual({ + origin: ErrorSource.CONSOLE, + stack: undefined, + }) + }) + + it('should not print the log twice when console handler is enabled', () => { + const sender = createSender(sendLogSpy) + const displaySpy = spyOn(display, 'log') + ;({ stop: stopConsolCollection } = startConsoleCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardConsoleLogs: ['log'] })!, + sender + )) + + sender.setHandler([HandlerType.console]) + /* eslint-disable-next-line no-console */ + console.log('foo', 'bar') + + expect(consoleLogSpy).toHaveBeenCalledTimes(1) + expect(displaySpy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/logs/src/domain/logsCollection/console/consoleCollection.ts b/packages/logs/src/domain/logsCollection/console/consoleCollection.ts new file mode 100644 index 0000000000..4fa13bf0c6 --- /dev/null +++ b/packages/logs/src/domain/logsCollection/console/consoleCollection.ts @@ -0,0 +1,44 @@ +import type { Context, ClocksState, ConsoleLog } from '@datadog/browser-core' +import { ConsoleApiName, ErrorSource, initConsoleObservable } from '@datadog/browser-core' +import type { LogsEvent } from '../../../logsEvent.types' +import type { LogsConfiguration } from '../../configuration' +import { StatusType } from '../../logger' +import type { Sender } from '../../sender' + +export interface ProvidedError { + startClocks: ClocksState + error: unknown + context?: Context + handlingStack: string +} + +const LogStatusForApi = { + [ConsoleApiName.log]: StatusType.info, + [ConsoleApiName.debug]: StatusType.debug, + [ConsoleApiName.info]: StatusType.info, + [ConsoleApiName.warn]: StatusType.warn, + [ConsoleApiName.error]: StatusType.error, +} +export function startConsoleCollection(configuration: LogsConfiguration, sender: Sender) { + const consoleObservable = initConsoleObservable(configuration.forwardConsoleLogs) + const consoleSubscription = consoleObservable.subscribe(reportConsoleLog) + + function reportConsoleLog(log: ConsoleLog) { + const messageContext: Partial = { + origin: ErrorSource.CONSOLE, + } + if (log.api === ConsoleApiName.error) { + messageContext.error = { + origin: ErrorSource.CONSOLE, // Todo: Remove in the next major release + stack: log.stack, + } + } + sender.sendToHttp(log.message, messageContext, LogStatusForApi[log.api]) + } + + return { + stop: () => { + consoleSubscription.unsubscribe() + }, + } +} diff --git a/packages/logs/src/domain/trackNetworkError.spec.ts b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.spec.ts similarity index 82% rename from packages/logs/src/domain/trackNetworkError.spec.ts rename to packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.spec.ts index 9ca459cedf..a0b109a123 100644 --- a/packages/logs/src/domain/trackNetworkError.spec.ts +++ b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.spec.ts @@ -1,27 +1,28 @@ -import type { RawError } from '@datadog/browser-core' -import { isIE, Observable } from '@datadog/browser-core' -import type { FetchStub, FetchStubManager } from '../../../core/test/specHelper' -import { SPEC_ENDPOINTS, ResponseStub, stubFetch } from '../../../core/test/specHelper' -import type { LogsConfiguration } from './configuration' +import { isIE, ErrorSource } from '@datadog/browser-core' +import type { FetchStub, FetchStubManager } from '@datadog/browser-core/test/specHelper' +import { SPEC_ENDPOINTS, ResponseStub, stubFetch } from '@datadog/browser-core/test/specHelper' +import type { LogsConfiguration } from '../../configuration' +import { StatusType } from '../../logger' +import { createSender } from '../../sender' import { computeFetchErrorText, computeFetchResponseText, computeXhrResponseData, - trackNetworkError, -} from './trackNetworkError' + startNetworkErrorCollection, +} from './networkErrorCollection' const CONFIGURATION = { requestErrorResponseLengthLimit: 64, ...SPEC_ENDPOINTS, } as LogsConfiguration -describe('network error tracker', () => { - let errorObservableSpy: jasmine.Spy +describe('network error collection', () => { let fetchStub: FetchStub let fetchStubManager: FetchStubManager - let stopNetworkErrorTracking: () => void - let errorObservable: Observable + let stopNetworkErrorCollection: () => void + let sendLogSpy: jasmine.Spy + const FAKE_URL = 'http://fake.com/' const DEFAULT_REQUEST = { duration: 10, @@ -36,16 +37,14 @@ describe('network error tracker', () => { if (isIE()) { pending('no fetch support') } - errorObservable = new Observable() - errorObservableSpy = spyOn(errorObservable, 'notify') - + sendLogSpy = jasmine.createSpy('sendLogSpy') fetchStubManager = stubFetch() - ;({ stop: stopNetworkErrorTracking } = trackNetworkError(CONFIGURATION, errorObservable)) + ;({ stop: stopNetworkErrorCollection } = startNetworkErrorCollection(CONFIGURATION, createSender(sendLogSpy))) fetchStub = window.fetch as FetchStub }) afterEach(() => { - stopNetworkErrorTracking() + stopNetworkErrorCollection() fetchStubManager.reset() }) @@ -53,16 +52,20 @@ describe('network error tracker', () => { fetchStub(FAKE_URL).resolveWith(DEFAULT_REQUEST) fetchStubManager.whenAllComplete(() => { - expect(errorObservableSpy).toHaveBeenCalledWith({ + expect(sendLogSpy).toHaveBeenCalledWith({ message: 'Fetch error GET http://fake.com/', - resource: { + date: jasmine.any(Number), + status: StatusType.error, + origin: ErrorSource.NETWORK, + error: { + origin: ErrorSource.NETWORK, + stack: 'Server error', + }, + http: { method: 'GET', - statusCode: 503, + status_code: 503, url: 'http://fake.com/', }, - source: 'network', - stack: 'Server error', - startClocks: jasmine.any(Object), }) done() }) @@ -72,16 +75,16 @@ describe('network error tracker', () => { fetchStub('https://logs-intake.com/v1/input/send?foo=bar').resolveWith(DEFAULT_REQUEST) fetchStubManager.whenAllComplete(() => { - expect(errorObservableSpy).not.toHaveBeenCalled() + expect(sendLogSpy).not.toHaveBeenCalled() done() }) }) - it('should track aborted requests ', (done) => { + it('should track aborted requests', (done) => { fetchStub(FAKE_URL).abort() fetchStubManager.whenAllComplete(() => { - expect(errorObservableSpy).toHaveBeenCalled() + expect(sendLogSpy).toHaveBeenCalled() done() }) }) @@ -90,7 +93,7 @@ describe('network error tracker', () => { fetchStub(FAKE_URL).resolveWith({ ...DEFAULT_REQUEST, status: 0 }) fetchStubManager.whenAllComplete(() => { - expect(errorObservableSpy).toHaveBeenCalled() + expect(sendLogSpy).toHaveBeenCalled() done() }) }) @@ -99,7 +102,7 @@ describe('network error tracker', () => { fetchStub(FAKE_URL).resolveWith({ ...DEFAULT_REQUEST, status: 400 }) fetchStubManager.whenAllComplete(() => { - expect(errorObservableSpy).not.toHaveBeenCalled() + expect(sendLogSpy).not.toHaveBeenCalled() done() }) }) @@ -108,7 +111,7 @@ describe('network error tracker', () => { fetchStub(FAKE_URL).resolveWith({ ...DEFAULT_REQUEST, status: 200 }) fetchStubManager.whenAllComplete(() => { - expect(errorObservableSpy).not.toHaveBeenCalled() + expect(sendLogSpy).not.toHaveBeenCalled() done() }) }) @@ -117,8 +120,8 @@ describe('network error tracker', () => { fetchStub(FAKE_URL).resolveWith({ ...DEFAULT_REQUEST, responseText: '' }) fetchStubManager.whenAllComplete(() => { - expect(errorObservableSpy).toHaveBeenCalled() - const stack = (errorObservableSpy.calls.mostRecent().args[0] as RawError).stack + expect(sendLogSpy).toHaveBeenCalled() + const stack = sendLogSpy.calls.mostRecent().args[0].error.stack expect(stack).toEqual('Failed to load') done() }) diff --git a/packages/logs/src/domain/trackNetworkError.ts b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.ts similarity index 88% rename from packages/logs/src/domain/trackNetworkError.ts rename to packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.ts index 178ce8274b..0a5d5e30e0 100644 --- a/packages/logs/src/domain/trackNetworkError.ts +++ b/packages/logs/src/domain/logsCollection/networkError/networkErrorCollection.ts @@ -1,4 +1,4 @@ -import type { FetchCompleteContext, Observable, RawError, XhrCompleteContext } from '@datadog/browser-core' +import type { FetchCompleteContext, XhrCompleteContext } from '@datadog/browser-core' import { ErrorSource, initXhrObservable, @@ -9,9 +9,12 @@ import { monitor, noop, } from '@datadog/browser-core' -import type { LogsConfiguration } from './configuration' +import type { LogsEvent } from '../../../logsEvent.types' +import type { LogsConfiguration } from '../../configuration' +import { StatusType } from '../../logger' +import type { Sender } from '../../sender' -export function trackNetworkError(configuration: LogsConfiguration, errorObservable: Observable) { +export function startNetworkErrorCollection(configuration: LogsConfiguration, sender: Sender) { const xhrSubscription = initXhrObservable().subscribe((context) => { if (context.state === 'complete') { handleCompleteRequest(RequestType.XHR, context) @@ -35,17 +38,21 @@ export function trackNetworkError(configuration: LogsConfiguration, errorObserva } function onResponseDataAvailable(responseData: unknown) { - errorObservable.notify({ - message: `${format(type)} error ${request.method} ${request.url}`, - resource: { - method: request.method, - statusCode: request.status, + const messageContext: Partial = { + date: request.startClocks.timeStamp, + error: { + origin: ErrorSource.NETWORK, // Todo: Remove in the next major release + stack: (responseData as string) || 'Failed to load', + }, + http: { + method: request.method as any, // Cast resource method because of case mismatch cf issue RUMF-1152 + status_code: request.status, url: request.url, }, - source: ErrorSource.NETWORK, - stack: (responseData as string) || 'Failed to load', - startClocks: request.startClocks, - }) + origin: ErrorSource.NETWORK, + } + + sender.sendToHttp(`${format(type)} error ${request.method} ${request.url}`, messageContext, StatusType.error) } } diff --git a/packages/logs/src/domain/logsCollection/report/reportCollection.spec.ts b/packages/logs/src/domain/logsCollection/report/reportCollection.spec.ts new file mode 100644 index 0000000000..6056a7ccda --- /dev/null +++ b/packages/logs/src/domain/logsCollection/report/reportCollection.spec.ts @@ -0,0 +1,68 @@ +import { ErrorSource, noop } from '@datadog/browser-core' +import { stubReportingObserver } from '@datadog/browser-core/test/stubReportApis' +import { validateAndBuildLogsConfiguration } from '../../configuration' +import { StatusType } from '../../logger' +import { createSender } from '../../sender' +import { startReportCollection } from './reportCollection' + +describe('reports', () => { + const initConfiguration = { clientToken: 'xxx', service: 'service' } + let sendLogSpy: jasmine.Spy + let reportingObserverStub: ReturnType + let stopReportCollection: () => void + + beforeEach(() => { + stopReportCollection = noop + sendLogSpy = jasmine.createSpy('sendLogSpy') + reportingObserverStub = stubReportingObserver() + }) + + afterEach(() => { + reportingObserverStub.reset() + stopReportCollection() + }) + + it('should send reports', () => { + ;({ stop: stopReportCollection } = startReportCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardReports: ['intervention'] })!, + createSender(sendLogSpy) + )) + + reportingObserverStub.raiseReport('intervention') + expect(sendLogSpy).toHaveBeenCalledOnceWith({ + error: { + kind: 'NavigatorVibrate', + origin: ErrorSource.REPORT, + stack: jasmine.any(String), + }, + message: 'intervention: foo bar', + status: StatusType.error, + origin: ErrorSource.REPORT, + }) + }) + + it('should not send reports when forwardReports init option not specified', () => { + ;({ stop: stopReportCollection } = startReportCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration })!, + createSender(sendLogSpy) + )) + reportingObserverStub.raiseReport('intervention') + + expect(sendLogSpy).not.toHaveBeenCalled() + }) + + it('should add the source file information to the message for non error reports', () => { + ;({ stop: stopReportCollection } = startReportCollection( + validateAndBuildLogsConfiguration({ ...initConfiguration, forwardReports: ['deprecation'] })!, + createSender(sendLogSpy) + )) + + reportingObserverStub.raiseReport('deprecation') + + expect(sendLogSpy).toHaveBeenCalledOnceWith({ + message: 'deprecation: foo bar Found in http://foo.bar/index.js:20:10', + status: StatusType.warn, + origin: ErrorSource.REPORT, + }) + }) +}) diff --git a/packages/logs/src/domain/logsCollection/report/reportCollection.ts b/packages/logs/src/domain/logsCollection/report/reportCollection.ts new file mode 100644 index 0000000000..c257f46a13 --- /dev/null +++ b/packages/logs/src/domain/logsCollection/report/reportCollection.ts @@ -0,0 +1,49 @@ +import type { Context, ClocksState, RawReport } from '@datadog/browser-core' +import { ErrorSource, RawReportType, getFileFromStackTraceString, initReportObservable } from '@datadog/browser-core' +import type { LogsEvent } from '../../../logsEvent.types' +import type { LogsConfiguration } from '../../configuration' +import { StatusType } from '../../logger' +import type { Sender } from '../../sender' + +export interface ProvidedError { + startClocks: ClocksState + error: unknown + context?: Context + handlingStack: string +} + +const LogStatusForReport = { + [RawReportType.cspViolation]: StatusType.error, + [RawReportType.intervention]: StatusType.error, + [RawReportType.deprecation]: StatusType.warn, +} + +export function startReportCollection(configuration: LogsConfiguration, sender: Sender) { + const reportObservable = initReportObservable(configuration.forwardReports) + const reportSubscription = reportObservable.subscribe(logReport) + + function logReport(report: RawReport) { + let message = report.message + const messageContext: Partial = { + origin: ErrorSource.REPORT, + } + const logStatus = LogStatusForReport[report.type] + if (logStatus === StatusType.error) { + messageContext.error = { + kind: report.subtype, + origin: ErrorSource.REPORT, // Todo: Remove in the next major release + stack: report.stack, + } + } else if (report.stack) { + message += ` Found in ${getFileFromStackTraceString(report.stack)!}` + } + + sender.sendToHttp(message, messageContext, logStatus) + } + + return { + stop: () => { + reportSubscription.unsubscribe() + }, + } +} diff --git a/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.spec.ts b/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.spec.ts new file mode 100644 index 0000000000..0569cc6df1 --- /dev/null +++ b/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.spec.ts @@ -0,0 +1,45 @@ +import { ErrorSource, Observable } from '@datadog/browser-core' +import type { RawError, RelativeTime, TimeStamp } from '@datadog/browser-core' +import type { LogsConfiguration } from '../../configuration' +import { createSender } from '../../sender' +import { StatusType } from '../../logger' +import { startRuntimeErrorCollection } from './runtimeErrorCollection' + +describe('runtime error collection', () => { + let rawErrorObservable: Observable + let sendLogSpy: jasmine.Spy + let stopRuntimeErrorCollection: () => void + beforeEach(() => { + rawErrorObservable = new Observable() + sendLogSpy = jasmine.createSpy('sendLogSpy') + ;({ stop: stopRuntimeErrorCollection } = startRuntimeErrorCollection( + {} as LogsConfiguration, + createSender(sendLogSpy), + rawErrorObservable + )) + }) + + afterEach(() => { + stopRuntimeErrorCollection() + }) + + it('should send runtime errors', () => { + rawErrorObservable.notify({ + message: 'error!', + source: ErrorSource.SOURCE, + startClocks: { relative: 1234 as RelativeTime, timeStamp: 123456789 as TimeStamp }, + type: 'Error', + }) + + expect(sendLogSpy).toHaveBeenCalled() + expect(sendLogSpy.calls.first().args).toEqual([ + { + date: 123456789 as TimeStamp, + error: { origin: ErrorSource.SOURCE, kind: 'Error', stack: undefined }, + 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 new file mode 100644 index 0000000000..2e1da2b745 --- /dev/null +++ b/packages/logs/src/domain/logsCollection/runtimeError/runtimeErrorCollection.ts @@ -0,0 +1,30 @@ +import type { Context, RawError, ClocksState } from '@datadog/browser-core' +import { trackRuntimeError, Observable } from '@datadog/browser-core' +import type { LogsConfiguration } from '../../configuration' +import { reportRawError } from '../../reportRawError' +import type { Sender } from '../../sender' + +export interface ProvidedError { + startClocks: ClocksState + error: unknown + context?: Context + handlingStack: string +} + +export function startRuntimeErrorCollection( + configuration: LogsConfiguration, + sender: Sender, + rawErrorObservable = new Observable() +) { + if (configuration.forwardErrorsToLogs) { + trackRuntimeError(rawErrorObservable) + } + + const rawErrorSubscription = rawErrorObservable.subscribe((rawError) => reportRawError(rawError, sender)) + + return { + stop: () => { + rawErrorSubscription.unsubscribe() + }, + } +} diff --git a/packages/logs/src/domain/reportRawError.ts b/packages/logs/src/domain/reportRawError.ts new file mode 100644 index 0000000000..d4345df0e5 --- /dev/null +++ b/packages/logs/src/domain/reportRawError.ts @@ -0,0 +1,17 @@ +import type { RawError } from '@datadog/browser-core' +import type { LogsEvent } from '../logsEvent.types' +import { StatusType } from './logger' +import type { Sender } from './sender' + +export function reportRawError(error: RawError, sender: Sender) { + const messageContext: Partial = { + date: error.startClocks.timeStamp, + error: { + kind: error.type, + origin: error.source, // Todo: Remove in the next major release + stack: error.stack, + }, + origin: error.source, + } + sender.sendToHttp(error.message, messageContext, StatusType.error) +} diff --git a/packages/logs/src/logsEvent.types.ts b/packages/logs/src/logsEvent.types.ts index 8adb2ff1d4..31ff7ec289 100644 --- a/packages/logs/src/logsEvent.types.ts +++ b/packages/logs/src/logsEvent.types.ts @@ -11,6 +11,10 @@ export interface LogsEvent { * The log status */ status: 'debug' | 'info' | 'warn' | 'error' + /** + * Origin of the log + */ + origin?: 'network' | 'source' | 'console' | 'logger' | 'agent' | 'report' | 'custom' /** * UUID of the application */ diff --git a/packages/rum-core/package.json b/packages/rum-core/package.json index 5ba2b54418..127565ae3a 100644 --- a/packages/rum-core/package.json +++ b/packages/rum-core/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-core", - "version": "4.7.0", + "version": "4.8.0", "license": "Apache-2.0", "main": "cjs/index.js", "module": "esm/index.js", @@ -12,7 +12,7 @@ "replace-build-env": "node ../../scripts/replace-build-env.js" }, "dependencies": { - "@datadog/browser-core": "4.7.0" + "@datadog/browser-core": "4.8.0" }, "devDependencies": { "ajv": "6.12.6" diff --git a/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.spec.ts index 6b33a4fe44..2afe69681d 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.spec.ts @@ -1,12 +1,5 @@ import type { RawError, Subscription } from '@datadog/browser-core' -import { - ErrorHandling, - ErrorSource, - Observable, - clocksNow, - updateExperimentalFeatures, - resetExperimentalFeatures, -} from '@datadog/browser-core' +import { ErrorHandling, ErrorSource, Observable, clocksNow } from '@datadog/browser-core' import type { Clock } from '../../../../../core/test/specHelper' import { mockClock } from '../../../../../core/test/specHelper' import { stubReportingObserver } from '../../../../../core/test/stubReportApis' @@ -31,12 +24,9 @@ describe('trackReportError', () => { subscription.unsubscribe() clock.cleanup() reportingObserverStub.reset() - resetExperimentalFeatures() }) - it('should report when ff forward-reports enabled', () => { - updateExperimentalFeatures(['forward-reports']) - + it('should track reports', () => { trackReportError(errorObservable) reportingObserverStub.raiseReport('intervention') @@ -49,12 +39,4 @@ describe('trackReportError', () => { type: 'NavigatorVibrate', }) }) - - it('should not report when ff forward-reports disabled', () => { - trackReportError(errorObservable) - - reportingObserverStub.raiseReport('intervention') - - expect(notifyLog).not.toHaveBeenCalled() - }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.ts b/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.ts index b87200a1dd..ba7bda0e67 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/error/trackReportError.ts @@ -1,21 +1,7 @@ import type { Observable, RawError } from '@datadog/browser-core' -import { - clocksNow, - ErrorHandling, - ErrorSource, - initReportObservable, - RawReportType, - isExperimentalFeatureEnabled, - noop, -} from '@datadog/browser-core' +import { clocksNow, ErrorHandling, ErrorSource, initReportObservable, RawReportType } from '@datadog/browser-core' export function trackReportError(errorObservable: Observable) { - if (!isExperimentalFeatureEnabled('forward-reports')) { - return { - stop: noop, - } - } - const subscription = initReportObservable([RawReportType.cspViolation, RawReportType.intervention]).subscribe( (reportError) => errorObservable.notify({ diff --git a/packages/rum-core/src/index.ts b/packages/rum-core/src/index.ts index b97b4aed59..64eed8067d 100644 --- a/packages/rum-core/src/index.ts +++ b/packages/rum-core/src/index.ts @@ -21,6 +21,7 @@ export { export { ViewContext, CommonContext, ReplayStats } from './rawRumEvent.types' export { startRum } from './boot/startRum' export { LifeCycle, LifeCycleEventType } from './domain/lifeCycle' +export { ViewCreatedEvent } from './domain/rumEventsCollection/view/trackViews' export { ViewContexts } from './domain/viewContexts' export { RumSessionManager, RumSessionPlan } from './domain/rumSessionManager' export { getMutationObserverConstructor } from './browser/domMutationObservable' diff --git a/packages/rum-slim/package.json b/packages/rum-slim/package.json index ebeb346039..8efd9abe6a 100644 --- a/packages/rum-slim/package.json +++ b/packages/rum-slim/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum-slim", - "version": "4.7.0", + "version": "4.8.0", "license": "Apache-2.0", "main": "cjs/entries/main.js", "module": "esm/entries/main.js", @@ -12,8 +12,8 @@ "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json" }, "dependencies": { - "@datadog/browser-core": "4.7.0", - "@datadog/browser-rum-core": "4.7.0" + "@datadog/browser-core": "4.8.0", + "@datadog/browser-rum-core": "4.8.0" }, "repository": { "type": "git", diff --git a/packages/rum/README.md b/packages/rum/README.md index 33083a0f04..2b06cdb6fa 100644 --- a/packages/rum/README.md +++ b/packages/rum/README.md @@ -44,6 +44,7 @@ datadogRum.init({ // env: 'production', // version: '1.0.0', sampleRate: 100, + replaySampleRate: 100, // if not included - default 100 trackInteractions: true, }) ``` @@ -71,6 +72,7 @@ Add the generated code snippet to the head tag of every HTML page you want to mo // env: 'production', // version: '1.0.0', sampleRate: 100, + replaySampleRate: 100, // if not included - default 100 trackInteractions: true, }) }) @@ -98,6 +100,7 @@ Add the generated code snippet to the head tag (in front of any other script tag // env: 'production', // version: '1.0.0', sampleRate: 100, + replaySampleRate: 100, // if not included - default 100 trackInteractions: true, }) @@ -302,6 +305,8 @@ datadogRum.init({ {{< partial name="whats-next/whats-next.html" >}} + + [1]: https://app.datadoghq.com/rum/list [2]: https://docs.datadoghq.com/real_user_monitoring/data_collected/ [3]: https://docs.datadoghq.com/real_user_monitoring/dashboards/ diff --git a/packages/rum/package.json b/packages/rum/package.json index 2f5270c808..630a3c7f7c 100644 --- a/packages/rum/package.json +++ b/packages/rum/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/browser-rum", - "version": "4.7.0", + "version": "4.8.0", "license": "Apache-2.0", "main": "cjs/entries/main.js", "module": "esm/entries/main.js", @@ -12,8 +12,8 @@ "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json" }, "dependencies": { - "@datadog/browser-core": "4.7.0", - "@datadog/browser-rum-core": "4.7.0" + "@datadog/browser-core": "4.8.0", + "@datadog/browser-rum-core": "4.8.0" }, "repository": { "type": "git", diff --git a/packages/rum/src/boot/startRecording.spec.ts b/packages/rum/src/boot/startRecording.spec.ts index d23f4f4e35..22f4c90b58 100644 --- a/packages/rum/src/boot/startRecording.spec.ts +++ b/packages/rum/src/boot/startRecording.spec.ts @@ -1,10 +1,11 @@ -import { HttpRequest, DefaultPrivacyLevel, noop, isIE } from '@datadog/browser-core' -import type { LifeCycle } from '@datadog/browser-rum-core' +import type { TimeStamp } from '@datadog/browser-core' +import { HttpRequest, DefaultPrivacyLevel, noop, isIE, timeStampNow } from '@datadog/browser-core' +import type { LifeCycle, ViewCreatedEvent } from '@datadog/browser-rum-core' import { LifeCycleEventType } from '@datadog/browser-rum-core' import { inflate } from 'pako' import type { RumSessionManagerMock } from '../../../rum-core/test/mockRumSessionManager' import { createRumSessionManagerMock } from '../../../rum-core/test/mockRumSessionManager' -import { createNewEvent } from '../../../core/test/specHelper' +import { createNewEvent, mockClock } from '../../../core/test/specHelper' import type { TestSetupBuilder } from '../../../rum-core/test/specHelper' import { setup } from '../../../rum-core/test/specHelper' @@ -16,6 +17,8 @@ import { RecordType } from '../types' import { resetReplayStats } from '../domain/replayStats' import { startRecording } from './startRecording' +const VIEW_TIMESTAMP = 1 as TimeStamp + describe('startRecording', () => { let setupBuilder: TestSetupBuilder let sessionManager: RumSessionManagerMock @@ -165,6 +168,33 @@ describe('startRecording', () => { }) }) + it('full snapshot related records should have the view change date', (done) => { + const clock = mockClock() + const { lifeCycle } = setupBuilder.build() + + changeView(lifeCycle) + flushSegment(lifeCycle) + + waitRequestSendCalls(2, (calls) => { + readRequestSegment(calls.first(), (segment) => { + expect(segment.records[0].timestamp).toEqual(timeStampNow()) + expect(segment.records[1].timestamp).toEqual(timeStampNow()) + expect(segment.records[2].timestamp).toEqual(timeStampNow()) + expect(segment.records[3].timestamp).toEqual(timeStampNow()) + + clock.cleanup() + + readRequestSegment(calls.mostRecent(), (segment) => { + expect(segment.records[0].timestamp).toEqual(VIEW_TIMESTAMP) + expect(segment.records[1].timestamp).toEqual(VIEW_TIMESTAMP) + expect(segment.records[2].timestamp).toEqual(VIEW_TIMESTAMP) + + expectNoExtraRequestSendCalls(done) + }) + }) + }) + }) + it('adds a ViewEnd record when the view ends', (done) => { const { lifeCycle } = setupBuilder.build() @@ -247,7 +277,9 @@ describe('startRecording', () => { function changeView(lifeCycle: LifeCycle) { lifeCycle.notify(LifeCycleEventType.VIEW_ENDED, {} as any) viewId = 'view-id-2' - lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {} as any) + lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, { + startClocks: { relative: 1, timeStamp: VIEW_TIMESTAMP }, + } as Partial as any) } }) diff --git a/packages/rum/src/boot/startRecording.ts b/packages/rum/src/boot/startRecording.ts index 1620270f87..158a983b9d 100644 --- a/packages/rum/src/boot/startRecording.ts +++ b/packages/rum/src/boot/startRecording.ts @@ -1,12 +1,17 @@ -import { assign } from '@datadog/browser-core' -import type { LifeCycle, ViewContexts, RumConfiguration, RumSessionManager } from '@datadog/browser-rum-core' +import { timeStampNow } from '@datadog/browser-core' +import type { + LifeCycle, + ViewContexts, + RumConfiguration, + RumSessionManager, + ViewCreatedEvent, +} from '@datadog/browser-rum-core' import { LifeCycleEventType } from '@datadog/browser-rum-core' import { record } from '../domain/record' import type { DeflateWorker } from '../domain/segmentCollection' import { startSegmentCollection } from '../domain/segmentCollection' import { send } from '../transport/send' -import type { RawRecord } from '../types' import { RecordType } from '../types' export function startRecording( @@ -26,26 +31,28 @@ export function startRecording( worker ) - function addRawRecord(rawRecord: RawRecord) { - addRecord(assign({ timestamp: Date.now() }, rawRecord)) - } - const { stop: stopRecording, takeFullSnapshot, flushMutations, } = record({ - emit: addRawRecord, + emit: addRecord, defaultPrivacyLevel: configuration.defaultPrivacyLevel, }) const { unsubscribe: unsubscribeViewEnded } = lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, () => { flushMutations() - addRawRecord({ + addRecord({ + timestamp: timeStampNow(), type: RecordType.ViewEnd, }) }) - const { unsubscribe: unsubscribeViewCreated } = lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, takeFullSnapshot) + const { unsubscribe: unsubscribeViewCreated } = lifeCycle.subscribe( + LifeCycleEventType.VIEW_CREATED, + (view: ViewCreatedEvent) => { + takeFullSnapshot(view.startClocks.timeStamp) + } + ) return { stop: () => { diff --git a/packages/rum/src/domain/record/mutationObserver.spec.ts b/packages/rum/src/domain/record/mutationObserver.spec.ts index 87aadef751..3458917c70 100644 --- a/packages/rum/src/domain/record/mutationObserver.spec.ts +++ b/packages/rum/src/domain/record/mutationObserver.spec.ts @@ -528,6 +528,43 @@ describe('startMutationCollection', () => { }) }) + it('emits a mutation with an empty string when an attribute is changed to an empty string', () => { + const serializedDocument = serializeDocument(document, NodePrivacyLevel.ALLOW) + const { mutationController, getLatestMutationPayload } = startMutationCollection() + + sandbox.setAttribute('foo', '') + mutationController.flush() + + const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument) + validate(getLatestMutationPayload(), { + attributes: [ + { + node: expectInitialNode({ idAttribute: 'sandbox' }), + attributes: { foo: '' }, + }, + ], + }) + }) + + it('emits a mutation with `null` when an attribute is removed', () => { + sandbox.setAttribute('foo', 'bar') + const serializedDocument = serializeDocument(document, NodePrivacyLevel.ALLOW) + const { mutationController, getLatestMutationPayload } = startMutationCollection() + + sandbox.removeAttribute('foo') + mutationController.flush() + + const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument) + validate(getLatestMutationPayload(), { + attributes: [ + { + node: expectInitialNode({ idAttribute: 'sandbox' }), + attributes: { foo: null }, + }, + ], + }) + }) + it('does not emit a mutation when an attribute keeps the same value', () => { sandbox.setAttribute('foo', 'bar') serializeDocument(document, NodePrivacyLevel.ALLOW) diff --git a/packages/rum/src/domain/record/mutationObserver.ts b/packages/rum/src/domain/record/mutationObserver.ts index 1de475235f..dbda47eff0 100644 --- a/packages/rum/src/domain/record/mutationObserver.ts +++ b/packages/rum/src/domain/record/mutationObserver.ts @@ -311,7 +311,7 @@ function processAttributesMutations( continue } transformedValue = inputValue - } else if (attributeValue && typeof attributeValue === 'string') { + } else if (typeof attributeValue === 'string') { transformedValue = attributeValue } else { transformedValue = null diff --git a/packages/rum/src/domain/record/record.spec.ts b/packages/rum/src/domain/record/record.spec.ts index 557505abf1..ec713080a7 100644 --- a/packages/rum/src/domain/record/record.spec.ts +++ b/packages/rum/src/domain/record/record.spec.ts @@ -2,7 +2,7 @@ import { DefaultPrivacyLevel, isIE } from '@datadog/browser-core' import type { Clock } from '../../../../core/test/specHelper' import { createNewEvent } from '../../../../core/test/specHelper' import { collectAsyncCalls, recordsPerFullSnapshot } from '../../../test/utils' -import type { RawRecord, IncrementalSnapshotRecord, FocusRecord } from '../../types' +import type { IncrementalSnapshotRecord, FocusRecord, Record } from '../../types' import { RecordType, IncrementalSource } from '../../types' import { record } from './record' import type { RecordAPI } from './types' @@ -10,7 +10,7 @@ import type { RecordAPI } from './types' describe('record', () => { let sandbox: HTMLElement let recordApi: RecordAPI - let emitSpy: jasmine.Spy<(record: RawRecord) => void> + let emitSpy: jasmine.Spy<(record: Record) => void> let waitEmitCalls: (expectedCallsCount: number, callback: () => void) => void let expectNoExtraEmitCalls: (done: () => void) => void let clock: Clock | undefined @@ -150,6 +150,7 @@ describe('record', () => { startRecording() expect(getEmittedRecords()[1]).toEqual({ type: RecordType.Focus, + timestamp: jasmine.any(Number), data: { has_focus: true, }, diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index 8459e6a38b..b35b37e5ad 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -1,4 +1,4 @@ -import { assign } from '@datadog/browser-core' +import { assign, timeStampNow } from '@datadog/browser-core' import type { IncrementalSnapshotRecord } from '../../types' import { RecordType } from '../../types' import { serializeDocument } from './serialize' @@ -30,7 +30,7 @@ export function record(options: RecordOptions): RecordAPI { const mutationController = new MutationController() - const takeFullSnapshot = () => { + const takeFullSnapshot = (timestamp = timeStampNow()) => { mutationController.flush() // process any pending mutation before taking a full snapshot emit({ @@ -40,6 +40,7 @@ export function record(options: RecordOptions): RecordAPI { width: getWindowWidth(), }, type: RecordType.Meta, + timestamp, }) emit({ @@ -47,6 +48,7 @@ export function record(options: RecordOptions): RecordAPI { has_focus: document.hasFocus(), }, type: RecordType.Focus, + timestamp, }) emit({ @@ -58,12 +60,14 @@ export function record(options: RecordOptions): RecordAPI { }, }, type: RecordType.FullSnapshot, + timestamp, }) if (window.visualViewport) { emit({ data: getVisualViewport(), type: RecordType.VisualViewport, + timestamp, }) } } @@ -86,13 +90,15 @@ export function record(options: RecordOptions): RecordAPI { focusCb: (data) => emit({ - type: RecordType.Focus, data, + type: RecordType.Focus, + timestamp: timeStampNow(), }), visualViewportResizeCb: (data) => { emit({ data, type: RecordType.VisualViewport, + timestamp: timeStampNow(), }) }, }) @@ -116,5 +122,6 @@ function assembleIncrementalSnapshot( data ) as Data, type: RecordType.IncrementalSnapshot, + timestamp: timeStampNow(), } } diff --git a/packages/rum/src/domain/record/types.ts b/packages/rum/src/domain/record/types.ts index 0fa7a0589e..57cf215da6 100644 --- a/packages/rum/src/domain/record/types.ts +++ b/packages/rum/src/domain/record/types.ts @@ -1,5 +1,5 @@ -import type { DefaultPrivacyLevel } from '@datadog/browser-core' -import type { FocusRecord, RawRecord, VisualViewportRecord } from '../../types' +import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core' +import type { FocusRecord, VisualViewportRecord, Record } from '../../types' import type { MutationController } from './mutationObserver' export const IncrementalSource = { @@ -63,13 +63,13 @@ export type IncrementalData = | StyleSheetRuleData export interface RecordOptions { - emit?: (record: RawRecord) => void + emit?: (record: Record) => void defaultPrivacyLevel: DefaultPrivacyLevel } export interface RecordAPI { stop: ListenerHandler - takeFullSnapshot: () => void + takeFullSnapshot: (timestamp?: TimeStamp) => void flushMutations: () => void } diff --git a/packages/rum/src/domain/segmentCollection/segment.spec.ts b/packages/rum/src/domain/segmentCollection/segment.spec.ts index e93e698fc8..a30cebdecf 100644 --- a/packages/rum/src/domain/segmentCollection/segment.spec.ts +++ b/packages/rum/src/domain/segmentCollection/segment.spec.ts @@ -1,3 +1,4 @@ +import type { TimeStamp } from '@datadog/browser-core' import { noop, setDebugMode, display, isIE } from '@datadog/browser-core' import { MockWorker, parseSegment } from '../../../test/utils' import type { CreationReason, Record, SegmentContext } from '../../types' @@ -6,9 +7,9 @@ import { getReplayStats, resetReplayStats } from '../replayStats' import { Segment } from './segment' const CONTEXT: SegmentContext = { application: { id: 'a' }, view: { id: 'b' }, session: { id: 'c' } } - -const RECORD: Record = { type: RecordType.ViewEnd, timestamp: 10 } -const FULL_SNAPSHOT_RECORD: Record = { type: RecordType.FullSnapshot, timestamp: 10, data: {} as any } +const RECORD_TIMESTAMP = 10 as TimeStamp +const RECORD: Record = { type: RecordType.ViewEnd, timestamp: RECORD_TIMESTAMP } +const FULL_SNAPSHOT_RECORD: Record = { type: RecordType.FullSnapshot, timestamp: RECORD_TIMESTAMP, data: {} as any } const ENCODED_SEGMENT_HEADER_SIZE = 12 // {"records":[ const ENCODED_RECORD_SIZE = 25 const ENCODED_FULL_SNAPSHOT_RECORD_SIZE = 35 @@ -53,7 +54,7 @@ describe('Segment', () => { has_full_snapshot: false, records: [ { - timestamp: 10, + timestamp: RECORD_TIMESTAMP, type: RecordType.ViewEnd, }, ], @@ -123,7 +124,7 @@ describe('Segment', () => { let segment: Segment beforeEach(() => { segment = createSegment() - segment.addRecord({ type: RecordType.ViewEnd, timestamp: 15 }) + segment.addRecord({ type: RecordType.ViewEnd, timestamp: 15 as TimeStamp }) }) it('increments records_count', () => { expect(segment.metadata.records_count).toBe(2) diff --git a/packages/rum/src/domain/segmentCollection/segmentCollection.spec.ts b/packages/rum/src/domain/segmentCollection/segmentCollection.spec.ts index 6f3171bcf5..ce31f067d1 100644 --- a/packages/rum/src/domain/segmentCollection/segmentCollection.spec.ts +++ b/packages/rum/src/domain/segmentCollection/segmentCollection.spec.ts @@ -1,3 +1,4 @@ +import type { TimeStamp } from '@datadog/browser-core' import { DOM_EVENT, isIE } from '@datadog/browser-core' import type { ViewContexts, ViewContext } from '@datadog/browser-rum-core' import { LifeCycle, LifeCycleEventType } from '@datadog/browser-rum-core' @@ -16,12 +17,12 @@ import { SEND_BEACON_BYTE_LENGTH_LIMIT } from '../../transport/send' import { computeSegmentContext, doStartSegmentCollection, MAX_SEGMENT_DURATION } from './segmentCollection' const CONTEXT: SegmentContext = { application: { id: 'a' }, view: { id: 'b' }, session: { id: 'c' } } -const RECORD: Record = { type: RecordType.ViewEnd, timestamp: 10 } +const RECORD: Record = { type: RecordType.ViewEnd, timestamp: 10 as TimeStamp } // A record that will make the segment size reach the SEND_BEACON_BYTE_LENGTH_LIMIT limit const VERY_BIG_RECORD: Record = { type: RecordType.FullSnapshot, - timestamp: 10, + timestamp: 10 as TimeStamp, data: Array(SEND_BEACON_BYTE_LENGTH_LIMIT).join('a') as any, } diff --git a/packages/rum/src/types.ts b/packages/rum/src/types.ts index 008015b41d..2a4f0a35da 100644 --- a/packages/rum/src/types.ts +++ b/packages/rum/src/types.ts @@ -1,3 +1,4 @@ +import type { TimeStamp } from '@datadog/browser-core' import type { IncrementalData, SerializedNodeWithId } from './domain/record' export { IncrementalSource, MutationData, ViewportResizeData, ScrollData } from './domain/record' @@ -29,7 +30,7 @@ export type CreationReason = | 'before_unload' | 'visibility_hidden' -export type RawRecord = +export type Record = | FullSnapshotRecord | IncrementalSnapshotRecord | MetaRecord @@ -37,11 +38,6 @@ export type RawRecord = | ViewEndRecord | VisualViewportRecord -export type Record = RawRecord & { - timestamp: number - delay?: number -} - export const RecordType = { FullSnapshot: 2, IncrementalSnapshot: 3, @@ -55,6 +51,7 @@ export type RecordType = typeof RecordType[keyof typeof RecordType] export interface FullSnapshotRecord { type: typeof RecordType.FullSnapshot + timestamp: TimeStamp data: { node: SerializedNodeWithId initialOffset: { @@ -66,11 +63,13 @@ export interface FullSnapshotRecord { export interface IncrementalSnapshotRecord { type: typeof RecordType.IncrementalSnapshot + timestamp: TimeStamp data: IncrementalData } export interface MetaRecord { type: typeof RecordType.Meta + timestamp: TimeStamp data: { href: string width: number @@ -80,6 +79,7 @@ export interface MetaRecord { export interface FocusRecord { type: typeof RecordType.Focus + timestamp: TimeStamp data: { has_focus: boolean } @@ -87,10 +87,12 @@ export interface FocusRecord { export interface ViewEndRecord { type: typeof RecordType.ViewEnd + timestamp: TimeStamp } export interface VisualViewportRecord { type: typeof RecordType.VisualViewport + timestamp: TimeStamp data: { scale: number offsetLeft: number diff --git a/performances/package.json b/performances/package.json index 22d7951db8..9c03b3336c 100644 --- a/performances/package.json +++ b/performances/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "performances", - "version": "4.7.0", + "version": "4.8.0", "scripts": { "start": "ts-node ./src/main.ts" }, diff --git a/test/app/yarn.lock b/test/app/yarn.lock index f2ea58fc02..40324228dd 100644 --- a/test/app/yarn.lock +++ b/test/app/yarn.lock @@ -2,24 +2,24 @@ # yarn lockfile v1 -"@datadog/browser-core@4.7.0", "@datadog/browser-core@file:../../packages/core": - version "4.7.0" +"@datadog/browser-core@4.8.0", "@datadog/browser-core@file:../../packages/core": + version "4.8.0" "@datadog/browser-logs@file:../../packages/logs": - version "4.7.0" + version "4.8.0" dependencies: - "@datadog/browser-core" "4.7.0" + "@datadog/browser-core" "4.8.0" -"@datadog/browser-rum-core@4.7.0", "@datadog/browser-rum-core@file:../../packages/rum-core": - version "4.7.0" +"@datadog/browser-rum-core@4.8.0", "@datadog/browser-rum-core@file:../../packages/rum-core": + version "4.8.0" dependencies: - "@datadog/browser-core" "4.7.0" + "@datadog/browser-core" "4.8.0" "@datadog/browser-rum@file:../../packages/rum": - version "4.7.0" + version "4.8.0" dependencies: - "@datadog/browser-core" "4.7.0" - "@datadog/browser-rum-core" "4.7.0" + "@datadog/browser-core" "4.8.0" + "@datadog/browser-rum-core" "4.8.0" "@types/eslint-scope@^3.7.0": version "3.7.3" @@ -48,14 +48,14 @@ integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== "@types/json-schema@*", "@types/json-schema@^7.0.8": - version "7.0.10" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.10.tgz#9b05b7896166cd00e9cbd59864853abf65d9ac23" - integrity sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/node@*": - version "17.0.22" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.22.tgz#38b6c4b9b2f3ed9f2e376cce42a298fb2375251e" - integrity sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw== + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" + integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== "@webassemblyjs/ast@1.11.0": version "1.11.0" @@ -220,7 +220,7 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -braces@^3.0.1: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -244,9 +244,9 @@ buffer-from@^1.0.0: integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== caniuse-lite@^1.0.30001317: - version "1.0.30001319" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001319.tgz#eb4da4eb3ecdd409f7ba1907820061d56096e88f" - integrity sha512-xjlIAFHucBRSMUo1kb5D4LYgcN1M45qdKP++lhqowDpwJwGkpIRTt5qQqnhxjj1vHcI7nrJxWhCC1ATrCEBTcw== + version "1.0.30001332" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd" + integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== chalk@^2.3.0: version "2.4.2" @@ -285,9 +285,9 @@ core-util-is@~1.0.0: integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== electron-to-chromium@^1.4.84: - version "1.4.90" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.90.tgz#4a518590f118828d54fff045728f547fef08143f" - integrity sha512-ZwKgSA0mQMyEhz+NR0F8dRzkrCLeHLzLkjx/CWf16+zV85hQ6meXPQbKanvhnpkYb7b2uJNj+enQJ/N877ND4Q== + version "1.4.116" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.116.tgz#cf0b106d462a78e43ef33cc269caf2ad70e3c7a8" + integrity sha512-sy2ol5DTH0sy8xvAglyHFxsNFXFsOBfa6rGmrtjiSdQOp53ossspduOzU+5Lx23H7GxEjjvtSF36XqkajV6Z5A== emojis-list@^3.0.0: version "3.0.0" @@ -304,9 +304,9 @@ enhanced-resolve@^4.0.0: tapable "^1.0.0" enhanced-resolve@^5.7.0: - version "5.9.2" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9" - integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== + version "5.9.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88" + integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -386,9 +386,9 @@ glob-to-regexp@^0.4.1: integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== graceful-fs@^4.1.2, graceful-fs@^4.2.4: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== has-flag@^3.0.0: version "3.0.0" @@ -442,9 +442,9 @@ json5@^1.0.1: minimist "^1.2.0" loader-runner@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" - integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== loader-utils@^1.0.2: version "1.4.0" @@ -469,12 +469,12 @@ merge-stream@^2.0.0: integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== micromatch@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.2" + picomatch "^2.3.1" mime-db@1.52.0: version "1.52.0" @@ -499,16 +499,16 @@ neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== node-releases@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" - integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96" + integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw== picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.2.3: +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== diff --git a/test/e2e/lib/framework/createTest.ts b/test/e2e/lib/framework/createTest.ts index 08074d472a..65ef093a93 100644 --- a/test/e2e/lib/framework/createTest.ts +++ b/test/e2e/lib/framework/createTest.ts @@ -15,11 +15,13 @@ import { createMockServerApp } from './serverApps/mock' const DEFAULT_RUM_CONFIGURATION = { applicationId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', clientToken: 'token', + telemetrySampleRate: 100, enableExperimentalFeatures: [], } const DEFAULT_LOGS_CONFIGURATION = { clientToken: 'token', + telemetrySampleRate: 100, } export function createTest(title: string) { diff --git a/yarn.lock b/yarn.lock index 399b821451..5c8113d960 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1881,17 +1881,17 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== -"@types/react-dom@17.0.14": - version "17.0.14" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.14.tgz#c8f917156b652ddf807711f5becbd2ab018dea9f" - integrity sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ== +"@types/react-dom@18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.0.tgz#b13f8d098e4b0c45df4f1ed123833143b0c71141" + integrity sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@17.0.43": - version "17.0.43" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" - integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== +"@types/react@*", "@types/react@18.0.1": + version "18.0.1" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.1.tgz#1b2e02fb7613212518733946e49fb963dfc66e19" + integrity sha512-VnWlrVgG0dYt+NqlfMI0yUYb8Rdl4XUROyH+c6gq/iFCiZ805Vi//26UW38DHnxQkbDhnrIWTBiy6oKZqL11cw== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -4432,18 +4432,17 @@ eslint-plugin-jasmine@4.1.3: resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-4.1.3.tgz#c4ced986a61dd5b180982bafe6da1cbac0941c52" integrity sha512-q8j8KnLH/4uwmPELFZvEyfEcuCuGxXScJaRdqHjOjz064GcfX6aoFbzy5VohZ5QYk2+WvoqMoqDSb9nRLf89GQ== -eslint-plugin-jsdoc@38.1.6: - version "38.1.6" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz#7dfa2a6d38f550935c6a3668a1fc5a05b19e4069" - integrity sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw== +eslint-plugin-jsdoc@39.1.1: + version "39.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.1.1.tgz#c5538e854c987abd30d72f2e0902772c2faaa313" + integrity sha512-sT2yhYh9PFIkvi8Sgz2gVBXF0hbD9Z7iZZBlZdCMk0SIhEVWREa9RXokF4S8tJ9BBsER1GNNToDkRok4uVmmig== dependencies: "@es-joy/jsdoccomment" "~0.22.1" comment-parser "1.3.1" debug "^4.3.4" escape-string-regexp "^4.0.0" esquery "^1.4.0" - regextras "^0.8.0" - semver "^7.3.5" + semver "^7.3.6" spdx-expression-parse "^3.0.1" eslint-plugin-local-rules@1.1.0: @@ -5194,7 +5193,19 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.2.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: +glob@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.1.tgz#00308f5c035aa0b2a447cd37ead267ddff1577d3" + integrity sha512-cF7FYZZ47YzmCu7dDy50xSRRfO3ErRfrXuLZcNIuyiJEco0XSrGtuilG19L5xp3NcwTx7Gn+X6Tv3fmsUPTbow== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -6691,6 +6702,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.4.0: + version "7.8.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.8.1.tgz#68ee3f4807a57d2ba185b7fd90827d5c21ce82bb" + integrity sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg== + lru-queue@0.1: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -6954,7 +6970,7 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@5.0.1, minimatch@^5.0.0: +minimatch@5.0.1, minimatch@^5.0.0, minimatch@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== @@ -7403,7 +7419,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -8101,14 +8117,13 @@ react-base16-styling@^0.6.0: lodash.flow "^3.3.0" pure-color "^1.2.0" -react-dom@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023" + integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.21.0" react-fast-compare@^3.0.1: version "3.2.0" @@ -8157,13 +8172,12 @@ react-textarea-autosize@^8.3.2: use-composed-ref "^1.0.0" use-latest "^1.0.0" -react@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" + integrity sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" read-cmd-shim@^2.0.0: version "2.0.0" @@ -8352,11 +8366,6 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regextras@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.8.0.tgz#ec0f99853d4912839321172f608b544814b02217" - integrity sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ== - relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -8548,13 +8557,12 @@ safe-regex@^2.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.21.0: + version "0.21.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" + integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" schema-utils@^2.7.0: version "2.7.1" @@ -8604,12 +8612,12 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6: + version "7.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.6.tgz#5d73886fb9c0c6602e79440b97165c29581cbb2b" + integrity sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w== dependencies: - lru-cache "^6.0.0" + lru-cache "^7.4.0" semver@~5.3.0: version "5.3.0"