Skip to content
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
46 changes: 45 additions & 1 deletion packages/core/src/domain/telemetry/telemetryEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,14 @@ export type TelemetryBrowserFeaturesUsage =
| StartDurationVital
| StopDurationVital
| AddDurationVital
| StartAction
| StopAction
| StartResource
| StopResource
/**
* Schema of mobile specific features usage
*/
export type TelemetryMobileFeaturesUsage = AddViewLoadingTime | TrackWebView
export type TelemetryMobileFeaturesUsage = AddViewLoadingTime | TrackWebView | AndroidNetworkInstrumentation

/**
* Schema of common properties of Telemetry events
Expand Down Expand Up @@ -579,6 +583,7 @@ export interface CommonTelemetryProperties {
| 'unity'
| 'kotlin-multiplatform'
| 'electron'
| 'rum-cpp'
/**
* The version of the SDK generating the telemetry event
*/
Expand Down Expand Up @@ -913,6 +918,34 @@ export interface AddDurationVital {
feature: 'add-duration-vital'
[k: string]: unknown
}
export interface StartAction {
/**
* startAction API
*/
feature: 'start-action'
[k: string]: unknown
}
export interface StopAction {
/**
* stopAction API
*/
feature: 'stop-action'
[k: string]: unknown
}
export interface StartResource {
/**
* startResource API
*/
feature: 'start-resource'
[k: string]: unknown
}
export interface StopResource {
/**
* stopResource API
*/
feature: 'stop-resource'
[k: string]: unknown
}
export interface AddViewLoadingTime {
/**
* addViewLoadingTime API
Expand All @@ -939,3 +972,14 @@ export interface TrackWebView {
feature: 'trackWebView'
[k: string]: unknown
}
export interface AndroidNetworkInstrumentation {
/**
* Android network instrumentation
*/
feature: 'androidNetworkInstrumentation'
/**
* The network instrumentation API used
*/
type: 'CRONET' | 'OKHTTP'
[k: string]: unknown
}
53 changes: 53 additions & 0 deletions packages/core/src/tools/valueHistory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,59 @@ describe('valueHistory', () => {
})
})

describe('getEntries', () => {
const END_OF_TIMES = Infinity as RelativeTime

it('should return an empty array when there is no entry for the given startTime', () => {
valueHistory.add('foo', 0 as RelativeTime).close(10 as RelativeTime)
valueHistory.add('bar', 20 as RelativeTime)

expect(valueHistory.getEntries(15 as RelativeTime)).toEqual([])
})

it('should return the entries for the given startTime', () => {
valueHistory.add('foo', 0 as RelativeTime)
valueHistory.add('bar', 5 as RelativeTime).close(10 as RelativeTime)
valueHistory.add('qux', 5 as RelativeTime).close(15 as RelativeTime)
valueHistory.add('baz', 10 as RelativeTime)

expect(valueHistory.getEntries(0 as RelativeTime)).toEqual([
{
value: 'foo',
startTime: 0 as RelativeTime,
endTime: END_OF_TIMES,
remove: jasmine.any(Function),
close: jasmine.any(Function),
},
])
expect(valueHistory.getEntries(5 as RelativeTime)).toEqual([
{
value: 'qux',
startTime: 5 as RelativeTime,
endTime: 15 as RelativeTime,
remove: jasmine.any(Function),
close: jasmine.any(Function),
},
{
value: 'bar',
startTime: 5 as RelativeTime,
endTime: 10 as RelativeTime,
remove: jasmine.any(Function),
close: jasmine.any(Function),
},
])
expect(valueHistory.getEntries(10 as RelativeTime)).toEqual([
{
value: 'baz',
startTime: 10 as RelativeTime,
endTime: END_OF_TIMES,
remove: jasmine.any(Function),
close: jasmine.any(Function),
},
])
})
})

describe('removing entries', () => {
it('should not return removed entries', () => {
valueHistory.add('foo', 0 as RelativeTime).remove()
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/tools/valueHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ValueHistory<Value> {

closeActive: (endTime: RelativeTime) => void
findAll: (startTime?: RelativeTime, duration?: Duration) => Value[]
getEntries: (startTime: RelativeTime) => Array<ValueHistoryEntry<Value>>
reset: () => void
stop: () => void
}
Expand Down Expand Up @@ -129,6 +130,13 @@ export function createValueHistory<Value>({
.map((entry) => entry.value)
}

/**
* Return all the entries whose start time is equal to the given startTime.
*/
function getEntries(startTime: RelativeTime): Array<ValueHistoryEntry<Value>> {
return entries.filter((entry) => entry.startTime === startTime)
}

/**
* Remove all entries from this collection.
*/
Expand All @@ -147,7 +155,7 @@ export function createValueHistory<Value>({
}
}

return { add, find, closeActive, findAll, reset, stop }
return { add, find, closeActive, findAll, getEntries, reset, stop }
}

/**
Expand Down
8 changes: 7 additions & 1 deletion packages/rum-core/src/boot/preStartRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,13 @@ describe('preStartRum', () => {
addDurationVital: addDurationVitalSpy,
} as unknown as StartRumResult)

const vitalAdd = { name: 'timing', type: VitalType.DURATION, startClocks: clocksNow(), duration: 100 as Duration }
const vitalAdd = {
id: 'id',
name: 'timing',
type: VitalType.DURATION,
startClocks: clocksNow(),
duration: 100 as Duration,
}
strategy.addDurationVital(vitalAdd)
strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API)
expect(addDurationVitalSpy).toHaveBeenCalledOnceWith(vitalAdd)
Expand Down
1 change: 1 addition & 0 deletions packages/rum-core/src/boot/rumPublicApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ describe('rum public api', () => {
description: 'description-value',
})
expect(addDurationVitalSpy).toHaveBeenCalledWith({
id: jasmine.any(String),
name: 'foo',
startClocks: timeStampToClocks(startTime),
duration: 100,
Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/boot/rumPublicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
isExperimentalFeatureEnabled,
ExperimentalFeature,
mockable,
generateUUID,
} from '@datadog/browser-core'

import type { LifeCycle } from '../domain/lifeCycle'
Expand Down Expand Up @@ -890,6 +891,7 @@ export function makeRumPublicApi(
callMonitored(() => {
addTelemetryUsage({ feature: 'add-duration-vital' })
strategy.addDurationVital({
id: generateUUID(),
name: sanitize(name)!,
type: VitalType.DURATION,
startClocks: timeStampToClocks(options.startTime as TimeStamp),
Expand Down
6 changes: 5 additions & 1 deletion packages/rum-core/src/domain/action/trackClickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ function newClick(
const clickKey = generateUUID()
const startClocks = relativeToClocks(startEvent.timeStamp)

actionTracker.start(clickKey, startClocks, clickActionBase, { isChildEvent: isActionChildEvent })
const startedClickAction = actionTracker.start(clickKey, startClocks, clickActionBase, {
isChildEvent: isActionChildEvent,
})

lifeCycle.notify(LifeCycleEventType.ACTION_STARTED, startedClickAction)

let status = ClickStatus.ONGOING
let actionTrackerFinishedEvent: StoppedEvent<ClickActionBase> | DiscardedEvent<ClickActionBase> | undefined
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)
Comment thread
amortemousque marked this conversation as resolved.
}

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
19 changes: 16 additions & 3 deletions packages/rum-core/src/domain/vital/vitalCollection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Duration } from '@datadog/browser-core'
import { mockClock, type Clock } from '@datadog/browser-core/test'
import { addExperimentalFeatures, clocksNow, ExperimentalFeature } from '@datadog/browser-core'
import { addExperimentalFeatures, clocksNow, ExperimentalFeature, generateUUID } from '@datadog/browser-core'
import { collectAndValidateRawRumEvents, mockPageStateHistory } from '../../../test'
import type { RawRumEvent, RawRumVitalEvent } from '../../rawRumEvent.types'
import { VitalType, RumEventType } from '../../rawRumEvent.types'
import type { RawRumEventCollectedData } from '../lifeCycle'
import { LifeCycle } from '../lifeCycle'
import { LifeCycle, LifeCycleEventType } from '../lifeCycle'
import { startDurationVital, stopDurationVital, startVitalCollection, createCustomVitalsState } from './vitalCollection'

const pageStateHistory = mockPageStateHistory()
Expand Down Expand Up @@ -36,7 +36,9 @@ describe('vitalCollection', () => {
clock.tick(100)
stopDurationVital(cbSpy, vitalsState, vitalRef)

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

it('should create duration vital from a vital name', () => {
Expand Down Expand Up @@ -195,6 +197,7 @@ describe('vitalCollection', () => {
wasInPageStateDuringPeriodSpy.and.returnValue(true)

vitalCollection.addDurationVital({
id: generateUUID(),
name: 'foo',
type: VitalType.DURATION,
startClocks: clocksNow(),
Expand Down Expand Up @@ -259,6 +262,7 @@ describe('vitalCollection', () => {

it('should create a duration vital from add API', () => {
vitalCollection.addDurationVital({
id: generateUUID(),
name: 'foo',
type: VitalType.DURATION,
startClocks: clocksNow(),
Expand Down Expand Up @@ -295,6 +299,15 @@ describe('vitalCollection', () => {
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.description).toBe('baz')
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).context).toEqual({ foo: 'bar' })
})

it('should notify lifecycle with vital started event when starting a duration vital', () => {
const subscriberSpy = jasmine.createSpy()
lifeCycle.subscribe(LifeCycleEventType.VITAL_STARTED, subscriberSpy)

vitalCollection.startDurationVital('foo')

expect(subscriberSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ name: 'foo' }))
})
})
})
})
Loading