Skip to content

Commit

Permalink
⚡[RUMF-1344] Serialize scroll positions only for full snapshots (#1670)
Browse files Browse the repository at this point in the history
* ♻️ round scroll values everywhere

* ♻️ rename observer to observers

* ⬆️️ bump safari to have consistent behavior across browsers

on safari 12, scroll attributes do not seem to be on html node...

* ✅ add scroll positions scenario

* ⚡ serialize scroll positions only for full snapshots
  • Loading branch information
bcaudan authored Aug 9, 2022
1 parent a83de91 commit 3777c3d
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ describe('rum track view metrics', () => {

describe('cumulativeLayoutShift', () => {
let isLayoutShiftSupported: boolean
let originalSupportedEntryTypes: PropertyDescriptor | undefined

function newLayoutShift(lifeCycle: LifeCycle, { value = 0.1, hadRecentInput = false }) {
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
{
Expand All @@ -351,10 +353,17 @@ describe('rum track view metrics', () => {
if (!('PerformanceObserver' in window) || !('supportedEntryTypes' in PerformanceObserver)) {
pending('No PerformanceObserver support')
}
originalSupportedEntryTypes = Object.getOwnPropertyDescriptor(PerformanceObserver, 'supportedEntryTypes')
isLayoutShiftSupported = true
spyOnProperty(PerformanceObserver, 'supportedEntryTypes', 'get').and.callFake(() =>
isLayoutShiftSupported ? ['layout-shift'] : []
)
Object.defineProperty(PerformanceObserver, 'supportedEntryTypes', {
get: () => (isLayoutShiftSupported ? ['layout-shift'] : []),
})
})

afterEach(() => {
if (originalSupportedEntryTypes) {
Object.defineProperty(PerformanceObserver, 'supportedEntryTypes', originalSupportedEntryTypes)
}
})

it('should be initialized to 0', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/rum/src/domain/record/mutationObserver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { AttributeMutation, Attributes } from '../../types'
import { NodeType } from '../../types'
import { serializeDocument } from './serialize'
import { sortAddedAndMovedNodes, startMutationObserver, MutationController } from './mutationObserver'
import type { MutationCallBack } from './observer'
import type { MutationCallBack } from './observers'

describe('startMutationCollection', () => {
let sandbox: HTMLElement
Expand Down
3 changes: 2 additions & 1 deletion packages/rum/src/domain/record/mutationObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { serializeNodeWithId, serializeAttribute } from './serialize'
import { forEach } from './utils'
import { createMutationBatch } from './mutationBatch'
import type { MutationCallBack } from './observer'
import type { MutationCallBack } from './observers'

type WithSerializedTarget<T> = T & { target: NodeWithSerializedNode }

Expand Down Expand Up @@ -211,6 +211,7 @@ function processChildListMutations(
document,
serializedNodeIds,
parentNodePrivacyLevel,
serializationContext: 'mutation',
})
if (!serializedNode) {
continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { RawRumEventCollectedData } from 'packages/rum-core/src/domain/life
import { createNewEvent } 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 } from './observer'
import { initFrustrationObserver, initInputObserver } from './observer'
import type { FrustrationCallback, InputCallback } from './observers'
import { initFrustrationObserver, initInputObserver } from './observers'
import { serializeDocument } from './serialize'

describe('initInputObserver', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ function initScrollObserver(cb: ScrollCallback, defaultPrivacyLevel: DefaultPriv
} else {
cb({
id,
x: (target as HTMLElement).scrollLeft,
y: (target as HTMLElement).scrollTop,
x: Math.round((target as HTMLElement).scrollLeft),
y: Math.round((target as HTMLElement).scrollTop),
})
}
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/rum/src/domain/record/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
} from '../../types'
import { RecordType, IncrementalSource } from '../../types'
import { serializeDocument } from './serialize'
import { initObservers } from './observer'
import { initObservers } from './observers'

import { MutationController } from './mutationObserver'
import { getVisualViewport, getScrollX, getScrollY } from './viewports'
Expand Down
51 changes: 36 additions & 15 deletions packages/rum/src/domain/record/serialize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import type { ElementNode, SerializedNodeWithId, TextNode } from '../../types'
import { NodeType } from '../../types'
import { hasSerializedNode } from './serializationUtils'
import type { SerializeOptions } from './serialize'
import type { SerializeOptions, SerializationContext } from './serialize'
import {
serializeDocument,
serializeNodeWithId,
Expand All @@ -31,6 +31,7 @@ import { MAX_ATTRIBUTE_VALUE_CHAR_LENGTH } from './privacy'
const DEFAULT_OPTIONS: SerializeOptions = {
document,
parentNodePrivacyLevel: NodePrivacyLevel.ALLOW,
serializationContext: 'full-snapshot',
}

describe('serializeNodeWithId', () => {
Expand Down Expand Up @@ -114,22 +115,41 @@ describe('serializeNodeWithId', () => {
style: 'width: 10px;',
})
})

it('serializes scroll position', () => {
const element = document.createElement('div')
Object.assign(element.style, { width: '100px', height: '100px', overflow: 'scroll' })
const inner = document.createElement('div')
Object.assign(inner.style, { width: '200px', height: '200px' })
element.appendChild(inner)
sandbox.appendChild(element)
element.scrollBy(10, 20)

expect((serializeNodeWithId(element, DEFAULT_OPTIONS)! as ElementNode).attributes).toEqual(
jasmine.objectContaining({
;[
{
description: 'serializes scroll position during full snapshot',
serializationContext: 'full-snapshot' as SerializationContext,
shouldSerializeScroll: true,
},
{
description: 'does not serialize scroll position during mutation',
serializationContext: 'mutation' as SerializationContext,
shouldSerializeScroll: false,
},
].forEach(({ description, serializationContext, shouldSerializeScroll }) => {
it(description, () => {
const element = document.createElement('div')
Object.assign(element.style, { width: '100px', height: '100px', overflow: 'scroll' })
const inner = document.createElement('div')
Object.assign(inner.style, { width: '200px', height: '200px' })
element.appendChild(inner)
sandbox.appendChild(element)
element.scrollBy(10, 20)

const serializedAttributes = (
serializeNodeWithId(element, { ...DEFAULT_OPTIONS, serializationContext })! as ElementNode
).attributes
const attributesWithScrollPositions = jasmine.objectContaining({
rr_scrollTop: 20,
rr_scrollLeft: 10,
})
)

if (shouldSerializeScroll) {
expect(serializedAttributes).toEqual(attributesWithScrollPositions)
} else {
expect(serializedAttributes).not.toEqual(attributesWithScrollPositions)
}
})
})

it('ignores white space in <head>', () => {
Expand Down Expand Up @@ -491,9 +511,10 @@ describe('serializeDocumentNode handles', function testAllowDomTree() {
})

it('a masked DOM Document itself is still serialized ', () => {
const serializeOptionsMask = {
const serializeOptionsMask: SerializeOptions = {
document,
parentNodePrivacyLevel: NodePrivacyLevel.MASK,
serializationContext: 'full-snapshot',
}
expect(serializeDocumentNode(document, serializeOptionsMask)).toEqual({
type: NodeType.Document,
Expand Down
23 changes: 15 additions & 8 deletions packages/rum/src/domain/record/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ type ParentNodePrivacyLevel =
| typeof NodePrivacyLevel.MASK
| typeof NodePrivacyLevel.MASK_USER_INPUT

export type SerializationContext = 'full-snapshot' | 'mutation'

export interface SerializeOptions {
document: Document
serializedNodeIds?: Set<number>
ignoreWhiteSpace?: boolean
parentNodePrivacyLevel: ParentNodePrivacyLevel
serializationContext: SerializationContext
}

export function serializeDocument(
Expand All @@ -49,6 +52,7 @@ export function serializeDocument(
return serializeNodeWithId(document, {
document,
parentNodePrivacyLevel: defaultPrivacyLevel,
serializationContext: 'full-snapshot',
})!
}

Expand Down Expand Up @@ -145,7 +149,7 @@ export function serializeElementNode(element: Element, options: SerializeOptions
return
}

const attributes = getAttributesForPrivacyLevel(element, nodePrivacyLevel)
const attributes = getAttributesForPrivacyLevel(element, nodePrivacyLevel, options.serializationContext)

let childNodes: SerializedNodeWithId[] = []
if (element.childNodes.length) {
Expand Down Expand Up @@ -304,7 +308,8 @@ function isSVGElement(el: Element): boolean {

function getAttributesForPrivacyLevel(
element: Element,
nodePrivacyLevel: NodePrivacyLevel
nodePrivacyLevel: NodePrivacyLevel,
serializationContext: SerializationContext
): Record<string, string | number | boolean> {
if (nodePrivacyLevel === NodePrivacyLevel.HIDDEN) {
return {}
Expand Down Expand Up @@ -394,13 +399,15 @@ function getAttributesForPrivacyLevel(
}

/**
* Serialize the scroll state for each element
* Serialize the scroll state for each element only for full snapshot
*/
if (element.scrollLeft) {
safeAttrs.rr_scrollLeft = Math.round(element.scrollLeft)
}
if (element.scrollTop) {
safeAttrs.rr_scrollTop = Math.round(element.scrollTop)
if (serializationContext === 'full-snapshot') {
if (element.scrollLeft) {
safeAttrs.rr_scrollLeft = Math.round(element.scrollLeft)
}
if (element.scrollTop) {
safeAttrs.rr_scrollTop = Math.round(element.scrollTop)
}
}

return safeAttrs
Expand Down
24 changes: 14 additions & 10 deletions packages/rum/src/domain/record/viewports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,27 @@ export const getVisualViewport = (): VisualViewportRecord['data'] => {
}

export function getScrollX() {
let scrollX
const visual = window.visualViewport
if (visual) {
return visual.pageLeft - visual.offsetLeft
}
if (window.scrollX !== undefined) {
return window.scrollX
scrollX = visual.pageLeft - visual.offsetLeft
} else if (window.scrollX !== undefined) {
scrollX = window.scrollX
} else {
scrollX = window.pageXOffset || 0
}
return window.pageXOffset || 0
return Math.round(scrollX)
}

export function getScrollY() {
let scrollY
const visual = window.visualViewport
if (visual) {
return visual.pageTop - visual.offsetTop
}
if (window.scrollY !== undefined) {
return window.scrollY
scrollY = visual.pageTop - visual.offsetTop
} else if (window.scrollY !== undefined) {
scrollY = window.scrollY
} else {
scrollY = window.pageYOffset || 0
}
return window.pageYOffset || 0
return Math.round(scrollY)
}
1 change: 1 addition & 0 deletions packages/rum/test/htmlAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const generateLeanSerializedDoc = (htmlContent: string, privacyTag: strin
serializeNodeWithId(newDoc, {
document: newDoc,
parentNodePrivacyLevel: NodePrivacyLevel.ALLOW,
serializationContext: 'full-snapshot',
})! as unknown as Record<string, unknown>
) as unknown as SerializedNodeWithId
return serializedDoc
Expand Down
4 changes: 2 additions & 2 deletions test/browsers.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ module.exports = [
{
sessionName: 'Safari desktop',
name: 'Safari',
version: '12.0',
version: '13.1',
os: 'OS X',
osVersion: 'Mojave',
osVersion: 'Catalina',
},
{
sessionName: 'Chrome mobile',
Expand Down
Loading

0 comments on commit 3777c3d

Please sign in to comment.