From 124285f1b2c938ab12738f58ba94d8cd4beaa16b Mon Sep 17 00:00:00 2001 From: Aymeric Date: Wed, 12 Oct 2022 11:26:46 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8A=20Collect=20configuration=20teleme?= =?UTF-8?q?try=20event=20(#1760)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/configuration/configuration.ts | 26 +++ .../core/src/domain/configuration/index.ts | 1 + packages/core/src/domain/telemetry/index.ts | 4 +- .../telemetry/rawTelemetryEvent.types.ts | 14 ++ .../src/domain/telemetry/telemetry.spec.ts | 57 ++++++- .../core/src/domain/telemetry/telemetry.ts | 33 ++-- .../domain/telemetry/telemetryEvent.types.ts | 152 +++++++++++++++++- packages/core/src/index.ts | 4 + packages/logs/src/boot/logsPublicApi.spec.ts | 2 +- packages/logs/src/boot/logsPublicApi.ts | 1 + packages/logs/src/boot/startLogs.spec.ts | 30 ++-- packages/logs/src/boot/startLogs.ts | 12 +- packages/logs/src/domain/assembly.spec.ts | 1 + packages/logs/src/domain/configuration.ts | 20 ++- .../rum-core/src/boot/rumPublicApi.spec.ts | 8 +- packages/rum-core/src/boot/rumPublicApi.ts | 11 +- packages/rum-core/src/boot/startRum.ts | 7 +- packages/rum-core/src/domain/configuration.ts | 25 ++- .../startDeflateWorker.spec.ts | 14 +- rum-events-format | 2 +- test/e2e/lib/framework/createTest.ts | 4 +- test/e2e/lib/framework/eventsRegistry.ts | 16 +- test/e2e/lib/types/serverEvents.ts | 9 ++ test/e2e/scenario/telemetry.scenario.ts | 35 +++- 24 files changed, 431 insertions(+), 57 deletions(-) create mode 100644 packages/core/src/domain/telemetry/rawTelemetryEvent.types.ts diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index a188a9f9bf..43eb42e97b 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -3,6 +3,7 @@ import { getCurrentSite } from '../../browser/cookie' import { catchUserErrors } from '../../tools/catchUserErrors' import { display } from '../../tools/display' import { assign, isPercentage, ONE_KIBI_BYTE, ONE_SECOND } from '../../tools/utils' +import type { RawTelemetryConfiguration } from '../telemetry' import { updateExperimentalFeatures } from './experimentalFeatures' import { initSimulation } from './simulation' import type { TransportConfiguration } from './transportConfiguration' @@ -41,6 +42,7 @@ export interface InitConfiguration { enableExperimentalFeatures?: string[] | undefined replica?: ReplicaUserConfiguration | undefined datacenter?: string + telemetryConfigurationSampleRate?: number // simulation options simulationStart?: string | undefined @@ -63,6 +65,7 @@ export interface Configuration extends TransportConfiguration { cookieOptions: CookieOptions sampleRate: number telemetrySampleRate: number + telemetryConfigurationSampleRate: number service: string | undefined silentMultipleInit: boolean @@ -93,6 +96,14 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati return } + if ( + initConfiguration.telemetryConfigurationSampleRate !== undefined && + !isPercentage(initConfiguration.telemetryConfigurationSampleRate) + ) { + display.error('Telemetry Configuration 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) @@ -105,6 +116,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati cookieOptions: buildCookieOptions(initConfiguration), sampleRate: initConfiguration.sampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, + telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, service: initConfiguration.service, silentMultipleInit: !!initConfiguration.silentMultipleInit, @@ -149,3 +161,17 @@ export function buildCookieOptions(initConfiguration: InitConfiguration) { function mustUseSecureCookie(initConfiguration: InitConfiguration) { return !!initConfiguration.useSecureSessionCookie || !!initConfiguration.useCrossSiteSessionCookie } + +export function serializeConfiguration(configuration: InitConfiguration): Partial { + return { + session_sample_rate: configuration.sampleRate, + telemetry_sample_rate: configuration.telemetrySampleRate, + telemetry_configuration_sample_rate: configuration.telemetryConfigurationSampleRate, + use_before_send: !!configuration.beforeSend, + use_cross_site_session_cookie: configuration.useCrossSiteSessionCookie, + use_secure_session_cookie: configuration.useSecureSessionCookie, + use_proxy: configuration.proxyUrl !== undefined ? !!configuration.proxyUrl : undefined, + silent_multiple_init: configuration.silentMultipleInit, + track_session_across_subdomains: configuration.trackSessionAcrossSubdomains, + } +} diff --git a/packages/core/src/domain/configuration/index.ts b/packages/core/src/domain/configuration/index.ts index c95f98924b..35414e400f 100644 --- a/packages/core/src/domain/configuration/index.ts +++ b/packages/core/src/domain/configuration/index.ts @@ -4,6 +4,7 @@ export { buildCookieOptions, DefaultPrivacyLevel, validateAndBuildConfiguration, + serializeConfiguration, } from './configuration' export { createEndpointBuilder, EndpointBuilder, EndpointType } from './endpointBuilder' export { diff --git a/packages/core/src/domain/telemetry/index.ts b/packages/core/src/domain/telemetry/index.ts index 9f301a292c..2ef280849d 100644 --- a/packages/core/src/domain/telemetry/index.ts +++ b/packages/core/src/domain/telemetry/index.ts @@ -1,11 +1,13 @@ export { Telemetry, - RawTelemetryEvent, addTelemetryDebug, addTelemetryError, startFakeTelemetry, resetTelemetry, startTelemetry, isTelemetryReplicationAllowed, + addTelemetryConfiguration, } from './telemetry' + +export * from './rawTelemetryEvent.types' export * from './telemetryEvent.types' diff --git a/packages/core/src/domain/telemetry/rawTelemetryEvent.types.ts b/packages/core/src/domain/telemetry/rawTelemetryEvent.types.ts new file mode 100644 index 0000000000..8a2cba8608 --- /dev/null +++ b/packages/core/src/domain/telemetry/rawTelemetryEvent.types.ts @@ -0,0 +1,14 @@ +import type { TelemetryEvent, TelemetryConfigurationEvent } from './telemetryEvent.types' + +export const TelemetryType = { + log: 'log', + configuration: 'configuration', +} as const + +export const enum StatusType { + debug = 'debug', + error = 'error', +} + +export type RawTelemetryEvent = TelemetryEvent['telemetry'] +export type RawTelemetryConfiguration = TelemetryConfigurationEvent['telemetry']['configuration'] diff --git a/packages/core/src/domain/telemetry/telemetry.spec.ts b/packages/core/src/domain/telemetry/telemetry.spec.ts index e026998fc0..f194294df5 100644 --- a/packages/core/src/domain/telemetry/telemetry.spec.ts +++ b/packages/core/src/domain/telemetry/telemetry.spec.ts @@ -1,8 +1,19 @@ import type { StackTrace } from '@datadog/browser-core' import { callMonitored } from '../../tools/monitor' import type { Configuration } from '../configuration' -import { updateExperimentalFeatures, INTAKE_SITE_US1, INTAKE_SITE_US1_FED } from '../configuration' -import { resetTelemetry, startTelemetry, scrubCustomerFrames, formatError } from './telemetry' +import { + resetExperimentalFeatures, + updateExperimentalFeatures, + INTAKE_SITE_US1, + INTAKE_SITE_US1_FED, +} from '../configuration' +import { + resetTelemetry, + startTelemetry, + scrubCustomerFrames, + formatError, + addTelemetryConfiguration, +} from './telemetry' function startAndSpyTelemetry(configuration?: Partial) { const telemetry = startTelemetry({ @@ -31,6 +42,48 @@ describe('telemetry', () => { expect(notifySpy).toHaveBeenCalledTimes(1) }) + describe('addTelemetryConfiguration', () => { + afterEach(() => { + resetExperimentalFeatures() + }) + + it('should collects configuration when sampled and ff is enabled', () => { + updateExperimentalFeatures(['telemetry_configuration']) + + const { notifySpy } = startAndSpyTelemetry({ telemetrySampleRate: 100, telemetryConfigurationSampleRate: 100 }) + + addTelemetryConfiguration({}) + + expect(notifySpy).toHaveBeenCalled() + }) + + it('should not notify configuration when sampled and ff is disabled', () => { + const { notifySpy } = startAndSpyTelemetry({ telemetrySampleRate: 100, telemetryConfigurationSampleRate: 100 }) + + addTelemetryConfiguration({}) + + expect(notifySpy).not.toHaveBeenCalled() + }) + + it('should not notify configuration when not sampled and ff is enabled', () => { + updateExperimentalFeatures(['telemetry_configuration']) + const { notifySpy } = startAndSpyTelemetry({ telemetrySampleRate: 100, telemetryConfigurationSampleRate: 0 }) + + addTelemetryConfiguration({}) + + expect(notifySpy).not.toHaveBeenCalled() + }) + + it('should not notify configuration when telemetrySampleRate is 0 and ff is enabled', () => { + updateExperimentalFeatures(['telemetry_configuration']) + const { notifySpy } = startAndSpyTelemetry({ telemetrySampleRate: 0, telemetryConfigurationSampleRate: 100 }) + + addTelemetryConfiguration({}) + + expect(notifySpy).not.toHaveBeenCalled() + }) + }) + it('should contains feature flags', () => { updateExperimentalFeatures(['foo']) const { notifySpy } = startAndSpyTelemetry() diff --git a/packages/core/src/domain/telemetry/telemetry.ts b/packages/core/src/domain/telemetry/telemetry.ts index f7e5860181..4d8dafef7d 100644 --- a/packages/core/src/domain/telemetry/telemetry.ts +++ b/packages/core/src/domain/telemetry/telemetry.ts @@ -4,6 +4,7 @@ import { toStackTraceString } from '../../tools/error' import { assign, combine, jsonStringify, performDraw, includes, startsWith, arrayFrom } from '../../tools/utils' import type { Configuration } from '../configuration' import { + isExperimentalFeatureEnabled, getExperimentalFeatures, getSimulationLabel, INTAKE_SITE_STAGING, @@ -16,15 +17,12 @@ import { Observable } from '../../tools/observable' import { timeStampNow } from '../../tools/timeUtils' import { displayIfDebugEnabled, startMonitorErrorCollection } from '../../tools/monitor' import type { TelemetryEvent } from './telemetryEvent.types' +import type { RawTelemetryConfiguration, RawTelemetryEvent } from './rawTelemetryEvent.types' +import { StatusType, TelemetryType } from './rawTelemetryEvent.types' // replaced at build time declare const __BUILD_ENV__SDK_VERSION__: string -const enum StatusType { - debug = 'debug', - error = 'error', -} - const ALLOWED_FRAME_URLS = [ 'https://www.datadoghq-browser-agent.com', 'https://www.datad0g-browser-agent.com', @@ -37,22 +35,14 @@ export interface Telemetry { observable: Observable } -export interface RawTelemetryEvent extends Context { - message: string - status: StatusType - error?: { - kind?: string - stack: string - } -} - const TELEMETRY_EXCLUDED_SITES: string[] = [INTAKE_SITE_US1_FED] const telemetryConfiguration: { maxEventsPerPage: number sentEventCount: number telemetryEnabled: boolean -} = { maxEventsPerPage: 0, sentEventCount: 0, telemetryEnabled: false } + telemetryConfigurationEnabled: boolean +} = { maxEventsPerPage: 0, sentEventCount: 0, telemetryEnabled: false, telemetryConfigurationEnabled: false } let onRawTelemetryEventCollected: ((event: RawTelemetryEvent) => void) | undefined @@ -61,6 +51,8 @@ export function startTelemetry(configuration: Configuration): Telemetry { const observable = new Observable() telemetryConfiguration.telemetryEnabled = performDraw(configuration.telemetrySampleRate) + telemetryConfiguration.telemetryConfigurationEnabled = + telemetryConfiguration.telemetryEnabled && performDraw(configuration.telemetryConfigurationSampleRate) onRawTelemetryEventCollected = (event: RawTelemetryEvent) => { if (!includes(TELEMETRY_EXCLUDED_SITES, configuration.site) && telemetryConfiguration.telemetryEnabled) { @@ -132,6 +124,7 @@ export function addTelemetryDebug(message: string, context?: Context) { addTelemetry( assign( { + type: TelemetryType.log, message, status: StatusType.debug, }, @@ -144,6 +137,7 @@ export function addTelemetryError(e: unknown) { addTelemetry( assign( { + type: TelemetryType.log, status: StatusType.error, }, formatError(e) @@ -151,6 +145,15 @@ export function addTelemetryError(e: unknown) { ) } +export function addTelemetryConfiguration(configuration: RawTelemetryConfiguration) { + if (isExperimentalFeatureEnabled('telemetry_configuration') && telemetryConfiguration.telemetryConfigurationEnabled) { + addTelemetry({ + type: TelemetryType.configuration, + configuration, + }) + } +} + function addTelemetry(event: RawTelemetryEvent) { if (onRawTelemetryEventCollected && telemetryConfiguration.sentEventCount < telemetryConfiguration.maxEventsPerPage) { telemetryConfiguration.sentEventCount += 1 diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index f3d52ced72..2ce10563cc 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -6,15 +6,19 @@ /** * Schema of all properties of a telemetry event */ -export type TelemetryEvent = TelemetryErrorEvent | TelemetryDebugEvent +export type TelemetryEvent = TelemetryErrorEvent | TelemetryDebugEvent | TelemetryConfigurationEvent /** * Schema of all properties of a telemetry error event */ export type TelemetryErrorEvent = CommonTelemetryProperties & { /** - * The telemetry information + * The telemetry log information */ telemetry: { + /** + * Telemetry type + */ + type?: 'log' /** * Level/severity of the log */ @@ -46,9 +50,13 @@ export type TelemetryErrorEvent = CommonTelemetryProperties & { */ export type TelemetryDebugEvent = CommonTelemetryProperties & { /** - * The telemetry information + * The telemetry log information */ telemetry: { + /** + * Telemetry type + */ + type?: 'log' /** * Level/severity of the log */ @@ -61,6 +69,144 @@ export type TelemetryDebugEvent = CommonTelemetryProperties & { } [k: string]: unknown } +/** + * Schema of all properties of a telemetry configuration event + */ +export type TelemetryConfigurationEvent = CommonTelemetryProperties & { + /** + * The telemetry configuration information + */ + telemetry: { + /** + * Telemetry type + */ + type: 'configuration' + /** + * Configuration properties + */ + configuration: { + /** + * The percentage of sessions tracked + */ + session_sample_rate?: number + /** + * The percentage of telemetry events sent + */ + telemetry_sample_rate?: number + /** + * The percentage of telemetry configuration events sent after being sampled by telemetry_sample_rate + */ + telemetry_configuration_sample_rate?: number + /** + * The percentage of requests traced + */ + trace_sample_rate?: number + /** + * The percentage of sessions with Browser RUM & Session Replay pricing tracked (deprecated in favor of session_replay_sample_rate) + */ + premium_sample_rate?: number + /** + * The percentage of sessions with Browser RUM & Session Replay pricing tracked (deprecated in favor of session_replay_sample_rate) + */ + replay_sample_rate?: number + /** + * The percentage of sessions with Browser RUM & Session Replay pricing tracked + */ + session_replay_sample_rate?: number + /** + * Whether a proxy configured is used + */ + use_proxy?: boolean + /** + * Whether beforeSend callback function is used + */ + use_before_send?: boolean + /** + * Whether initialization fails silently if the SDK is already initialized + */ + silent_multiple_init?: boolean + /** + * Whether sessions across subdomains for the same site are tracked + */ + track_session_across_subdomains?: boolean + /** + * Whether a secure cross-site session cookie is used + */ + use_cross_site_session_cookie?: boolean + /** + * Whether a secure session cookie is used + */ + use_secure_session_cookie?: boolean + /** + * Attribute to be used to name actions + */ + action_name_attribute?: string + /** + * Whether the allowed tracing origins list is used + */ + use_allowed_tracing_origins?: boolean + /** + * Session replay default privacy level + */ + default_privacy_level?: string + /** + * Whether the request origins list to ignore when computing the page activity is used + */ + use_excluded_activity_urls?: boolean + /** + * Whether user frustrations are tracked + */ + track_frustrations?: boolean + /** + * Whether the RUM views creation is handled manually + */ + track_views_manually?: boolean + /** + * Whether user actions are tracked + */ + track_interactions?: boolean + /** + * Whether console.error logs, uncaught exceptions and network errors are tracked + */ + forward_errors_to_logs?: boolean + /** + * The console.* tracked + */ + forward_console_logs?: string[] | 'all' + /** + * The reports from the Reporting API tracked + */ + forward_reports?: string[] | 'all' + /** + * Whether local encryption is used + */ + use_local_encryption?: boolean + /** + * View tracking strategy + */ + view_tracking_strategy?: + | 'ActivityViewTrackingStrategy' + | 'FragmentViewTrackingStrategy' + | 'MixedViewTrackingStrategy' + | 'NavigationViewTrackingStrategy' + /** + * Whether RUM events are tracked when the application is in Background + */ + track_background_events?: boolean + /** + * The period between each Mobile Vital sample (in milliseconds) + */ + mobile_vitals_update_period?: number + /** + * Whether native crashes are tracked + */ + track_native_crashes?: boolean + [k: string]: unknown + } + [k: string]: unknown + } + [k: string]: unknown +} /** * Schema of common properties of Telemetry events diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ced1c6a462..990d012c2a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,6 +10,7 @@ export { resetExperimentalFeatures, isSimulationActive, getSimulationLabel, + serializeConfiguration, } from './domain/configuration' export { trackRuntimeError } from './domain/error/trackRuntimeError' export { computeStackTrace, StackTrace } from './domain/tracekit' @@ -19,6 +20,7 @@ export { startTelemetry, Telemetry, RawTelemetryEvent, + RawTelemetryConfiguration, addTelemetryDebug, addTelemetryError, startFakeTelemetry, @@ -26,7 +28,9 @@ export { TelemetryEvent, TelemetryErrorEvent, TelemetryDebugEvent, + TelemetryConfigurationEvent, isTelemetryReplicationAllowed, + addTelemetryConfiguration, } from './domain/telemetry' export { monitored, monitor, callMonitored, setDebugMode } from './tools/monitor' export { Observable, Subscription } from './tools/observable' diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index bbae4fa081..86448d0b4b 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -140,7 +140,7 @@ describe('logs entry', () => { it('should have the current date, view and global context', () => { LOGS.addLoggerGlobalContext('foo', 'bar') - const getCommonContext = startLogs.calls.mostRecent().args[1] + const getCommonContext = startLogs.calls.mostRecent().args[2] expect(getCommonContext()).toEqual({ view: { referrer: document.referrer, diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index 700e1f79da..b7a8739832 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -78,6 +78,7 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs) { } ;({ handleLog: handleLogStrategy, getInternalContext: getInternalContextStrategy } = startLogsImpl( + initConfiguration, configuration, getCommonContext, mainLogger diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index f07bccce48..2fd71307a3 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -36,7 +36,7 @@ const COMMON_CONTEXT = { } describe('logs', () => { - const initConfiguration = { clientToken: 'xxx', service: 'service' } + const initConfiguration = { clientToken: 'xxx', service: 'service', telemetrySampleRate: 0 } let baseConfiguration: LogsConfiguration let interceptor: ReturnType let requests: Request[] @@ -67,7 +67,7 @@ describe('logs', () => { describe('request', () => { it('should send the needed data', () => { - ;({ handleLog: handleLog } = startLogs(baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog: handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) handleLog({ message: 'message', status: StatusType.warn, context: { foo: 'bar' } }, logger, COMMON_CONTEXT) @@ -89,7 +89,12 @@ describe('logs', () => { }) it('should all use the same batch', () => { - ;({ handleLog } = startLogs({ ...baseConfiguration, batchMessagesLimit: 3 }, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs( + initConfiguration, + { ...baseConfiguration, batchMessagesLimit: 3 }, + () => COMMON_CONTEXT, + logger + )) handleLog(DEFAULT_MESSAGE, logger) handleLog(DEFAULT_MESSAGE, logger) @@ -100,7 +105,7 @@ describe('logs', () => { it('should send bridge event when bridge is present', () => { const sendSpy = spyOn(initEventBridgeStub(), 'send') - ;({ handleLog: handleLog } = startLogs(baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog: handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) handleLog(DEFAULT_MESSAGE, logger) @@ -119,13 +124,13 @@ describe('logs', () => { const sendSpy = spyOn(initEventBridgeStub(), 'send') let configuration = { ...baseConfiguration, sampleRate: 0 } - ;({ handleLog } = startLogs(configuration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, configuration, () => COMMON_CONTEXT, logger)) handleLog(DEFAULT_MESSAGE, logger) expect(sendSpy).not.toHaveBeenCalled() configuration = { ...baseConfiguration, sampleRate: 100 } - ;({ handleLog } = startLogs(configuration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, configuration, () => COMMON_CONTEXT, logger)) handleLog(DEFAULT_MESSAGE, logger) expect(sendSpy).toHaveBeenCalled() @@ -134,7 +139,12 @@ describe('logs', () => { it('should not print the log twice when console handler is enabled', () => { logger.setHandler([HandlerType.console]) - ;({ handleLog } = startLogs({ ...baseConfiguration, forwardConsoleLogs: ['log'] }, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs( + initConfiguration, + { ...baseConfiguration, forwardConsoleLogs: ['log'] }, + () => COMMON_CONTEXT, + logger + )) /* eslint-disable-next-line no-console */ console.log('foo', 'bar') @@ -149,21 +159,21 @@ describe('logs', () => { }) it('creates a session on normal conditions', () => { - ;({ handleLog } = startLogs(baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) expect(getCookie(SESSION_COOKIE_NAME)).not.toBeUndefined() }) it('does not create a session if event bridge is present', () => { initEventBridgeStub() - ;({ handleLog } = startLogs(baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) it('does not create a session if synthetics worker will inject RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) - ;({ handleLog } = startLogs(baseConfiguration, () => COMMON_CONTEXT, logger)) + ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 0ea2604df8..faa96ec502 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -8,9 +8,11 @@ import { startBatchWithReplica, isTelemetryReplicationAllowed, ErrorSource, + addTelemetryConfiguration, } from '@datadog/browser-core' import { startLogsSessionManager, startLogsSessionManagerStub } from '../domain/logsSessionManager' -import type { LogsConfiguration } from '../domain/configuration' +import type { LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' +import { serializeLogsConfiguration } from '../domain/configuration' import { startLogsAssembly, getRUMInternalContext } from '../domain/assembly' import { startConsoleCollection } from '../domain/logsCollection/console/consoleCollection' import { startReportCollection } from '../domain/logsCollection/report/reportCollection' @@ -25,7 +27,12 @@ import type { Logger } from '../domain/logger' import { StatusType } from '../domain/logger' import { startInternalContext } from '../domain/internalContext' -export function startLogs(configuration: LogsConfiguration, getCommonContext: () => CommonContext, mainLogger: Logger) { +export function startLogs( + initConfiguration: LogsInitConfiguration, + configuration: LogsConfiguration, + getCommonContext: () => CommonContext, + mainLogger: Logger +) { const lifeCycle = new LifeCycle() const reportError = (error: RawError) => @@ -75,6 +82,7 @@ export function startLogs(configuration: LogsConfiguration, getCommonContext: () startLogsBridge(lifeCycle) } + addTelemetryConfiguration(serializeLogsConfiguration(initConfiguration)) const internalContext = startInternalContext(session) return { diff --git a/packages/logs/src/domain/assembly.spec.ts b/packages/logs/src/domain/assembly.spec.ts index 8907e68ba5..2feb8b316d 100644 --- a/packages/logs/src/domain/assembly.spec.ts +++ b/packages/logs/src/domain/assembly.spec.ts @@ -490,6 +490,7 @@ describe('getRUMInternalContext', () => { telemetry: { message: 'Logs sent before RUM is injected by the synthetics worker', status: 'debug', + type: 'log', testId: 'test-id', resultId: 'result-id', }, diff --git a/packages/logs/src/domain/configuration.ts b/packages/logs/src/domain/configuration.ts index 4df81c06f4..f554913101 100644 --- a/packages/logs/src/domain/configuration.ts +++ b/packages/logs/src/domain/configuration.ts @@ -1,5 +1,6 @@ -import type { Configuration, InitConfiguration } from '@datadog/browser-core' +import type { Configuration, InitConfiguration, RawTelemetryConfiguration } from '@datadog/browser-core' import { + serializeConfiguration, assign, ONE_KIBI_BYTE, validateAndBuildConfiguration, @@ -15,8 +16,8 @@ import type { LogsEvent } from '../logsEvent.types' export interface LogsInitConfiguration extends InitConfiguration { beforeSend?: ((event: LogsEvent) => void | boolean) | undefined forwardErrorsToLogs?: boolean | undefined - forwardConsoleLogs?: readonly ConsoleApiName[] | 'all' | undefined - forwardReports?: readonly RawReportType[] | 'all' | undefined + forwardConsoleLogs?: ConsoleApiName[] | 'all' | undefined + forwardReports?: RawReportType[] | 'all' | undefined } export type HybridInitConfiguration = Omit @@ -85,3 +86,16 @@ export function validateAndBuildForwardOption( return option === 'all' ? allowedValues : removeDuplicates(option) } + +export function serializeLogsConfiguration(configuration: LogsInitConfiguration): RawTelemetryConfiguration { + const baseSerializedInitConfiguration = serializeConfiguration(configuration) + + return assign( + { + forward_errors_to_logs: configuration.forwardErrorsToLogs, + forward_console_logs: configuration.forwardConsoleLogs, + forward_reports: configuration.forwardReports, + }, + baseSerializedInitConfiguration + ) +} diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index f9eac470f3..30b7176485 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -686,7 +686,7 @@ describe('rum public api', () => { rumPublicApi.init(MANUAL_CONFIGURATION) expect(startRumSpy).toHaveBeenCalled() - expect(startRumSpy.calls.argsFor(0)[3]).toEqual({ name: 'foo' }) + expect(startRumSpy.calls.argsFor(0)[4]).toEqual({ name: 'foo' }) expect(recorderApiOnRumStartSpy).toHaveBeenCalled() expect(startViewSpy).not.toHaveBeenCalled() }) @@ -698,7 +698,7 @@ describe('rum public api', () => { rumPublicApi.startView('foo') expect(startRumSpy).toHaveBeenCalled() - expect(startRumSpy.calls.argsFor(0)[3]).toEqual({ name: 'foo' }) + expect(startRumSpy.calls.argsFor(0)[4]).toEqual({ name: 'foo' }) expect(recorderApiOnRumStartSpy).toHaveBeenCalled() expect(startViewSpy).not.toHaveBeenCalled() }) @@ -709,7 +709,7 @@ describe('rum public api', () => { rumPublicApi.startView('bar') expect(startRumSpy).toHaveBeenCalled() - expect(startRumSpy.calls.argsFor(0)[3]).toEqual({ name: 'foo' }) + expect(startRumSpy.calls.argsFor(0)[4]).toEqual({ name: 'foo' }) expect(recorderApiOnRumStartSpy).toHaveBeenCalled() expect(startViewSpy).toHaveBeenCalled() expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'bar' }) @@ -754,7 +754,7 @@ describe('rum public api', () => { let startRumSpy: jasmine.Spy function getCommonContext() { - return startRumSpy.calls.argsFor(0)[1]() + return startRumSpy.calls.argsFor(0)[2]() } beforeEach(() => { diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index a3d49f5e43..f54dddd426 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -112,7 +112,7 @@ export function makeRumPublicApi( } if (!configuration.trackViewsManually) { - doStartRum(configuration) + doStartRum(initConfiguration, configuration) } else { // drain beforeInitCalls by buffering them until we start RUM // if we get a startView, drain re-buffered calls before continuing to drain beforeInitCalls @@ -121,7 +121,7 @@ export function makeRumPublicApi( bufferApiCalls = new BoundedBuffer() startViewStrategy = (options) => { - doStartRum(configuration, options) + doStartRum(initConfiguration, configuration, options) } beforeInitCalls.drain() } @@ -130,8 +130,13 @@ export function makeRumPublicApi( isAlreadyInitialized = true } - function doStartRum(configuration: RumConfiguration, initialViewOptions?: ViewOptions) { + function doStartRum( + initConfiguration: RumInitConfiguration, + configuration: RumConfiguration, + initialViewOptions?: ViewOptions + ) { const startRumResults = startRumImpl( + initConfiguration, configuration, () => ({ user: userContextManager.getContext(), diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 32853c2197..ece4c1a7de 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -1,5 +1,5 @@ import type { Observable, TelemetryEvent, RawError } from '@datadog/browser-core' -import { startTelemetry, canUseEventBridge, getEventBridge } from '@datadog/browser-core' +import { addTelemetryConfiguration, startTelemetry, canUseEventBridge, getEventBridge } from '@datadog/browser-core' import { createDOMMutationObservable } from '../browser/domMutationObservable' import { startPerformanceCollection } from '../browser/performanceCollection' import { startRumAssembly } from '../domain/assembly' @@ -21,11 +21,13 @@ import { startRumEventBridge } from '../transport/startRumEventBridge' import { startUrlContexts } from '../domain/contexts/urlContexts' import type { LocationChange } from '../browser/locationChangeObservable' import { createLocationChangeObservable } from '../browser/locationChangeObservable' -import type { RumConfiguration } from '../domain/configuration' +import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration' +import { serializeRumConfiguration } from '../domain/configuration' import type { ViewOptions } from '../domain/rumEventsCollection/view/trackViews' import type { RecorderApi } from './rumPublicApi' export function startRum( + initConfiguration: RumInitConfiguration, configuration: RumConfiguration, getCommonContext: () => CommonContext, recorderApi: RecorderApi, @@ -72,6 +74,7 @@ export function startRum( getCommonContext, reportError ) + addTelemetryConfiguration(serializeRumConfiguration(initConfiguration)) startLongTaskCollection(lifeCycle, session) startResourceCollection(lifeCycle, configuration, session) diff --git a/packages/rum-core/src/domain/configuration.ts b/packages/rum-core/src/domain/configuration.ts index bb5927845c..c7eb5f7333 100644 --- a/packages/rum-core/src/domain/configuration.ts +++ b/packages/rum-core/src/domain/configuration.ts @@ -1,5 +1,6 @@ -import type { Configuration, InitConfiguration } from '@datadog/browser-core' +import type { Configuration, InitConfiguration, RawTelemetryConfiguration } from '@datadog/browser-core' import { + serializeConfiguration, assign, DefaultPrivacyLevel, display, @@ -142,3 +143,25 @@ export function validateAndBuildRumConfiguration( baseConfiguration ) } + +export function serializeRumConfiguration(configuration: RumInitConfiguration): RawTelemetryConfiguration { + const baseSerializedConfiguration = serializeConfiguration(configuration) + + return assign( + { + premium_sample_rate: configuration.premiumSampleRate, + replay_sample_rate: configuration.replaySampleRate, + session_replay_sample_rate: configuration.sessionReplaySampleRate, + action_name_attribute: configuration.actionNameAttribute, + use_allowed_tracing_origins: + Array.isArray(configuration.allowedTracingOrigins) && configuration.allowedTracingOrigins.length > 0, + default_privacy_level: configuration.defaultPrivacyLevel, + use_excluded_activity_urls: + Array.isArray(configuration.allowedTracingOrigins) && configuration.allowedTracingOrigins.length > 0, + track_frustrations: configuration.trackFrustrations, + track_views_manually: configuration.trackViewsManually, + track_interactions: configuration.trackInteractions, + }, + baseSerializedConfiguration + ) +} diff --git a/packages/rum/src/domain/segmentCollection/startDeflateWorker.spec.ts b/packages/rum/src/domain/segmentCollection/startDeflateWorker.spec.ts index 7b05031f01..c0dc857da0 100644 --- a/packages/rum/src/domain/segmentCollection/startDeflateWorker.spec.ts +++ b/packages/rum/src/domain/segmentCollection/startDeflateWorker.spec.ts @@ -142,7 +142,12 @@ describe('startDeflateWorker', () => { throw UNKNOWN_ERROR }) expect(telemetryEvents).toEqual([ - { status: 'error' as any, message: 'boom', error: { kind: 'Error', stack: jasmine.any(String) } }, + { + type: 'log', + status: 'error', + message: 'boom', + error: { kind: 'Error', stack: jasmine.any(String) }, + }, ]) }) @@ -160,7 +165,12 @@ describe('startDeflateWorker', () => { deflateWorker.dispatchErrorMessage('boom') expect(telemetryEvents).toEqual([ - { status: 'error' as any, message: 'Uncaught "boom"', error: { stack: jasmine.any(String) } }, + { + type: 'log', + status: 'error', + message: 'Uncaught "boom"', + error: { stack: jasmine.any(String) }, + }, ]) }) }) diff --git a/rum-events-format b/rum-events-format index 53ebd2b5d2..7320f7c483 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit 53ebd2b5d23d3f02e595df314cbaca44f38f08e0 +Subproject commit 7320f7c483c80f5fc0d868b1f40c97f22af8b0d1 diff --git a/test/e2e/lib/framework/createTest.ts b/test/e2e/lib/framework/createTest.ts index 2527073c67..a541873e31 100644 --- a/test/e2e/lib/framework/createTest.ts +++ b/test/e2e/lib/framework/createTest.ts @@ -19,12 +19,14 @@ const DEFAULT_RUM_CONFIGURATION = { trackResources: true, trackLongTasks: true, telemetrySampleRate: 100, + telemetryConfigurationSampleRate: 100, enableExperimentalFeatures: [], } const DEFAULT_LOGS_CONFIGURATION = { clientToken: 'token', telemetrySampleRate: 100, + telemetryConfigurationSampleRate: 100, } export function createTest(title: string) { @@ -190,7 +192,7 @@ async function setUpTest({ baseUrl }: TestContext) { async function tearDownTest({ serverEvents, bridgeEvents }: TestContext) { await flushEvents() - expect(serverEvents.telemetry).toEqual([]) + expect(serverEvents.telemetryErrors).toEqual([]) validateRumFormat(serverEvents.rum) validateRumFormat(bridgeEvents.rum) await withBrowserLogs((logs) => { diff --git a/test/e2e/lib/framework/eventsRegistry.ts b/test/e2e/lib/framework/eventsRegistry.ts index 22143c7408..7a1832903b 100644 --- a/test/e2e/lib/framework/eventsRegistry.ts +++ b/test/e2e/lib/framework/eventsRegistry.ts @@ -2,7 +2,14 @@ import type { LogsEvent } from '@datadog/browser-logs' import type { RumEvent } from '@datadog/browser-rum' import type { TelemetryEvent } from '@datadog/browser-core' import type { SessionReplayCall } from '../types/serverEvents' -import { isRumErrorEvent, isRumResourceEvent, isRumActionEvent, isRumViewEvent } from '../types/serverEvents' +import { + isTelemetryConfigurationEvent, + isRumErrorEvent, + isRumResourceEvent, + isRumActionEvent, + isRumViewEvent, + isTelemetryErrorEvent, +} from '../types/serverEvents' export type IntakeType = 'logs' | 'rum' | 'sessionReplay' | 'telemetry' @@ -36,6 +43,13 @@ export class EventRegistry { return this.rum.filter(isRumViewEvent) } + get telemetryErrors() { + return this.telemetry.filter(isTelemetryErrorEvent) + } + + get telemetryConfigurations() { + return this.telemetry.filter(isTelemetryConfigurationEvent) + } empty() { this.rum.length = 0 this.telemetry.length = 0 diff --git a/test/e2e/lib/types/serverEvents.ts b/test/e2e/lib/types/serverEvents.ts index e862076e8f..616f177a6f 100644 --- a/test/e2e/lib/types/serverEvents.ts +++ b/test/e2e/lib/types/serverEvents.ts @@ -1,3 +1,4 @@ +import type { TelemetryErrorEvent, TelemetryEvent, TelemetryConfigurationEvent } from '@datadog/browser-core' import type { RumActionEvent, RumErrorEvent, RumEvent, RumResourceEvent, RumViewEvent } from '@datadog/browser-rum' import type { BrowserSegment } from '@datadog/browser-rum/src/types' @@ -17,6 +18,14 @@ export function isRumErrorEvent(event: RumEvent): event is RumErrorEvent { return event.type === 'error' } +export function isTelemetryErrorEvent(event: TelemetryEvent): event is TelemetryErrorEvent { + return event.type === 'telemetry' && event.telemetry.status === 'error' +} + +export function isTelemetryConfigurationEvent(event: TelemetryEvent): event is TelemetryConfigurationEvent { + return event.type === 'telemetry' && event.telemetry.type === 'configuration' +} + export interface SegmentFile { filename: string encoding: string diff --git a/test/e2e/scenario/telemetry.scenario.ts b/test/e2e/scenario/telemetry.scenario.ts index a2b4d9dd97..be1846298b 100644 --- a/test/e2e/scenario/telemetry.scenario.ts +++ b/test/e2e/scenario/telemetry.scenario.ts @@ -1,4 +1,3 @@ -import type { TelemetryErrorEvent } from '@datadog/browser-core' import { bundleSetup, createTest, flushEvents } from '../lib/framework' import { browserExecute } from '../lib/helpers/browser' @@ -16,8 +15,8 @@ describe('telemetry', () => { window.DD_LOGS!.logger.log('hop', context as any) }) await flushEvents() - expect(serverEvents.telemetry.length).toBe(1) - const event = serverEvents.telemetry[0] as TelemetryErrorEvent + expect(serverEvents.telemetryErrors.length).toBe(1) + const event = serverEvents.telemetryErrors[0] expect(event.telemetry.message).toBe('bar') expect(event.telemetry.error!.kind).toBe('Error') expect(event.telemetry.status).toBe('error') @@ -37,11 +36,37 @@ describe('telemetry', () => { window.DD_RUM!.addAction('hop', context as any) }) await flushEvents() - expect(serverEvents.telemetry.length).toBe(1) - const event = serverEvents.telemetry[0] as TelemetryErrorEvent + expect(serverEvents.telemetryErrors.length).toBe(1) + const event = serverEvents.telemetryErrors[0] expect(event.telemetry.message).toBe('bar') expect(event.telemetry.error!.kind).toBe('Error') expect(event.telemetry.status).toBe('error') serverEvents.empty() }) + + createTest('send init configuration for logs') + .withSetup(bundleSetup) + .withLogs({ + enableExperimentalFeatures: ['telemetry_configuration'], + forwardErrorsToLogs: true, + }) + .run(async ({ serverEvents }) => { + await flushEvents() + expect(serverEvents.telemetryConfigurations.length).toBe(1) + const event = serverEvents.telemetryConfigurations[0] + expect(event.telemetry.configuration.forward_errors_to_logs).toEqual(true) + }) + + createTest('send init configuration for RUM') + .withSetup(bundleSetup) + .withRum({ + enableExperimentalFeatures: ['telemetry_configuration'], + trackInteractions: true, + }) + .run(async ({ serverEvents }) => { + await flushEvents() + expect(serverEvents.telemetryConfigurations.length).toBe(1) + const event = serverEvents.telemetryConfigurations[0] + expect(event.telemetry.configuration.track_interactions).toEqual(true) + }) })