11import type { ViewHistoryEntry } from '@datadog/browser-rum-core'
2- import { LifeCycle , LifeCycleEventType , RumPerformanceEntryType , createHooks } from '@datadog/browser-rum-core'
2+ import {
3+ LifeCycle ,
4+ LifeCycleEventType ,
5+ RumPerformanceEntryType ,
6+ VitalType ,
7+ createHooks ,
8+ } from '@datadog/browser-rum-core'
39import type { Duration , SessionRenewalEvent } from '@datadog/browser-core'
410import {
511 addDuration ,
@@ -8,6 +14,7 @@ import {
814 createIdentityEncoder ,
915 createValueHistory ,
1016 deepClone ,
17+ elapsed ,
1118 ONE_DAY ,
1219 relativeNow ,
1320 timeStampNow ,
@@ -141,6 +148,18 @@ describe('profiler', () => {
141148 addVital : ( vital : VitalContext ) => {
142149 vitalHistory . add ( vital , relativeNow ( ) ) . close ( addDuration ( relativeNow ( ) , vital . duration ?? ( 0 as Duration ) ) )
143150 } ,
151+ startOperationStep : ( id : string , label : string , operationKey ?: string ) => {
152+ const startClocks = clocksNow ( )
153+ const entry = vitalHistory . add (
154+ { id, type : VitalType . OPERATION_STEP , label, operationKey, startClocks, duration : undefined } ,
155+ startClocks . relative
156+ )
157+ return ( ) => {
158+ const endTime = relativeNow ( )
159+ entry . value . duration = elapsed ( entry . startTime , endTime )
160+ entry . close ( endTime )
161+ }
162+ } ,
144163 }
145164 }
146165
@@ -415,6 +434,7 @@ describe('profiler', () => {
415434 expect ( profilingContextManager . get ( ) ?. status ) . toBe ( 'running' )
416435 addVital ( {
417436 id : 'vital-id-1' ,
437+ type : VitalType . DURATION ,
418438 label : 'vital-label-1' ,
419439 startClocks : clocksNow ( ) ,
420440 duration : 50 as Duration ,
@@ -423,6 +443,7 @@ describe('profiler', () => {
423443
424444 addVital ( {
425445 id : 'vital-id-2' ,
446+ type : VitalType . DURATION ,
426447 label : 'vital-label-2' ,
427448 startClocks : clocksNow ( ) ,
428449 duration : 100 as Duration ,
@@ -442,6 +463,7 @@ describe('profiler', () => {
442463
443464 addVital ( {
444465 id : 'vital-id-3' ,
466+ type : VitalType . DURATION ,
445467 label : 'vital-label-3' ,
446468 startClocks : clocksNow ( ) ,
447469 duration : 100 as Duration ,
@@ -495,6 +517,79 @@ describe('profiler', () => {
495517 ] )
496518 } )
497519
520+ it ( 'should collect all ongoing operations during a profiling session' , async ( ) => {
521+ const clock = mockClock ( )
522+ const { profiler, startOperationStep } = setupProfiler ( )
523+
524+ // Profile 1: start all three operations, end op1
525+ profiler . start ( )
526+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
527+
528+ const endOp1 = startOperationStep ( 'op-id-1' , 'op-label-1' )
529+ clock . tick ( 10 )
530+ const endOp2 = startOperationStep ( 'op-id-2' , 'op-label-2' )
531+ clock . tick ( 10 )
532+ const endOp3 = startOperationStep ( 'op-id-3' , 'op-label-3' )
533+ clock . tick ( 10 )
534+ endOp1 ( ) // op1 ends during profile 1
535+
536+ clock . tick ( 70 )
537+ profiler . stop ( )
538+ await waitNextMicrotask ( )
539+
540+ // Profile 2: end op2
541+ profiler . start ( )
542+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
543+
544+ clock . tick ( 50 )
545+ endOp2 ( ) // op2 ends during profile 2
546+
547+ clock . tick ( 50 )
548+ profiler . stop ( )
549+ await waitNextMicrotask ( )
550+
551+ // Profile 3: end op3
552+ profiler . start ( )
553+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
554+
555+ clock . tick ( 50 )
556+ endOp3 ( ) // op3 ends during profile 3
557+
558+ clock . tick ( 50 )
559+ profiler . stop ( )
560+ await waitNextMicrotask ( )
561+ await waitNextMicrotask ( )
562+
563+ expect ( interceptor . requests . length ) . toBe ( 3 )
564+
565+ const req1 = await readFormDataRequest < ProfileEventPayload > ( interceptor . requests [ 0 ] )
566+ const req2 = await readFormDataRequest < ProfileEventPayload > ( interceptor . requests [ 1 ] )
567+ const req3 = await readFormDataRequest < ProfileEventPayload > ( interceptor . requests [ 2 ] )
568+
569+ const vitals1 = req1 [ 'wall-time.json' ] . vitals
570+ const vitals2 = req2 [ 'wall-time.json' ] . vitals
571+ const vitals3 = req3 [ 'wall-time.json' ] . vitals
572+
573+ // Profile 1: all three operations present, only op1 has a duration
574+ expect ( vitals1 ?. map ( ( v ) => v . id ) ) . toEqual ( jasmine . arrayContaining ( [ 'op-id-1' , 'op-id-2' , 'op-id-3' ] ) )
575+ expect ( vitals1 ?. find ( ( v ) => v . id === 'op-id-1' ) ?. duration ) . toBe ( 30 as Duration )
576+ expect ( vitals1 ?. find ( ( v ) => v . id === 'op-id-2' ) ?. duration ) . toBeUndefined ( )
577+ expect ( vitals1 ?. find ( ( v ) => v . id === 'op-id-3' ) ?. duration ) . toBeUndefined ( )
578+
579+ // Profile 2: op1 is gone (ended before profile 2 started), op2 and op3 present, only op2 has a duration
580+ expect ( vitals2 ?. map ( ( v ) => v . id ) ) . not . toContain ( 'op-id-1' )
581+ expect ( vitals2 ?. map ( ( v ) => v . id ) ) . toEqual ( jasmine . arrayContaining ( [ 'op-id-2' , 'op-id-3' ] ) )
582+ expect ( vitals2 ?. find ( ( v ) => v . id === 'op-id-2' ) ?. duration ) . toBe ( 140 as Duration )
583+ expect ( vitals2 ?. find ( ( v ) => v . id === 'op-id-3' ) ?. duration ) . toBeUndefined ( )
584+
585+ // Profile 3: only op3 remains, with a duration
586+ expect ( vitals3 ?. map ( ( v ) => v . id ) ) . not . toContain ( 'op-id-1' )
587+ expect ( vitals3 ?. map ( ( v ) => v . id ) ) . not . toContain ( 'op-id-2' )
588+ expect ( vitals3 ?. length ) . toBe ( 1 )
589+ expect ( vitals3 ?. [ 0 ] . id ) . toBe ( 'op-id-3' )
590+ expect ( vitals3 ?. [ 0 ] . duration ) . toBe ( 230 as Duration )
591+ } )
592+
498593 it ( 'should collect views and set default view name in the Profile' , async ( ) => {
499594 const initialViewEntry = {
500595 id : 'view-user' ,
0 commit comments