From 25d31ffabe4d0ae0d1023b934e10bc20cdfe1afe Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Mon, 9 Mar 2026 12:28:19 +0100 Subject: [PATCH 1/2] Source code context for feature operation --- .../rum-core/src/boot/rumPublicApi.spec.ts | 1 + packages/rum-core/src/boot/rumPublicApi.ts | 11 +++-- .../src/domain/vital/vitalCollection.spec.ts | 11 +++++ .../src/domain/vital/vitalCollection.ts | 13 +++--- test/apps/microfrontend/common.ts | 4 ++ test/apps/microfrontend/types.d.ts | 1 + test/apps/microfrontend/yarn.lock | 12 +++--- test/e2e/scenario/microfrontend.scenario.ts | 40 +++++++++++++++++++ 8 files changed, 76 insertions(+), 17 deletions(-) diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 353b353539..77a180a45f 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -949,6 +949,7 @@ describe('rum public api', () => { rumPublicApi.startFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' }) expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'start', { operationKey: '00000000-0000-0000-0000-000000000000', + handlingStack: jasmine.any(String), }) }) }) diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index a730370037..157371f51f 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -923,10 +923,13 @@ export function makeRumPublicApi( }) }), - startFeatureOperation: monitor((name, options) => { - addTelemetryUsage({ feature: 'add-operation-step-vital', action_type: 'start' }) - strategy.addOperationStepVital(name, 'start', options) - }), + startFeatureOperation: (name, options) => { + const handlingStack = createHandlingStack('vital') + callMonitored(() => { + addTelemetryUsage({ feature: 'add-operation-step-vital', action_type: 'start' }) + strategy.addOperationStepVital(name, 'start', { ...options, handlingStack }) + }) + }, succeedFeatureOperation: monitor((name, options) => { addTelemetryUsage({ feature: 'add-operation-step-vital', action_type: 'succeed' }) diff --git a/packages/rum-core/src/domain/vital/vitalCollection.spec.ts b/packages/rum-core/src/domain/vital/vitalCollection.spec.ts index 169d22d5b3..915209087e 100644 --- a/packages/rum-core/src/domain/vital/vitalCollection.spec.ts +++ b/packages/rum-core/src/domain/vital/vitalCollection.spec.ts @@ -260,6 +260,17 @@ describe('vitalCollection', () => { expect(rawRumEvents[0].domainContext).toEqual({}) }) + it('should create operation step vital with handling stack in domainContext', () => { + addExperimentalFeatures([ExperimentalFeature.FEATURE_OPERATION_VITAL]) + vitalCollection.addOperationStepVital('foo', 'start', { + handlingStack: 'Error\n at foo\n at bar', + }) + + expect(rawRumEvents[0].domainContext).toEqual({ + handlingStack: 'Error\n at foo\n at bar', + }) + }) + it('should create a duration vital from add API', () => { vitalCollection.addDurationVital({ id: generateUUID(), diff --git a/packages/rum-core/src/domain/vital/vitalCollection.ts b/packages/rum-core/src/domain/vital/vitalCollection.ts index 60f5ee56b7..2958afe09d 100644 --- a/packages/rum-core/src/domain/vital/vitalCollection.ts +++ b/packages/rum-core/src/domain/vital/vitalCollection.ts @@ -29,18 +29,16 @@ export interface VitalOptions { * Vital description */ description?: string + + handlingStack?: string } /** * Duration vital options */ -export interface DurationVitalOptions extends VitalOptions { - /** - * Handling stack (internal use only) - */ - handlingStack?: string -} +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface DurationVitalOptions extends VitalOptions {} export interface FeatureOperationOptions extends VitalOptions { operationKey?: string @@ -128,7 +126,7 @@ export function startVitalCollection( return } - const { operationKey, context, description } = options || {} + const { operationKey, context, description, handlingStack } = options || {} const vital: OperationStepVital = { name, @@ -139,6 +137,7 @@ export function startVitalCollection( startClocks: clocksNow(), context: sanitize(context), description, + handlingStack, } lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processVital(vital)) } diff --git a/test/apps/microfrontend/common.ts b/test/apps/microfrontend/common.ts index 89125a531d..283a2d8170 100644 --- a/test/apps/microfrontend/common.ts +++ b/test/apps/microfrontend/common.ts @@ -61,6 +61,10 @@ export function createApp(id: string, title: string, borderColor: string) { window.DD_RUM.stopDurationVital(ref) }) + createButton(container, 'feature-operation', () => { + window.DD_RUM.startFeatureOperation(`${id}-feature-operation`) + }) + createButton(container, 'view', () => { window.DD_RUM.startView({ name: `${id}-view` }) }) diff --git a/test/apps/microfrontend/types.d.ts b/test/apps/microfrontend/types.d.ts index f426c18ffb..57e02c9f9d 100644 --- a/test/apps/microfrontend/types.d.ts +++ b/test/apps/microfrontend/types.d.ts @@ -4,6 +4,7 @@ interface Window { addAction: (name: string, context?: any) => void startDurationVital: (name: string) => any stopDurationVital: (ref: any) => void + startFeatureOperation: (name: string) => void startView: (options: { name: string }) => void } } diff --git a/test/apps/microfrontend/yarn.lock b/test/apps/microfrontend/yarn.lock index a7ce3c5fbb..c54753a47b 100644 --- a/test/apps/microfrontend/yarn.lock +++ b/test/apps/microfrontend/yarn.lock @@ -12,7 +12,7 @@ __metadata: languageName: node linkType: hard -"@datadog/webpack-plugin@npm:^3.1.0": +"@datadog/webpack-plugin@npm:3.1.0": version: 3.1.0 resolution: "@datadog/webpack-plugin@npm:3.1.0" dependencies: @@ -192,7 +192,7 @@ __metadata: languageName: node linkType: hard -"@module-federation/enhanced@npm:^2.0.1": +"@module-federation/enhanced@npm:2.0.1": version: 2.0.1 resolution: "@module-federation/enhanced@npm:2.0.1" dependencies: @@ -1886,12 +1886,12 @@ __metadata: version: 0.0.0-use.local resolution: "microfrontend-test-app@workspace:." dependencies: - "@datadog/webpack-plugin": "npm:^3.1.0" - "@module-federation/enhanced": "npm:^2.0.1" + "@datadog/webpack-plugin": "npm:3.1.0" + "@module-federation/enhanced": "npm:2.0.1" ts-loader: "npm:9.5.4" typescript: "npm:5.9.3" webpack: "npm:5.105.2" - webpack-cli: "npm:^6.0.1" + webpack-cli: "npm:6.0.1" languageName: unknown linkType: soft @@ -2708,7 +2708,7 @@ __metadata: languageName: node linkType: hard -"webpack-cli@npm:^6.0.1": +"webpack-cli@npm:6.0.1": version: 6.0.1 resolution: "webpack-cli@npm:6.0.1" dependencies: diff --git a/test/e2e/scenario/microfrontend.scenario.ts b/test/e2e/scenario/microfrontend.scenario.ts index b3764983f9..35cc42ebdf 100644 --- a/test/e2e/scenario/microfrontend.scenario.ts +++ b/test/e2e/scenario/microfrontend.scenario.ts @@ -1,7 +1,9 @@ import type { RumEvent, RumEventDomainContext, RumInitConfiguration } from '@datadog/browser-rum-core' import type { LogsEvent, LogsInitConfiguration, LogsEventDomainContext } from '@datadog/browser-logs' import { test, expect } from '@playwright/test' +import { ExperimentalFeature } from '@datadog/browser-core' import { createTest, microfrontendSetup } from '../lib/framework' + const HANDLING_STACK_REGEX = /^HandlingStack: .*\n\s+at testHandlingStack @/ const RUM_CONFIG: Partial = { @@ -180,6 +182,26 @@ test.describe('microfrontend', () => { expect(event?.context?.handlingStack).toMatch(HANDLING_STACK_REGEX) }) + createTest('expose handling stack for DD_RUM.startFeatureOperation') + .withRum({ ...RUM_CONFIG, enableExperimentalFeatures: [ExperimentalFeature.FEATURE_OPERATION_VITAL] }) + .withRumInit((configuration) => { + window.DD_RUM!.init(configuration) + + function testHandlingStack() { + window.DD_RUM!.startFeatureOperation('test-operation') + } + + testHandlingStack() + }) + .run(async ({ intakeRegistry, flushEvents }) => { + await flushEvents() + + const event = intakeRegistry.rumVitalEvents.find((event) => event.vital.name === 'test-operation') + + expect(event).toBeTruthy() + expect(event?.context?.handlingStack).toMatch(HANDLING_STACK_REGEX) + }) + createTest('resource: allow to modify service and version') .withRum(RUM_CONFIG) .withRumInit((configuration) => { @@ -384,6 +406,24 @@ test.describe('microfrontend', () => { expect.objectContaining({ service: 'mfe-app2-service', version: '0.2.0' }), ]) }) + + createTest('feature operations should have service and version from source code context') + .withRum({ ...RUM_CONFIG, enableExperimentalFeatures: [ExperimentalFeature.FEATURE_OPERATION_VITAL] }) + .withSetup(microfrontendSetup) + .run(async ({ intakeRegistry, flushEvents, page }) => { + await page.click('#app1-feature-operation') + await page.click('#app2-feature-operation') + await flushEvents() + + const featureOperationEvents = intakeRegistry.rumVitalEvents.filter( + (event) => event.vital.step_type === 'start' + ) + + expect(featureOperationEvents).toMatchObject([ + expect.objectContaining({ service: 'mfe-app1-service', version: '1.0.0' }), + expect.objectContaining({ service: 'mfe-app2-service', version: '0.2.0' }), + ]) + }) }) }) From a0b4cc937966c156f74bc4963ee6d63b6909baca Mon Sep 17 00:00:00 2001 From: Aymeric Mortemousque Date: Mon, 9 Mar 2026 14:00:16 +0100 Subject: [PATCH 2/2] Remove handlingStack from public type --- packages/rum-core/src/domain/vital/vitalCollection.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/rum-core/src/domain/vital/vitalCollection.ts b/packages/rum-core/src/domain/vital/vitalCollection.ts index 2958afe09d..d1bb2d4629 100644 --- a/packages/rum-core/src/domain/vital/vitalCollection.ts +++ b/packages/rum-core/src/domain/vital/vitalCollection.ts @@ -29,8 +29,6 @@ export interface VitalOptions { * Vital description */ description?: string - - handlingStack?: string } /** @@ -119,7 +117,7 @@ export function startVitalCollection( function addOperationStepVital( name: string, stepType: 'start' | 'end', - options?: FeatureOperationOptions, + options?: FeatureOperationOptions & { handlingStack?: string }, failureReason?: FailureReason ) { if (!isExperimentalFeatureEnabled(ExperimentalFeature.FEATURE_OPERATION_VITAL)) { @@ -145,7 +143,7 @@ export function startVitalCollection( return { addOperationStepVital, addDurationVital, - startDurationVital: (name: string, options: DurationVitalOptions = {}) => { + startDurationVital: (name: string, options: DurationVitalOptions & { handlingStack?: string } = {}) => { const ref = startDurationVital(customVitalsState, name, options) const vitalState = customVitalsState.vitalsByReference.get(ref) if (vitalState) { @@ -162,7 +160,7 @@ export function startVitalCollection( export function startDurationVital( { vitalsByName, vitalsByReference }: CustomVitalsState, name: string, - options: DurationVitalOptions = {} + options: DurationVitalOptions & { handlingStack?: string } = {} ) { const vital = { id: generateUUID(),