diff --git a/packages/rum-core/src/domain/assembly.spec.ts b/packages/rum-core/src/domain/assembly.spec.ts index 52a8c15053..81c2460599 100644 --- a/packages/rum-core/src/domain/assembly.spec.ts +++ b/packages/rum-core/src/domain/assembly.spec.ts @@ -575,6 +575,69 @@ describe('rum assembly', () => { }) }) }) + + describe('STREAM event processing', () => { + it('should convert STREAM events to VIEW events', () => { + const { lifeCycle, serverRumEvents } = setupAssemblyTestWithDefaults({}) + + const streamData = { + id: 'stream-id-123', + document_version: 42, + time_spent: 5000000000, // 5 seconds in nanoseconds + } + + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.STREAM, { + stream: streamData, + view: { id: 'original-view-id', url: '/test' }, + }), + }) + + expect(serverRumEvents.length).toBe(1) + const resultEvent = serverRumEvents[0] + + expect(resultEvent.type).toBe('view') + }) + + it('should map stream properties correctly in converted VIEW event', () => { + const { lifeCycle, serverRumEvents } = setupAssemblyTestWithDefaults({}) + + const streamData = { + id: 'stream-id-456', + document_version: 25, + time_spent: 3000000000, // 3 seconds in nanoseconds + } + + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.STREAM, { + stream: streamData, + view: { id: 'original-view-id', url: '/test-page' }, + }), + }) + + expect(serverRumEvents.length).toBe(1) + const resultEvent = serverRumEvents[0] as any + + expect(resultEvent.stream).toBeDefined() + + // Check _dd.document_version is set from stream.document_version + expect(resultEvent._dd.document_version).toBe(25) + + // Check view.id is set from stream.id + expect(resultEvent.view.id).toBe('stream-id-456') + + // Check view.time_spent is set from stream.time_spent + expect(resultEvent.view.time_spent).toBe(3000000000) + + // Check stream.time_spent is undefined in the stream object + expect(resultEvent.stream.time_spent).toBeUndefined() + + // Check action/error/resource counts are set to 0 + expect(resultEvent.view.action.count).toBe(0) + expect(resultEvent.view.error.count).toBe(0) + expect(resultEvent.view.resource.count).toBe(0) + }) + }) }) function notifyRawRumEvent( diff --git a/packages/rum-core/src/domain/assembly.ts b/packages/rum-core/src/domain/assembly.ts index e5d5c5255d..63506251ca 100644 --- a/packages/rum-core/src/domain/assembly.ts +++ b/packages/rum-core/src/domain/assembly.ts @@ -13,6 +13,7 @@ import { import type { RumEventDomainContext } from '../domainContext.types' import type { AssembledRumEvent } from '../rawRumEvent.types' import { RumEventType } from '../rawRumEvent.types' +import type { RumViewEvent } from '../rumEvent.types' import type { LifeCycle } from './lifeCycle' import { LifeCycleEventType } from './lifeCycle' import type { RumConfiguration } from './configuration' @@ -86,6 +87,16 @@ export function startRumAssembly( ...VIEW_MODIFIABLE_FIELD_PATHS, ...ROOT_MODIFIABLE_FIELD_PATHS, }, + [RumEventType.STREAM]: { + ...USER_CUSTOMIZABLE_FIELD_PATHS, + ...VIEW_MODIFIABLE_FIELD_PATHS, + ...ROOT_MODIFIABLE_FIELD_PATHS, + }, + [RumEventType.TRANSITION]: { + ...USER_CUSTOMIZABLE_FIELD_PATHS, + ...VIEW_MODIFIABLE_FIELD_PATHS, + ...ROOT_MODIFIABLE_FIELD_PATHS, + }, } const eventRateLimiters = { [RumEventType.ERROR]: createEventRateLimiter( @@ -109,7 +120,7 @@ export function startRumAssembly( LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, ({ startTime, duration, rawRumEvent, domainContext }) => { const defaultRumEventAttributes = hooks.triggerHook(HookNames.Assemble, { - eventType: rawRumEvent.type, + eventType: rawRumEvent.type === 'stream' ? 'view' : rawRumEvent.type, startTime, duration, })! @@ -126,7 +137,40 @@ export function startRumAssembly( if (isEmptyObject(serverRumEvent.context!)) { delete serverRumEvent.context } - lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, serverRumEvent) + + if (rawRumEvent.type === 'stream') { + const streamEvent = { + ...(serverRumEvent as RumViewEvent), + _dd: { + ...serverRumEvent._dd, + document_version: serverRumEvent.stream?.document_version, + }, + stream: { + ...serverRumEvent.stream, + time_spent: undefined, + }, + view: { + ...serverRumEvent.view, + id: serverRumEvent.stream?.id, + is_active: true, + action: { + count: 0, + }, + error: { + count: 0, + }, + resource: { + count: 0, + }, + time_spent: serverRumEvent.stream?.time_spent, + }, + type: 'view', + } + + lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, streamEvent as AssembledRumEvent) + } else { + lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, serverRumEvent) + } } } ) diff --git a/packages/rum-core/src/domain/event/eventCollection.ts b/packages/rum-core/src/domain/event/eventCollection.ts index 2f10bf8593..6b100dcfd0 100644 --- a/packages/rum-core/src/domain/event/eventCollection.ts +++ b/packages/rum-core/src/domain/event/eventCollection.ts @@ -10,6 +10,7 @@ import type { RawRumLongTaskEvent, RawRumResourceEvent, RawRumVitalEvent, + RawRumStreamEvent, } from '../../rawRumEvent.types' import { RumEventType } from '../../rawRumEvent.types' @@ -18,6 +19,9 @@ const allowedEventTypes = [ RumEventType.ERROR, RumEventType.LONG_TASK, RumEventType.RESOURCE, + RumEventType.STREAM, + RumEventType.STREAM, + RumEventType.TRANSITION, RumEventType.VITAL, ] as const @@ -28,6 +32,7 @@ export type AllowedRawRumEvent = ( | RawRumLongAnimationFrameEvent | RawRumActionEvent | RawRumVitalEvent + | RawRumStreamEvent ) & { context?: Context } export function startEventCollection(lifeCycle: LifeCycle) { diff --git a/packages/rum-core/src/domain/trackEventCounts.ts b/packages/rum-core/src/domain/trackEventCounts.ts index c7a0cc856b..d9d4b43d95 100644 --- a/packages/rum-core/src/domain/trackEventCounts.ts +++ b/packages/rum-core/src/domain/trackEventCounts.ts @@ -30,7 +30,13 @@ export function trackEventCounts({ } const subscription = lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (event): void => { - if (event.type === 'view' || event.type === 'vital' || !isChildEvent(event)) { + if ( + event.type === 'view' || + event.type === 'vital' || + event.type === 'transition' || + !isChildEvent(event) || + ['stream'].includes(event.type) + ) { return } switch (event.type) { diff --git a/packages/rum-core/src/domainContext.types.ts b/packages/rum-core/src/domainContext.types.ts index b95870b70e..872aa659cd 100644 --- a/packages/rum-core/src/domainContext.types.ts +++ b/packages/rum-core/src/domainContext.types.ts @@ -16,7 +16,9 @@ export type RumEventDomainContext = T extends type ? RumLongTaskEventDomainContext : T extends typeof RumEventType.VITAL ? RumVitalEventDomainContext - : never + : T extends typeof RumEventType.STREAM + ? RumStreamEventDomainContext + : never export interface RumViewEventDomainContext { location: Readonly @@ -59,3 +61,6 @@ export interface RumLongTaskEventDomainContext { // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface RumVitalEventDomainContext {} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface RumStreamEventDomainContext {} diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 9229ae390b..7369d08ad4 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -17,6 +17,7 @@ import type { RumErrorEvent, RumLongTaskEvent, RumResourceEvent, + RumTransitionEvent, RumViewEvent, RumVitalEvent, } from './rumEvent.types' @@ -28,6 +29,8 @@ export const RumEventType = { VIEW: 'view', RESOURCE: 'resource', VITAL: 'vital', + STREAM: 'stream', + TRANSITION: 'transition', } as const export type RumEventType = (typeof RumEventType)[keyof typeof RumEventType] @@ -39,6 +42,7 @@ export type AssembledRumEvent = ( | RumErrorEvent | RumVitalEvent | RumLongTaskEvent + | RumTransitionEvent ) & Context @@ -331,6 +335,9 @@ export interface RawRumActionEvent { pointer_up_delay?: Duration } } + stream?: { + id: string + } context?: Context } @@ -377,6 +384,40 @@ export const VitalType = { export type VitalType = (typeof VitalType)[keyof typeof VitalType] +export interface RawRumStreamEvent { + date: TimeStamp + type: typeof RumEventType.STREAM + stream: { + id: string + bitrate?: number + document_version: number + duration?: number + format?: string + fps?: number + resolution?: string + time_spent: number + timestamp?: number + watch_time?: number + } +} + +export interface RawRumTransitionEvent { + date: TimeStamp + type: typeof RumEventType.TRANSITION + stream: { + id: string + } + transition: { + type: string + id?: string + timestamp?: number + buffer_starvation_duration?: number + media_start_delay?: number + error_code?: number + duration?: number + } +} + export type RawRumEvent = | RawRumErrorEvent | RawRumResourceEvent @@ -385,3 +426,5 @@ export type RawRumEvent = | RawRumLongAnimationFrameEvent | RawRumActionEvent | RawRumVitalEvent + | RawRumStreamEvent + | RawRumTransitionEvent diff --git a/packages/rum-core/test/fixtures.ts b/packages/rum-core/test/fixtures.ts index d5c15aceff..b2d1f57165 100644 --- a/packages/rum-core/test/fixtures.ts +++ b/packages/rum-core/test/fixtures.ts @@ -119,6 +119,45 @@ export function createRawRumEvent(type: RumEventType, overrides?: Context): RawR }, overrides ) + case RumEventType.STREAM: + return combine( + { + type, + _dd: { + document_version: 0, + }, + date: 0 as TimeStamp, + view: { + id: generateUUID(), + action: { count: 0 }, + error: { count: 0 }, + long_task: { count: 0 }, + resource: { count: 0 }, + time_spent: 0 as ServerDuration, + }, + stream: { + id: generateUUID(), + document_version: 0, + time_spent: 0 as ServerDuration, + }, + }, + overrides + ) + case RumEventType.TRANSITION: + return combine( + { + type, + date: 0 as TimeStamp, + stream: { + id: generateUUID(), + }, + transition: { + id: generateUUID(), + type: 'MEDIA_PLAYER_PLAY', + }, + }, + overrides + ) } }