|
1 | 1 | import type { Duration } from '@datadog/browser-core' |
2 | 2 | import { mockClock, type Clock } from '@datadog/browser-core/test' |
3 | | -import { addExperimentalFeatures, clocksNow, ExperimentalFeature, generateUUID } from '@datadog/browser-core' |
| 3 | +import { addExperimentalFeatures, clocksNow, display, ExperimentalFeature, generateUUID } from '@datadog/browser-core' |
4 | 4 | import { collectAndValidateRawRumEvents, mockPageStateHistory } from '../../../test' |
5 | 5 | import type { RawRumEvent, RawRumVitalEvent } from '../../rawRumEvent.types' |
6 | 6 | import { VitalType, RumEventType } from '../../rawRumEvent.types' |
@@ -271,6 +271,60 @@ describe('vitalCollection', () => { |
271 | 271 | }) |
272 | 272 | }) |
273 | 273 |
|
| 274 | + // Mirrors the backend's `[\w.@$-]*` server-side validation regex. Names |
| 275 | + // that fail the pattern generate a `display.warn` but the event is |
| 276 | + // still emitted — the backend is the single source of truth, so a |
| 277 | + // client-side drop would force a customer SDK bump if the policy is |
| 278 | + // ever relaxed. Blank/empty names are dropped with a warning instead, |
| 279 | + // matching the backend's own non-empty precondition. |
| 280 | + describe('operation name character set', () => { |
| 281 | + beforeEach(() => { |
| 282 | + addExperimentalFeatures([ExperimentalFeature.FEATURE_OPERATION_VITAL]) |
| 283 | + spyOn(display, 'warn') |
| 284 | + }) |
| 285 | + ;['user login', 'api/v1', 'checkout:step', 'a,b', 'login!', 'login\ttwo', 'ログイン', 'login🔐'].forEach( |
| 286 | + (invalidName) => { |
| 287 | + it(`should warn but still emit on name outside the backend pattern: ${JSON.stringify(invalidName)}`, () => { |
| 288 | + vitalCollection.addOperationStepVital(invalidName, 'start') |
| 289 | + |
| 290 | + expect(rawRumEvents.length).toBe(1) |
| 291 | + expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.name).toBe(invalidName) |
| 292 | + expect(display.warn).toHaveBeenCalledTimes(1) |
| 293 | + expect((display.warn as jasmine.Spy).calls.mostRecent().args[0]).toContain('does not match') |
| 294 | + expect((display.warn as jasmine.Spy).calls.mostRecent().args[0]).toContain('still be sent') |
| 295 | + }) |
| 296 | + } |
| 297 | + ) |
| 298 | + ;['', ' ', '\t\n'].forEach((blankName) => { |
| 299 | + it(`should reject and warn on blank name: ${JSON.stringify(blankName)}`, () => { |
| 300 | + vitalCollection.addOperationStepVital(blankName, 'start') |
| 301 | + |
| 302 | + expect(rawRumEvents.length).toBe(0) |
| 303 | + expect(display.warn).toHaveBeenCalledTimes(1) |
| 304 | + expect((display.warn as jasmine.Spy).calls.mostRecent().args[0]).toContain('cannot be empty or blank') |
| 305 | + }) |
| 306 | + }) |
| 307 | + ;['login', 'step42', 'login-v2', 'user_login', 'login.v2', 'login@prod', 'login$1', 'LoginV2'].forEach( |
| 308 | + (validName) => { |
| 309 | + it(`should accept name that matches the backend pattern without warning: ${JSON.stringify(validName)}`, () => { |
| 310 | + vitalCollection.addOperationStepVital(validName, 'start') |
| 311 | + |
| 312 | + expect(rawRumEvents.length).toBe(1) |
| 313 | + expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.name).toBe(validName) |
| 314 | + expect(display.warn).not.toHaveBeenCalled() |
| 315 | + }) |
| 316 | + } |
| 317 | + ) |
| 318 | + |
| 319 | + it('should not restrict operationKey to the same character set', () => { |
| 320 | + vitalCollection.addOperationStepVital('foo', 'start', { operationKey: 'session 42 / user foo' }) |
| 321 | + |
| 322 | + expect(rawRumEvents.length).toBe(1) |
| 323 | + expect((rawRumEvents[0].rawRumEvent as RawRumVitalEvent).vital.operation_key).toBe('session 42 / user foo') |
| 324 | + expect(display.warn).not.toHaveBeenCalled() |
| 325 | + }) |
| 326 | + }) |
| 327 | + |
274 | 328 | it('should create a duration vital from add API', () => { |
275 | 329 | vitalCollection.addDurationVital({ |
276 | 330 | id: generateUUID(), |
|
0 commit comments