Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a49b4e9
refactor manual tracking, add s/s resource
BeltranBulbarellaDD Jan 22, 2026
40945d9
Fix format and lint
BeltranBulbarellaDD Jan 22, 2026
aeae65f
Remove stopResourceWithError
BeltranBulbarellaDD Jan 22, 2026
998c7a9
Add e2e test, merge unit tests, add telemetry
BeltranBulbarellaDD Jan 23, 2026
668bda0
Prevent event.resource.type access for manual resources
BeltranBulbarellaDD Jan 23, 2026
e4a5a36
Merge branch 'main' into beltran.bulbarella/start-stopResource
BeltranBulbarellaDD Jan 26, 2026
684d9dc
Add optional fields to RawRumResourceEvent, rename manualEventLifecyc…
BeltranBulbarellaDD Jan 26, 2026
69aa0b9
Remove unneded check because of combine.
BeltranBulbarellaDD Jan 26, 2026
65d9f8b
Fix sanitizeIfLongDataUrl in trackManualResources
BeltranBulbarellaDD Jan 27, 2026
0dd0466
Remove duplicate tests to use manualEventRegistry
BeltranBulbarellaDD Jan 27, 2026
e16e523
Merge branch 'main' into beltran.bulbarella/start-stopResource
BeltranBulbarellaDD Jan 27, 2026
144b1ec
Refactored action and resource tracking to use the new event tracker …
BeltranBulbarellaDD Jan 29, 2026
1d19e7c
Merge branch 'main' into beltran.bulbarella/start-stopResource
BeltranBulbarellaDD Jan 29, 2026
83054aa
Updated eventTracker.ts to pass startClocks as third parameter to onD…
BeltranBulbarellaDD Jan 29, 2026
c6ec7eb
Don't allow resources to be started with undefined URL.
BeltranBulbarellaDD Jan 30, 2026
7d07e62
Filter out intake requests in manual resource tracking
BeltranBulbarellaDD Jan 30, 2026
9294b1d
Add stopObservable.notify() in the discard() method
BeltranBulbarellaDD Jan 30, 2026
1161c24
Simplify activityEndTime checks
BeltranBulbarellaDD Jan 30, 2026
a1e6d11
Simplify and remove unnecessary code.
BeltranBulbarellaDD Feb 3, 2026
8169e5a
Return getCounts in eventTracker. Rollback hasPageActivity.
BeltranBulbarellaDD Feb 3, 2026
d8ef180
Merge branch 'main' into beltran.bulbarella/start-stopResource
BeltranBulbarellaDD Feb 3, 2026
7c4a484
Make name required in ActionEventData.
BeltranBulbarellaDD Feb 4, 2026
555d774
Refactor action to have click and manual event trackers. Remove onDis…
BeltranBulbarellaDD Feb 9, 2026
6f43388
Fix tests
BeltranBulbarellaDD Feb 9, 2026
116783f
Refactor isChildEvent, add getCounts to eventTracker because of final…
BeltranBulbarellaDD Feb 10, 2026
bb72b7f
Merge branch 'main' into beltran.bulbarella/start-stopResource
BeltranBulbarellaDD Feb 10, 2026
e6c9249
Merge branch 'main' into beltran.bulbarella/start-stopResource
BeltranBulbarellaDD Feb 16, 2026
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/core/src/tools/experimentalFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum ExperimentalFeature {
SHORT_SESSION_INVESTIGATION = 'short_session_investigation',
AVOID_FETCH_KEEPALIVE = 'avoid_fetch_keepalive',
START_STOP_ACTION = 'start_stop_action',
START_STOP_RESOURCE = 'start_stop_resource',
USE_CHANGE_RECORDS = 'use_change_records',
SOURCE_CODE_CONTEXT = 'source_code_context',
LCP_SUBPARTS = 'lcp_subparts',
Expand Down
10 changes: 10 additions & 0 deletions packages/rum-core/src/boot/preStartRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ export function createPreStartStrategy(
bufferApiCalls.add((startRumResult) => startRumResult.stopAction(name, options, stopClocks))
},

startResource(url, options) {
const startClocks = clocksNow()
bufferApiCalls.add((startRumResult) => startRumResult.startResource(url, options, startClocks))
},

stopResource(url, options) {
const stopClocks = clocksNow()
bufferApiCalls.add((startRumResult) => startRumResult.stopResource(url, options, stopClocks))
},

addError(providedError) {
bufferApiCalls.add((startRumResult) => startRumResult.addError(providedError))
},
Expand Down
91 changes: 91 additions & 0 deletions packages/rum-core/src/boot/rumPublicApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DefaultPrivacyLevel,
timeStampToClocks,
ExperimentalFeature,
ResourceType,
startTelemetry,
} from '@datadog/browser-core'
import type { Clock } from '@datadog/browser-core/test'
Expand Down Expand Up @@ -51,6 +52,8 @@ const noopStartRum = (): ReturnType<StartRum> => ({
addOperationStepVital: () => undefined,
startAction: () => undefined,
stopAction: () => undefined,
startResource: () => undefined,
stopResource: () => undefined,
})
const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' }
const FAKE_WORKER = {} as DeflateWorker
Expand Down Expand Up @@ -820,6 +823,94 @@ describe('rum public api', () => {
})
})

describe('startResource / stopResource', () => {
it('should call startResource and stopResource on the strategy', () => {
mockExperimentalFeatures([ExperimentalFeature.START_STOP_RESOURCE])

const startResourceSpy = jasmine.createSpy()
const stopResourceSpy = jasmine.createSpy()
const { rumPublicApi } = makeRumPublicApiWithDefaults({
startRumResult: {
startResource: startResourceSpy,
stopResource: stopResourceSpy,
},
})

rumPublicApi.init(DEFAULT_INIT_CONFIGURATION)
rumPublicApi.startResource('https://api.example.com/data', {
type: ResourceType.FETCH,
method: 'POST',
context: { requestId: 'abc' },
})
rumPublicApi.stopResource('https://api.example.com/data', {
statusCode: 200,
context: { responseSize: 1024 },
})

expect(startResourceSpy).toHaveBeenCalledWith(
'https://api.example.com/data',
jasmine.objectContaining({
type: ResourceType.FETCH,
method: 'POST',
context: { requestId: 'abc' },
})
)
expect(stopResourceSpy).toHaveBeenCalledWith(
'https://api.example.com/data',
jasmine.objectContaining({
statusCode: 200,
context: { responseSize: 1024 },
})
)
})

it('should sanitize startResource and stopResource inputs', () => {
mockExperimentalFeatures([ExperimentalFeature.START_STOP_RESOURCE])

const startResourceSpy = jasmine.createSpy()
const { rumPublicApi } = makeRumPublicApiWithDefaults({
startRumResult: {
startResource: startResourceSpy,
},
})

rumPublicApi.init(DEFAULT_INIT_CONFIGURATION)
rumPublicApi.startResource('https://api.example.com/data', {
type: ResourceType.XHR,
method: 'GET',
context: { count: 123, nested: { foo: 'bar' } } as any,
resourceKey: 'resource_key',
})

expect(startResourceSpy.calls.argsFor(0)[1]).toEqual(
jasmine.objectContaining({
type: ResourceType.XHR,
method: 'GET',
context: { count: 123, nested: { foo: 'bar' } },
resourceKey: 'resource_key',
})
)
})

it('should not call startResource/stopResource when feature flag is disabled', () => {
const startResourceSpy = jasmine.createSpy()
const stopResourceSpy = jasmine.createSpy()
const { rumPublicApi } = makeRumPublicApiWithDefaults({
startRumResult: {
startResource: startResourceSpy,
stopResource: stopResourceSpy,
},
})

rumPublicApi.init(DEFAULT_INIT_CONFIGURATION)
rumPublicApi.startResource('https://api.example.com/data', { type: ResourceType.FETCH })
rumPublicApi.stopResource('https://api.example.com/data')

expect(startResourceSpy).not.toHaveBeenCalled()
expect(stopResourceSpy).not.toHaveBeenCalled()
})
})

describe('addDurationVital', () => {
it('should call addDurationVital on the startRum result', () => {
const addDurationVitalSpy = jasmine.createSpy()
Expand Down
48 changes: 48 additions & 0 deletions packages/rum-core/src/boot/rumPublicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
RumInternalContext,
Telemetry,
Encoder,
ResourceType,
} from '@datadog/browser-core'
import {
ContextManagerMethod,
Expand Down Expand Up @@ -57,6 +58,7 @@ import type { Hooks } from '../domain/hooks'
import type { SdkName } from '../domain/contexts/defaultContext'
import type { LongTaskContexts } from '../domain/longTask/longTaskCollection'
import type { ActionOptions } from '../domain/action/trackManualActions'
import type { ResourceOptions, ResourceStopOptions } from '../domain/resource/trackManualResources'
import { createPreStartStrategy } from './preStartRum'
import type { StartRumResult } from './startRum'
import { startRum } from './startRum'
Expand Down Expand Up @@ -191,6 +193,24 @@ export interface RumPublicApi extends PublicApi {
*/
stopAction: (name: string, options?: ActionOptions) => void

/**
* [Experimental] Start tracking a resource, stored in `@resource`
*
* @category Data Collection
* @param url - URL of the resource
* @param options - Options of the resource (@default type: 'other')
*/
startResource: (url: string, options?: ResourceOptions) => void

/**
* [Experimental] Stop tracking a resource, stored in `@resource`
*
* @category Data Collection
* @param url - URL of the resource
* @param options - Options of the resource
*/
stopResource: (url: string, options?: ResourceStopOptions) => void

/**
* Add a custom error, stored in `@error`.
*
Expand Down Expand Up @@ -548,6 +568,8 @@ export interface Strategy {
addAction: StartRumResult['addAction']
startAction: StartRumResult['startAction']
stopAction: StartRumResult['stopAction']
startResource: StartRumResult['startResource']
stopResource: StartRumResult['stopResource']
addError: StartRumResult['addError']
addFeatureFlagEvaluation: StartRumResult['addFeatureFlagEvaluation']
startDurationVital: StartRumResult['startDurationVital']
Expand Down Expand Up @@ -707,6 +729,32 @@ export function makeRumPublicApi(
})
}),

startResource: monitor((url, options) => {
// Check feature flag only after init; pre-init calls should be buffered
if (strategy.initConfiguration && !isExperimentalFeatureEnabled(ExperimentalFeature.START_STOP_RESOURCE)) {
return
Comment thread
BeltranBulbarellaDD marked this conversation as resolved.
}
// addTelemetryUsage({ feature: 'start-resource' })
strategy.startResource(sanitize(url)!, {
type: sanitize(options && options.type) as ResourceType | undefined,
method: sanitize(options && options.method) as string | undefined,
context: sanitize(options && options.context) as Context,
resourceKey: options && options.resourceKey,
})
}),

stopResource: monitor((url, options) => {
if (strategy.initConfiguration && !isExperimentalFeatureEnabled(ExperimentalFeature.START_STOP_RESOURCE)) {
return
}
// addTelemetryUsage({ feature: 'stop-resource' })
strategy.stopResource(sanitize(url)!, {
statusCode: options && options.statusCode,
context: sanitize(options && options.context) as Context,
resourceKey: options && options.resourceKey,
})
}),

addError: (error, context) => {
const handlingStack = createHandlingStack('error')
callMonitored(() => {
Expand Down
6 changes: 4 additions & 2 deletions packages/rum-core/src/boot/startRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ export function startRumEventCollection(

cleanupTasks.push(stopViewCollection)

const { stop: stopResourceCollection } = startResourceCollection(lifeCycle, configuration, pageStateHistory)
cleanupTasks.push(stopResourceCollection)
const resourceCollection = startResourceCollection(lifeCycle, configuration, pageStateHistory)
cleanupTasks.push(resourceCollection.stop)

const { stop: stopLongTaskCollection, longTaskContexts } = startLongTaskCollection(lifeCycle, configuration)
cleanupTasks.push(stopLongTaskCollection)
Expand All @@ -248,6 +248,8 @@ export function startRumEventCollection(
addAction: actionCollection.addAction,
startAction: actionCollection.startAction,
stopAction: actionCollection.stopAction,
startResource: resourceCollection.startResource,
stopResource: resourceCollection.stopResource,
addEvent: eventCollection.addEvent,
addError,
addTiming,
Expand Down
18 changes: 9 additions & 9 deletions packages/rum-core/src/domain/action/actionCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createHooks } from '../hooks'
import type { RumMutationRecord } from '../../browser/domMutationObservable'
import { LONG_TASK_START_TIME_CORRECTION, startActionCollection } from './actionCollection'
import { ActionNameSource } from './actionNameConstants'
import type { ActionContexts } from './trackAction'
import type { ActionContexts } from './actionCollection'

describe('actionCollection', () => {
const lifeCycle = new LifeCycle()
Expand Down Expand Up @@ -172,7 +172,7 @@ describe('actionCollection', () => {
describe('assembly hook', () => {
;[RumEventType.RESOURCE, RumEventType.LONG_TASK, RumEventType.ERROR].forEach((eventType) => {
it(`should add action properties on ${eventType} from the context`, () => {
const actionId = '1'
const actionId = ['1']
spyOn(actionContexts, 'findActionId').and.returnValue(actionId)
const defaultRumEventAttributes = hooks.triggerHook(HookNames.Assemble, {
eventType,
Expand All @@ -184,7 +184,7 @@ describe('actionCollection', () => {
})
;[RumEventType.VIEW, RumEventType.VITAL].forEach((eventType) => {
it(`should not add action properties on ${eventType} from the context`, () => {
const actionId = '1'
const actionId = ['1']
spyOn(actionContexts, 'findActionId').and.returnValue(actionId)
const defaultRumEventAttributes = hooks.triggerHook(HookNames.Assemble, {
eventType,
Expand All @@ -197,7 +197,7 @@ describe('actionCollection', () => {

it('should add action properties on long task from the context when the start time is slightly before the action start time', () => {
const longTaskStartTime = 100 as RelativeTime
const findActionIdSpy = spyOn(actionContexts, 'findActionId')
const findActionIdSpy = spyOn(actionContexts, 'findActionId').and.returnValue([])

hooks.triggerHook(HookNames.Assemble, {
eventType: RumEventType.LONG_TASK,
Expand All @@ -212,22 +212,22 @@ describe('actionCollection', () => {

describe('assemble telemetry hook', () => {
it('should add action id', () => {
const actionId = '1'
const actionId = ['1']
spyOn(actionContexts, 'findActionId').and.returnValue(actionId)
const telemetryEventAttributes = hooks.triggerHook(HookNames.AssembleTelemetry, {
startTime: 0 as RelativeTime,
}) as DefaultTelemetryEventAttributes

expect(telemetryEventAttributes.action?.id).toEqual(actionId)
// todo: fix telemetry event type
expect(telemetryEventAttributes.action?.id).toEqual(actionId as unknown as string)
})

it('should not add action id if the action is not found', () => {
spyOn(actionContexts, 'findActionId').and.returnValue(undefined)
spyOn(actionContexts, 'findActionId').and.returnValue([])
const telemetryEventAttributes = hooks.triggerHook(HookNames.AssembleTelemetry, {
startTime: 0 as RelativeTime,
}) as DefaultTelemetryEventAttributes

expect(telemetryEventAttributes.action?.id).toBeUndefined()
expect(telemetryEventAttributes.action?.id).toEqual([] as unknown as string)
})
})
})
37 changes: 18 additions & 19 deletions packages/rum-core/src/domain/action/actionCollection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Duration, Observable } from '@datadog/browser-core'
import type { Duration, Observable, RelativeTime } from '@datadog/browser-core'
import { noop, toServerDuration, SKIPPED, HookNames, addDuration } from '@datadog/browser-core'
import { discardNegativeDuration } from '../discardNegativeDuration'
import type { RawRumActionEvent } from '../../rawRumEvent.types'
Expand All @@ -10,13 +10,15 @@ import type { DefaultRumEventAttributes, DefaultTelemetryEventAttributes, Hooks
import type { RumMutationRecord } from '../../browser/domMutationObservable'
import { trackClickActions } from './trackClickActions'
import type { ClickAction } from './trackClickActions'
import { startActionTracker } from './trackAction'
import type { ActionContexts } from './trackAction'
import { trackManualActions } from './trackManualActions'
import type { ManualAction } from './trackManualActions'

export type AutoAction = ClickAction

export interface ActionContexts {
findActionId: (startTime?: RelativeTime) => string[]
}

export const LONG_TASK_START_TIME_CORRECTION = 1 as Duration

export function startActionCollection(
Expand All @@ -26,34 +28,30 @@ export function startActionCollection(
windowOpenObservable: Observable<void>,
configuration: RumConfiguration
) {
// Shared action tracker for both click and manual actions
const actionTracker = startActionTracker(lifeCycle)

const { unsubscribe: unsubscribeAutoAction } = lifeCycle.subscribe(
LifeCycleEventType.AUTO_ACTION_COMPLETED,
(action) => {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processAction(action))
}
)

let stopClickActions: () => void = noop
const stopClickActions: () => void = noop
let clickActions: ReturnType<typeof trackClickActions> | undefined

if (configuration.trackUserInteractions) {
;({ stop: stopClickActions } = trackClickActions(
lifeCycle,
domMutationObservable,
windowOpenObservable,
configuration,
actionTracker
))
clickActions = trackClickActions(lifeCycle, domMutationObservable, windowOpenObservable, configuration)
}

const manualActions = trackManualActions(lifeCycle, actionTracker, (action) => {
const manualActions = trackManualActions(lifeCycle, (action) => {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processAction(action))
})

const actionContexts: ActionContexts = {
findActionId: actionTracker.findActionId,
findActionId: (startTime?: RelativeTime) => {
const manualActionId = manualActions.findActionId(startTime)
const clickActionId = clickActions?.findActionId(startTime) ?? []
return manualActionId.concat(clickActionId)
},
}

hooks.register(HookNames.Assemble, ({ startTime, eventType }): DefaultRumEventAttributes | SKIPPED => {
Expand All @@ -75,7 +73,7 @@ export function startActionCollection(

const actionId = actionContexts.findActionId(correctedStartTime)

if (!actionId) {
if (!actionId.length) {
return SKIPPED
}

Expand All @@ -88,7 +86,8 @@ export function startActionCollection(
hooks.register(
HookNames.AssembleTelemetry,
({ startTime }): DefaultTelemetryEventAttributes => ({
action: { id: actionContexts.findActionId(startTime) as string },
// todo: fix telemetry event type
action: { id: actionContexts.findActionId(startTime) as unknown as string },
})
)

Expand All @@ -101,7 +100,7 @@ export function startActionCollection(
unsubscribeAutoAction()
stopClickActions()
manualActions.stop()
actionTracker.stop()
clickActions?.stop()
},
}
}
Expand Down
Loading