Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔊 [RUM-3501] add tracking_consent to configuration telemetry #2640

Merged
merged 3 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion packages/core/src/domain/configuration/configuration.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'
Expand Down Expand Up @@ -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<MapInitConfigurationKey<keyof InitConfiguration>> =
serializeConfiguration(EXHAUSTIVE_INIT_CONFIGURATION)

expect(serializedConfiguration).toEqual(SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION)
})
})
})
5 changes: 3 additions & 2 deletions packages/core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati
)
}

export function serializeConfiguration(initConfiguration: InitConfiguration): Partial<RawTelemetryConfiguration> {
export function serializeConfiguration(initConfiguration: InitConfiguration) {
return {
session_sample_rate: initConfiguration.sessionSampleRate,
telemetry_sample_rate: initConfiguration.telemetrySampleRate,
Expand All @@ -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
}
16 changes: 16 additions & 0 deletions packages/core/src/domain/telemetry/telemetryEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions packages/core/test/coreConfiguration.ts
Original file line number Diff line number Diff line change
@@ -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<InitConfiguration> = {
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<Key extends string> =
// Some keys are prefixed with `use_` to indicate that they are boolean flags
Key extends 'proxy' | 'beforeSend'
? `use_${CamelToSnakeCase<Key>}`
: // 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<Key>

/**
* 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<Keys extends keyof RemoveIndex<RawTelemetryConfiguration>> = {
[Key in Keys]: RawTelemetryConfiguration[Key]
}
2 changes: 2 additions & 0 deletions packages/core/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
18 changes: 18 additions & 0 deletions packages/core/test/typeUtils.ts
Original file line number Diff line number Diff line change
@@ -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<Foo> // { a: string, b: number }
*/
export type RemoveIndex<T> = {
[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 string> = S extends `${infer T}${infer U}`
? `${T extends Capitalize<T> ? '_' : ''}${Lowercase<T>}${CamelToSnakeCase<U>}`
: S
44 changes: 43 additions & 1 deletion packages/logs/src/domain/configuration.spec.ts
Original file line number Diff line number Diff line change
@@ -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' }

Expand Down Expand Up @@ -97,3 +110,32 @@ describe('validateAndBuildForwardOption', () => {
expect(validateAndBuildForwardOption('all', allowedValues, label)).toEqual(allowedValues)
})
})

describe('serializeLogsConfiguration', () => {
it('should serialize the configuration', () => {
const exhaustiveLogsInitConfiguration: Required<LogsInitConfiguration> = {
...EXHAUSTIVE_INIT_CONFIGURATION,
beforeSend: () => true,
forwardErrorsToLogs: true,
forwardConsoleLogs: 'all',
forwardReports: 'all',
}

type MapLogsInitConfigurationKey<Key extends string> = Key extends keyof InitConfiguration
? MapInitConfigurationKey<Key>
: CamelToSnakeCase<Key>

// By specifying the type here, we can ensure that serializeConfiguration is returning an
// object containing all expected properties.
const serializedConfiguration: ExtractTelemetryConfiguration<
MapLogsInitConfigurationKey<keyof LogsInitConfiguration>
> = serializeLogsConfiguration(exhaustiveLogsInitConfiguration)

expect(serializedConfiguration).toEqual({
...SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION,
forward_errors_to_logs: true,
forward_console_logs: 'all',
forward_reports: 'all',
})
})
})
4 changes: 2 additions & 2 deletions packages/logs/src/domain/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function validateAndBuildForwardOption<T>(
return option === 'all' ? allowedValues : removeDuplicates<T>(option)
}

export function serializeLogsConfiguration(configuration: LogsInitConfiguration): RawTelemetryConfiguration {
export function serializeLogsConfiguration(configuration: LogsInitConfiguration) {
const baseSerializedInitConfiguration = serializeConfiguration(configuration)

return assign(
Expand All @@ -97,5 +97,5 @@ export function serializeLogsConfiguration(configuration: LogsInitConfiguration)
forward_reports: configuration.forwardReports,
},
baseSerializedInitConfiguration
)
) satisfies RawTelemetryConfiguration
}
61 changes: 61 additions & 0 deletions packages/rum-core/src/domain/configuration.spec.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -394,3 +397,61 @@ describe('validateAndBuildRumConfiguration', () => {
})
})
})

describe('serializeRumConfiguration', () => {
it('should serialize the configuration', () => {
const exhaustiveRumInitConfiguration: Required<RumInitConfiguration> = {
...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 string> = Key extends keyof InitConfiguration
? MapInitConfigurationKey<Key>
: Key extends 'workerUrl' | 'allowedTracingUrls' | 'excludedActivityUrls'
? `use_${CamelToSnakeCase<Key>}`
: Key extends 'trackLongTasks'
? 'track_long_task' // oops
: Key extends 'applicationId' | 'subdomain'
? never
: CamelToSnakeCase<Key>

// By specifying the type here, we can ensure that serializeConfiguration is returning an
// object containing all expected properties.
const serializedConfiguration: ExtractTelemetryConfiguration<
MapRumInitConfigurationKey<keyof RumInitConfiguration> | '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,
})
})
})
4 changes: 2 additions & 2 deletions packages/rum-core/src/domain/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -209,5 +209,5 @@ export function serializeRumConfiguration(configuration: RumInitConfiguration):
track_long_task: configuration.trackLongTasks,
},
baseSerializedConfiguration
)
) satisfies RawTelemetryConfiguration
}
14 changes: 14 additions & 0 deletions packages/rum-core/src/rumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down