@@ -23,10 +23,12 @@ import {
2323 waitNextMicrotask ,
2424 replaceMockable ,
2525 createSessionManagerMock ,
26+ replaceMockableWithSpy ,
2627} from '@datadog/browser-core/test'
2728import { mockRumConfiguration , mockViewHistory } from '../../../../rum-core/test'
2829import { mockProfiler } from '../../../test'
2930import type { BrowserProfilerTrace } from '../../types'
31+ import { checkProfilingQuota } from './quotaCheck'
3032import { mockedTrace } from './test-utils/mockedTrace'
3133import { createRumProfiler } from './datadogProfiler'
3234import type { ProfilerTrace , RUMProfilerConfiguration } from './types'
@@ -43,10 +45,14 @@ describe('profiler', () => {
4345 // Store the original pathname
4446 const originalPathname = document . location . pathname
4547 let interceptor : ReturnType < typeof interceptRequests >
48+ let checkProfilingQuotaSpy : jasmine . Spy
4649
4750 beforeEach ( ( ) => {
4851 interceptor = interceptRequests ( )
4952 interceptor . withFetch ( DEFAULT_FETCH_MOCK , DEFAULT_FETCH_MOCK , DEFAULT_FETCH_MOCK )
53+ // Default: quota always ok. Individual quota-check tests can reconfigure via spy.and.callFake(...)
54+ checkProfilingQuotaSpy = replaceMockableWithSpy ( checkProfilingQuota )
55+ checkProfilingQuotaSpy . and . returnValue ( Promise . resolve ( { decision : 'quota_ok' , reason : 'quota_ok' } ) )
5056 } )
5157
5258 afterEach ( ( ) => {
@@ -1027,6 +1033,246 @@ describe('profiler', () => {
10271033 profiler . stop ( )
10281034 expect ( profiler . isStopped ( ) ) . toBe ( true )
10291035 } )
1036+
1037+ describe ( 'quota check' , ( ) => {
1038+ it ( 'should stop profiler and set quota_exceeded context when quota check returns quota_exceeded' , async ( ) => {
1039+ checkProfilingQuotaSpy . and . returnValue ( Promise . resolve ( { decision : 'quota_ko' , reason : 'quota_exceeded' } ) )
1040+ const { profiler, profilingContextManager } = setupProfiler ( )
1041+
1042+ profiler . start ( )
1043+ await waitForBoolean ( ( ) => profiler . isStopped ( ) )
1044+
1045+ expect ( profilingContextManager . get ( ) ) . toEqual ( {
1046+ status : 'stopped' ,
1047+ error_reason : undefined ,
1048+ quota_reason : 'quota_exceeded' ,
1049+ } as any )
1050+ expect ( interceptor . requests . length ) . toBe ( 0 ) // no data sent
1051+ } )
1052+
1053+ it ( 'should stop profiler and set org_disabled context when quota check returns org_disabled' , async ( ) => {
1054+ checkProfilingQuotaSpy . and . returnValue ( Promise . resolve ( { decision : 'quota_ko' , reason : 'org_disabled' } ) )
1055+ const { profiler, profilingContextManager } = setupProfiler ( )
1056+
1057+ profiler . start ( )
1058+ await waitForBoolean ( ( ) => profiler . isStopped ( ) )
1059+
1060+ expect ( profilingContextManager . get ( ) ) . toEqual ( {
1061+ status : 'stopped' ,
1062+ error_reason : undefined ,
1063+ quota_reason : 'org_disabled' ,
1064+ } as any )
1065+ expect ( interceptor . requests . length ) . toBe ( 0 ) // no data sent
1066+ } )
1067+
1068+ it ( 'should stop profiler and set unknown_reason context when quota check returns unknown_reason' , async ( ) => {
1069+ checkProfilingQuotaSpy . and . returnValue ( Promise . resolve ( { decision : 'quota_ko' , reason : 'unknown_reason' } ) )
1070+ const { profiler, profilingContextManager } = setupProfiler ( )
1071+
1072+ profiler . start ( )
1073+ await waitForBoolean ( ( ) => profiler . isStopped ( ) )
1074+
1075+ expect ( profilingContextManager . get ( ) ) . toEqual ( {
1076+ status : 'stopped' ,
1077+ error_reason : undefined ,
1078+ quota_reason : 'unknown_reason' ,
1079+ } as any )
1080+ expect ( interceptor . requests . length ) . toBe ( 0 ) // no data sent
1081+ } )
1082+
1083+ it ( 'should keep profiler running when quota check returns quota-ok' , async ( ) => {
1084+ // default spy already returns quota-ok
1085+ const { profiler, profilingContextManager } = setupProfiler ( )
1086+
1087+ profiler . start ( )
1088+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1089+
1090+ expect ( profiler . isRunning ( ) ) . toBe ( true )
1091+ expect ( profilingContextManager . get ( ) ?. status ) . toBe ( 'running' )
1092+
1093+ profiler . stop ( )
1094+ } )
1095+
1096+ it ( 'should not call quota check and proceed when sessionId is undefined at start' , async ( ) => {
1097+ // default spy already returns quota-ok; we just verify it's never called
1098+ mockProfiler ( deepClone ( mockedTrace ) )
1099+ const hooks = createHooks ( )
1100+ const profilingContextManager = startProfilingContext ( hooks )
1101+ const noSessionManager = createSessionManagerMock ( )
1102+ spyOn ( noSessionManager , 'findTrackedSession' ) . and . returnValue ( undefined )
1103+ const profilerNoSession = createRumProfiler (
1104+ mockRumConfiguration ( { profilingSampleRate : 100 } ) ,
1105+ new LifeCycle ( ) ,
1106+ noSessionManager ,
1107+ profilingContextManager ,
1108+ createIdentityEncoder ,
1109+ mockViewHistory ( ) ,
1110+ { sampleIntervalMs : 10 , collectIntervalMs : 60000 , minProfileDurationMs : 0 }
1111+ )
1112+
1113+ profilerNoSession . start ( )
1114+ await waitForBoolean ( ( ) => profilerNoSession . isRunning ( ) )
1115+
1116+ expect ( checkProfilingQuotaSpy ) . not . toHaveBeenCalled ( )
1117+ expect ( profilerNoSession . isRunning ( ) ) . toBe ( true )
1118+
1119+ profilerNoSession . stop ( )
1120+ } )
1121+
1122+ it ( 'should discard quota-exceeded result when profiler was already stopped by user' , async ( ) => {
1123+ let resolveQuota ! : ( result : { decision : string ; reason : string } ) => void
1124+ checkProfilingQuotaSpy . and . callFake (
1125+ ( ) =>
1126+ new Promise ( ( resolve ) => {
1127+ resolveQuota = resolve
1128+ } )
1129+ )
1130+ const { profiler, profilingContextManager } = setupProfiler ( )
1131+
1132+ profiler . start ( )
1133+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1134+
1135+ profiler . stop ( )
1136+ expect ( profiler . isStopped ( ) ) . toBe ( true )
1137+ expect ( profilingContextManager . get ( ) ?. status ) . toBe ( 'stopped' )
1138+ expect ( profilingContextManager . get ( ) ?. error_reason ) . toBeUndefined ( )
1139+
1140+ resolveQuota ( { decision : 'quota_ko' , reason : 'quota_exceeded' } )
1141+ await waitNextMicrotask ( )
1142+
1143+ expect ( profilingContextManager . get ( ) ?. error_reason ) . toBeUndefined ( )
1144+ } )
1145+
1146+ it ( 'should discard quota-exceeded result when SESSION_EXPIRED fired before quota resolved' , async ( ) => {
1147+ let resolveQuota ! : ( result : { decision : string ; reason : string } ) => void
1148+ checkProfilingQuotaSpy . and . callFake (
1149+ ( ) =>
1150+ new Promise ( ( resolve ) => {
1151+ resolveQuota = resolve
1152+ } )
1153+ )
1154+ const { profiler, profilingContextManager } = setupProfiler ( )
1155+
1156+ profiler . start ( )
1157+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1158+
1159+ lifeCycle . notify ( LifeCycleEventType . SESSION_EXPIRED )
1160+ expect ( profiler . isStopped ( ) ) . toBe ( true )
1161+
1162+ resolveQuota ( { decision : 'quota_ko' , reason : 'quota_exceeded' } )
1163+ await waitNextMicrotask ( )
1164+
1165+ expect ( profilingContextManager . get ( ) ?. error_reason ) . toBeUndefined ( )
1166+
1167+ // data IS sent (normal session-expired collection happens)
1168+ await waitForBoolean ( ( ) => interceptor . requests . length >= 1 )
1169+ expect ( interceptor . requests . length ) . toBeGreaterThanOrEqual ( 1 )
1170+ } )
1171+
1172+ it ( 'should stop profiler and not resume when quota-exceeded resolves while paused' , async ( ) => {
1173+ let resolveQuota ! : ( result : { decision : string ; reason : string } ) => void
1174+ checkProfilingQuotaSpy . and . callFake (
1175+ ( ) =>
1176+ new Promise ( ( resolve ) => {
1177+ resolveQuota = resolve
1178+ } )
1179+ )
1180+ const { profiler, profilingContextManager } = setupProfiler ( )
1181+
1182+ profiler . start ( )
1183+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1184+
1185+ setVisibilityState ( 'hidden' )
1186+ await waitForBoolean ( ( ) => profiler . isPaused ( ) )
1187+
1188+ resolveQuota ( { decision : 'quota_ko' , reason : 'quota_exceeded' } )
1189+ await waitNextMicrotask ( )
1190+
1191+ expect ( profiler . isStopped ( ) ) . toBe ( true )
1192+ expect ( profilingContextManager . get ( ) ) . toEqual ( {
1193+ status : 'stopped' ,
1194+ error_reason : undefined ,
1195+ quota_reason : 'quota_exceeded' ,
1196+ } as any )
1197+
1198+ setVisibilityState ( 'visible' )
1199+ await waitNextMicrotask ( )
1200+
1201+ expect ( profiler . isStopped ( ) ) . toBe ( true )
1202+ } )
1203+
1204+ it ( 'should discard stale quota result when SESSION_RENEWED restarts the profiler' , async ( ) => {
1205+ let resolveOldQuota ! : ( result : { decision : string ; reason : string } ) => void
1206+ let callCount = 0
1207+ checkProfilingQuotaSpy . and . callFake ( ( ) => {
1208+ callCount ++
1209+ if ( callCount === 1 ) {
1210+ return new Promise ( ( resolve ) => {
1211+ resolveOldQuota = resolve
1212+ } )
1213+ }
1214+ return Promise . resolve ( { decision : 'quota_ok' , reason : 'quota_ok' } )
1215+ } )
1216+ const { profiler, profilingContextManager } = setupProfiler ( )
1217+
1218+ profiler . start ( )
1219+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1220+
1221+ lifeCycle . notify ( LifeCycleEventType . SESSION_EXPIRED )
1222+ lifeCycle . notify ( LifeCycleEventType . SESSION_RENEWED , { } as SessionRenewalEvent )
1223+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1224+
1225+ resolveOldQuota ( { decision : 'quota_ko' , reason : 'quota_exceeded' } )
1226+ await waitNextMicrotask ( )
1227+
1228+ expect ( profiler . isRunning ( ) ) . toBe ( true )
1229+ expect ( profilingContextManager . get ( ) ?. status ) . toBe ( 'running' )
1230+
1231+ profiler . stop ( )
1232+ } )
1233+
1234+ it ( 'should restart profiler and re-check quota on SESSION_RENEWED after quota_exceeded or org_disabled' , async ( ) => {
1235+ let callCount = 0
1236+ checkProfilingQuotaSpy . and . callFake ( ( ) => {
1237+ callCount ++
1238+ return Promise . resolve (
1239+ callCount === 1
1240+ ? { decision : 'quota_ko' , reason : 'quota_exceeded' }
1241+ : { decision : 'quota_ok' , reason : 'quota_ok' }
1242+ )
1243+ } )
1244+ const { profiler } = setupProfiler ( )
1245+
1246+ profiler . start ( )
1247+ await waitForBoolean ( ( ) => profiler . isStopped ( ) )
1248+
1249+ expect ( callCount ) . toBe ( 1 )
1250+
1251+ lifeCycle . notify ( LifeCycleEventType . SESSION_RENEWED , { } as SessionRenewalEvent )
1252+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1253+
1254+ expect ( callCount ) . toBe ( 2 )
1255+ expect ( profiler . isRunning ( ) ) . toBe ( true )
1256+
1257+ profiler . stop ( )
1258+ } )
1259+
1260+ it ( 'should NOT restart profiler on SESSION_RENEWED after stopped-by-user' , async ( ) => {
1261+ // default spy already returns quota-ok
1262+ const { profiler } = setupProfiler ( )
1263+
1264+ profiler . start ( )
1265+ await waitForBoolean ( ( ) => profiler . isRunning ( ) )
1266+
1267+ profiler . stop ( )
1268+ expect ( profiler . isStopped ( ) ) . toBe ( true )
1269+
1270+ lifeCycle . notify ( LifeCycleEventType . SESSION_RENEWED , { } as SessionRenewalEvent )
1271+ await waitNextMicrotask ( )
1272+
1273+ expect ( profiler . isStopped ( ) ) . toBe ( true )
1274+ } )
1275+ } )
10301276} )
10311277
10321278function waitForBoolean ( booleanCallback : ( ) => boolean ) {
0 commit comments