Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/rum-core/src/boot/preStartRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export function createPreStartStrategy(
},

startDurationVital(name, options) {
return startDurationVital(customVitalsState, name, options)
return startDurationVital(customVitalsState, undefined, name, options)
},

stopDurationVital(name, options) {
Expand Down
5 changes: 4 additions & 1 deletion packages/rum-core/src/domain/action/trackManualActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ActionType as ActionTypeEnum, FrustrationType as FrustrationTypeEnum }
import type { EventCounts } from '../trackEventCounts'
import { startEventTracker } from '../eventTracker'
import type { LifeCycle } from '../lifeCycle'
import { LifeCycleEventType } from '../lifeCycle'
import { isActionChildEvent } from './isActionChildEvent'

export type ActionCounts = EventCounts
Expand Down Expand Up @@ -51,7 +52,7 @@ export function trackManualActions(lifeCycle: LifeCycle, onManualActionCompleted
function startManualAction(name: string, options: ActionOptions = {}, startClocks = clocksNow()) {
const lookupKey = options.actionKey ?? name

actionTracker.start(
const startedManualAction = actionTracker.start(
lookupKey,
startClocks,
{
Expand All @@ -60,6 +61,8 @@ export function trackManualActions(lifeCycle: LifeCycle, onManualActionCompleted
},
{ isChildEvent: isActionChildEvent }
)

lifeCycle.notify(LifeCycleEventType.ACTION_STARTED, startedManualAction)
Copy link
Collaborator

@amortemousque amortemousque Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏You could make this part of eventTracker so you have the lifecycle event for click and manual actions. We also are planning to use eventTracker for vitals.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really have a way to know which event type I'm dealing with in the start method of the event tracker, right? So do you imagine one of the two following things:

  • start takes an optional function executed before returning, and that would in our case emit the life cycle event?
  • we pass an event type at the creation of the event tracker so that we know what we're dealing with (which might actually have other uses later, it can be useful to know which event type we're dealing with 🤷‍♂️)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes right, it’s a bit tricky. Let’s keep it simple.

You can keep it here, but we also need to handle click actions. So it should be added here as well.

}

function stopManualAction(name: string, options: ActionOptions = {}, stopClocks = clocksNow()) {
Expand Down
13 changes: 8 additions & 5 deletions packages/rum-core/src/domain/eventTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ export interface StartOptions {
}

export interface EventTracker<TData> {
start: (key: string, startClocks: ClocksState, data: TData, options?: StartOptions) => void
start: (key: string, startClocks: ClocksState, data: TData, options?: StartOptions) => TrackedEventData<TData>
stop: (key: string, stopClocks: ClocksState, data?: Partial<TData>) => StoppedEvent<TData> | undefined
discard: (key: string) => DiscardedEvent<TData> | undefined
getCounts: (key: string) => EventCounts | undefined
findId: (startTime?: RelativeTime) => string[]
stopAll: () => void
}

interface TrackedEventData<TData> {
export interface TrackedEventData<TData> {
id: string
key: string
startClocks: ClocksState
Expand Down Expand Up @@ -66,7 +66,7 @@ export function startEventTracker<TData>(lifeCycle: LifeCycle): EventTracker<TDa

const sessionRenewalSubscription = lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, discardAll)

function start(key: string, startClocks: ClocksState, data: TData, options?: StartOptions) {
function start(key: string, startClocks: ClocksState, data: TData, options?: StartOptions): TrackedEventData<TData> {
const id = generateUUID()

const historyEntry = history.add(id, startClocks.relative)
Expand All @@ -83,14 +83,17 @@ export function startEventTracker<TData>(lifeCycle: LifeCycle): EventTracker<TDa
})
: undefined

keyedEvents.set(key, {
const trackedEventData: TrackedEventData<TData> = {
id,
key,
startClocks,
data,
historyEntry,
eventCounts,
})
}

keyedEvents.set(key, trackedEventData)
return trackedEventData
}

function stop(key: string, stopClocks: ClocksState, extraData?: Partial<TData>): StoppedEvent<TData> | undefined {
Expand Down
9 changes: 9 additions & 0 deletions packages/rum-core/src/domain/lifeCycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type { RawRumEvent, AssembledRumEvent } from '../rawRumEvent.types'
import type { RequestCompleteEvent, RequestStartEvent } from './requestCollection'
import type { AutoAction } from './action/actionCollection'
import type { ViewEvent, ViewCreatedEvent, ViewEndedEvent, BeforeViewUpdateEvent } from './view/trackViews'
import type { DurationVitalStart } from './vital/vitalCollection'
import type { TrackedEventData } from './eventTracker'
import type { ActionEventData } from './action/trackManualActions'

export const enum LifeCycleEventType {
// Contexts (like viewHistory) should be opened using prefixed BEFORE_XXX events and closed using prefixed AFTER_XXX events
Expand Down Expand Up @@ -35,6 +38,8 @@ export const enum LifeCycleEventType {
RAW_RUM_EVENT_COLLECTED,
RUM_EVENT_COLLECTED,
RAW_ERROR_COLLECTED,
ACTION_STARTED,
VITAL_STARTED,
}

// This is a workaround for an issue occurring when the Browser SDK is included in a TypeScript
Expand All @@ -51,6 +56,7 @@ export const enum LifeCycleEventType {
// * https://github.com/DataDog/browser-sdk/issues/2208
// * https://github.com/microsoft/TypeScript/issues/54152
declare const LifeCycleEventTypeAsConst: {
ACTION_STARTED: LifeCycleEventType.ACTION_STARTED
AUTO_ACTION_COMPLETED: LifeCycleEventType.AUTO_ACTION_COMPLETED
BEFORE_VIEW_CREATED: LifeCycleEventType.BEFORE_VIEW_CREATED
VIEW_CREATED: LifeCycleEventType.VIEW_CREATED
Expand All @@ -66,11 +72,13 @@ declare const LifeCycleEventTypeAsConst: {
RAW_RUM_EVENT_COLLECTED: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED
RUM_EVENT_COLLECTED: LifeCycleEventType.RUM_EVENT_COLLECTED
RAW_ERROR_COLLECTED: LifeCycleEventType.RAW_ERROR_COLLECTED
VITAL_STARTED: LifeCycleEventType.VITAL_STARTED
}

// Note: this interface needs to be exported even if it is not used outside of this module, else TS
// fails to build the rum-core package with error TS4058
export interface LifeCycleEventMap {
[LifeCycleEventTypeAsConst.ACTION_STARTED]: TrackedEventData<ActionEventData>
[LifeCycleEventTypeAsConst.AUTO_ACTION_COMPLETED]: AutoAction
[LifeCycleEventTypeAsConst.BEFORE_VIEW_CREATED]: ViewCreatedEvent
[LifeCycleEventTypeAsConst.VIEW_CREATED]: ViewCreatedEvent
Expand All @@ -89,6 +97,7 @@ export interface LifeCycleEventMap {
error: RawError
customerContext?: Context
}
[LifeCycleEventTypeAsConst.VITAL_STARTED]: DurationVitalStart
}

export interface RawRumEventCollectedData<E extends RawRumEvent = RawRumEvent> {
Expand Down
69 changes: 46 additions & 23 deletions packages/rum-core/src/domain/vital/vitalCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,48 @@ describe('vitalCollection', () => {
it('should create duration vital from a vital reference', () => {
const cbSpy = jasmine.createSpy()

const vitalRef = startDurationVital(vitalsState, 'foo')
const vitalRef = startDurationVital(vitalsState, lifeCycle, 'foo')
clock.tick(100)
stopDurationVital(cbSpy, vitalsState, vitalRef)

expect(cbSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ name: 'foo', duration: 100 }))
expect(cbSpy).toHaveBeenCalledOnceWith(
jasmine.objectContaining({ name: 'foo', duration: 100 }),
jasmine.any(String)
)
})

it('should create duration vital from a vital name', () => {
const cbSpy = jasmine.createSpy()

startDurationVital(vitalsState, 'foo')
startDurationVital(vitalsState, lifeCycle, 'foo')
clock.tick(100)
stopDurationVital(cbSpy, vitalsState, 'foo')

expect(cbSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ name: 'foo', duration: 100 }))
expect(cbSpy).toHaveBeenCalledOnceWith(
jasmine.objectContaining({ name: 'foo', duration: 100 }),
jasmine.any(String)
)
})

it('should only create a single duration vital from a vital name', () => {
const cbSpy = jasmine.createSpy()

startDurationVital(vitalsState, 'foo')
startDurationVital(vitalsState, lifeCycle, 'foo')
clock.tick(100)
stopDurationVital(cbSpy, vitalsState, 'foo')
clock.tick(100)
stopDurationVital(cbSpy, vitalsState, 'foo')

expect(cbSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ name: 'foo', duration: 100 }))
expect(cbSpy).toHaveBeenCalledOnceWith(
jasmine.objectContaining({ name: 'foo', duration: 100 }),
jasmine.any(String)
)
})

it('should not create multiple duration vitals by calling "stopDurationVital" on the same vital ref multiple times', () => {
const cbSpy = jasmine.createSpy()

const vital = startDurationVital(vitalsState, 'foo')
const vital = startDurationVital(vitalsState, lifeCycle, 'foo')
stopDurationVital(cbSpy, vitalsState, vital)
stopDurationVital(cbSpy, vitalsState, vital)

Expand All @@ -74,7 +83,7 @@ describe('vitalCollection', () => {
it('should not create multiple duration vitals by calling "stopDurationVital" on the same vital name multiple times', () => {
const cbSpy = jasmine.createSpy()

startDurationVital(vitalsState, 'bar')
startDurationVital(vitalsState, lifeCycle, 'bar')
stopDurationVital(cbSpy, vitalsState, 'bar')
stopDurationVital(cbSpy, vitalsState, 'bar')

Expand All @@ -84,9 +93,9 @@ describe('vitalCollection', () => {
it('should create multiple duration vitals from multiple vital refs', () => {
const cbSpy = jasmine.createSpy()

const vitalRef1 = startDurationVital(vitalsState, 'foo', { description: 'component 1' })
const vitalRef1 = startDurationVital(vitalsState, lifeCycle, 'foo', { description: 'component 1' })
clock.tick(100)
const vitalRef2 = startDurationVital(vitalsState, 'foo', { description: 'component 2' })
const vitalRef2 = startDurationVital(vitalsState, lifeCycle, 'foo', { description: 'component 2' })
clock.tick(100)
stopDurationVital(cbSpy, vitalsState, vitalRef2)
clock.tick(100)
Expand All @@ -95,56 +104,70 @@ describe('vitalCollection', () => {
expect(cbSpy).toHaveBeenCalledTimes(2)
expect(cbSpy.calls.argsFor(0)).toEqual([
jasmine.objectContaining({ description: 'component 2', duration: 100 }),
jasmine.any(String),
])
expect(cbSpy.calls.argsFor(1)).toEqual([
jasmine.objectContaining({ description: 'component 1', duration: 300 }),
jasmine.any(String),
])
})

it('should merge startDurationVital and stopDurationVital description', () => {
const cbSpy = jasmine.createSpy()

startDurationVital(vitalsState, 'both-undefined')
startDurationVital(vitalsState, lifeCycle, 'both-undefined')
stopDurationVital(cbSpy, vitalsState, 'both-undefined')

startDurationVital(vitalsState, 'start-defined', { description: 'start-defined' })
startDurationVital(vitalsState, lifeCycle, 'start-defined', { description: 'start-defined' })
stopDurationVital(cbSpy, vitalsState, 'start-defined')

startDurationVital(vitalsState, 'stop-defined')
startDurationVital(vitalsState, lifeCycle, 'stop-defined')
stopDurationVital(cbSpy, vitalsState, 'stop-defined', { description: 'stop-defined' })

startDurationVital(vitalsState, 'both-defined', { description: 'start-defined' })
startDurationVital(vitalsState, lifeCycle, 'both-defined', { description: 'start-defined' })
stopDurationVital(cbSpy, vitalsState, 'both-defined', { description: 'stop-defined' })

expect(cbSpy).toHaveBeenCalledTimes(4)
expect(cbSpy.calls.argsFor(0)).toEqual([jasmine.objectContaining({ description: undefined })])
expect(cbSpy.calls.argsFor(1)).toEqual([jasmine.objectContaining({ description: 'start-defined' })])
expect(cbSpy.calls.argsFor(2)).toEqual([jasmine.objectContaining({ description: 'stop-defined' })])
expect(cbSpy.calls.argsFor(3)).toEqual([jasmine.objectContaining({ description: 'stop-defined' })])
expect(cbSpy.calls.argsFor(0)).toEqual([
jasmine.objectContaining({ description: undefined }),
jasmine.any(String),
])
expect(cbSpy.calls.argsFor(1)).toEqual([
jasmine.objectContaining({ description: 'start-defined' }),
jasmine.any(String),
])
expect(cbSpy.calls.argsFor(2)).toEqual([
jasmine.objectContaining({ description: 'stop-defined' }),
jasmine.any(String),
])
expect(cbSpy.calls.argsFor(3)).toEqual([
jasmine.objectContaining({ description: 'stop-defined' }),
jasmine.any(String),
])
})

it('should merge startDurationVital and stopDurationVital contexts', () => {
const cbSpy = jasmine.createSpy()

const vitalRef1 = startDurationVital(vitalsState, 'both-undefined')
const vitalRef1 = startDurationVital(vitalsState, lifeCycle, 'both-undefined')
stopDurationVital(cbSpy, vitalsState, vitalRef1)

const vitalRef2 = startDurationVital(vitalsState, 'start-defined', {
const vitalRef2 = startDurationVital(vitalsState, lifeCycle, 'start-defined', {
context: { start: 'defined' },
})
stopDurationVital(cbSpy, vitalsState, vitalRef2)

const vitalRef3 = startDurationVital(vitalsState, 'stop-defined', {
const vitalRef3 = startDurationVital(vitalsState, lifeCycle, 'stop-defined', {
context: { stop: 'defined' },
})
stopDurationVital(cbSpy, vitalsState, vitalRef3)

const vitalRef4 = startDurationVital(vitalsState, 'both-defined', {
const vitalRef4 = startDurationVital(vitalsState, lifeCycle, 'both-defined', {
context: { start: 'defined' },
})
stopDurationVital(cbSpy, vitalsState, vitalRef4, { context: { stop: 'defined' } })

const vitalRef5 = startDurationVital(vitalsState, 'stop-precedence', {
const vitalRef5 = startDurationVital(vitalsState, lifeCycle, 'stop-precedence', {
context: { precedence: 'start' },
})
stopDurationVital(cbSpy, vitalsState, vitalRef5, { context: { precedence: 'stop' } })
Expand Down
29 changes: 23 additions & 6 deletions packages/rum-core/src/domain/vital/vitalCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface DurationVitalReference {
}

export interface DurationVitalStart extends DurationVitalOptions {
id: string
name: string
startClocks: ClocksState
handlingStack?: string
Expand Down Expand Up @@ -116,6 +117,12 @@ export function startVitalCollection(
}
}

function addDurationVitalWithId(vital: DurationVital, vitalId: string) {
if (isValid(vital)) {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processVital(vital, vitalId))
}
}

Comment on lines +120 to +125
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: ‏Why do you need this function?
Adding the id to BaseVital to follows the same logic as the other vital attributes should be good.

function addOperationStepVital(
name: string,
stepType: 'start' | 'end',
Expand Down Expand Up @@ -145,19 +152,22 @@ export function startVitalCollection(
addOperationStepVital,
addDurationVital,
startDurationVital: (name: string, options: DurationVitalOptions = {}) =>
startDurationVital(customVitalsState, name, options),
startDurationVital(customVitalsState, lifeCycle, name, options),
stopDurationVital: (nameOrRef: string | DurationVitalReference, options: DurationVitalOptions = {}) => {
stopDurationVital(addDurationVital, customVitalsState, nameOrRef, options)
stopDurationVital(addDurationVitalWithId, customVitalsState, nameOrRef, options)
},
}
}

export function startDurationVital(
{ vitalsByName, vitalsByReference }: CustomVitalsState,
lifeCycle: LifeCycle | undefined, // the lifecycle might not be defined in case datadogRum is not initialized yet
name: string,
options: DurationVitalOptions = {}
) {
const vitalId = generateUUID()
const vital = {
id: vitalId,
Comment on lines +168 to +170
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥜 nitpick:

Suggested change
const vitalId = generateUUID()
const vital = {
id: vitalId,
const vital = {
id: generateUUID(),

name,
startClocks: clocksNow(),
...options,
Expand All @@ -171,11 +181,15 @@ export function startDurationVital(
// To avoid memory leaks caused by the creation of numerous references (e.g., from improper useEffect implementations), we use a WeakMap.
vitalsByReference.set(reference, vital)

if (lifeCycle) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏Instead of having an optional lifeCycle, you could send the event here here. Like so

startDurationVital: (name: string, options: DurationVitalOptions = {}) => {
  const ref = startDurationVital(customVitalsState, name, options)
  lifeCycle.notify(LifeCycleEventType.VITAL_STARTED, customVitalsState.vitalsByReference.get(ref)!)
  return ref
},

lifeCycle.notify(LifeCycleEventType.VITAL_STARTED, vital)
}

return reference
}

export function stopDurationVital(
stopCallback: (vital: DurationVital) => void,
stopCallback: (vital: DurationVital, vitalId: string) => void,
{ vitalsByName, vitalsByReference }: CustomVitalsState,
nameOrRef: string | DurationVitalReference,
options: DurationVitalOptions = {}
Expand All @@ -186,7 +200,7 @@ export function stopDurationVital(
return
}

stopCallback(buildDurationVital(vitalStart, vitalStart.startClocks, options, clocksNow()))
stopCallback(buildDurationVital(vitalStart, vitalStart.startClocks, options, clocksNow()), vitalStart.id)

if (typeof nameOrRef === 'string') {
vitalsByName.delete(nameOrRef)
Expand All @@ -212,10 +226,13 @@ function buildDurationVital(
}
}

function processVital(vital: DurationVital | OperationStepVital): RawRumEventCollectedData<RawRumVitalEvent> {
function processVital(
vital: DurationVital | OperationStepVital,
vitalId?: string
): RawRumEventCollectedData<RawRumVitalEvent> {
const { startClocks, type, name, description, context, handlingStack } = vital
const vitalData = {
id: generateUUID(),
id: vitalId ?? generateUUID(),
type,
name,
description,
Expand Down
Loading