Skip to content

Commit 3863583

Browse files
🚩 [RUMF-902] use the new mutation observer via a FF
1 parent ad81ffe commit 3863583

File tree

8 files changed

+191
-7
lines changed

8 files changed

+191
-7
lines changed

‎packages/rum-recorder/src/boot/recorder.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function startRecording(
2727

2828
const { stop: stopRecording, takeFullSnapshot } = record({
2929
emit: addRawRecord,
30+
useNewMutationObserver: configuration.isEnabled('new-mutation-observer'),
3031
})
3132

3233
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, takeFullSnapshot)

‎packages/rum-recorder/src/domain/rrweb/observer.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ import {
2121
ViewportResizeCallback,
2222
} from './types'
2323
import { forEach, getWindowHeight, getWindowWidth, hookSetter, isTouchEvent } from './utils'
24+
import { startMutationObserver } from './mutationObserver'
2425

2526
const MOUSE_MOVE_OBSERVER_THRESHOLD = 50
2627
const SCROLL_OBSERVER_THRESHOLD = 100
2728

2829
export function initObservers(o: ObserverParam): ListenerHandler {
29-
const mutationHandler = initMutationObserver(o.mutationController, o.mutationCb)
30+
const mutationHandler = initMutationObserver(o.useNewMutationObserver, o.mutationController, o.mutationCb)
3031
const mousemoveHandler = initMoveObserver(o.mousemoveCb)
3132
const mouseInteractionHandler = initMouseInteractionObserver(o.mouseInteractionCb)
3233
const scrollHandler = initScrollObserver(o.scrollCb)
@@ -49,7 +50,14 @@ export function initObservers(o: ObserverParam): ListenerHandler {
4950
}
5051
}
5152

52-
function initMutationObserver(mutationController: MutationController, cb: MutationCallBack) {
53+
function initMutationObserver(
54+
useNewMutationObserver: boolean,
55+
mutationController: MutationController,
56+
cb: MutationCallBack
57+
) {
58+
if (useNewMutationObserver) {
59+
return startMutationObserver(mutationController, cb).stop
60+
}
5361
const mutationObserverWrapper = new MutationObserverWrapper(mutationController, cb)
5462
return () => mutationObserverWrapper.stop()
5563
}

‎packages/rum-recorder/src/domain/rrweb/record.spec.ts‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ describe('record', () => {
2323
let emitSpy: jasmine.Spy<(record: RawRecord) => void>
2424
let waitEmitCalls: (expectedCallsCount: number, callback: () => void) => void
2525
let expectNoExtraEmitCalls: (done: () => void) => void
26+
let useNewMutationObserver: boolean
2627

2728
beforeEach(() => {
2829
if (isIE()) {
2930
pending('IE not supported')
3031
}
3132

33+
useNewMutationObserver = false
3234
emitSpy = jasmine.createSpy()
3335
;({ waitAsyncCalls: waitEmitCalls, expectNoExtraAsyncCall: expectNoExtraEmitCalls } = collectAsyncCalls(emitSpy))
3436
;({ sandbox, input } = createDOMSandbox())
@@ -52,7 +54,7 @@ describe('record', () => {
5254
expect(records.filter((record) => record.type === RecordType.FullSnapshot).length).toEqual(1)
5355
})
5456

55-
it('is safe to checkout during async callbacks', (done) => {
57+
it('is safe to checkout during async callbacks (old mutation observer)', (done) => {
5658
startRecording()
5759

5860
const p = document.createElement('p')
@@ -123,6 +125,72 @@ describe('record', () => {
123125
})
124126
})
125127

128+
it('is safe to checkout during async callbacks (new mutation observer)', (done) => {
129+
useNewMutationObserver = true
130+
startRecording()
131+
132+
const p = document.createElement('p')
133+
const span = document.createElement('span')
134+
135+
setTimeout(() => {
136+
sandbox.appendChild(p)
137+
p.appendChild(span)
138+
sandbox.removeChild(document.querySelector('input')!)
139+
}, 0)
140+
141+
setTimeout(() => {
142+
span.innerText = 'test'
143+
recordApi.takeFullSnapshot()
144+
}, 10)
145+
146+
setTimeout(() => {
147+
p.removeChild(span)
148+
sandbox.appendChild(span)
149+
}, 10)
150+
151+
waitEmitCalls(9, () => {
152+
const records = getEmittedRecords()
153+
expect(records[0].type).toBe(RecordType.Meta)
154+
expect(records[1].type).toBe(RecordType.Focus)
155+
156+
expect(records[2].type).toBe(RecordType.FullSnapshot)
157+
158+
expect(records[3].type).toBe(RecordType.IncrementalSnapshot)
159+
160+
const { validate: validateMutationPayload, expectNewNode, expectInitialNode } = createMutationPayloadValidator(
161+
(records[2] as FullSnapshotRecord).data.node
162+
)
163+
164+
const p = expectNewNode({ type: NodeType.Element, tagName: 'p' })
165+
const span = expectNewNode({ type: NodeType.Element, tagName: 'span' })
166+
const text = expectNewNode({ type: NodeType.Text, textContent: 'test' })
167+
const sandbox = expectInitialNode({ idAttribute: 'sandbox' })
168+
169+
validateMutationPayload((records[3] as IncrementalSnapshotRecord).data as MutationData, {
170+
adds: [{ parent: sandbox, node: p.withChildren(span) }],
171+
removes: [{ node: expectInitialNode({ tag: 'input' }), parent: sandbox }],
172+
})
173+
174+
expect(records[4].type).toBe(RecordType.IncrementalSnapshot)
175+
validateMutationPayload((records[4] as IncrementalSnapshotRecord).data as MutationData, {
176+
adds: [{ parent: span, node: text }],
177+
})
178+
179+
expect(records[5].type).toBe(RecordType.Meta)
180+
expect(records[6].type).toBe(RecordType.Focus)
181+
182+
expect(records[7].type).toBe(RecordType.FullSnapshot)
183+
184+
expect(records[8].type).toBe(RecordType.IncrementalSnapshot)
185+
validateMutationPayload((records[8] as IncrementalSnapshotRecord).data as MutationData, {
186+
adds: [{ parent: sandbox, node: span.withChildren(text) }],
187+
removes: [{ parent: p, node: span }],
188+
})
189+
190+
expectNoExtraEmitCalls(done)
191+
})
192+
})
193+
126194
it('captures stylesheet rules', (done) => {
127195
startRecording()
128196

@@ -260,6 +328,7 @@ describe('record', () => {
260328
function startRecording() {
261329
recordApi = record({
262330
emit: emitSpy,
331+
useNewMutationObserver,
263332
})
264333
}
265334

‎packages/rum-recorder/src/domain/rrweb/record.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { IncrementalSource, ListenerHandler, RecordAPI, RecordOptions } from './
66
import { getWindowHeight, getWindowWidth, mirror } from './utils'
77
import { MutationController } from './mutation'
88

9-
export function record(options: RecordOptions = {}): RecordAPI {
10-
const { emit } = options
9+
export function record(options: RecordOptions): RecordAPI {
10+
const { emit, useNewMutationObserver } = options
1111
// runtime checks for user options
1212
if (!emit) {
1313
throw new Error('emit function is required')
@@ -77,6 +77,7 @@ export function record(options: RecordOptions = {}): RecordAPI {
7777

7878
handlers.push(
7979
initObservers({
80+
useNewMutationObserver,
8081
mutationController,
8182
inputCb: (v) =>
8283
emit({

‎packages/rum-recorder/src/domain/rrweb/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export type IncrementalData =
6262

6363
export interface RecordOptions {
6464
emit?: (record: RawRecord, isCheckout?: boolean) => void
65+
useNewMutationObserver: boolean
6566
}
6667

6768
export interface RecordAPI {
@@ -70,6 +71,7 @@ export interface RecordAPI {
7071
}
7172

7273
export interface ObserverParam {
74+
useNewMutationObserver: boolean
7375
mutationController: MutationController
7476
mutationCb: MutationCallBack
7577
mousemoveCb: MousemoveCallBack

‎packages/rum-recorder/test/utils.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ export function collectAsyncCalls<F extends jasmine.Func>(spy: jasmine.Spy<F>) {
103103
waitAsyncCalls: (expectedCallsCount: number, callback: (calls: jasmine.Calls<F>) => void) => {
104104
if (spy.calls.count() === expectedCallsCount) {
105105
callback(spy.calls)
106+
} else if (spy.calls.count() > expectedCallsCount) {
107+
fail('Unexpected extra call')
106108
} else {
107109
spy.and.callFake((() => {
108110
if (spy.calls.count() === expectedCallsCount) {

‎test/e2e/lib/framework/pageSetups.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface RumSetupOptions {
55
allowedTracingOrigins?: string[]
66
service?: string
77
trackInteractions?: boolean
8+
enableExperimentalFeatures?: string[]
89
}
910

1011
export interface LogsSetupOptions {

‎test/e2e/scenario/recorder.scenario.ts‎

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ describe('recorder', () => {
268268
expect(findAllIncrementalSnapshots(segment, IncrementalSource.Mutation)).toEqual([])
269269
})
270270

271-
createTest('record DOM node movement 1')
271+
createTest('record DOM node movement 1 (old mutation observer)')
272272
.withSetup(bundleSetup)
273273
.withRumRecorder()
274274
.withBody(
@@ -341,7 +341,57 @@ describe('recorder', () => {
341341
})
342342
})
343343

344-
createTest('record DOM node movement 2')
344+
createTest('record DOM node movement 1 (new mutation observer)')
345+
.withSetup(bundleSetup)
346+
.withRumRecorder({ enableExperimentalFeatures: ['new-mutation-observer'] })
347+
.withBody(
348+
// prettier-ignore
349+
html`
350+
<div>a<p></p>b</div>
351+
<span>c<i>d<b>e</b>f</i>g</span>
352+
`
353+
)
354+
.run(async ({ events }) => {
355+
await browserExecute(() => {
356+
const div = document.querySelector('div')!
357+
const p = document.querySelector('p')!
358+
const span = document.querySelector('span')!
359+
document.body.removeChild(span)
360+
p.appendChild(span)
361+
p.removeChild(span)
362+
div.appendChild(span)
363+
})
364+
365+
await flushEvents()
366+
367+
const { validate, expectInitialNode } = createMutationPayloadValidatorFromSegment(
368+
events.sessionReplay[0].segment.data
369+
)
370+
validate({
371+
adds: [
372+
{
373+
parent: expectInitialNode({ tag: 'div' }),
374+
node: expectInitialNode({ tag: 'span' }).withChildren(
375+
expectInitialNode({ text: 'c' }),
376+
expectInitialNode({ tag: 'i' }).withChildren(
377+
expectInitialNode({ text: 'd' }),
378+
expectInitialNode({ tag: 'b' }).withChildren(expectInitialNode({ text: 'e' })),
379+
expectInitialNode({ text: 'f' })
380+
),
381+
expectInitialNode({ text: 'g' })
382+
),
383+
},
384+
],
385+
removes: [
386+
{
387+
parent: expectInitialNode({ tag: 'body' }),
388+
node: expectInitialNode({ tag: 'span' }),
389+
},
390+
],
391+
})
392+
})
393+
394+
createTest('record DOM node movement 2 (old mutation observer)')
345395
.withSetup(bundleSetup)
346396
.withRumRecorder()
347397
.withBody(
@@ -417,6 +467,56 @@ describe('recorder', () => {
417467
],
418468
})
419469
})
470+
createTest('record DOM node movement 2 (new mutation observer)')
471+
.withSetup(bundleSetup)
472+
.withRumRecorder({ enableExperimentalFeatures: ['new-mutation-observer'] })
473+
.withBody(
474+
// prettier-ignore
475+
html`
476+
<span>c<i>d<b>e</b>f</i>g</span>
477+
`
478+
)
479+
.run(async ({ events }) => {
480+
await browserExecute(() => {
481+
const div = document.createElement('div')
482+
const span = document.querySelector('span')!
483+
document.body.appendChild(div)
484+
div.appendChild(span)
485+
})
486+
487+
await flushEvents()
488+
489+
const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidatorFromSegment(
490+
events.sessionReplay[0].segment.data
491+
)
492+
493+
const div = expectNewNode({ type: NodeType.Element, tagName: 'div' })
494+
495+
validate({
496+
adds: [
497+
{
498+
parent: expectInitialNode({ tag: 'body' }),
499+
node: div.withChildren(
500+
expectInitialNode({ tag: 'span' }).withChildren(
501+
expectInitialNode({ text: 'c' }),
502+
expectInitialNode({ tag: 'i' }).withChildren(
503+
expectInitialNode({ text: 'd' }),
504+
expectInitialNode({ tag: 'b' }).withChildren(expectInitialNode({ text: 'e' })),
505+
expectInitialNode({ text: 'f' })
506+
),
507+
expectInitialNode({ text: 'g' })
508+
)
509+
),
510+
},
511+
],
512+
removes: [
513+
{
514+
parent: expectInitialNode({ tag: 'body' }),
515+
node: expectInitialNode({ tag: 'span' }),
516+
},
517+
],
518+
})
519+
})
420520

421521
createTest('serialize node before record')
422522
.withSetup(bundleSetup)

0 commit comments

Comments
 (0)