@mtdt.temp/browser-rum-core
Version:
Datadog browser RUM core utilities.
788 lines (710 loc) • 24 kB
text/typescript
import type {
Context,
TimeStamp,
RelativeTime,
DeflateWorker,
DeflateEncoderStreamId,
DeflateEncoder,
PublicApi,
Duration,
ContextManager,
TrackingConsent,
User,
Account,
RumInternalContext,
Telemetry,
} from '@mtdt.temp/browser-core'
import {
ContextManagerMethod,
addTelemetryUsage,
deepClone,
makePublicApi,
monitor,
clocksNow,
callMonitored,
createHandlingStack,
sanitize,
createIdentityEncoder,
displayAlreadyInitializedError,
createTrackingConsentState,
timeStampToClocks,
CustomerContextKey,
defineContextMethod,
startBufferingData,
} from '@mtdt.temp/browser-core'
import type { LifeCycle } from '../domain/lifeCycle'
import type { ViewHistory } from '../domain/contexts/viewHistory'
import type { RumSessionManager } from '../domain/rumSessionManager'
import type { ReplayStats } from '../rawRumEvent.types'
import { ActionType, VitalType } from '../rawRumEvent.types'
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
import type { ViewOptions } from '../domain/view/trackViews'
import type {
AddDurationVitalOptions,
DurationVitalOptions,
DurationVitalReference,
} from '../domain/vital/vitalCollection'
import { createCustomVitalsState } from '../domain/vital/vitalCollection'
import { callPluginsMethod } from '../domain/plugins'
import type { Hooks } from '../domain/hooks'
import { createPreStartStrategy } from './preStartRum'
import type { StartRum, StartRumResult } from './startRum'
export interface StartRecordingOptions {
force: boolean
}
/**
* Public API for the RUM browser SDK.
*
* See your company's documentation for RUM browser monitoring setup.
*
* @category API
*/
export interface RumPublicApi extends PublicApi {
/**
* Init the RUM browser SDK.
*
* See your company's documentation for RUM browser monitoring setup.
*
* @category Init
* @param initConfiguration - Configuration options of the SDK
* @example Init RUM Browser SDK example
* ```ts
* motadataRum.init({
* applicationId: '<DATADOG_APPLICATION_ID>',
* clientToken: '<DATADOG_CLIENT_TOKEN>',
* site: '<DATADOG_SITE>',
* // service: 'my-web-application',
* // env: 'production',
* // version: '1.0.0',
* sessionSampleRate: 100,
* sessionReplaySampleRate: 100,
* trackResources: true,
* trackLongTasks: true,
* trackUserInteractions: true,
* })
* ```
*/
init: (initConfiguration: RumInitConfiguration) => void
/**
* Set the tracking consent of the current user.
*
* Data will be sent only if it is set to "granted". This value won't be stored by the library
* across page loads: you will need to call this method or set the appropriate `trackingConsent`
* field in the init() method at each page load.
*
* If this method is called before the init() method, the provided value will take precedence
* over the one provided as initialization parameter.
*
* See your company's documentation for user tracking consent.
*
* @category Tracking Consent
* @param trackingConsent - The user tracking consent
*/
setTrackingConsent: (trackingConsent: TrackingConsent) => void
/**
* Set View Name.
*
* Enable to manually change the name of the current view.
* See your company's documentation for overriding default RUM view names.
*
* @category View
* @param name - Name of the view
*/
setViewName: (name: string) => void
/**
* Set View Context.
*
* Enable to manually set the context of the current view.
*
* @category View
* @param context - Context of the view
*/
setViewContext: (context: Context) => void
/**
* Set View Context Property.
*
* Enable to manually set a property of the context of the current view.
*
* @category View
* @param key - key of the property
* @param value - value of the property
*/
setViewContextProperty: (key: string, value: any) => void
/**
* Get View Context.
*
* @category View
*/
getViewContext(): Context
/**
* [Internal API] Get the internal SDK context
*
* @internal
*/
getInternalContext: (startTime?: number) => RumInternalContext | undefined
/**
* Get the init configuration
*
* @category Init
*/
getInitConfiguration: () => RumInitConfiguration | undefined
/**
* Add a custom action, stored in `@action`
*
* See [Send RUM Custom Actions](https://docs.yourcompany.com/rum) for further information.
*
* @category Custom Actions
* @param name - Name of the action
* @param context - Context of the action
*/
addAction: (name: string, context?: object) => void
/**
* Add a custom error, stored in `@error`.
*
* See [Send RUM Custom Actions](https://docs.yourcompany.com/rum) for further information.
*
* @category Custom Errors
* @param error - Error. Favor sending a [Javascript Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) to have a stack trace attached to the error event.
* @param context - Context of the error
*/
addError: (error: unknown, context?: object) => void
/**
* Add a custom timing relative to the start of the current view,
* stored in `@view.custom_timings.<timing_name>`
*
* Note: passing a relative time is discouraged since it is actually used as-is but displayed relative to the view start.
* We currently don't provide a way to retrieve the view start time, so it can be challenging to provide a timing relative to the view start.
* see https://github.com/DataDog/browser-sdk/issues/2552
*
* @category Custom Timings
* @param name - Name of the custom timing
* @param [time] - Epoch timestamp of the custom timing (if not set, will use current time)
*/
addTiming: (name: string, time?: number) => void
/**
* Set the global context information to all events, stored in `@context`
* See [Global context](https://docs.yourcompany.com/rum) for further information.
*
* @category Global Context
* @param context - Global context
*/
setGlobalContext: (context: Context) => void
/**
* Get the global Context
*
* See [Global context](https://docs.yourcompany.com/rum) for further information.
*
* @category Global Context
*/
getGlobalContext: () => Context
/**
* Set or update a global context property, stored in `@context.<key>`
*
* See [Global context](https://docs.yourcompany.com/rum) for further information.
*
* @category Global Context
* @param key - Key of the property
* @param value - Value of the property
*/
setGlobalContextProperty: (key: any, value: any) => void
/**
* Remove a global context property
*
* See [Global context](https://docs.yourcompany.com/rum) for further information.
*
* @category Global Context
*/
removeGlobalContextProperty: (key: any) => void
/**
* Clear the global context
*
* See [Global context](https://docs.yourcompany.com/rum) for further information.
*
* @category Global Context
*/
clearGlobalContext(): void
/**
* Set user information to all events, stored in `@usr`
*
* See [User session](https://docs.yourcompany.com/rum) for further information.
*
* @category User
* @param newUser - User information
*/
setUser(newUser: User & { id: string }): void
/**
* Set user information to all events, stored in `@usr`
*
* @category User
* @deprecated You must specify a user id, favor using {@link setUser} instead
* @param newUser - User information with optional id
*/
setUser(newUser: User): void
/**
* Get user information
*
* See [User session](https://docs.yourcompany.com/rum) for further information.
*
* @category User
* @returns User information
*/
getUser: () => Context
/**
* Set or update the user property, stored in `@usr.<key>`
*
* See [User session](https://docs.yourcompany.com/rum) for further information.
*
* @category User
* @param key - Key of the property
* @param property - Value of the property
*/
setUserProperty: (key: any, property: any) => void
/**
* Remove a user property
*
* @category User
* @param key - Key of the property to remove
* @see [User session](https://docs.yourcompany.com/rum) for further information.
*/
removeUserProperty: (key: any) => void
/**
* Clear all user information
*
* See [User session](https://docs.yourcompany.com/rum) for further information.
*
* @category User
*/
clearUser: () => void
/**
* Set account information to all events, stored in `@account`
*
* @category Account
* @param newAccount - Account information
*/
setAccount: (newAccount: Account) => void
/**
* Get account information
*
* @category Account
* @returns Account information
*/
getAccount: () => Context
/**
* Set or update the account property, stored in `@account.<key>`
*
* @category Account
* @param key - Key of the property
* @param property - Value of the property
*/
setAccountProperty: (key: string, property: any) => void
/**
* Remove an account property
*
* @category Account
* @param key - Key of the property to remove
*/
removeAccountProperty: (key: string) => void
/**
* Clear all account information
*
* @category Account
* @returns Clear all account information
*/
clearAccount: () => void
/**
* Start a view manually.
* Enable to manual start a view, use `trackViewsManually: true` init parameter and call `startView()` to create RUM views and be aligned with how you’ve defined them in your SPA application routing.
*
* See [Override default RUM view names](https://docs.yourcompany.com/rum) for further information.
*
* @category View
* @param nameOrOptions - Name or options (name, service, version) for the view
*/
startView(nameOrOptions?: string | ViewOptions): void
/**
* Stop the session. A new session will start at the next user interaction with the page.
*
* @category Session
*/
stopSession(): void
/**
* Add a feature flag evaluation,
* stored in `@feature_flags.<feature_flag_key>`
*
* We recommend enabling the intake request compression when using feature flags `compressIntakeRequests: true`.
*
* See [Feature Flag Tracking](https://docs.yourcompany.com/rum) for further information.
*
* @param key - The key of the feature flag.
* @param value - The value of the feature flag.
*/
addFeatureFlagEvaluation: (key: string, value: any) => void
/**
* Get the Session Replay Link.
*
* See [Connect Session Replay To Your Third-Party Tools](https://docs.yourcompany.com/rum) for further information.
*
* @category Session Replay
*/
getSessionReplayLink: () => string | undefined
/**
* Start Session Replay recording.
* Enable to conditionally start the recording, use the `startSessionReplayRecordingManually:true` init parameter and call `startSessionReplayRecording()`
*
* See [Browser Session Replay](https://docs.yourcompany.com/rum) for further information.
*
* @category Session Replay
*/
startSessionReplayRecording: (options?: StartRecordingOptions) => void
/**
* Stop Session Replay recording.
*
* See [Browser Session Replay](https://docs.yourcompany.com/rum) for further information.
*
* @category Session Replay
*/
stopSessionReplayRecording: () => void
/**
* Add a custom duration vital
*
* @category Vital
* @param name - Name of the custom vital
* @param options - Options for the custom vital (startTime, duration, context, description)
*/
addDurationVital: (name: string, options: AddDurationVitalOptions) => void
/**
* Start a custom duration vital.
*
* If you plan to have multiple durations for the same vital, you should use the reference returned by this method.
*
* @category Vital
* @param name - Name of the custom vital
* @param options - Options for the custom vital (context, description)
* @returns reference to the custom vital
*/
startDurationVital: (name: string, options?: DurationVitalOptions) => DurationVitalReference
/**
* Stop a custom duration vital
*
* @category Vital
* @param nameOrRef - Name or reference of the custom vital
* @param options - Options for the custom vital (context, description)
*/
stopDurationVital: (nameOrRef: string | DurationVitalReference, options?: DurationVitalOptions) => void
}
export interface RecorderApi {
start: (options?: StartRecordingOptions) => void
stop: () => void
onRumStart: (
lifeCycle: LifeCycle,
configuration: RumConfiguration,
sessionManager: RumSessionManager,
viewHistory: ViewHistory,
deflateWorker: DeflateWorker | undefined,
telemetry: Telemetry
) => void
isRecording: () => boolean
getReplayStats: (viewId: string) => ReplayStats | undefined
getSessionReplayLink: () => string | undefined
}
export interface ProfilerApi {
stop: () => void
onRumStart: (
lifeCycle: LifeCycle,
hooks: Hooks,
configuration: RumConfiguration,
sessionManager: RumSessionManager,
viewHistory: ViewHistory
) => void
}
export interface RumPublicApiOptions {
ignoreInitIfSyntheticsWillInjectRum?: boolean
startDeflateWorker?: (
configuration: RumConfiguration,
source: string,
onInitializationFailure: () => void
) => DeflateWorker | undefined
createDeflateEncoder?: (
configuration: RumConfiguration,
worker: DeflateWorker,
streamId: DeflateEncoderStreamId
) => DeflateEncoder
sdkName?: 'rum' | 'rum-slim' | 'rum-synthetics'
}
export interface Strategy {
init: (initConfiguration: RumInitConfiguration, publicApi: RumPublicApi) => void
initConfiguration: RumInitConfiguration | undefined
getInternalContext: StartRumResult['getInternalContext']
stopSession: StartRumResult['stopSession']
addTiming: StartRumResult['addTiming']
startView: StartRumResult['startView']
setViewName: StartRumResult['setViewName']
setViewContext: StartRumResult['setViewContext']
setViewContextProperty: StartRumResult['setViewContextProperty']
getViewContext: StartRumResult['getViewContext']
globalContext: ContextManager
userContext: ContextManager
accountContext: ContextManager
addAction: StartRumResult['addAction']
addError: StartRumResult['addError']
addFeatureFlagEvaluation: StartRumResult['addFeatureFlagEvaluation']
startDurationVital: StartRumResult['startDurationVital']
stopDurationVital: StartRumResult['stopDurationVital']
addDurationVital: StartRumResult['addDurationVital']
}
export function makeRumPublicApi(
startRumImpl: StartRum,
recorderApi: RecorderApi,
profilerApi: ProfilerApi,
options: RumPublicApiOptions = {}
): RumPublicApi {
const trackingConsentState = createTrackingConsentState()
const customVitalsState = createCustomVitalsState()
const bufferedDataObservable = startBufferingData().observable
let strategy = createPreStartStrategy(
options,
trackingConsentState,
customVitalsState,
(configuration, deflateWorker, initialViewOptions) => {
const startRumResult = startRumImpl(
configuration,
recorderApi,
profilerApi,
initialViewOptions,
deflateWorker && options.createDeflateEncoder
? (streamId) => options.createDeflateEncoder!(configuration, deflateWorker, streamId)
: createIdentityEncoder,
trackingConsentState,
customVitalsState,
bufferedDataObservable,
options.sdkName
)
recorderApi.onRumStart(
startRumResult.lifeCycle,
configuration,
startRumResult.session,
startRumResult.viewHistory,
deflateWorker,
startRumResult.telemetry
)
profilerApi.onRumStart(
startRumResult.lifeCycle,
startRumResult.hooks,
configuration,
startRumResult.session,
startRumResult.viewHistory
)
strategy = createPostStartStrategy(strategy, startRumResult)
callPluginsMethod(configuration.plugins, 'onRumStart', {
strategy, // TODO: remove this in the next major release
addEvent: startRumResult.addEvent,
})
return startRumResult
}
)
const getStrategy = () => strategy
const startView: {
(name?: string): void
(options: ViewOptions): void
} = monitor((options?: string | ViewOptions) => {
const sanitizedOptions = typeof options === 'object' ? options : { name: options }
strategy.startView(sanitizedOptions)
addTelemetryUsage({ feature: 'start-view' })
})
const rumPublicApi: RumPublicApi = makePublicApi<RumPublicApi>({
init: monitor((initConfiguration) => {
strategy.init(initConfiguration, rumPublicApi)
}),
setTrackingConsent: monitor((trackingConsent) => {
trackingConsentState.update(trackingConsent)
addTelemetryUsage({ feature: 'set-tracking-consent', tracking_consent: trackingConsent })
}),
setViewName: monitor((name: string) => {
strategy.setViewName(name)
addTelemetryUsage({ feature: 'set-view-name' })
}),
setViewContext: monitor((context: Context) => {
strategy.setViewContext(context)
addTelemetryUsage({ feature: 'set-view-context' })
}),
setViewContextProperty: monitor((key: string, value: any) => {
strategy.setViewContextProperty(key, value)
addTelemetryUsage({ feature: 'set-view-context-property' })
}),
getViewContext: monitor(() => {
addTelemetryUsage({ feature: 'set-view-context-property' })
return strategy.getViewContext()
}),
getInternalContext: monitor((startTime) => strategy.getInternalContext(startTime)),
getInitConfiguration: monitor(() => deepClone(strategy.initConfiguration)),
addAction: (name, context) => {
const handlingStack = createHandlingStack('action')
callMonitored(() => {
strategy.addAction({
name: sanitize(name)!,
context: sanitize(context) as Context,
startClocks: clocksNow(),
type: ActionType.CUSTOM,
handlingStack,
})
addTelemetryUsage({ feature: 'add-action' })
})
},
addError: (error, context) => {
const handlingStack = createHandlingStack('error')
callMonitored(() => {
strategy.addError({
error, // Do not sanitize error here, it is needed unserialized by computeRawError()
handlingStack,
context: sanitize(context) as Context,
startClocks: clocksNow(),
})
addTelemetryUsage({ feature: 'add-error' })
})
},
addTiming: monitor((name, time) => {
// TODO: next major decide to drop relative time support or update its behaviour
strategy.addTiming(sanitize(name)!, time as RelativeTime | TimeStamp | undefined)
}),
setGlobalContext: defineContextMethod(
getStrategy,
CustomerContextKey.globalContext,
ContextManagerMethod.setContext,
'set-global-context'
),
getGlobalContext: defineContextMethod(
getStrategy,
CustomerContextKey.globalContext,
ContextManagerMethod.getContext,
'get-global-context'
),
setGlobalContextProperty: defineContextMethod(
getStrategy,
CustomerContextKey.globalContext,
ContextManagerMethod.setContextProperty,
'set-global-context-property'
),
removeGlobalContextProperty: defineContextMethod(
getStrategy,
CustomerContextKey.globalContext,
ContextManagerMethod.removeContextProperty,
'remove-global-context-property'
),
clearGlobalContext: defineContextMethod(
getStrategy,
CustomerContextKey.globalContext,
ContextManagerMethod.clearContext,
'clear-global-context'
),
setUser: defineContextMethod(
getStrategy,
CustomerContextKey.userContext,
ContextManagerMethod.setContext,
'set-user'
),
getUser: defineContextMethod(
getStrategy,
CustomerContextKey.userContext,
ContextManagerMethod.getContext,
'get-user'
),
setUserProperty: defineContextMethod(
getStrategy,
CustomerContextKey.userContext,
ContextManagerMethod.setContextProperty,
'set-user-property'
),
removeUserProperty: defineContextMethod(
getStrategy,
CustomerContextKey.userContext,
ContextManagerMethod.removeContextProperty,
'remove-user-property'
),
clearUser: defineContextMethod(
getStrategy,
CustomerContextKey.userContext,
ContextManagerMethod.clearContext,
'clear-user'
),
setAccount: defineContextMethod(
getStrategy,
CustomerContextKey.accountContext,
ContextManagerMethod.setContext,
'set-account'
),
getAccount: defineContextMethod(
getStrategy,
CustomerContextKey.accountContext,
ContextManagerMethod.getContext,
'get-account'
),
setAccountProperty: defineContextMethod(
getStrategy,
CustomerContextKey.accountContext,
ContextManagerMethod.setContextProperty,
'set-account-property'
),
removeAccountProperty: defineContextMethod(
getStrategy,
CustomerContextKey.accountContext,
ContextManagerMethod.removeContextProperty,
'remove-account-property'
),
clearAccount: defineContextMethod(
getStrategy,
CustomerContextKey.accountContext,
ContextManagerMethod.clearContext,
'clear-account'
),
startView,
stopSession: monitor(() => {
strategy.stopSession()
addTelemetryUsage({ feature: 'stop-session' })
}),
addFeatureFlagEvaluation: monitor((key, value) => {
strategy.addFeatureFlagEvaluation(sanitize(key)!, sanitize(value))
addTelemetryUsage({ feature: 'add-feature-flag-evaluation' })
}),
getSessionReplayLink: monitor(() => recorderApi.getSessionReplayLink()),
startSessionReplayRecording: monitor((options?: StartRecordingOptions) => {
recorderApi.start(options)
addTelemetryUsage({ feature: 'start-session-replay-recording', force: options && options.force })
}),
stopSessionReplayRecording: monitor(() => recorderApi.stop()),
addDurationVital: monitor((name, options) => {
addTelemetryUsage({ feature: 'add-duration-vital' })
strategy.addDurationVital({
name: sanitize(name)!,
type: VitalType.DURATION,
startClocks: timeStampToClocks(options.startTime as TimeStamp),
duration: options.duration as Duration,
context: sanitize(options && options.context) as Context,
description: sanitize(options && options.description) as string | undefined,
})
}),
startDurationVital: monitor((name, options) => {
addTelemetryUsage({ feature: 'start-duration-vital' })
return strategy.startDurationVital(sanitize(name)!, {
context: sanitize(options && options.context) as Context,
description: sanitize(options && options.description) as string | undefined,
})
}),
stopDurationVital: monitor((nameOrRef, options) => {
addTelemetryUsage({ feature: 'stop-duration-vital' })
strategy.stopDurationVital(typeof nameOrRef === 'string' ? sanitize(nameOrRef)! : nameOrRef, {
context: sanitize(options && options.context) as Context,
description: sanitize(options && options.description) as string | undefined,
})
}),
})
return rumPublicApi
}
function createPostStartStrategy(preStartStrategy: Strategy, startRumResult: StartRumResult): Strategy {
return {
init: (initConfiguration: RumInitConfiguration) => {
displayAlreadyInitializedError('MD_RUM', initConfiguration)
},
initConfiguration: preStartStrategy.initConfiguration,
...startRumResult,
}
}