From 73865127192d468b7230fce2a258b45c2eaadd63 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Thu, 7 Jul 2022 13:45:00 +0200 Subject: [PATCH 01/19] Recording dead and error clicks for session replay --- packages/rum/src/boot/startRecording.ts | 3 +- packages/rum/src/domain/record/observer.ts | 46 ++++++++++++++++++++-- packages/rum/src/domain/record/record.ts | 8 ++++ packages/rum/src/domain/record/utils.ts | 10 +++++ packages/rum/src/types/record.ts | 12 ++++++ 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/packages/rum/src/boot/startRecording.ts b/packages/rum/src/boot/startRecording.ts index 442c96121a..79ddec5cc3 100644 --- a/packages/rum/src/boot/startRecording.ts +++ b/packages/rum/src/boot/startRecording.ts @@ -19,7 +19,7 @@ export function startRecording( configuration: RumConfiguration, sessionManager: RumSessionManager, viewContexts: ViewContexts, - worker: DeflateWorker + worker: DeflateWorker, ) { if (isExperimentalFeatureEnabled('lower-batch-size')) { setSegmentBytesLimit(22 * ONE_KILO_BYTE) @@ -42,6 +42,7 @@ export function startRecording( } = record({ emit: addRecord, defaultPrivacyLevel: configuration.defaultPrivacyLevel, + lifeCycle }) const { unsubscribe: unsubscribeViewEnded } = lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, () => { diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index 6b0585476a..338ece87cb 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -1,4 +1,4 @@ -import type { DefaultPrivacyLevel } from '@datadog/browser-core' +import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core' import { instrumentSetter, instrumentMethodAndCallOriginal, @@ -10,7 +10,9 @@ import { addEventListener, noop, } from '@datadog/browser-core' -import { initViewportObservable } from '@datadog/browser-rum-core' +import type { LifeCycle, RumActionEventDomainContext} from '@datadog/browser-rum-core'; +import { initViewportObservable, LifeCycleEventType } from '@datadog/browser-rum-core' +import { ActionType, FrustrationType, RumEventType } from 'packages/rum-core/src/rawRumEvent.types' import { NodePrivacyLevel } from '../../constants' import type { InputState, @@ -23,11 +25,12 @@ import type { MediaInteraction, FocusRecord, VisualViewportRecord, + FrustrationRecord, } from '../../types' import { IncrementalSource, MediaInteractionType, MouseInteractionType } from '../../types' import { getNodePrivacyLevel, shouldMaskNode } from './privacy' import { getElementInputValue, getSerializedNodeId, hasSerializedNode } from './serializationUtils' -import { forEach, isTouchEvent } from './utils' +import { forEach, getFrustrationFromAction, isTouchEvent } from './utils' import type { MutationController } from './mutationObserver' import { startMutationObserver } from './mutationObserver' @@ -37,6 +40,16 @@ const MOUSE_MOVE_OBSERVER_THRESHOLD = 50 const SCROLL_OBSERVER_THRESHOLD = 100 const VISUAL_VIEWPORT_OBSERVER_THRESHOLD = 200 +const eventIdByEventPointer = new WeakMap() +let nextEventId = 0; + +function getRecordIdForEvent(event: Event): number { + if (!eventIdByEventPointer.has(event)) { + eventIdByEventPointer.set(event, nextEventId++) + } + return eventIdByEventPointer.get(event)! +} + type ListenerHandler = () => void type MousemoveCallBack = ( @@ -62,7 +75,10 @@ type FocusCallback = (data: FocusRecord['data']) => void type VisualViewportResizeCallback = (data: VisualViewportRecord['data']) => void +type FrustrationCallback = (data: FrustrationRecord['data'] & { timestamp: TimeStamp }) => void + interface ObserverParam { + lifeCycle: LifeCycle defaultPrivacyLevel: DefaultPrivacyLevel mutationController: MutationController mutationCb: MutationCallBack @@ -74,7 +90,8 @@ interface ObserverParam { inputCb: InputCallback mediaInteractionCb: MediaInteractionCallback styleSheetRuleCb: StyleSheetRuleCallback - focusCb: FocusCallback + focusCb: FocusCallback, + frustrationCb: FrustrationCallback, } export function initObservers(o: ObserverParam): ListenerHandler { @@ -88,6 +105,7 @@ export function initObservers(o: ObserverParam): ListenerHandler { const styleSheetObserver = initStyleSheetObserver(o.styleSheetRuleCb) const focusHandler = initFocusObserver(o.focusCb) const visualViewportResizeHandler = initVisualViewportResizeObserver(o.visualViewportResizeCb) + const frustartionHandler = initFrustrationObserver(o.lifeCycle, o.frustrationCb); return () => { mutationHandler() @@ -100,6 +118,7 @@ export function initObservers(o: ObserverParam): ListenerHandler { styleSheetObserver() focusHandler() visualViewportResizeHandler() + frustartionHandler() } } @@ -399,3 +418,22 @@ function initVisualViewportResizeObserver(cb: VisualViewportResizeCallback): Lis cancelThrottle() } } + +function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: FrustrationCallback): ListenerHandler { + return lifeCycle.subscribe(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, (data) => { + if ( + data.rawRumEvent.type === RumEventType.ACTION + && data.rawRumEvent.action.type === ActionType.CLICK + && data.rawRumEvent.action.frustration?.type + && 'event' in data.domainContext + && data.domainContext.event + ) { + const frustrationType = getFrustrationFromAction(data.rawRumEvent.action.frustration.type) + frustrationCb({ + timestamp: data.rawRumEvent.date, + frustrationType, + recordIds: frustrationType === FrustrationType.RAGE_CLICK ? [] : [getRecordIdForEvent(data.domainContext.event)] + }) + } + }).unsubscribe +} diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index c70e9c32cf..1545b16ebf 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -1,5 +1,6 @@ import { assign, timeStampNow } from '@datadog/browser-core' import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core' +import type { LifeCycle } from '@datadog/browser-rum-core'; import { getViewportDimension } from '@datadog/browser-rum-core' import type { IncrementalSnapshotRecord, @@ -24,6 +25,7 @@ import { getVisualViewport, getScrollX, getScrollY } from './viewports' export interface RecordOptions { emit?: (record: Record) => void defaultPrivacyLevel: DefaultPrivacyLevel + lifeCycle: LifeCycle } export interface RecordAPI { @@ -86,6 +88,7 @@ export function record(options: RecordOptions): RecordAPI { takeFullSnapshot() const stopObservers = initObservers({ + lifeCycle: options.lifeCycle, mutationController, defaultPrivacyLevel: options.defaultPrivacyLevel, inputCb: (v) => emit(assembleIncrementalSnapshot(IncrementalSource.Input, v)), @@ -99,6 +102,11 @@ export function record(options: RecordOptions): RecordAPI { styleSheetRuleCb: (r) => emit(assembleIncrementalSnapshot(IncrementalSource.StyleSheetRule, r)), viewportResizeCb: (d) => emit(assembleIncrementalSnapshot(IncrementalSource.ViewportResize, d)), + frustrationCb: (data) => emit({ + data: { frustrationType: data.frustrationType, recordIds: data.recordIds }, + type: RecordType.FrustrationRecord, + timestamp: data.timestamp, + }), focusCb: (data) => emit({ data, diff --git a/packages/rum/src/domain/record/utils.ts b/packages/rum/src/domain/record/utils.ts index 4151d30518..d15210d47b 100644 --- a/packages/rum/src/domain/record/utils.ts +++ b/packages/rum/src/domain/record/utils.ts @@ -1,3 +1,5 @@ +import { FrustrationType } from 'packages/rum-core/src/rawRumEvent.types' + export function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { return Boolean((event as TouchEvent).changedTouches) } @@ -8,3 +10,11 @@ export function forEach( ) { Array.prototype.forEach.call(list, callback as any) } + +export function getFrustrationFromAction(frustrations: FrustrationType[]): FrustrationType { + return frustrations.some((f) => f === FrustrationType.RAGE_CLICK) + ? FrustrationType.RAGE_CLICK + : frustrations.some((f) => f === FrustrationType.ERROR_CLICK) + ? FrustrationType.ERROR_CLICK + : FrustrationType.DEAD_CLICK +} diff --git a/packages/rum/src/types/record.ts b/packages/rum/src/types/record.ts index e6c050457a..e7b8c3b128 100644 --- a/packages/rum/src/types/record.ts +++ b/packages/rum/src/types/record.ts @@ -1,4 +1,5 @@ import type { TimeStamp } from '@datadog/browser-core' +import type { FrustrationType } from 'packages/rum-core/src/rawRumEvent.types' import type { SerializedNodeWithId } from './serializedNode' export type Record = @@ -8,6 +9,7 @@ export type Record = | FocusRecord | ViewEndRecord | VisualViewportRecord + | FrustrationRecord export const RecordType = { FullSnapshot: 2, @@ -16,6 +18,7 @@ export const RecordType = { Focus: 6, ViewEnd: 7, VisualViewport: 8, + FrustrationRecord: 9, } as const export type RecordType = typeof RecordType[keyof typeof RecordType] @@ -38,6 +41,15 @@ export interface IncrementalSnapshotRecord { data: IncrementalData } +export interface FrustrationRecord { + type: typeof RecordType.FrustrationRecord, + timestamp: TimeStamp, + data: { + frustrationType: FrustrationType, + recordIds: number[], + } +} + export interface MetaRecord { type: typeof RecordType.Meta timestamp: TimeStamp From 2fc12b1063a002246aa3b4789cc41399bc202a83 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Thu, 7 Jul 2022 15:15:05 +0200 Subject: [PATCH 02/19] Add record type to incremental snapshot with mouseInteraction source --- packages/rum/src/domain/record/observer.ts | 15 ++++++++------- packages/rum/src/domain/record/record.spec.ts | 2 ++ packages/rum/src/domain/record/record.ts | 5 ++++- packages/rum/src/types/record.ts | 1 + 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index 338ece87cb..dd981248a5 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -40,14 +40,14 @@ const MOUSE_MOVE_OBSERVER_THRESHOLD = 50 const SCROLL_OBSERVER_THRESHOLD = 100 const VISUAL_VIEWPORT_OBSERVER_THRESHOLD = 200 -const eventIdByEventPointer = new WeakMap() -let nextEventId = 0; +const recordIds = new WeakMap() +let nextId = 1; function getRecordIdForEvent(event: Event): number { - if (!eventIdByEventPointer.has(event)) { - eventIdByEventPointer.set(event, nextEventId++) + if (!recordIds.has(event)) { + recordIds.set(event, nextId++) } - return eventIdByEventPointer.get(event)! + return recordIds.get(event)! } type ListenerHandler = () => void @@ -59,7 +59,7 @@ type MousemoveCallBack = ( export type MutationCallBack = (m: MutationPayload) => void -type MouseInteractionCallBack = (d: MouseInteraction) => void +type MouseInteractionCallBack = (d: MouseInteraction & { recordId: number }) => void type ScrollCallback = (p: ScrollPosition) => void @@ -183,8 +183,9 @@ function initMouseInteractionObserver( return } const { clientX, clientY } = isTouchEvent(event) ? event.changedTouches[0] : event - const position: MouseInteraction = { + const position: MouseInteraction & { recordId: number } = { id: getSerializedNodeId(target), + recordId: getRecordIdForEvent(event), type: eventTypeToMouseInteraction[event.type as keyof typeof eventTypeToMouseInteraction], x: clientX, y: clientY, diff --git a/packages/rum/src/domain/record/record.spec.ts b/packages/rum/src/domain/record/record.spec.ts index e37546be97..cf3e91a7e3 100644 --- a/packages/rum/src/domain/record/record.spec.ts +++ b/packages/rum/src/domain/record/record.spec.ts @@ -1,4 +1,5 @@ import { DefaultPrivacyLevel, isIE } from '@datadog/browser-core' +import { LifeCycle } from '@datadog/browser-rum-core' import type { Clock } from '../../../../core/test/specHelper' import { createNewEvent } from '../../../../core/test/specHelper' import { collectAsyncCalls, recordsPerFullSnapshot } from '../../../test/utils' @@ -198,6 +199,7 @@ describe('record', () => { recordApi = record({ emit: emitSpy, defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, + lifeCycle: new LifeCycle() }) } diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index 1545b16ebf..465cad9614 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -95,7 +95,10 @@ export function record(options: RecordOptions): RecordAPI { mediaInteractionCb: (p) => emit(assembleIncrementalSnapshot(IncrementalSource.MediaInteraction, p)), mouseInteractionCb: (d) => - emit(assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, d)), + emit(assign( + { recordId: d.recordId }, + assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, d) + )), mousemoveCb: (positions, source) => emit(assembleIncrementalSnapshot(source, { positions })), mutationCb: (m) => emit(assembleIncrementalSnapshot(IncrementalSource.Mutation, m)), scrollCb: (p) => emit(assembleIncrementalSnapshot(IncrementalSource.Scroll, p)), diff --git a/packages/rum/src/types/record.ts b/packages/rum/src/types/record.ts index e7b8c3b128..19f07ef341 100644 --- a/packages/rum/src/types/record.ts +++ b/packages/rum/src/types/record.ts @@ -39,6 +39,7 @@ export interface IncrementalSnapshotRecord { type: typeof RecordType.IncrementalSnapshot timestamp: TimeStamp data: IncrementalData + recordId?: number } export interface FrustrationRecord { From 57c10485758b9fded83464cbea908d90024bd810 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Thu, 7 Jul 2022 17:03:52 +0200 Subject: [PATCH 03/19] Fix linter errors --- packages/rum/src/domain/record/observer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index dd981248a5..d6c87eb25c 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -10,7 +10,7 @@ import { addEventListener, noop, } from '@datadog/browser-core' -import type { LifeCycle, RumActionEventDomainContext} from '@datadog/browser-rum-core'; +import type { LifeCycle } from '@datadog/browser-rum-core'; import { initViewportObservable, LifeCycleEventType } from '@datadog/browser-rum-core' import { ActionType, FrustrationType, RumEventType } from 'packages/rum-core/src/rawRumEvent.types' import { NodePrivacyLevel } from '../../constants' @@ -424,6 +424,7 @@ function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: Frustratio return lifeCycle.subscribe(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, (data) => { if ( data.rawRumEvent.type === RumEventType.ACTION + && 'action' in data.rawRumEvent && data.rawRumEvent.action.type === ActionType.CLICK && data.rawRumEvent.action.frustration?.type && 'event' in data.domainContext From 04f317c63ec74e68443ce44cce2a304bf6ca179a Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Thu, 7 Jul 2022 18:11:54 +0200 Subject: [PATCH 04/19] Solve ci issues --- packages/rum-core/src/index.ts | 2 +- packages/rum/src/domain/record/observer.ts | 12 ++++++++---- packages/rum/src/domain/record/utils.ts | 2 +- packages/rum/src/types/record.ts | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/rum-core/src/index.ts b/packages/rum-core/src/index.ts index bae4de32b8..059d5afe96 100644 --- a/packages/rum-core/src/index.ts +++ b/packages/rum-core/src/index.ts @@ -18,7 +18,7 @@ export { RumViewEventDomainContext, RumEventDomainContext, } from './domainContext.types' -export { CommonContext, ReplayStats } from './rawRumEvent.types' +export { CommonContext, ReplayStats, ActionType, RumEventType, FrustrationType } from './rawRumEvent.types' export { startRum } from './boot/startRum' export { LifeCycle, LifeCycleEventType } from './domain/lifeCycle' export { ViewCreatedEvent } from './domain/rumEventsCollection/view/trackViews' diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index d6c87eb25c..7919402c2d 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -10,9 +10,14 @@ import { addEventListener, noop, } from '@datadog/browser-core' -import type { LifeCycle } from '@datadog/browser-rum-core'; -import { initViewportObservable, LifeCycleEventType } from '@datadog/browser-rum-core' -import { ActionType, FrustrationType, RumEventType } from 'packages/rum-core/src/rawRumEvent.types' +import type { LifeCycle} from '@datadog/browser-rum-core' +import { + initViewportObservable, + ActionType, + FrustrationType, + RumEventType, + LifeCycleEventType, +} from '@datadog/browser-rum-core' import { NodePrivacyLevel } from '../../constants' import type { InputState, @@ -424,7 +429,6 @@ function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: Frustratio return lifeCycle.subscribe(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, (data) => { if ( data.rawRumEvent.type === RumEventType.ACTION - && 'action' in data.rawRumEvent && data.rawRumEvent.action.type === ActionType.CLICK && data.rawRumEvent.action.frustration?.type && 'event' in data.domainContext diff --git a/packages/rum/src/domain/record/utils.ts b/packages/rum/src/domain/record/utils.ts index d15210d47b..ef50802321 100644 --- a/packages/rum/src/domain/record/utils.ts +++ b/packages/rum/src/domain/record/utils.ts @@ -1,4 +1,4 @@ -import { FrustrationType } from 'packages/rum-core/src/rawRumEvent.types' +import { FrustrationType } from '@datadog/browser-rum-core' export function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { return Boolean((event as TouchEvent).changedTouches) diff --git a/packages/rum/src/types/record.ts b/packages/rum/src/types/record.ts index 19f07ef341..8d05faf5cb 100644 --- a/packages/rum/src/types/record.ts +++ b/packages/rum/src/types/record.ts @@ -1,5 +1,5 @@ import type { TimeStamp } from '@datadog/browser-core' -import type { FrustrationType } from 'packages/rum-core/src/rawRumEvent.types' +import type { FrustrationType } from '@datadog/browser-rum-core' import type { SerializedNodeWithId } from './serializedNode' export type Record = From a18b53ffbd3d0ab84cca3ad3ed32e907f3b10a82 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Fri, 8 Jul 2022 10:21:11 +0200 Subject: [PATCH 05/19] Fix prettier errors --- packages/rum/src/boot/startRecording.ts | 4 +-- packages/rum/src/domain/record/observer.ts | 25 ++++++++++--------- packages/rum/src/domain/record/record.spec.ts | 2 +- packages/rum/src/domain/record/record.ts | 23 +++++++++-------- packages/rum/src/domain/record/utils.ts | 4 +-- packages/rum/src/types/record.ts | 8 +++--- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/rum/src/boot/startRecording.ts b/packages/rum/src/boot/startRecording.ts index 79ddec5cc3..5549ac3284 100644 --- a/packages/rum/src/boot/startRecording.ts +++ b/packages/rum/src/boot/startRecording.ts @@ -19,7 +19,7 @@ export function startRecording( configuration: RumConfiguration, sessionManager: RumSessionManager, viewContexts: ViewContexts, - worker: DeflateWorker, + worker: DeflateWorker ) { if (isExperimentalFeatureEnabled('lower-batch-size')) { setSegmentBytesLimit(22 * ONE_KILO_BYTE) @@ -42,7 +42,7 @@ export function startRecording( } = record({ emit: addRecord, defaultPrivacyLevel: configuration.defaultPrivacyLevel, - lifeCycle + lifeCycle, }) const { unsubscribe: unsubscribeViewEnded } = lifeCycle.subscribe(LifeCycleEventType.VIEW_ENDED, () => { diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index 7919402c2d..36d6dde1c5 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -10,7 +10,7 @@ import { addEventListener, noop, } from '@datadog/browser-core' -import type { LifeCycle} from '@datadog/browser-rum-core' +import type { LifeCycle } from '@datadog/browser-rum-core' import { initViewportObservable, ActionType, @@ -46,7 +46,7 @@ const SCROLL_OBSERVER_THRESHOLD = 100 const VISUAL_VIEWPORT_OBSERVER_THRESHOLD = 200 const recordIds = new WeakMap() -let nextId = 1; +let nextId = 1 function getRecordIdForEvent(event: Event): number { if (!recordIds.has(event)) { @@ -95,8 +95,8 @@ interface ObserverParam { inputCb: InputCallback mediaInteractionCb: MediaInteractionCallback styleSheetRuleCb: StyleSheetRuleCallback - focusCb: FocusCallback, - frustrationCb: FrustrationCallback, + focusCb: FocusCallback + frustrationCb: FrustrationCallback } export function initObservers(o: ObserverParam): ListenerHandler { @@ -110,7 +110,7 @@ export function initObservers(o: ObserverParam): ListenerHandler { const styleSheetObserver = initStyleSheetObserver(o.styleSheetRuleCb) const focusHandler = initFocusObserver(o.focusCb) const visualViewportResizeHandler = initVisualViewportResizeObserver(o.visualViewportResizeCb) - const frustartionHandler = initFrustrationObserver(o.lifeCycle, o.frustrationCb); + const frustartionHandler = initFrustrationObserver(o.lifeCycle, o.frustrationCb) return () => { mutationHandler() @@ -428,17 +428,18 @@ function initVisualViewportResizeObserver(cb: VisualViewportResizeCallback): Lis function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: FrustrationCallback): ListenerHandler { return lifeCycle.subscribe(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, (data) => { if ( - data.rawRumEvent.type === RumEventType.ACTION - && data.rawRumEvent.action.type === ActionType.CLICK - && data.rawRumEvent.action.frustration?.type - && 'event' in data.domainContext - && data.domainContext.event + data.rawRumEvent.type === RumEventType.ACTION && + data.rawRumEvent.action.type === ActionType.CLICK && + data.rawRumEvent.action.frustration?.type && + 'event' in data.domainContext && + data.domainContext.event ) { - const frustrationType = getFrustrationFromAction(data.rawRumEvent.action.frustration.type) + const frustrationType = getFrustrationFromAction(data.rawRumEvent.action.frustration.type) frustrationCb({ timestamp: data.rawRumEvent.date, frustrationType, - recordIds: frustrationType === FrustrationType.RAGE_CLICK ? [] : [getRecordIdForEvent(data.domainContext.event)] + recordIds: + frustrationType === FrustrationType.RAGE_CLICK ? [] : [getRecordIdForEvent(data.domainContext.event)], }) } }).unsubscribe diff --git a/packages/rum/src/domain/record/record.spec.ts b/packages/rum/src/domain/record/record.spec.ts index cf3e91a7e3..be5521b027 100644 --- a/packages/rum/src/domain/record/record.spec.ts +++ b/packages/rum/src/domain/record/record.spec.ts @@ -199,7 +199,7 @@ describe('record', () => { recordApi = record({ emit: emitSpy, defaultPrivacyLevel: DefaultPrivacyLevel.ALLOW, - lifeCycle: new LifeCycle() + lifeCycle: new LifeCycle(), }) } diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index 465cad9614..d826d27441 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -1,6 +1,6 @@ import { assign, timeStampNow } from '@datadog/browser-core' import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core' -import type { LifeCycle } from '@datadog/browser-rum-core'; +import type { LifeCycle } from '@datadog/browser-rum-core' import { getViewportDimension } from '@datadog/browser-rum-core' import type { IncrementalSnapshotRecord, @@ -95,21 +95,24 @@ export function record(options: RecordOptions): RecordAPI { mediaInteractionCb: (p) => emit(assembleIncrementalSnapshot(IncrementalSource.MediaInteraction, p)), mouseInteractionCb: (d) => - emit(assign( - { recordId: d.recordId }, - assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, d) - )), + emit( + assign( + { recordId: d.recordId }, + assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, d) + ) + ), mousemoveCb: (positions, source) => emit(assembleIncrementalSnapshot(source, { positions })), mutationCb: (m) => emit(assembleIncrementalSnapshot(IncrementalSource.Mutation, m)), scrollCb: (p) => emit(assembleIncrementalSnapshot(IncrementalSource.Scroll, p)), styleSheetRuleCb: (r) => emit(assembleIncrementalSnapshot(IncrementalSource.StyleSheetRule, r)), viewportResizeCb: (d) => emit(assembleIncrementalSnapshot(IncrementalSource.ViewportResize, d)), - frustrationCb: (data) => emit({ - data: { frustrationType: data.frustrationType, recordIds: data.recordIds }, - type: RecordType.FrustrationRecord, - timestamp: data.timestamp, - }), + frustrationCb: (data) => + emit({ + data: { frustrationType: data.frustrationType, recordIds: data.recordIds }, + type: RecordType.FrustrationRecord, + timestamp: data.timestamp, + }), focusCb: (data) => emit({ data, diff --git a/packages/rum/src/domain/record/utils.ts b/packages/rum/src/domain/record/utils.ts index ef50802321..1e937d3b22 100644 --- a/packages/rum/src/domain/record/utils.ts +++ b/packages/rum/src/domain/record/utils.ts @@ -15,6 +15,6 @@ export function getFrustrationFromAction(frustrations: FrustrationType[]): Frust return frustrations.some((f) => f === FrustrationType.RAGE_CLICK) ? FrustrationType.RAGE_CLICK : frustrations.some((f) => f === FrustrationType.ERROR_CLICK) - ? FrustrationType.ERROR_CLICK - : FrustrationType.DEAD_CLICK + ? FrustrationType.ERROR_CLICK + : FrustrationType.DEAD_CLICK } diff --git a/packages/rum/src/types/record.ts b/packages/rum/src/types/record.ts index 8d05faf5cb..b84dc2f6b2 100644 --- a/packages/rum/src/types/record.ts +++ b/packages/rum/src/types/record.ts @@ -43,11 +43,11 @@ export interface IncrementalSnapshotRecord { } export interface FrustrationRecord { - type: typeof RecordType.FrustrationRecord, - timestamp: TimeStamp, + type: typeof RecordType.FrustrationRecord + timestamp: TimeStamp data: { - frustrationType: FrustrationType, - recordIds: number[], + frustrationType: FrustrationType + recordIds: number[] } } From 2ab1f9bc9586e690538f4352e07cbbdef09e3962 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Fri, 8 Jul 2022 13:44:49 +0200 Subject: [PATCH 06/19] Change segment start date to the timestamp of the earliest record & Review --- packages/rum/src/domain/record/observer.ts | 38 ++++++++++--------- packages/rum/src/domain/record/record.ts | 37 ++---------------- packages/rum/src/domain/record/utils.ts | 23 ++++++++++- .../src/domain/segmentCollection/segment.ts | 7 +++- packages/rum/src/types/record.ts | 2 +- 5 files changed, 52 insertions(+), 55 deletions(-) diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index 36d6dde1c5..d4e4141ba9 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -1,4 +1,4 @@ -import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core' +import type { DefaultPrivacyLevel } from '@datadog/browser-core' import { instrumentSetter, instrumentMethodAndCallOriginal, @@ -11,13 +11,7 @@ import { noop, } from '@datadog/browser-core' import type { LifeCycle } from '@datadog/browser-rum-core' -import { - initViewportObservable, - ActionType, - FrustrationType, - RumEventType, - LifeCycleEventType, -} from '@datadog/browser-rum-core' +import { initViewportObservable, ActionType, RumEventType, LifeCycleEventType } from '@datadog/browser-rum-core' import { NodePrivacyLevel } from '../../constants' import type { InputState, @@ -31,11 +25,13 @@ import type { FocusRecord, VisualViewportRecord, FrustrationRecord, + IncrementalSnapshotRecord, + MouseInteractionData, } from '../../types' -import { IncrementalSource, MediaInteractionType, MouseInteractionType } from '../../types' +import { RecordType, IncrementalSource, MediaInteractionType, MouseInteractionType } from '../../types' import { getNodePrivacyLevel, shouldMaskNode } from './privacy' import { getElementInputValue, getSerializedNodeId, hasSerializedNode } from './serializationUtils' -import { forEach, getFrustrationFromAction, isTouchEvent } from './utils' +import { assembleIncrementalSnapshot, forEach, getFrustrationFromAction, isTouchEvent } from './utils' import type { MutationController } from './mutationObserver' import { startMutationObserver } from './mutationObserver' @@ -64,7 +60,7 @@ type MousemoveCallBack = ( export type MutationCallBack = (m: MutationPayload) => void -type MouseInteractionCallBack = (d: MouseInteraction & { recordId: number }) => void +type MouseInteractionCallBack = (record: IncrementalSnapshotRecord) => void type ScrollCallback = (p: ScrollPosition) => void @@ -80,7 +76,7 @@ type FocusCallback = (data: FocusRecord['data']) => void type VisualViewportResizeCallback = (data: VisualViewportRecord['data']) => void -type FrustrationCallback = (data: FrustrationRecord['data'] & { timestamp: TimeStamp }) => void +type FrustrationCallback = (record: FrustrationRecord) => void interface ObserverParam { lifeCycle: LifeCycle @@ -188,9 +184,8 @@ function initMouseInteractionObserver( return } const { clientX, clientY } = isTouchEvent(event) ? event.changedTouches[0] : event - const position: MouseInteraction & { recordId: number } = { + const position: MouseInteraction = { id: getSerializedNodeId(target), - recordId: getRecordIdForEvent(event), type: eventTypeToMouseInteraction[event.type as keyof typeof eventTypeToMouseInteraction], x: clientX, y: clientY, @@ -200,7 +195,12 @@ function initMouseInteractionObserver( position.x = visualViewportX position.y = visualViewportY } - cb(position) + + const record = assign( + { id: getRecordIdForEvent(event) }, + assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, position) + ) + cb(record) } return addEventListeners(document, Object.keys(eventTypeToMouseInteraction) as DOM_EVENT[], handler, { capture: true, @@ -437,9 +437,11 @@ function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: Frustratio const frustrationType = getFrustrationFromAction(data.rawRumEvent.action.frustration.type) frustrationCb({ timestamp: data.rawRumEvent.date, - frustrationType, - recordIds: - frustrationType === FrustrationType.RAGE_CLICK ? [] : [getRecordIdForEvent(data.domainContext.event)], + type: RecordType.FrustrationRecord, + data: { + frustrationType, + recordIds: [getRecordIdForEvent(data.domainContext.event)], + }, }) } }).unsubscribe diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index d826d27441..504124288f 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -1,13 +1,10 @@ -import { assign, timeStampNow } from '@datadog/browser-core' +import { timeStampNow } from '@datadog/browser-core' import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core' import type { LifeCycle } from '@datadog/browser-rum-core' import { getViewportDimension } from '@datadog/browser-rum-core' import type { - IncrementalSnapshotRecord, - IncrementalData, InputData, MediaInteractionData, - MouseInteractionData, MousemoveData, MutationData, ScrollData, @@ -21,6 +18,7 @@ import { initObservers } from './observer' import { MutationController } from './mutationObserver' import { getVisualViewport, getScrollX, getScrollY } from './viewports' +import { assembleIncrementalSnapshot } from './utils' export interface RecordOptions { emit?: (record: Record) => void @@ -94,25 +92,14 @@ export function record(options: RecordOptions): RecordAPI { inputCb: (v) => emit(assembleIncrementalSnapshot(IncrementalSource.Input, v)), mediaInteractionCb: (p) => emit(assembleIncrementalSnapshot(IncrementalSource.MediaInteraction, p)), - mouseInteractionCb: (d) => - emit( - assign( - { recordId: d.recordId }, - assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, d) - ) - ), + mouseInteractionCb: (mouseInteractionRecord) => emit(mouseInteractionRecord), mousemoveCb: (positions, source) => emit(assembleIncrementalSnapshot(source, { positions })), mutationCb: (m) => emit(assembleIncrementalSnapshot(IncrementalSource.Mutation, m)), scrollCb: (p) => emit(assembleIncrementalSnapshot(IncrementalSource.Scroll, p)), styleSheetRuleCb: (r) => emit(assembleIncrementalSnapshot(IncrementalSource.StyleSheetRule, r)), viewportResizeCb: (d) => emit(assembleIncrementalSnapshot(IncrementalSource.ViewportResize, d)), - frustrationCb: (data) => - emit({ - data: { frustrationType: data.frustrationType, recordIds: data.recordIds }, - type: RecordType.FrustrationRecord, - timestamp: data.timestamp, - }), + frustrationCb: (frustrationRecord) => emit(frustrationRecord), focusCb: (data) => emit({ data, @@ -134,19 +121,3 @@ export function record(options: RecordOptions): RecordAPI { flushMutations: () => mutationController.flush(), } } - -function assembleIncrementalSnapshot( - source: Data['source'], - data: Omit -): IncrementalSnapshotRecord { - return { - data: assign( - { - source, - }, - data - ) as Data, - type: RecordType.IncrementalSnapshot, - timestamp: timeStampNow(), - } -} diff --git a/packages/rum/src/domain/record/utils.ts b/packages/rum/src/domain/record/utils.ts index 1e937d3b22..12b580f26f 100644 --- a/packages/rum/src/domain/record/utils.ts +++ b/packages/rum/src/domain/record/utils.ts @@ -1,4 +1,7 @@ +import { assign, includes, timeStampNow } from '@datadog/browser-core' import { FrustrationType } from '@datadog/browser-rum-core' +import type { IncrementalData, IncrementalSnapshotRecord } from '../../types' +import { RecordType } from '../../types' export function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { return Boolean((event as TouchEvent).changedTouches) @@ -12,9 +15,25 @@ export function forEach( } export function getFrustrationFromAction(frustrations: FrustrationType[]): FrustrationType { - return frustrations.some((f) => f === FrustrationType.RAGE_CLICK) + return includes(frustrations, FrustrationType.RAGE_CLICK) ? FrustrationType.RAGE_CLICK - : frustrations.some((f) => f === FrustrationType.ERROR_CLICK) + : includes(frustrations, FrustrationType.ERROR_CLICK) ? FrustrationType.ERROR_CLICK : FrustrationType.DEAD_CLICK } + +export function assembleIncrementalSnapshot( + source: Data['source'], + data: Omit +): IncrementalSnapshotRecord { + return { + data: assign( + { + source, + }, + data + ) as Data, + type: RecordType.IncrementalSnapshot, + timestamp: timeStampNow(), + } +} diff --git a/packages/rum/src/domain/segmentCollection/segment.ts b/packages/rum/src/domain/segmentCollection/segment.ts index 98d4c8315c..e35c42ed29 100644 --- a/packages/rum/src/domain/segmentCollection/segment.ts +++ b/packages/rum/src/domain/segmentCollection/segment.ts @@ -71,7 +71,12 @@ export class Segment { } addRecord(record: Record): void { - this.metadata.end = record.timestamp + if (record.timestamp < this.metadata.start) { + this.metadata.start = record.timestamp + } + if (this.metadata.end < record.timestamp) { + this.metadata.end = record.timestamp + } this.metadata.records_count += 1 replayStats.addRecord(this.metadata.view.id) this.metadata.has_full_snapshot ||= record.type === RecordType.FullSnapshot diff --git a/packages/rum/src/types/record.ts b/packages/rum/src/types/record.ts index b84dc2f6b2..d170932718 100644 --- a/packages/rum/src/types/record.ts +++ b/packages/rum/src/types/record.ts @@ -39,7 +39,7 @@ export interface IncrementalSnapshotRecord { type: typeof RecordType.IncrementalSnapshot timestamp: TimeStamp data: IncrementalData - recordId?: number + id?: number } export interface FrustrationRecord { From 7cdc8f410da6ad949701aeb2f9017c273d1fd517 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Fri, 8 Jul 2022 14:08:09 +0200 Subject: [PATCH 07/19] Adding more tests for segment collection --- .../src/domain/segmentCollection/segment.spec.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/rum/src/domain/segmentCollection/segment.spec.ts b/packages/rum/src/domain/segmentCollection/segment.spec.ts index 487ac755c0..03beff7fe7 100644 --- a/packages/rum/src/domain/segmentCollection/segment.spec.ts +++ b/packages/rum/src/domain/segmentCollection/segment.spec.ts @@ -131,11 +131,19 @@ describe('Segment', () => { it('increments records_count', () => { expect(segment.metadata.records_count).toBe(2) }) - it('increases end timestamp', () => { + it('does not change start timestamp when receiving a later record', () => { + expect(segment.metadata.start).toBe(10) + }) + it('should change the start timestamp when receiving an earlier record', () => { + segment.addRecord({ type: RecordType.ViewEnd, timestamp: 5 as TimeStamp }) + expect(segment.metadata.start).toBe(5) + }) + it('increases end timestamp when receiving a later record', () => { expect(segment.metadata.end).toBe(15) }) - it('does not change start timestamp', () => { - expect(segment.metadata.start).toBe(10) + it('should not change the end timestamp when receiving an earlier record', () => { + segment.addRecord({ type: RecordType.ViewEnd, timestamp: 5 as TimeStamp }) + expect(segment.metadata.end).toBe(15) }) }) From 88432be2bff60e6488762eef6005cff02a456f0f Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Mon, 11 Jul 2022 13:50:03 +0200 Subject: [PATCH 08/19] Record rage clicks for session replay --- .../domain/rumEventsCollection/action/actionCollection.ts | 2 +- .../domain/rumEventsCollection/action/trackClickActions.ts | 6 ++++-- packages/rum-core/src/domainContext.types.ts | 1 + packages/rum/src/domain/record/observer.ts | 7 +++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts index dc80fb21e5..8f6f617d71 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts @@ -101,7 +101,7 @@ function processAction( customerContext, rawRumEvent: actionEvent, startTime: action.startClocks.relative, - domainContext: isAutoAction(action) ? { event: action.event } : {}, + domainContext: isAutoAction(action) ? { event: action.event, eventsSequence: action.eventsSequence } : {}, } } diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts index 8d885381ab..b1fb12d08a 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts @@ -48,6 +48,7 @@ export interface ClickAction { counts: ActionCounts event: MouseEvent & { target: Element } frustrationTypes: FrustrationType[] + eventsSequence?: Event[] } export interface ActionContexts { @@ -247,7 +248,7 @@ function newClick( clone: () => newClick(lifeCycle, history, getUserActivity, base), - validate: () => { + validate: (eventsSequence?: Event[]) => { stop() if (status !== ClickStatus.STOPPED) { return @@ -267,6 +268,7 @@ function newClick( errorCount, longTaskCount, }, + eventsSequence, }, base ) @@ -286,7 +288,7 @@ export function finalizeClicks(clicks: Click[], rageClick: Click) { if (isRage) { clicks.forEach((click) => click.discard()) rageClick.stop(timeStampNow()) - rageClick.validate() + rageClick.validate(clicks.map((click) => click.event)) } else { rageClick.discard() clicks.forEach((click) => click.validate()) diff --git a/packages/rum-core/src/domainContext.types.ts b/packages/rum-core/src/domainContext.types.ts index b1edaaabe8..16fb530842 100644 --- a/packages/rum-core/src/domainContext.types.ts +++ b/packages/rum-core/src/domainContext.types.ts @@ -22,6 +22,7 @@ export interface RumViewEventDomainContext { export interface RumActionEventDomainContext { event?: Event + eventsSequence?: Event[] } export interface RumFetchResourceEventDomainContext { diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index d4e4141ba9..36dbcd9ce1 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -10,7 +10,7 @@ import { addEventListener, noop, } from '@datadog/browser-core' -import type { LifeCycle } from '@datadog/browser-rum-core' +import { FrustrationType, LifeCycle } from '@datadog/browser-rum-core' import { initViewportObservable, ActionType, RumEventType, LifeCycleEventType } from '@datadog/browser-rum-core' import { NodePrivacyLevel } from '../../constants' import type { @@ -440,7 +440,10 @@ function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: Frustratio type: RecordType.FrustrationRecord, data: { frustrationType, - recordIds: [getRecordIdForEvent(data.domainContext.event)], + recordIds: + frustrationType === FrustrationType.RAGE_CLICK && data.domainContext.eventsSequence + ? data.domainContext.eventsSequence.map((e) => getRecordIdForEvent(e)) + : [getRecordIdForEvent(data.domainContext.event)], }, }) } From cd1b477353e6a2f1d3a723db63b16f1009f2a387 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Mon, 11 Jul 2022 13:50:56 +0200 Subject: [PATCH 09/19] Fix linter --- packages/rum/src/domain/record/observer.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index 36dbcd9ce1..0cbc1fe794 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -10,8 +10,14 @@ import { addEventListener, noop, } from '@datadog/browser-core' -import { FrustrationType, LifeCycle } from '@datadog/browser-rum-core' -import { initViewportObservable, ActionType, RumEventType, LifeCycleEventType } from '@datadog/browser-rum-core' +import type { LifeCycle } from '@datadog/browser-rum-core'; +import { + FrustrationType, + initViewportObservable, + ActionType, + RumEventType, + LifeCycleEventType, +} from '@datadog/browser-rum-core' import { NodePrivacyLevel } from '../../constants' import type { InputState, From be88297a4a7e812cb9bdc5cb00dbd67968f6a3e9 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Mon, 11 Jul 2022 13:55:13 +0200 Subject: [PATCH 10/19] Fix format job --- packages/rum/src/domain/record/observer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index 0cbc1fe794..22b900a44f 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -10,7 +10,7 @@ import { addEventListener, noop, } from '@datadog/browser-core' -import type { LifeCycle } from '@datadog/browser-rum-core'; +import type { LifeCycle } from '@datadog/browser-rum-core' import { FrustrationType, initViewportObservable, From a8d0e86f700c8a840ab574515087e6bbc591def2 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Mon, 11 Jul 2022 14:13:26 +0200 Subject: [PATCH 11/19] Fix and add more tests --- .../action/actionCollection.spec.ts | 1 + .../action/trackClickActions.spec.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts index 10a8a9e331..8edc0c10c0 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts @@ -85,6 +85,7 @@ describe('actionCollection', () => { }) expect(rawRumEvents[0].domainContext).toEqual({ event, + eventsSequence: undefined, }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts index b12621d095..ebb0195270 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts @@ -102,6 +102,7 @@ describe('trackClickActions', () => { frustrationTypes: [], target: undefined, position: undefined, + eventsSequence: undefined, }, ]) }) @@ -326,6 +327,19 @@ describe('trackClickActions', () => { expect(events[0].duration).toBe((MAX_DURATION_BETWEEN_CLICKS + 2 * actionDuration) as Duration) }) + it('should contain original events from of rage sequence', () => { + const { domMutationObservable, clock } = setupBuilder.build() + const actionDuration = 5 + emulateClickWithActivity(domMutationObservable, clock, undefined, actionDuration) + emulateClickWithActivity(domMutationObservable, clock, undefined, actionDuration) + emulateClickWithActivity(domMutationObservable, clock, undefined, actionDuration) + + clock.tick(EXPIRE_DELAY) + expect(events.length).toBe(1) + expect(events[0].frustrationTypes).toEqual([FrustrationType.RAGE_CLICK]) + expect(events[0].eventsSequence?.length).toBe(3) + }) + it('aggregates frustrationTypes from all clicks', () => { const { lifeCycle, domMutationObservable, clock } = setupBuilder.build() From 55b36f8150c668eec551a34328d2b0fca5543fb8 Mon Sep 17 00:00:00 2001 From: "najib.boutaib" Date: Wed, 13 Jul 2022 16:25:41 +0200 Subject: [PATCH 12/19] Review and e2e tests --- .../action/actionCollection.spec.ts | 4 +- .../action/actionCollection.ts | 2 +- .../action/trackClickActions.spec.ts | 7 +- .../action/trackClickActions.ts | 6 +- packages/rum-core/src/domainContext.types.ts | 3 +- packages/rum/src/domain/record/observer.ts | 26 +++----- packages/rum/src/domain/record/utils.ts | 11 +--- .../domain/segmentCollection/segment.spec.ts | 8 +-- .../src/domain/segmentCollection/segment.ts | 8 +-- packages/rum/src/types/record.ts | 2 +- packages/rum/test/utils.ts | 17 +++++ .../scenario/recorder/recorder.scenario.ts | 64 ++++++++++++++++++- 12 files changed, 107 insertions(+), 51 deletions(-) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts index 8edc0c10c0..60cfe4b5f7 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts @@ -46,6 +46,7 @@ describe('actionCollection', () => { height: 2, }, position: { x: 1, y: 2 }, + events: [event], }) expect(rawRumEvents[0].startTime).toBe(1234 as RelativeTime) @@ -84,8 +85,7 @@ describe('actionCollection', () => { }, }) expect(rawRumEvents[0].domainContext).toEqual({ - event, - eventsSequence: undefined, + events: [event], }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts index 8f6f617d71..72d3349259 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts @@ -101,7 +101,7 @@ function processAction( customerContext, rawRumEvent: actionEvent, startTime: action.startClocks.relative, - domainContext: isAutoAction(action) ? { event: action.event, eventsSequence: action.eventsSequence } : {}, + domainContext: isAutoAction(action) ? { events: action.events } : {}, } } diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts index ebb0195270..7d3fce117c 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.spec.ts @@ -86,6 +86,7 @@ describe('trackClickActions', () => { emulateClickWithActivity(domMutationObservable, clock) expect(findActionId()).not.toBeUndefined() clock.tick(EXPIRE_DELAY) + const domEvent = createNewEvent('click', { target: document.createElement('button') }) expect(events).toEqual([ { counts: { @@ -98,11 +99,11 @@ describe('trackClickActions', () => { name: 'Click me', startClocks: jasmine.any(Object), type: ActionType.CLICK, - event: createNewEvent('click', { target: document.createElement('button') }), + event: domEvent, frustrationTypes: [], target: undefined, position: undefined, - eventsSequence: undefined, + events: [domEvent], }, ]) }) @@ -337,7 +338,7 @@ describe('trackClickActions', () => { clock.tick(EXPIRE_DELAY) expect(events.length).toBe(1) expect(events[0].frustrationTypes).toEqual([FrustrationType.RAGE_CLICK]) - expect(events[0].eventsSequence?.length).toBe(3) + expect(events[0].events?.length).toBe(3) }) it('aggregates frustrationTypes from all clicks', () => { diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts index b1fb12d08a..105ff88ac3 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/trackClickActions.ts @@ -48,7 +48,7 @@ export interface ClickAction { counts: ActionCounts event: MouseEvent & { target: Element } frustrationTypes: FrustrationType[] - eventsSequence?: Event[] + events: Event[] } export interface ActionContexts { @@ -248,7 +248,7 @@ function newClick( clone: () => newClick(lifeCycle, history, getUserActivity, base), - validate: (eventsSequence?: Event[]) => { + validate: (domEvents?: Event[]) => { stop() if (status !== ClickStatus.STOPPED) { return @@ -268,7 +268,7 @@ function newClick( errorCount, longTaskCount, }, - eventsSequence, + events: domEvents ?? [base.event], }, base ) diff --git a/packages/rum-core/src/domainContext.types.ts b/packages/rum-core/src/domainContext.types.ts index 16fb530842..25b14bc3b0 100644 --- a/packages/rum-core/src/domainContext.types.ts +++ b/packages/rum-core/src/domainContext.types.ts @@ -21,8 +21,7 @@ export interface RumViewEventDomainContext { } export interface RumActionEventDomainContext { - event?: Event - eventsSequence?: Event[] + events?: Event[] } export interface RumFetchResourceEventDomainContext { diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index 22b900a44f..61bbe3a8b7 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -11,13 +11,7 @@ import { noop, } from '@datadog/browser-core' import type { LifeCycle } from '@datadog/browser-rum-core' -import { - FrustrationType, - initViewportObservable, - ActionType, - RumEventType, - LifeCycleEventType, -} from '@datadog/browser-rum-core' +import { initViewportObservable, ActionType, RumEventType, LifeCycleEventType } from '@datadog/browser-rum-core' import { NodePrivacyLevel } from '../../constants' import type { InputState, @@ -37,7 +31,7 @@ import type { import { RecordType, IncrementalSource, MediaInteractionType, MouseInteractionType } from '../../types' import { getNodePrivacyLevel, shouldMaskNode } from './privacy' import { getElementInputValue, getSerializedNodeId, hasSerializedNode } from './serializationUtils' -import { assembleIncrementalSnapshot, forEach, getFrustrationFromAction, isTouchEvent } from './utils' +import { assembleIncrementalSnapshot, forEach, isTouchEvent } from './utils' import type { MutationController } from './mutationObserver' import { startMutationObserver } from './mutationObserver' @@ -112,7 +106,7 @@ export function initObservers(o: ObserverParam): ListenerHandler { const styleSheetObserver = initStyleSheetObserver(o.styleSheetRuleCb) const focusHandler = initFocusObserver(o.focusCb) const visualViewportResizeHandler = initVisualViewportResizeObserver(o.visualViewportResizeCb) - const frustartionHandler = initFrustrationObserver(o.lifeCycle, o.frustrationCb) + const frustrationHandler = initFrustrationObserver(o.lifeCycle, o.frustrationCb) return () => { mutationHandler() @@ -125,7 +119,7 @@ export function initObservers(o: ObserverParam): ListenerHandler { styleSheetObserver() focusHandler() visualViewportResizeHandler() - frustartionHandler() + frustrationHandler() } } @@ -437,19 +431,15 @@ function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: Frustratio data.rawRumEvent.type === RumEventType.ACTION && data.rawRumEvent.action.type === ActionType.CLICK && data.rawRumEvent.action.frustration?.type && - 'event' in data.domainContext && - data.domainContext.event + 'events' in data.domainContext && + data.domainContext.events ) { - const frustrationType = getFrustrationFromAction(data.rawRumEvent.action.frustration.type) frustrationCb({ timestamp: data.rawRumEvent.date, type: RecordType.FrustrationRecord, data: { - frustrationType, - recordIds: - frustrationType === FrustrationType.RAGE_CLICK && data.domainContext.eventsSequence - ? data.domainContext.eventsSequence.map((e) => getRecordIdForEvent(e)) - : [getRecordIdForEvent(data.domainContext.event)], + frustrationTypes: data.rawRumEvent.action.frustration.type, + recordIds: data.domainContext.events.map((e) => getRecordIdForEvent(e)), }, }) } diff --git a/packages/rum/src/domain/record/utils.ts b/packages/rum/src/domain/record/utils.ts index 12b580f26f..d19cf38600 100644 --- a/packages/rum/src/domain/record/utils.ts +++ b/packages/rum/src/domain/record/utils.ts @@ -1,5 +1,4 @@ -import { assign, includes, timeStampNow } from '@datadog/browser-core' -import { FrustrationType } from '@datadog/browser-rum-core' +import { assign, timeStampNow } from '@datadog/browser-core' import type { IncrementalData, IncrementalSnapshotRecord } from '../../types' import { RecordType } from '../../types' @@ -14,14 +13,6 @@ export function forEach( Array.prototype.forEach.call(list, callback as any) } -export function getFrustrationFromAction(frustrations: FrustrationType[]): FrustrationType { - return includes(frustrations, FrustrationType.RAGE_CLICK) - ? FrustrationType.RAGE_CLICK - : includes(frustrations, FrustrationType.ERROR_CLICK) - ? FrustrationType.ERROR_CLICK - : FrustrationType.DEAD_CLICK -} - export function assembleIncrementalSnapshot( source: Data['source'], data: Omit diff --git a/packages/rum/src/domain/segmentCollection/segment.spec.ts b/packages/rum/src/domain/segmentCollection/segment.spec.ts index 03beff7fe7..21ae5c8f4c 100644 --- a/packages/rum/src/domain/segmentCollection/segment.spec.ts +++ b/packages/rum/src/domain/segmentCollection/segment.spec.ts @@ -128,20 +128,20 @@ describe('Segment', () => { segment = createSegment() segment.addRecord({ type: RecordType.ViewEnd, timestamp: 15 as TimeStamp }) }) - it('increments records_count', () => { + it('does increment records_count', () => { expect(segment.metadata.records_count).toBe(2) }) it('does not change start timestamp when receiving a later record', () => { expect(segment.metadata.start).toBe(10) }) - it('should change the start timestamp when receiving an earlier record', () => { + it('does change the start timestamp when receiving an earlier record', () => { segment.addRecord({ type: RecordType.ViewEnd, timestamp: 5 as TimeStamp }) expect(segment.metadata.start).toBe(5) }) - it('increases end timestamp when receiving a later record', () => { + it('does increase end timestamp when receiving a later record', () => { expect(segment.metadata.end).toBe(15) }) - it('should not change the end timestamp when receiving an earlier record', () => { + it('does not change the end timestamp when receiving an earlier record', () => { segment.addRecord({ type: RecordType.ViewEnd, timestamp: 5 as TimeStamp }) expect(segment.metadata.end).toBe(15) }) diff --git a/packages/rum/src/domain/segmentCollection/segment.ts b/packages/rum/src/domain/segmentCollection/segment.ts index e35c42ed29..75988efe45 100644 --- a/packages/rum/src/domain/segmentCollection/segment.ts +++ b/packages/rum/src/domain/segmentCollection/segment.ts @@ -71,12 +71,8 @@ export class Segment { } addRecord(record: Record): void { - if (record.timestamp < this.metadata.start) { - this.metadata.start = record.timestamp - } - if (this.metadata.end < record.timestamp) { - this.metadata.end = record.timestamp - } + this.metadata.start = Math.min(this.metadata.start, record.timestamp) + this.metadata.end = Math.max(this.metadata.end, record.timestamp) this.metadata.records_count += 1 replayStats.addRecord(this.metadata.view.id) this.metadata.has_full_snapshot ||= record.type === RecordType.FullSnapshot diff --git a/packages/rum/src/types/record.ts b/packages/rum/src/types/record.ts index d170932718..222fd1b8b3 100644 --- a/packages/rum/src/types/record.ts +++ b/packages/rum/src/types/record.ts @@ -46,7 +46,7 @@ export interface FrustrationRecord { type: typeof RecordType.FrustrationRecord timestamp: TimeStamp data: { - frustrationType: FrustrationType + frustrationTypes: FrustrationType[] recordIds: number[] } } diff --git a/packages/rum/test/utils.ts b/packages/rum/test/utils.ts index 494e1453b4..2c0169661f 100644 --- a/packages/rum/test/utils.ts +++ b/packages/rum/test/utils.ts @@ -11,6 +11,8 @@ import type { MetaRecord, VisualViewportRecord, Segment, + FrustrationRecord, + MouseInteractionType, } from '../src/types' import { RecordType, IncrementalSource, NodeType } from '../src/types' @@ -206,6 +208,21 @@ export function findAllIncrementalSnapshots(segment: Segment, source: Incrementa ) as IncrementalSnapshotRecord[] } +// Returns all the FrustrationRecords in the given Segment, if any. +export function findAllFrustrationRecords(segment: Segment): FrustrationRecord[] { + return segment.records.filter((record) => record.type === RecordType.FrustrationRecord) as FrustrationRecord[] +} + +// Returns all the IncrementalSnapshotRecords of the given MouseInteraction source, if any +export function findMouseInteractionRecords( + segment: Segment, + source: MouseInteractionType +): IncrementalSnapshotRecord[] { + return findAllIncrementalSnapshots(segment, IncrementalSource.MouseInteraction).filter( + (record) => 'type' in record.data && record.data.type === source + ) +} + // Returns the textContent of a ElementNode, if any. export function findTextContent(elem: ElementNode): string | null { const text = elem.childNodes.find((child) => child.type === NodeType.Text) as TextNode diff --git a/test/e2e/scenario/recorder/recorder.scenario.ts b/test/e2e/scenario/recorder/recorder.scenario.ts index 834994d390..162881d8b5 100644 --- a/test/e2e/scenario/recorder/recorder.scenario.ts +++ b/test/e2e/scenario/recorder/recorder.scenario.ts @@ -1,7 +1,8 @@ import type { InputData, StyleSheetRuleData, CreationReason, Segment } from '@datadog/browser-rum/src/types' -import { NodeType, IncrementalSource, RecordType } from '@datadog/browser-rum/src/types' +import { MouseInteractionType, NodeType, IncrementalSource, RecordType } from '@datadog/browser-rum/src/types' import type { RumInitConfiguration } from '@datadog/browser-rum-core' +import { FrustrationType } from '@datadog/browser-rum-core' import { DefaultPrivacyLevel } from '@datadog/browser-rum' import { @@ -13,6 +14,8 @@ import { findMeta, findTextContent, createMutationPayloadValidatorFromSegment, + findAllFrustrationRecords, + findMouseInteractionRecords, } from '@datadog/browser-rum/test/utils' import { renewSession } from '../../lib/helpers/session' import type { EventRegistry } from '../../lib/framework' @@ -658,6 +661,65 @@ describe('recorder', () => { expect(segment.records.slice(3).every((record) => record.type !== RecordType.FullSnapshot)).toBe(true) }) }) + + describe('frustration records', () => { + createTest('should detect a dead click and match it to mouse interaction record') + .withRum({ trackFrustrations: true }) + .withRumInit(initRumAndStartRecording) + .withSetup(bundleSetup) + .run(async ({ serverEvents }) => { + await browserExecute(() => document.documentElement.outerHTML) + const html = await $('html') + await html.click() + await flushEvents() + + expect(serverEvents.sessionReplay.length).toBe(1) + const { segment } = serverEvents.sessionReplay[0] + + const clickRecords = findMouseInteractionRecords(segment.data, MouseInteractionType.Click) + const frustrations = findAllFrustrationRecords(segment.data) + + expect(clickRecords.length).toBe(1) + expect(clickRecords[0].id).toBeTruthy('mouse ineraction record should have an id') + expect(frustrations.length).toBe(1) + expect(frustrations[0].data).toEqual({ + frustrationTypes: [FrustrationType.DEAD_CLICK], + recordIds: [clickRecords[0].id!], + }) + }) + + createTest('should detect a rage click and match it to mouse interaction records') + .withRum({ trackFrustrations: true }) + .withRumInit(initRumAndStartRecording) + .withSetup(bundleSetup) + .withBody( + html` +
+