Skip to content
Open
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
50 changes: 49 additions & 1 deletion packages/rum-core/src/domain/vital/vitalCollection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Duration } from '@datadog/browser-core'
import { mockClock, type Clock } from '@datadog/browser-core/test'
import { addExperimentalFeatures, clocksNow, ExperimentalFeature, generateUUID } from '@datadog/browser-core'
import { addExperimentalFeatures, clocksNow, display, ExperimentalFeature, generateUUID } from '@datadog/browser-core'
import { collectAndValidateRawRumEvents, mockPageStateHistory } from '../../../test'
import type { RawRumEvent, RawRumVitalEvent } from '../../rawRumEvent.types'
import { VitalType, RumEventType } from '../../rawRumEvent.types'
Expand Down Expand Up @@ -271,6 +271,54 @@ describe('vitalCollection', () => {
})
})

describe('operation name character set', () => {
beforeEach(() => {
addExperimentalFeatures([ExperimentalFeature.FEATURE_OPERATION_VITAL])
spyOn(display, 'warn')
})
;['user login', 'api/v1', 'checkout:step', 'a,b', 'login!', 'login\ttwo', 'ログイン', 'login🔐'].forEach(
(invalidName) => {
it(`should warn but still emit on name outside the backend pattern: ${JSON.stringify(invalidName)}`, () => {
vitalCollection.addOperationStepVital(invalidName, 'start')

expect(rawRumEvents.length).toBe(1)
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.name).toBe(invalidName)
expect(display.warn).toHaveBeenCalledTimes(1)
expect((display.warn as jasmine.Spy).calls.mostRecent().args[0]).toContain('does not match')
expect((display.warn as jasmine.Spy).calls.mostRecent().args[0]).toContain('still be sent')
})
}
)
;['', ' ', '\t\n'].forEach((blankName) => {
it(`should reject and warn on blank name: ${JSON.stringify(blankName)}`, () => {
vitalCollection.addOperationStepVital(blankName, 'start')

expect(rawRumEvents.length).toBe(0)
expect(display.warn).toHaveBeenCalledTimes(1)
expect((display.warn as jasmine.Spy).calls.mostRecent().args[0]).toContain('cannot be empty or blank')
})
})
;['login', 'step42', 'login-v2', 'user_login', 'login.v2', 'login@prod', 'login$1', 'LoginV2'].forEach(
(validName) => {
it(`should accept name that matches the backend pattern without warning: ${JSON.stringify(validName)}`, () => {
vitalCollection.addOperationStepVital(validName, 'start')

expect(rawRumEvents.length).toBe(1)
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.name).toBe(validName)
expect(display.warn).not.toHaveBeenCalled()
})
}
)

it('should not restrict operationKey to the same character set', () => {
vitalCollection.addOperationStepVital('foo', 'start', { operationKey: 'session 42 / user foo' })

expect(rawRumEvents.length).toBe(1)
expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.operation_key).toBe('session 42 / user foo')
expect(display.warn).not.toHaveBeenCalled()
})
})

it('should create a duration vital from add API', () => {
vitalCollection.addDurationVital({
id: generateUUID(),
Expand Down
30 changes: 30 additions & 0 deletions packages/rum-core/src/domain/vital/vitalCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ClocksState, Duration } from '@datadog/browser-core'
import {
clocksNow,
combine,
display,
elapsed,
ExperimentalFeature,
generateUUID,
Expand Down Expand Up @@ -124,6 +125,10 @@ export function startVitalCollection(
return
}

if (!validateOperationName(name)) {
return
}

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

const vital: OperationStepVital = {
Expand Down Expand Up @@ -249,3 +254,28 @@ function processVital(vital: DurationVital | OperationStepVital): RawRumEventCol
domainContext: handlingStack ? { handlingStack } : {},
}
}

/**
* Blank / empty names are rejected (the backend rejects them with its own
* non-empty precondition before evaluating the character-set regex). Names
* that fail the backend's `[\w.@$-]*` character-set regex trigger a warning
* but the event is still emitted — the backend is the source of truth on
* character-set policy, so client-side drop would force a customer SDK bump
* if the rule is ever relaxed.
Comment thread
Valpertui marked this conversation as resolved.
*
* Returns `true` when the event should be emitted.
*/
const BACKEND_OPERATION_NAME_REGEX = /^[\w.@$-]*$/

function validateOperationName(name: string): boolean {
if (typeof name !== 'string' || name.trim().length === 0) {
display.warn('Feature operation name cannot be empty or blank. Event will not be sent.')
Copy link
Copy Markdown
Collaborator

@bcaudan bcaudan May 4, 2026

Choose a reason for hiding this comment

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

💬 suggestion: ‏Similar to what has been done on electron, we should probably display an error if we won't send the event.

return false
}
if (!BACKEND_OPERATION_NAME_REGEX.test(name)) {
display.warn(
`Feature operation name '${name}' does not match the backend-accepted pattern [\\w.@$-]* (letters, digits, _ . @ $ -). The event will still be sent and may be rejected by the backend.`
)
}
return true
}
Loading