Skip to content

Commit a361b05

Browse files
cy-moiBenoitZugmeyer
authored andcommitted
✨⚗️[RUM-11398] Add feature operation step vital APIs (#3804)
* Add feature operation step vital apis * update sanitzation * Update API signature * Update specific types and jsdocs * Make failureReason required * Update packages/rum-core/src/boot/rumPublicApi.ts Co-authored-by: Benoît <[email protected]> * fix-comment: remove computedValueBySdk * Update rum-event-format and remove unused ff * Leverage type from schema updates --------- Co-authored-by: Benoît <[email protected]>
1 parent 1d72a21 commit a361b05

File tree

13 files changed

+373
-91
lines changed

13 files changed

+373
-91
lines changed

developer-extension/src/panel/components/tabs/eventsTab/eventRow.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { default as clsx } from 'clsx'
66
import type { TelemetryEvent } from '../../../../../../packages/core/src/domain/telemetry'
77
import type { LogsEvent } from '../../../../../../packages/logs/src/logsEvent.types'
88
import type {
9+
DurationProperties,
910
RumActionEvent,
1011
RumErrorEvent,
1112
RumLongTaskEvent,
@@ -371,7 +372,7 @@ function LongTaskDescription({ event }: { event: RumLongTaskEvent }) {
371372

372373
function VitalDescription({ event }: { event: RumVitalEvent }) {
373374
const vitalName = event.vital.name
374-
const vitalValue = event.vital.duration
375+
const vitalValue = (event.vital as DurationProperties).duration
375376
const vitalDescription = event.vital.description
376377
return (
377378
<>

packages/core/src/tools/experimentalFeatures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export enum ExperimentalFeature {
1717
TRACK_INTAKE_REQUESTS = 'track_intake_requests',
1818
WRITABLE_RESOURCE_GRAPHQL = 'writable_resource_graphql',
1919
EARLY_REQUEST_COLLECTION = 'early_request_collection',
20-
WATCH_COOKIE_WITHOUT_LOCK = 'watch_cookie_without_lock',
2120
USE_TREE_WALKER_FOR_ACTION_NAME = 'use_tree_walker_for_action_name',
2221
GRAPHQL_TRACKING = 'graphql_tracking',
22+
FEATURE_OPERATION_VITAL = 'feature_operation_vital',
2323
SHORT_SESSION_INVESTIGATION = 'short_session_investigation',
2424
}
2525

packages/rum-core/src/boot/preStartRum.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,17 @@ describe('preStartRum', () => {
742742
strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API)
743743
expect(addDurationVitalSpy).toHaveBeenCalledOnceWith(vitalAdd)
744744
})
745+
746+
it('addOperationStepVital', () => {
747+
const addOperationStepVitalSpy = jasmine.createSpy()
748+
doStartRumSpy.and.returnValue({
749+
addOperationStepVital: addOperationStepVitalSpy,
750+
} as unknown as StartRumResult)
751+
752+
strategy.addOperationStepVital('foo', 'start')
753+
strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API)
754+
expect(addOperationStepVitalSpy).toHaveBeenCalledOnceWith('foo', 'start', undefined, undefined)
755+
})
745756
})
746757

747758
describe('tracking consent', () => {

packages/rum-core/src/boot/preStartRum.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
buildGlobalContextManager,
1818
buildUserContextManager,
1919
monitorError,
20+
sanitize,
2021
} from '@datadog/browser-core'
2122
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
2223
import {
@@ -25,7 +26,12 @@ import {
2526
serializeRumConfiguration,
2627
} from '../domain/configuration'
2728
import type { ViewOptions } from '../domain/view/trackViews'
28-
import type { DurationVital, CustomVitalsState } from '../domain/vital/vitalCollection'
29+
import type {
30+
DurationVital,
31+
CustomVitalsState,
32+
FeatureOperationOptions,
33+
FailureReason,
34+
} from '../domain/vital/vitalCollection'
2935
import { startDurationVital, stopDurationVital } from '../domain/vital/vitalCollection'
3036
import { callPluginsMethod } from '../domain/plugins'
3137
import type { StartRumResult } from './startRum'
@@ -148,6 +154,22 @@ export function createPreStartStrategy(
148154
bufferApiCalls.add((startRumResult) => startRumResult.addDurationVital(vital))
149155
}
150156

157+
const addOperationStepVital = (
158+
name: string,
159+
stepType: 'start' | 'end',
160+
options?: FeatureOperationOptions,
161+
failureReason?: FailureReason
162+
) => {
163+
bufferApiCalls.add((startRumResult) =>
164+
startRumResult.addOperationStepVital(
165+
sanitize(name)!,
166+
stepType,
167+
sanitize(options) as FeatureOperationOptions,
168+
sanitize(failureReason) as FailureReason | undefined
169+
)
170+
)
171+
}
172+
151173
const strategy: Strategy = {
152174
init(initConfiguration, publicApi) {
153175
if (!initConfiguration) {
@@ -248,6 +270,7 @@ export function createPreStartStrategy(
248270
},
249271

250272
addDurationVital,
273+
addOperationStepVital,
251274
}
252275

253276
return strategy

packages/rum-core/src/boot/rumPublicApi.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const noopStartRum = (): ReturnType<StartRum> => ({
3434
accountContext: {} as any,
3535
hooks: {} as any,
3636
telemetry: {} as any,
37+
addOperationStepVital: () => undefined,
3738
})
3839
const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' }
3940
const FAKE_WORKER = {} as DeflateWorker
@@ -784,6 +785,57 @@ describe('rum public api', () => {
784785
})
785786
})
786787

788+
describe('startFeatureOperation', () => {
789+
it('should call addOperationStepVital on the startRum result with start status', () => {
790+
const addOperationStepVitalSpy = jasmine.createSpy()
791+
const rumPublicApi = makeRumPublicApi(
792+
() => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }),
793+
noopRecorderApi,
794+
noopProfilerApi
795+
)
796+
rumPublicApi.init(DEFAULT_INIT_CONFIGURATION)
797+
rumPublicApi.startFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' })
798+
expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'start', {
799+
operationKey: '00000000-0000-0000-0000-000000000000',
800+
})
801+
})
802+
})
803+
804+
describe('succeedFeatureOperation', () => {
805+
it('should call addOperationStepVital on the startRum result with end status', () => {
806+
const addOperationStepVitalSpy = jasmine.createSpy()
807+
const rumPublicApi = makeRumPublicApi(
808+
() => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }),
809+
noopRecorderApi,
810+
noopProfilerApi
811+
)
812+
rumPublicApi.init(DEFAULT_INIT_CONFIGURATION)
813+
rumPublicApi.succeedFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' })
814+
expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'end', {
815+
operationKey: '00000000-0000-0000-0000-000000000000',
816+
})
817+
})
818+
})
819+
820+
describe('failFeatureOperation', () => {
821+
it('should call addOperationStepVital on the startRum result with end status and failure reason', () => {
822+
const addOperationStepVitalSpy = jasmine.createSpy()
823+
const rumPublicApi = makeRumPublicApi(
824+
() => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }),
825+
noopRecorderApi,
826+
noopProfilerApi
827+
)
828+
rumPublicApi.init(DEFAULT_INIT_CONFIGURATION)
829+
rumPublicApi.failFeatureOperation('foo', 'error', { operationKey: '00000000-0000-0000-0000-000000000000' })
830+
expect(addOperationStepVitalSpy).toHaveBeenCalledWith(
831+
'foo',
832+
'end',
833+
{ operationKey: '00000000-0000-0000-0000-000000000000' },
834+
'error'
835+
)
836+
})
837+
})
838+
787839
it('should provide sdk version', () => {
788840
const rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi)
789841
expect(rumPublicApi.version).toBe('test')

packages/rum-core/src/boot/rumPublicApi.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ import type { RumConfiguration, RumInitConfiguration } from '../domain/configura
4242
import type { ViewOptions } from '../domain/view/trackViews'
4343
import type {
4444
AddDurationVitalOptions,
45-
DurationVitalOptions,
4645
DurationVitalReference,
46+
DurationVitalOptions,
47+
FeatureOperationOptions,
48+
FailureReason,
4749
} from '../domain/vital/vitalCollection'
4850
import { createCustomVitalsState } from '../domain/vital/vitalCollection'
4951
import { callPluginsMethod } from '../domain/plugins'
@@ -424,9 +426,37 @@ export interface RumPublicApi extends PublicApi {
424426
*
425427
* @category Vital
426428
* @param nameOrRef - Name or reference of the custom vital
427-
* @param options - Options for the custom vital (context, description)
429+
* @param options - Options for the custom vital (operationKey, context, description)
428430
*/
429431
stopDurationVital: (nameOrRef: string | DurationVitalReference, options?: DurationVitalOptions) => void
432+
433+
/**
434+
* [Experimental] start a feature operation
435+
*
436+
* @category Vital
437+
* @param name - Name of the operation step
438+
* @param options - Options for the operation step (operationKey, context, description)
439+
*/
440+
startFeatureOperation: (name: string, options?: FeatureOperationOptions) => void
441+
442+
/**
443+
* [Experimental] succeed a feature operation
444+
*
445+
* @category Vital
446+
* @param name - Name of the operation step
447+
* @param options - Options for the operation step (operationKey, context, description)
448+
*/
449+
succeedFeatureOperation: (name: string, options?: FeatureOperationOptions) => void
450+
451+
/**
452+
* [Experimental] fail a feature operation
453+
*
454+
* @category Vital
455+
* @param name - Name of the operation step
456+
* @param failureReason
457+
* @param options - Options for the operation step (operationKey, context, description)
458+
*/
459+
failFeatureOperation: (name: string, failureReaon: FailureReason, options?: FeatureOperationOptions) => void
430460
}
431461

432462
export interface RecorderApi {
@@ -494,6 +524,7 @@ export interface Strategy {
494524
startDurationVital: StartRumResult['startDurationVital']
495525
stopDurationVital: StartRumResult['stopDurationVital']
496526
addDurationVital: StartRumResult['addDurationVital']
527+
addOperationStepVital: StartRumResult['addOperationStepVital']
497528
}
498529

499530
export function makeRumPublicApi(
@@ -771,6 +802,21 @@ export function makeRumPublicApi(
771802
description: sanitize(options && options.description) as string | undefined,
772803
})
773804
}),
805+
806+
startFeatureOperation: monitor((name, options) => {
807+
addTelemetryUsage({ feature: 'add-operation-step-vital', action_type: 'start' })
808+
strategy.addOperationStepVital(name, 'start', options)
809+
}),
810+
811+
succeedFeatureOperation: monitor((name, options) => {
812+
addTelemetryUsage({ feature: 'add-operation-step-vital', action_type: 'succeed' })
813+
strategy.addOperationStepVital(name, 'end', options)
814+
}),
815+
816+
failFeatureOperation: monitor((name, failureReason, options) => {
817+
addTelemetryUsage({ feature: 'add-operation-step-vital', action_type: 'fail' })
818+
strategy.addOperationStepVital(name, 'end', options, failureReason)
819+
}),
774820
})
775821

776822
return rumPublicApi

packages/rum-core/src/boot/startRum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export function startRum(
236236
startDurationVital: vitalCollection.startDurationVital,
237237
stopDurationVital: vitalCollection.stopDurationVital,
238238
addDurationVital: vitalCollection.addDurationVital,
239+
addOperationStepVital: vitalCollection.addOperationStepVital,
239240
globalContext,
240241
userContext,
241242
accountContext,

packages/rum-core/src/domain/vital/vitalCollection.spec.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Duration } from '@datadog/browser-core'
2-
import { mockClock, type Clock } from '@datadog/browser-core/test'
3-
import { clocksNow } from '@datadog/browser-core'
2+
import { mockClock, mockExperimentalFeatures, type Clock } from '@datadog/browser-core/test'
3+
import { clocksNow, ExperimentalFeature } from '@datadog/browser-core'
44
import { collectAndValidateRawRumEvents, mockPageStateHistory } from '../../../test'
55
import type { RawRumEvent, RawRumVitalEvent } from '../../rawRumEvent.types'
66
import { VitalType, RumEventType } from '../../rawRumEvent.types'
@@ -220,16 +220,33 @@ describe('vitalCollection', () => {
220220
},
221221
context: undefined,
222222
type: RumEventType.VITAL,
223-
_dd: {
224-
vital: {
225-
computed_value: true,
226-
},
223+
})
224+
expect(rawRumEvents[0].domainContext).toEqual({})
225+
})
226+
227+
it('should collect raw rum event from operation step vital', () => {
228+
mockExperimentalFeatures([ExperimentalFeature.FEATURE_OPERATION_VITAL])
229+
vitalCollection.addOperationStepVital('foo', 'start')
230+
231+
expect(rawRumEvents[0].startTime).toEqual(jasmine.any(Number))
232+
expect(rawRumEvents[0].rawRumEvent).toEqual({
233+
date: jasmine.any(Number),
234+
vital: {
235+
id: jasmine.any(String),
236+
type: VitalType.OPERATION_STEP,
237+
name: 'foo',
238+
step_type: 'start',
239+
operation_key: undefined,
240+
failure_reason: undefined,
241+
description: undefined,
227242
},
243+
context: undefined,
244+
type: RumEventType.VITAL,
228245
})
229246
expect(rawRumEvents[0].domainContext).toEqual({})
230247
})
231248

232-
it('should create a vital from add API', () => {
249+
it('should create a duration vital from add API', () => {
233250
vitalCollection.addDurationVital({
234251
name: 'foo',
235252
type: VitalType.DURATION,
@@ -244,6 +261,29 @@ describe('vitalCollection', () => {
244261
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.description).toBe('baz')
245262
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).context).toEqual({ foo: 'bar' })
246263
})
264+
265+
it('should create a operation step vital from add API', () => {
266+
mockExperimentalFeatures([ExperimentalFeature.FEATURE_OPERATION_VITAL])
267+
vitalCollection.addOperationStepVital(
268+
'foo',
269+
'end',
270+
{
271+
operationKey: '00000000-0000-0000-0000-000000000000',
272+
context: { foo: 'bar' },
273+
description: 'baz',
274+
},
275+
'error'
276+
)
277+
278+
expect(rawRumEvents.length).toBe(1)
279+
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.step_type).toBe('end')
280+
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.operation_key).toBe(
281+
'00000000-0000-0000-0000-000000000000'
282+
)
283+
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.failure_reason).toBe('error')
284+
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.description).toBe('baz')
285+
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).context).toEqual({ foo: 'bar' })
286+
})
247287
})
248288
})
249289
})

0 commit comments

Comments
 (0)