diff --git a/packages/rum/src/domain/record/observers.spec.ts b/packages/rum/src/domain/record/observers.spec.ts index b4301c9f91..e59bcb2392 100644 --- a/packages/rum/src/domain/record/observers.spec.ts +++ b/packages/rum/src/domain/record/observers.spec.ts @@ -4,9 +4,14 @@ import { ActionType, LifeCycle, LifeCycleEventType, RumEventType, FrustrationTyp import type { RawRumEventCollectedData } from 'packages/rum-core/src/domain/lifeCycle' import { createNewEvent, isFirefox } from '../../../../core/test/specHelper' import { NodePrivacyLevel, PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_MASK_USER_INPUT } from '../../constants' -import { RecordType } from '../../types' -import type { FrustrationCallback, InputCallback, StyleSheetCallback } from './observers' -import { initStyleSheetObserver, initFrustrationObserver, initInputObserver } from './observers' +import { IncrementalSource, MouseInteractionType, RecordType } from '../../types' +import type { FrustrationCallback, InputCallback, MouseInteractionCallBack, StyleSheetCallback } from './observers' +import { + initStyleSheetObserver, + initFrustrationObserver, + initInputObserver, + initMouseInteractionObserver, +} from './observers' import { serializeDocument, SerializationContextStatus } from './serialize' import { createElementsScrollPositions } from './elementsScrollPositions' import type { ShadowRootsController } from './shadowRootsController' @@ -328,3 +333,103 @@ describe('initStyleSheetObserver', () => { }) }) }) + +describe('initMouseInteractionObserver', () => { + let mouseInteractionCallbackSpy: jasmine.Spy + let stopObserver: () => void + let sandbox: HTMLDivElement + let a: HTMLAnchorElement + + beforeEach(() => { + if (isIE()) { + pending('IE not supported') + } + + sandbox = document.createElement('div') + a = document.createElement('a') + a.setAttribute('tabindex', '0') // make the element focusable + sandbox.appendChild(a) + document.body.appendChild(sandbox) + a.focus() + + serializeDocument(document, DEFAULT_CONFIGURATION, { + shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER, + status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, + elementsScrollPositions: createElementsScrollPositions(), + }) + + mouseInteractionCallbackSpy = jasmine.createSpy() + stopObserver = initMouseInteractionObserver(mouseInteractionCallbackSpy, DefaultPrivacyLevel.ALLOW) + }) + + afterEach(() => { + sandbox.remove() + stopObserver() + }) + + it('should generate click record', () => { + a.click() + + expect(mouseInteractionCallbackSpy).toHaveBeenCalledWith({ + id: jasmine.any(Number), + type: RecordType.IncrementalSnapshot, + timestamp: jasmine.any(Number), + data: { + source: IncrementalSource.MouseInteraction, + type: MouseInteractionType.Click, + id: jasmine.any(Number), + x: jasmine.any(Number), + y: jasmine.any(Number), + }, + }) + }) + + it('should generate blur record', () => { + a.blur() + + expect(mouseInteractionCallbackSpy).toHaveBeenCalledWith({ + id: jasmine.any(Number), + type: RecordType.IncrementalSnapshot, + timestamp: jasmine.any(Number), + data: { + source: IncrementalSource.MouseInteraction, + type: MouseInteractionType.Blur, + id: jasmine.any(Number), + }, + }) + }) + + // related to safari issue, see RUMF-1450 + describe('forced layout issue', () => { + let coordinatesComputed: boolean + + beforeEach(() => { + if (!window.visualViewport) { + pending('no visualViewport') + } + + coordinatesComputed = false + Object.defineProperty(window.visualViewport, 'offsetTop', { + get() { + coordinatesComputed = true + return 0 + }, + configurable: true, + }) + }) + + afterEach(() => { + delete (window.visualViewport as any).offsetTop + }) + + it('should compute x/y coordinates for click record', () => { + a.click() + expect(coordinatesComputed).toBeTrue() + }) + + it('should not compute x/y coordinates for blur record', () => { + a.blur() + expect(coordinatesComputed).toBeFalse() + }) + }) +}) diff --git a/packages/rum/src/domain/record/observers.ts b/packages/rum/src/domain/record/observers.ts index 4be948dcf7..576dcafb94 100644 --- a/packages/rum/src/domain/record/observers.ts +++ b/packages/rum/src/domain/record/observers.ts @@ -68,7 +68,7 @@ type MousemoveCallBack = ( export type MutationCallBack = (m: BrowserMutationPayload) => void -type MouseInteractionCallBack = (record: BrowserIncrementalSnapshotRecord) => void +export type MouseInteractionCallBack = (record: BrowserIncrementalSnapshotRecord) => void type ScrollCallback = (p: ScrollPosition) => void @@ -194,7 +194,7 @@ const eventTypeToMouseInteraction = { [DOM_EVENT.TOUCH_START]: MouseInteractionType.TouchStart, [DOM_EVENT.TOUCH_END]: MouseInteractionType.TouchEnd, } -function initMouseInteractionObserver( +export function initMouseInteractionObserver( cb: MouseInteractionCallBack, defaultPrivacyLevel: DefaultPrivacyLevel ): ListenerHandler { @@ -203,22 +203,20 @@ function initMouseInteractionObserver( if (getNodePrivacyLevel(target, defaultPrivacyLevel) === NodePrivacyLevel.HIDDEN || !hasSerializedNode(target)) { return } - const { clientX, clientY } = isTouchEvent(event) ? event.changedTouches[0] : event - const position: MouseInteraction = { - id: getSerializedNodeId(target), - type: eventTypeToMouseInteraction[event.type as keyof typeof eventTypeToMouseInteraction], - x: clientX, - y: clientY, - } - if (window.visualViewport) { - const { visualViewportX, visualViewportY } = convertMouseEventToLayoutCoordinates(clientX, clientY) - position.x = visualViewportX - position.y = visualViewportY + const id = getSerializedNodeId(target) + const type = eventTypeToMouseInteraction[event.type as keyof typeof eventTypeToMouseInteraction] + + let interaction: MouseInteraction + if (type !== MouseInteractionType.Blur && type !== MouseInteractionType.Focus) { + const { x, y } = computeCoordinates(event) + interaction = { id, type, x, y } + } else { + interaction = { id, type } } const record = assign( { id: getRecordIdForEvent(event) }, - assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, position) + assembleIncrementalSnapshot(IncrementalSource.MouseInteraction, interaction) ) cb(record) } @@ -228,6 +226,16 @@ function initMouseInteractionObserver( }).stop } +function computeCoordinates(event: MouseEvent | TouchEvent) { + let { clientX: x, clientY: y } = isTouchEvent(event) ? event.changedTouches[0] : event + if (window.visualViewport) { + const { visualViewportX, visualViewportY } = convertMouseEventToLayoutCoordinates(x, y) + x = visualViewportX + y = visualViewportY + } + return { x, y } +} + function initScrollObserver( cb: ScrollCallback, defaultPrivacyLevel: DefaultPrivacyLevel, diff --git a/packages/rum/src/types/sessionReplay.ts b/packages/rum/src/types/sessionReplay.ts index 77d897a446..b04188e4f5 100644 --- a/packages/rum/src/types/sessionReplay.ts +++ b/packages/rum/src/types/sessionReplay.ts @@ -127,6 +127,38 @@ export type MouseInteractionData = { */ readonly source: 2 } & MouseInteraction +/** + * Browser-specific. Schema of a MouseInteraction. + */ +export type MouseInteraction = + | { + /** + * The type of MouseInteraction: 0=mouseup, 1=mousedown, 2=click, 3=contextmenu, 4=dblclick, 7=touchstart, 9=touchend + */ + readonly type: 0 | 1 | 2 | 3 | 4 | 7 | 9 + /** + * Id for the target node for this MouseInteraction. + */ + id: number + /** + * X-axis coordinate for this MouseInteraction. + */ + x: number + /** + * Y-axis coordinate for this MouseInteraction. + */ + y: number + } + | { + /** + * The type of MouseInteraction: 5=focus, 6=blur + */ + readonly type: 5 | 6 + /** + * Id for the target node for this MouseInteraction. + */ + id: number + } /** * Browser-specific. Schema of a ScrollData. */ @@ -583,27 +615,6 @@ export interface MousePosition { */ timeOffset: number } -/** - * Browser-specific. Schema of a MouseInteraction. - */ -export interface MouseInteraction { - /** - * The type of MouseInteraction. - */ - readonly type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 - /** - * Id for the target node for this MouseInteraction. - */ - id: number - /** - * X-axis coordinate for this MouseInteraction. - */ - x: number - /** - * Y-axis coordinate for this MouseInteraction. - */ - y: number -} /** * Browser-specific. Schema of a ScrollPosition. */ diff --git a/rum-events-format b/rum-events-format index b2fa6680c6..e834fcf752 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit b2fa6680c6cbad197c8e611212ce9f50cbaea1ca +Subproject commit e834fcf7527972979a77a2436fae633b23f06f0b