11import type { ViewHistoryEntry } from '@datadog/browser-rum-core'
2- import { LifeCycle , LifeCycleEventType , RumPerformanceEntryType , createHooks } from '@datadog/browser-rum-core'
2+ import { LifeCycle , LifeCycleEventType , RumPerformanceEntryType , VitalType , createHooks } from '@datadog/browser-rum-core'
33import type { Duration , SessionRenewalEvent } from '@datadog/browser-core'
44import {
55 addDuration ,
88 createIdentityEncoder ,
99 createValueHistory ,
1010 deepClone ,
11+ elapsed ,
1112 ONE_DAY ,
1213 relativeNow ,
1314 timeStampNow ,
@@ -141,6 +142,18 @@ describe('profiler', () => {
141142 addVital : ( vital : VitalContext ) => {
142143 vitalHistory . add ( vital , relativeNow ( ) ) . close ( addDuration ( relativeNow ( ) , vital . duration ?? ( 0 as Duration ) ) )
143144 } ,
145+ startOperationStep : ( id : string , label : string , operationKey ?: string ) => {
146+ const startClocks = clocksNow ( )
147+ const entry = vitalHistory . add (
148+ { id, type : VitalType . OPERATION_STEP , label, operationKey, startClocks, duration : undefined } ,
149+ startClocks . relative
150+ )
151+ return ( ) => {
152+ const endTime = relativeNow ( )
153+ entry . value . duration = elapsed ( entry . startTime , endTime )
154+ entry . close ( endTime )
155+ }
156+ } ,
144157 }
145158 }
146159
@@ -415,6 +428,7 @@ describe('profiler', () => {
415428 expect ( profilingContextManager . get ( ) ?. status ) . toBe ( 'running' )
416429 addVital ( {
417430 id : 'vital-id-1' ,
431+ type : VitalType . DURATION ,
418432 label : 'vital-label-1' ,
419433 startClocks : clocksNow ( ) ,
420434 duration : 50 as Duration ,
@@ -423,6 +437,7 @@ describe('profiler', () => {
423437
424438 addVital ( {
425439 id : 'vital-id-2' ,
440+ type : VitalType . DURATION ,
426441 label : 'vital-label-2' ,
427442 startClocks : clocksNow ( ) ,
428443 duration : 100 as Duration ,
@@ -442,6 +457,7 @@ describe('profiler', () => {
442457
443458 addVital ( {
444459 id : 'vital-id-3' ,
460+ type : VitalType . DURATION ,
445461 label : 'vital-label-3' ,
446462 startClocks : clocksNow ( ) ,
447463 duration : 100 as Duration ,
@@ -495,6 +511,79 @@ describe('profiler', () => {
495511 ] )
496512 } )
497513
514+ it ( 'should collect all ongoing operations during a profiling session' , async ( ) => {
515+ const clock = mockClock ( )
516+ const { profiler, startOperationStep } = setupProfiler ( )
517+
518+ // Profile 1: start all three operations, end op1
519+ profiler . start ( )
520+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
521+
522+ const endOp1 = startOperationStep ( 'op-id-1' , 'op-label-1' )
523+ clock . tick ( 10 )
524+ const endOp2 = startOperationStep ( 'op-id-2' , 'op-label-2' )
525+ clock . tick ( 10 )
526+ const endOp3 = startOperationStep ( 'op-id-3' , 'op-label-3' )
527+ clock . tick ( 10 )
528+ endOp1 ( ) // op1 ends during profile 1
529+
530+ clock . tick ( 70 )
531+ profiler . stop ( )
532+ await waitNextMicrotask ( )
533+
534+ // Profile 2: end op2
535+ profiler . start ( )
536+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
537+
538+ clock . tick ( 50 )
539+ endOp2 ( ) // op2 ends during profile 2
540+
541+ clock . tick ( 50 )
542+ profiler . stop ( )
543+ await waitNextMicrotask ( )
544+
545+ // Profile 3: end op3
546+ profiler . start ( )
547+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
548+
549+ clock . tick ( 50 )
550+ endOp3 ( ) // op3 ends during profile 3
551+
552+ clock . tick ( 50 )
553+ profiler . stop ( )
554+ await waitNextMicrotask ( )
555+ await waitNextMicrotask ( )
556+
557+ expect ( interceptor . requests . length ) . toBe ( 3 )
558+
559+ const req1 = await readFormDataRequest < ProfileEventPayload > ( interceptor . requests [ 0 ] )
560+ const req2 = await readFormDataRequest < ProfileEventPayload > ( interceptor . requests [ 1 ] )
561+ const req3 = await readFormDataRequest < ProfileEventPayload > ( interceptor . requests [ 2 ] )
562+
563+ const vitals1 = req1 [ 'wall-time.json' ] . vitals
564+ const vitals2 = req2 [ 'wall-time.json' ] . vitals
565+ const vitals3 = req3 [ 'wall-time.json' ] . vitals
566+
567+ // Profile 1: all three operations present, only op1 has a duration
568+ expect ( vitals1 ?. map ( ( v ) => v . id ) ) . toEqual ( jasmine . arrayContaining ( [ 'op-id-1' , 'op-id-2' , 'op-id-3' ] ) )
569+ expect ( vitals1 ?. find ( ( v ) => v . id === 'op-id-1' ) ?. duration ) . toBe ( 30 as Duration )
570+ expect ( vitals1 ?. find ( ( v ) => v . id === 'op-id-2' ) ?. duration ) . toBeUndefined ( )
571+ expect ( vitals1 ?. find ( ( v ) => v . id === 'op-id-3' ) ?. duration ) . toBeUndefined ( )
572+
573+ // Profile 2: op1 is gone (ended before profile 2 started), op2 and op3 present, only op2 has a duration
574+ expect ( vitals2 ?. map ( ( v ) => v . id ) ) . not . toContain ( 'op-id-1' )
575+ expect ( vitals2 ?. map ( ( v ) => v . id ) ) . toEqual ( jasmine . arrayContaining ( [ 'op-id-2' , 'op-id-3' ] ) )
576+ expect ( vitals2 ?. find ( ( v ) => v . id === 'op-id-2' ) ?. duration ) . toBe ( 140 as Duration )
577+ expect ( vitals2 ?. find ( ( v ) => v . id === 'op-id-3' ) ?. duration ) . toBeUndefined ( )
578+
579+ // Profile 3: only op3 remains, with a duration
580+ expect ( vitals3 ?. map ( ( v ) => v . id ) ) . not . toContain ( 'op-id-1' )
581+ expect ( vitals3 ?. map ( ( v ) => v . id ) ) . not . toContain ( 'op-id-2' )
582+ expect ( vitals3 ?. length ) . toBe ( 1 )
583+ expect ( vitals3 ?. [ 0 ] . id ) . toBe ( 'op-id-3' )
584+ expect ( vitals3 ?. [ 0 ] . duration ) . toBe ( 230 as Duration )
585+ } )
586+
498587 it ( 'should collect views and set default view name in the Profile' , async ( ) => {
499588 const initialViewEntry = {
500589 id : 'view-user' ,
@@ -891,9 +980,9 @@ describe('profiler', () => {
891980 // Advance to T=1100ms
892981 clock . tick ( 100 )
893982
894- // Simulate clock drift: Date.now() drifted 1000ms ahead of performance.now()
895- // This mimics NTP sync or system clock adjustments in production
896- ; ( performance . now as jasmine . Spy ) . and . callFake ( ( ) => Date . now ( ) - timeOrigin - 1000 )
983+ // Simulate clock drift: Date.now() drifted 1000ms ahead of performance.now()
984+ // This mimics NTP sync or system clock adjustments in production
985+ ; ( performance . now as jasmine . Spy ) . and . callFake ( ( ) => Date . now ( ) - timeOrigin - 1000 )
897986
898987 // Stop profiler — state changes synchronously, data collection is async via Promise
899988 profiler . stop ( )
0 commit comments