Skip to content

Commit

Permalink
Integrate dad32fd (#2640) from benoit/poc-validate-configuration-keys…
Browse files Browse the repository at this point in the history
… into staging-11

Co-authored-by: Benoît Zugmeyer <[email protected]>
  • Loading branch information
dd-mergequeue[bot] and BenoitZugmeyer authored Mar 11, 2024
2 parents 44b1e9a + dad32fd commit 305283d
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 9 deletions.
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

0 comments on commit 305283d

Please sign in to comment.