Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/browser-core/src/domain/resourceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
4 changes: 4 additions & 0 deletions packages/browser-rum-core/src/domain/requestCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ export interface RequestCompleteEvent {

let nextRequestIndex = 1

export function getNextRequestIndex(): number {
return nextRequestIndex++
}

export function startRequestCollection(
lifeCycle: LifeCycle,
configuration: RumConfiguration,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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<RumMutationRecord[]>()
const windowOpenObservable = new Observable<void>()
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()
})
})
})
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -64,16 +65,24 @@ export interface ManualResourceData {
type?: ResourceType
method?: string
context?: Context
requestIndex: number
}

export function trackManualResources(lifeCycle: LifeCycle, resourceTracker: EventTracker<ManualResourceData>) {
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()) {
Expand Down Expand Up @@ -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 {
Expand Down
Loading