Skip to content
Merged
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/rum-core/src/boot/rumPublicApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})
})
})
Expand Down
11 changes: 7 additions & 4 deletions packages/rum-core/src/boot/rumPublicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture handling stack for end operation-step vitals

startFeatureOperation() now enriches only the start step with handlingStack, but succeedFeatureOperation() / failFeatureOperation() still call addOperationStepVital(..., 'end', options) without it. Because source-code context resolution reads domainContext.handlingStack to set per-event service/version (packages/rum-core/src/domain/contexts/sourceCodeContext.ts), end-step vital events from microfrontend code paths will not be enriched and will fall back to default attribution, producing inconsistent metadata within the same feature operation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature operation event is reduced in the backend with the start and end events. It keeps the start handlingStack event which is what we want.

})
},

succeedFeatureOperation: monitor((name, options) => {
addTelemetryUsage({ feature: 'add-operation-step-vital', action_type: 'succeed' })
Expand Down
11 changes: 11 additions & 0 deletions packages/rum-core/src/domain/vital/vitalCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
17 changes: 7 additions & 10 deletions packages/rum-core/src/domain/vital/vitalCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,8 @@ export interface VitalOptions {
* 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
Expand Down Expand Up @@ -121,14 +117,14 @@ export function startVitalCollection(
function addOperationStepVital(
name: string,
stepType: 'start' | 'end',
options?: FeatureOperationOptions,
options?: FeatureOperationOptions & { handlingStack?: string },

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn’t add handlingStack to FeatureOperationOptions because FeatureOperationOptions is publicly exposed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not in the scope of this PR but we should probably remove it from other public options.
I saw it on ViewOptions, maybe there are others places.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll make a followup pr

failureReason?: FailureReason
) {
if (!isExperimentalFeatureEnabled(ExperimentalFeature.FEATURE_OPERATION_VITAL)) {
return
}

const { operationKey, context, description } = options || {}
const { operationKey, context, description, handlingStack } = options || {}

const vital: OperationStepVital = {
name,
Expand All @@ -139,14 +135,15 @@ export function startVitalCollection(
startClocks: clocksNow(),
context: sanitize(context),
description,
handlingStack,
}
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processVital(vital))
}

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) {
Expand All @@ -163,7 +160,7 @@ export function startVitalCollection(
export function startDurationVital(
{ vitalsByName, vitalsByReference }: CustomVitalsState,
name: string,
options: DurationVitalOptions = {}
options: DurationVitalOptions & { handlingStack?: string } = {}
) {
const vital = {
id: generateUUID(),
Expand Down
4 changes: 4 additions & 0 deletions test/apps/microfrontend/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` })
})
Expand Down
1 change: 1 addition & 0 deletions test/apps/microfrontend/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
12 changes: 6 additions & 6 deletions test/apps/microfrontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
40 changes: 40 additions & 0 deletions test/e2e/scenario/microfrontend.scenario.ts
Original file line number Diff line number Diff line change
@@ -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<RumInitConfiguration> = {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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' }),
])
})
})
})

Expand Down
Loading