From 3a22369f2c9e0b39fe8cf3289058da146f4d17e4 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Fri, 14 Aug 2026 11:38:04 +0200 Subject: [PATCH] feat: manual resources participate in pending-activity tracking (prototype) --- .../browser-core/src/domain/resourceUtils.ts | 1 + .../src/domain/requestCollection.ts | 4 ++ .../resource/trackManualResources.spec.ts | 51 +++++++++++++++++-- .../domain/resource/trackManualResources.ts | 26 +++++++++- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/packages/browser-core/src/domain/resourceUtils.ts b/packages/browser-core/src/domain/resourceUtils.ts index 1d79857a1b..327f24a94c 100644 --- a/packages/browser-core/src/domain/resourceUtils.ts +++ b/packages/browser-core/src/domain/resourceUtils.ts @@ -17,6 +17,7 @@ export type ResourceType = (typeof ResourceType)[keyof typeof ResourceType] export const RequestType = { FETCH: ResourceType.FETCH, XHR: ResourceType.XHR, + OTHER: ResourceType.OTHER, } as const export type RequestType = (typeof RequestType)[keyof typeof RequestType] diff --git a/packages/browser-rum-core/src/domain/requestCollection.ts b/packages/browser-rum-core/src/domain/requestCollection.ts index c0621cd51a..55408aa93b 100644 --- a/packages/browser-rum-core/src/domain/requestCollection.ts +++ b/packages/browser-rum-core/src/domain/requestCollection.ts @@ -68,6 +68,10 @@ export interface RequestCompleteEvent { let nextRequestIndex = 1 +export function getNextRequestIndex(): number { + return nextRequestIndex++ +} + export function startRequestCollection( lifeCycle: LifeCycle, configuration: RumConfiguration, diff --git a/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts b/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts index 8771b50ab9..93784079a2 100644 --- a/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts +++ b/packages/browser-rum-core/src/domain/resource/trackManualResources.spec.ts @@ -1,12 +1,15 @@ import type { Duration, ServerDuration } from '@datadog/js-core/time' -import { ResourceType } from '@datadog/browser-core' +import { Observable, ResourceType } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { mockClock } from '@datadog/browser-core/test' -import { collectAndValidateRawRumEvents } from '../../../test' +import { collectAndValidateRawRumEvents, mockRumConfiguration } from '../../../test' import type { RawRumResourceEvent, RawRumEvent } from '../../rawRumEvent.types' import { RumEventType } from '../../rawRumEvent.types' -import { type RawRumEventCollectedData, LifeCycle } from '../lifeCycle' +import { type RawRumEventCollectedData, LifeCycle, LifeCycleEventType } from '../lifeCycle' import { startEventTracker } from '../eventTracker' +import type { RumMutationRecord } from '../../browser/domMutationObservable' +import type { PageActivityEvent } from '../waitPageActivityEnd' +import { createPageActivityObservable } from '../waitPageActivityEnd' import type { ManualResourceData } from './trackManualResources' import { trackManualResources } from './trackManualResources' @@ -184,4 +187,46 @@ describe('trackManualResources', () => { expect((rawRumEvents[0].rawRumEvent as RawRumResourceEvent).resource.url).toBe('https://api.example.com/data') }) }) + + describe('pending activity tracking', () => { + it('notifies REQUEST_STARTED when a manual resource starts and REQUEST_COMPLETED when it stops', () => { + const startedSpy = jasmine.createSpy('started') + const completedSpy = jasmine.createSpy('completed') + lifeCycle.subscribe(LifeCycleEventType.REQUEST_STARTED, startedSpy) + lifeCycle.subscribe(LifeCycleEventType.REQUEST_COMPLETED, completedSpy) + + startResource('my-channel') + expect(startedSpy).toHaveBeenCalledTimes(1) + expect(startedSpy.calls.argsFor(0)[0].url).toBe('my-channel') + expect(typeof startedSpy.calls.argsFor(0)[0].requestIndex).toBe('number') + + clock.tick(42) + stopResource('my-channel') + expect(completedSpy).toHaveBeenCalledTimes(1) + const completedArg = completedSpy.calls.argsFor(0)[0] + expect(completedArg.requestIndex).toBe(startedSpy.calls.argsFor(0)[0].requestIndex) + expect(completedArg.duration).toBe(42) + }) + + it('holds page activity busy while a manual resource is in flight', () => { + const domMutationObservable = new Observable() + const windowOpenObservable = new Observable() + const pageActivityObservable = createPageActivityObservable( + lifeCycle, + domMutationObservable, + windowOpenObservable, + mockRumConfiguration() + ) + const events: PageActivityEvent[] = [] + const subscription = pageActivityObservable.subscribe((event) => events.push(event)) + + startResource('my-channel') + expect(events).toEqual([{ isBusy: true }]) + + stopResource('my-channel') + expect(events).toEqual([{ isBusy: true }, { isBusy: false }]) + + subscription.unsubscribe() + }) + }) }) diff --git a/packages/browser-rum-core/src/domain/resource/trackManualResources.ts b/packages/browser-rum-core/src/domain/resource/trackManualResources.ts index 9c4063c798..e6e65634eb 100644 --- a/packages/browser-rum-core/src/domain/resource/trackManualResources.ts +++ b/packages/browser-rum-core/src/domain/resource/trackManualResources.ts @@ -1,11 +1,12 @@ import type { Context, ResourceType } from '@datadog/browser-core' import { elapsed, toServerDuration, clocksNow } from '@datadog/js-core/time' -import { ResourceType as ResourceTypeEnum } from '@datadog/browser-core' +import { RequestType, ResourceType as ResourceTypeEnum } from '@datadog/browser-core' import type { RawRumResourceEvent } from '../../rawRumEvent.types' import { RumEventType } from '../../rawRumEvent.types' import type { LifeCycle } from '../lifeCycle' import { LifeCycleEventType } from '../lifeCycle' import type { EventTracker } from '../eventTracker' +import { getNextRequestIndex } from '../requestCollection' import { sanitizeIfLongDataUrl } from './resourceUtils' export interface ResourceOptions { @@ -64,16 +65,24 @@ export interface ManualResourceData { type?: ResourceType method?: string context?: Context + requestIndex: number } export function trackManualResources(lifeCycle: LifeCycle, resourceTracker: EventTracker) { function startManualResource(url: string, options: ResourceOptions = {}, startClocks = clocksNow()) { const lookupKey = options.resourceKey ?? url + const requestIndex = getNextRequestIndex() resourceTracker.start(lookupKey, startClocks, { url, + requestIndex, ...options, }) + + // Prototype note (electron-sdk IPC RUM events, 2026-08): manual resources always participate in + // pending-activity tracking. Revisit with the browser-sdk team whether this should be opt-in before + // shipping outside the prototype — existing public API callers get new behavior unconditionally. + lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, { requestIndex, url }) } function stopManualResource(url: string, options: ResourceStopOptions = {}, stopClocks = clocksNow()) { @@ -112,6 +121,21 @@ export function trackManualResources(lifeCycle: LifeCycle, resourceTracker: Even duration, domainContext: { isManual: true }, }) + + // Prototype note (electron-sdk IPC RUM events, 2026-08): manual resources always participate in + // pending-activity tracking. Revisit with the browser-sdk team whether this should be opt-in before + // shipping outside the prototype — existing public API callers get new behavior unconditionally. + lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, { + requestIndex: stopped.requestIndex, + type: RequestType.OTHER, + method: stopped.method ?? '', + url: stopped.url, + status: options.statusCode ?? 0, + startClocks: stopped.startClocks, + duration, + isAborted: false, + isAbortedOnStart: false, + }) } return {