diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index 5a3653006c..55d5579815 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -1,4 +1,6 @@ import type { RumEvent } from '../../../../rum-core/src' +import { EXHAUSTIVE_INIT_CONFIGURATION, SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION } from '../../../test' +import type { ExtractTelemetryConfiguration, MapInitConfigurationKey } from '../../../test' import { display } from '../../tools/display' import { ExperimentalFeature, @@ -7,7 +9,7 @@ import { } from '../../tools/experimentalFeatures' import { TrackingConsent } from '../trackingConsent' import type { InitConfiguration } from './configuration' -import { validateAndBuildConfiguration } from './configuration' +import { serializeConfiguration, validateAndBuildConfiguration } from './configuration' describe('validateAndBuildConfiguration', () => { const clientToken = 'some_client_token' @@ -207,4 +209,15 @@ describe('validateAndBuildConfiguration', () => { expect(displaySpy).toHaveBeenCalledOnceWith('Tracking Consent should be either "granted" or "not-granted"') }) }) + + describe('serializeConfiguration', () => { + it('should serialize the configuration', () => { + // By specifying the type here, we can ensure that serializeConfiguration is returning an + // object containing all expected properties. + const serializedConfiguration: ExtractTelemetryConfiguration> = + serializeConfiguration(EXHAUSTIVE_INIT_CONFIGURATION) + + expect(serializedConfiguration).toEqual(SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION) + }) + }) }) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index bd2f7b6dae..fa14c8ff6d 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -178,7 +178,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati ) } -export function serializeConfiguration(initConfiguration: InitConfiguration): Partial { +export function serializeConfiguration(initConfiguration: InitConfiguration) { return { session_sample_rate: initConfiguration.sessionSampleRate, telemetry_sample_rate: initConfiguration.telemetrySampleRate, @@ -193,5 +193,6 @@ export function serializeConfiguration(initConfiguration: InitConfiguration): Pa allow_fallback_to_local_storage: !!initConfiguration.allowFallbackToLocalStorage, store_contexts_across_pages: !!initConfiguration.storeContextsAcrossPages, allow_untrusted_events: !!initConfiguration.allowUntrustedEvents, - } + tracking_consent: initConfiguration.trackingConsent, + } satisfies RawTelemetryConfiguration } diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index 22ddf7884e..cabd91f99a 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -113,6 +113,10 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & { * The percentage of sessions with RUM & Session Replay pricing tracked */ session_replay_sample_rate?: number + /** + * The initial tracking consent value + */ + tracking_consent?: string /** * Whether the session replay start is handled manually */ @@ -193,6 +197,10 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & { * Whether the Worker is loaded from an external URL */ use_worker_url?: boolean + /** + * Whether intake requests are compressed + */ + compress_intake_requests?: boolean /** * Whether user frustrations are tracked */ @@ -309,6 +317,14 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & { * The version of Dart used in a Flutter application */ dart_version?: string + /** + * The version of Unity used in a Unity application + */ + unity_version?: string + /** + * The threshold used for iOS App Hangs monitoring (in milliseconds) + */ + app_hang_threshold?: number [k: string]: unknown } [k: string]: unknown diff --git a/packages/core/test/coreConfiguration.ts b/packages/core/test/coreConfiguration.ts new file mode 100644 index 0000000000..9c32befc3f --- /dev/null +++ b/packages/core/test/coreConfiguration.ts @@ -0,0 +1,100 @@ +import type { InitConfiguration } from '../src/domain/configuration' +import type { RawTelemetryConfiguration } from '../src/domain/telemetry' +import type { CamelToSnakeCase, RemoveIndex } from './typeUtils' + +// Defines a few constants and types related to the core package configuration, so it can be used in +// other packages tests. + +/** + * An object containing every single possible configuration initialization parameters, with + * arbitrary values. + */ +export const EXHAUSTIVE_INIT_CONFIGURATION: Required = { + clientToken: 'yes', + beforeSend: () => true, + sessionSampleRate: 50, + telemetrySampleRate: 60, + silentMultipleInit: true, + allowFallbackToLocalStorage: true, + allowUntrustedEvents: true, + storeContextsAcrossPages: true, + trackingConsent: 'not-granted', + proxy: 'proxy', + site: 'site', + service: 'service', + env: 'env', + version: 'version', + useCrossSiteSessionCookie: true, + usePartitionedCrossSiteSessionCookie: true, + useSecureSessionCookie: true, + trackSessionAcrossSubdomains: true, + enableExperimentalFeatures: ['foo'], + replica: { + clientToken: 'yes', + }, + datacenter: 'datacenter', + internalAnalyticsSubdomain: 'internal-analytics-subdomain.com', + telemetryConfigurationSampleRate: 70, +} + +export const SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION = { + session_sample_rate: 50, + telemetry_sample_rate: 60, + telemetry_configuration_sample_rate: 70, + use_before_send: true, + use_cross_site_session_cookie: true, + use_partitioned_cross_site_session_cookie: true, + use_secure_session_cookie: true, + use_proxy: true, + silent_multiple_init: true, + track_session_across_subdomains: true, + allow_fallback_to_local_storage: true, + store_contexts_across_pages: true, + allow_untrusted_events: true, + tracking_consent: 'not-granted', +} + +/** + * Maps the keys of InitConfiguration to their serialized version. + */ +export type MapInitConfigurationKey = + // Some keys are prefixed with `use_` to indicate that they are boolean flags + Key extends 'proxy' | 'beforeSend' + ? `use_${CamelToSnakeCase}` + : // Those keys should not be serialized + Key extends + | 'site' + | 'service' + | 'clientToken' + | 'env' + | 'version' + | 'datacenter' + | 'internalAnalyticsSubdomain' + | 'replica' + | 'enableExperimentalFeatures' + ? never + : // Other keys are simply snake cased + CamelToSnakeCase + +/** + * Extracts a sub-set of RawTelemetryConfiguration from the passed InitConfiguration keys, with all + * properties required, to make sure they are all defined. + * + * This type is only used in tests because "template literal types" were introduced in (TS 4.1)[1] and we + * still support TS 3.8.2. + * + * [1]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types + * + * @example + * type SerializedInitConfiguration = ExtractTelemetryConfiguration< + * "session_sample_rate" | "track_user_interactions" + * > + * // equivalent to: + * // type SerializedInitConfiguration = { + * // session_sample_rate: number | undefined; + * // track_user_interactions: boolean | undefined; + * // } + */ +export type ExtractTelemetryConfiguration> = { + [Key in Keys]: RawTelemetryConfiguration[Key] +} diff --git a/packages/core/test/index.ts b/packages/core/test/index.ts index e57b8fbc38..c1ad3e201b 100644 --- a/packages/core/test/index.ts +++ b/packages/core/test/index.ts @@ -15,3 +15,5 @@ export * from './emulate/eventBridge' export * from './emulate/stubStorages' export * from './emulate/mockFlushController' export * from './emulate/mockExperimentalFeatures' +export * from './typeUtils' +export * from './coreConfiguration' diff --git a/packages/core/test/typeUtils.ts b/packages/core/test/typeUtils.ts new file mode 100644 index 0000000000..40fcb26a34 --- /dev/null +++ b/packages/core/test/typeUtils.ts @@ -0,0 +1,18 @@ +/** + * Remove the index signature from an object type + * @example + * type Foo = { a: string, b: number, [key: string]: any } + * type Bar = RemoveIndex // { a: string, b: number } + */ +export type RemoveIndex = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K] +} + +/** + * Turn a camel case string into a snake case string + * @example + * type Foo = CamelToSnakeCase<'fooBarBaz'> // 'foo_bar_baz' + */ +export type CamelToSnakeCase = S extends `${infer T}${infer U}` + ? `${T extends Capitalize ? '_' : ''}${Lowercase}${CamelToSnakeCase}` + : S diff --git a/packages/logs/src/domain/configuration.spec.ts b/packages/logs/src/domain/configuration.spec.ts index f10b709073..557b0240cf 100644 --- a/packages/logs/src/domain/configuration.spec.ts +++ b/packages/logs/src/domain/configuration.spec.ts @@ -1,5 +1,18 @@ +import type { InitConfiguration } from '@datadog/browser-core' import { display } from '@datadog/browser-core' -import { validateAndBuildForwardOption, validateAndBuildLogsConfiguration } from './configuration' +import { + EXHAUSTIVE_INIT_CONFIGURATION, + type CamelToSnakeCase, + type ExtractTelemetryConfiguration, + type MapInitConfigurationKey, + SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION, +} from '../../../core/test' +import type { LogsInitConfiguration } from './configuration' +import { + serializeLogsConfiguration, + validateAndBuildForwardOption, + validateAndBuildLogsConfiguration, +} from './configuration' const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx' } @@ -97,3 +110,32 @@ describe('validateAndBuildForwardOption', () => { expect(validateAndBuildForwardOption('all', allowedValues, label)).toEqual(allowedValues) }) }) + +describe('serializeLogsConfiguration', () => { + it('should serialize the configuration', () => { + const exhaustiveLogsInitConfiguration: Required = { + ...EXHAUSTIVE_INIT_CONFIGURATION, + beforeSend: () => true, + forwardErrorsToLogs: true, + forwardConsoleLogs: 'all', + forwardReports: 'all', + } + + type MapLogsInitConfigurationKey = Key extends keyof InitConfiguration + ? MapInitConfigurationKey + : CamelToSnakeCase + + // By specifying the type here, we can ensure that serializeConfiguration is returning an + // object containing all expected properties. + const serializedConfiguration: ExtractTelemetryConfiguration< + MapLogsInitConfigurationKey + > = serializeLogsConfiguration(exhaustiveLogsInitConfiguration) + + expect(serializedConfiguration).toEqual({ + ...SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION, + forward_errors_to_logs: true, + forward_console_logs: 'all', + forward_reports: 'all', + }) + }) +}) diff --git a/packages/logs/src/domain/configuration.ts b/packages/logs/src/domain/configuration.ts index f2c8959f71..6e49ac7b7f 100644 --- a/packages/logs/src/domain/configuration.ts +++ b/packages/logs/src/domain/configuration.ts @@ -87,7 +87,7 @@ export function validateAndBuildForwardOption( return option === 'all' ? allowedValues : removeDuplicates(option) } -export function serializeLogsConfiguration(configuration: LogsInitConfiguration): RawTelemetryConfiguration { +export function serializeLogsConfiguration(configuration: LogsInitConfiguration) { const baseSerializedInitConfiguration = serializeConfiguration(configuration) return assign( @@ -97,5 +97,5 @@ export function serializeLogsConfiguration(configuration: LogsInitConfiguration) forward_reports: configuration.forwardReports, }, baseSerializedInitConfiguration - ) + ) satisfies RawTelemetryConfiguration } diff --git a/packages/rum-core/src/domain/configuration.spec.ts b/packages/rum-core/src/domain/configuration.spec.ts index 1c0453dae1..55f462a0d4 100644 --- a/packages/rum-core/src/domain/configuration.spec.ts +++ b/packages/rum-core/src/domain/configuration.spec.ts @@ -1,4 +1,7 @@ +import type { InitConfiguration } from '@datadog/browser-core' import { DefaultPrivacyLevel, display } from '@datadog/browser-core' +import { EXHAUSTIVE_INIT_CONFIGURATION, SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION } from '../../../core/test' +import type { ExtractTelemetryConfiguration, CamelToSnakeCase, MapInitConfigurationKey } from '../../../core/test' import type { RumInitConfiguration } from './configuration' import { DEFAULT_PROPAGATOR_TYPES, serializeRumConfiguration, validateAndBuildRumConfiguration } from './configuration' @@ -394,3 +397,61 @@ describe('validateAndBuildRumConfiguration', () => { }) }) }) + +describe('serializeRumConfiguration', () => { + it('should serialize the configuration', () => { + const exhaustiveRumInitConfiguration: Required = { + ...EXHAUSTIVE_INIT_CONFIGURATION, + applicationId: 'applicationId', + beforeSend: () => true, + excludedActivityUrls: ['toto.com'], + workerUrl: './worker.js', + compressIntakeRequests: true, + allowedTracingUrls: ['foo'], + traceSampleRate: 50, + defaultPrivacyLevel: 'allow', + subdomain: 'foo', + sessionReplaySampleRate: 60, + startSessionReplayRecordingManually: true, + trackUserInteractions: true, + actionNameAttribute: 'test-id', + trackViewsManually: true, + trackResources: true, + trackLongTasks: true, + } + + type MapRumInitConfigurationKey = Key extends keyof InitConfiguration + ? MapInitConfigurationKey + : Key extends 'workerUrl' | 'allowedTracingUrls' | 'excludedActivityUrls' + ? `use_${CamelToSnakeCase}` + : Key extends 'trackLongTasks' + ? 'track_long_task' // oops + : Key extends 'applicationId' | 'subdomain' + ? never + : CamelToSnakeCase + + // By specifying the type here, we can ensure that serializeConfiguration is returning an + // object containing all expected properties. + const serializedConfiguration: ExtractTelemetryConfiguration< + MapRumInitConfigurationKey | 'selected_tracing_propagators' + > = serializeRumConfiguration(exhaustiveRumInitConfiguration) + + expect(serializedConfiguration).toEqual({ + ...SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION, + session_replay_sample_rate: 60, + trace_sample_rate: 50, + use_allowed_tracing_urls: true, + selected_tracing_propagators: ['tracecontext', 'datadog'], + use_excluded_activity_urls: true, + track_user_interactions: true, + track_views_manually: true, + start_session_replay_recording_manually: true, + action_name_attribute: 'test-id', + default_privacy_level: 'allow', + track_resources: true, + track_long_task: true, + use_worker_url: true, + compress_intake_requests: true, + }) + }) +}) diff --git a/packages/rum-core/src/domain/configuration.ts b/packages/rum-core/src/domain/configuration.ts index a432f6d8fb..6048053d1b 100644 --- a/packages/rum-core/src/domain/configuration.ts +++ b/packages/rum-core/src/domain/configuration.ts @@ -186,7 +186,7 @@ function getSelectedTracingPropagators(configuration: RumInitConfiguration): Pro return arrayFrom(usedTracingPropagators) } -export function serializeRumConfiguration(configuration: RumInitConfiguration): RawTelemetryConfiguration { +export function serializeRumConfiguration(configuration: RumInitConfiguration) { const baseSerializedConfiguration = serializeConfiguration(configuration) return assign( @@ -209,5 +209,5 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration): track_long_task: configuration.trackLongTasks, }, baseSerializedConfiguration - ) + ) satisfies RawTelemetryConfiguration } diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 41079c0ba3..cada08399e 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -220,6 +220,10 @@ export type RumErrorEvent = CommonProperties & * The type of the error */ readonly type?: string + /** + * The specific category of the error. It provides a high-level grouping for different types of errors. + */ + readonly category?: 'ANR' | 'App Hang' | 'Exception' /** * Whether the error has been handled manually in the source code or not */ @@ -383,6 +387,16 @@ export type RumErrorEvent = CommonProperties & } [k: string]: unknown } + /** + * Properties of App Hang and ANR errors + */ + readonly freeze?: { + /** + * Duration of the main thread freeze (in ns) + */ + readonly duration: number + [k: string]: unknown + } /** * View properties */ diff --git a/rum-events-format b/rum-events-format index 810811b623..e76a333d8d 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit 810811b623d65dea647ca1d634567c0265778d06 +Subproject commit e76a333d8d2f4841e73116c32ad396e806889053