Skip to content

Commit 60a8b6b

Browse files
✨[PANA-5156] Expose better session replay internal API
1 parent b9b819a commit 60a8b6b

File tree

5 files changed

+269
-46
lines changed

5 files changed

+269
-46
lines changed

packages/rum/src/domain/record/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { takeFullSnapshot, takeNodeSnapshot } from './internalApi'
12
export { record } from './record'
23
export type { SerializationMetric, SerializationStats } from './serialization'
34
export { createSerializationStats, aggregateSerializationStats } from './serialization'
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { NodeType, RecordType } from '../../types'
2+
import { appendElement } from '../../../../rum-core/test'
3+
import { takeFullSnapshot, takeNodeSnapshot } from './internalApi'
4+
5+
describe('takeFullSnapshot', () => {
6+
it('should produce Meta, Focus, and FullSnapshot records', () => {
7+
expect(takeFullSnapshot()).toEqual(
8+
jasmine.arrayContaining([
9+
{
10+
data: {
11+
height: jasmine.any(Number),
12+
href: window.location.href,
13+
width: jasmine.any(Number),
14+
},
15+
type: RecordType.Meta,
16+
timestamp: jasmine.any(Number),
17+
},
18+
{
19+
data: {
20+
has_focus: document.hasFocus(),
21+
},
22+
type: RecordType.Focus,
23+
timestamp: jasmine.any(Number),
24+
},
25+
{
26+
data: {
27+
node: jasmine.any(Object),
28+
initialOffset: {
29+
left: jasmine.any(Number),
30+
top: jasmine.any(Number),
31+
},
32+
},
33+
type: RecordType.FullSnapshot,
34+
timestamp: jasmine.any(Number),
35+
},
36+
])
37+
)
38+
})
39+
40+
it('should produce VisualViewport records when supported', () => {
41+
if (!window.visualViewport) {
42+
pending('visualViewport not supported')
43+
}
44+
45+
expect(takeFullSnapshot()).toEqual(
46+
jasmine.arrayContaining([
47+
{
48+
data: jasmine.any(Object),
49+
type: RecordType.VisualViewport,
50+
timestamp: jasmine.any(Number),
51+
},
52+
])
53+
)
54+
})
55+
})
56+
57+
describe('takeNodeSnapshot', () => {
58+
it('should serialize nodes', () => {
59+
const node = appendElement('<div>Hello <b>world</b></div>', document.body)
60+
expect(takeNodeSnapshot(node)).toEqual({
61+
type: NodeType.Element,
62+
id: 0,
63+
tagName: 'div',
64+
isSVG: undefined,
65+
attributes: {},
66+
childNodes: [
67+
{
68+
type: NodeType.Text,
69+
id: 1,
70+
textContent: 'Hello ',
71+
},
72+
{
73+
type: NodeType.Element,
74+
id: 2,
75+
tagName: 'b',
76+
isSVG: undefined,
77+
attributes: {},
78+
childNodes: [
79+
{
80+
type: NodeType.Text,
81+
id: 3,
82+
textContent: 'world',
83+
},
84+
],
85+
},
86+
],
87+
})
88+
})
89+
90+
it('should serialize shadow hosts', () => {
91+
const node = appendElement('<div>Hello</div>', document.body)
92+
const shadowRoot = node.attachShadow({ mode: 'open' })
93+
shadowRoot.appendChild(document.createTextNode('world'))
94+
expect(takeNodeSnapshot(node)).toEqual({
95+
type: NodeType.Element,
96+
id: 0,
97+
tagName: 'div',
98+
isSVG: undefined,
99+
attributes: {},
100+
childNodes: [
101+
{
102+
type: NodeType.Text,
103+
id: 1,
104+
textContent: 'Hello',
105+
},
106+
{
107+
type: NodeType.DocumentFragment,
108+
id: 2,
109+
isShadowRoot: true,
110+
adoptedStyleSheets: undefined,
111+
childNodes: [
112+
{
113+
type: NodeType.Text,
114+
id: 3,
115+
textContent: 'world',
116+
},
117+
],
118+
},
119+
],
120+
})
121+
})
122+
})
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { noop, timeStampNow } from '@datadog/browser-core'
2+
import type { RumConfiguration } from '@datadog/browser-rum-core'
3+
import { getNodePrivacyLevel, NodePrivacyLevel } from '@datadog/browser-rum-core'
4+
import type { BrowserRecord, SerializedNodeWithId } from '../../types'
5+
import { takeFullSnapshot as doTakeFullSnapshot } from './startFullSnapshots'
6+
import type { ShadowRootsController } from './shadowRootsController'
7+
import type { RecordingScope } from './recordingScope'
8+
import { createRecordingScope } from './recordingScope'
9+
import { createElementsScrollPositions } from './elementsScrollPositions'
10+
import { createEventIds } from './eventIds'
11+
import { createNodeIds } from './nodeIds'
12+
import type { EmitRecordCallback } from './record.types'
13+
import type { SerializationTransaction } from './serialization'
14+
import { SerializationKind, serializeInTransaction, serializeNode } from './serialization'
15+
16+
/**
17+
* Take a full snapshot of the document, generating the same records that the browser SDK
18+
* would generate.
19+
*
20+
* This is an internal API function. Be sure to update Datadog-internal callers if you
21+
* change its signature or behavior.
22+
*/
23+
export function takeFullSnapshot({
24+
configuration,
25+
}: { configuration?: Partial<RumConfiguration> } = {}): BrowserRecord[] {
26+
const records: BrowserRecord[] = []
27+
const emitRecord: EmitRecordCallback = (record: BrowserRecord) => {
28+
records.push(record)
29+
}
30+
31+
doTakeFullSnapshot(
32+
timeStampNow(),
33+
SerializationKind.INITIAL_FULL_SNAPSHOT,
34+
emitRecord,
35+
noop,
36+
createTemporaryRecordingScope(configuration)
37+
)
38+
39+
return records
40+
}
41+
42+
/**
43+
* Take a snapshot of a DOM node, generating the serialized representation that the
44+
* browser SDK would generate.
45+
*
46+
* This is an internal API function. Be sure to update Datadog-internal callers if you
47+
* change its signature or behavior.
48+
*/
49+
export function takeNodeSnapshot(
50+
node: Node,
51+
{ configuration }: { configuration?: Partial<RumConfiguration> } = {}
52+
): SerializedNodeWithId | null {
53+
let serializedNode: SerializedNodeWithId | null = null
54+
55+
serializeInTransaction(
56+
SerializationKind.INITIAL_FULL_SNAPSHOT,
57+
noop,
58+
noop,
59+
createTemporaryRecordingScope(configuration),
60+
(transaction: SerializationTransaction): void => {
61+
const privacyLevel = getNodePrivacyLevel(node, transaction.scope.configuration.defaultPrivacyLevel)
62+
if (privacyLevel === NodePrivacyLevel.HIDDEN || privacyLevel === NodePrivacyLevel.IGNORE) {
63+
return
64+
}
65+
serializedNode = serializeNode(node, privacyLevel, transaction)
66+
}
67+
)
68+
69+
return serializedNode
70+
}
71+
72+
function createTemporaryRecordingScope(configuration?: Partial<RumConfiguration>): RecordingScope {
73+
return createRecordingScope(
74+
{
75+
defaultPrivacyLevel: NodePrivacyLevel.ALLOW,
76+
...configuration,
77+
} as RumConfiguration,
78+
createElementsScrollPositions(),
79+
createEventIds(),
80+
createNodeIds(),
81+
{
82+
addShadowRoot: noop,
83+
removeShadowRoot: noop,
84+
flush: noop,
85+
stop: noop,
86+
} as ShadowRootsController
87+
)
88+
}

packages/rum/src/domain/record/startFullSnapshots.ts

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,57 +16,69 @@ export function startFullSnapshots(
1616
flushMutations: () => void,
1717
scope: RecordingScope
1818
) {
19-
const takeFullSnapshot = (timestamp: TimeStamp, kind: SerializationKind) => {
20-
serializeInTransaction(kind, emitRecord, emitStats, scope, (transaction: SerializationTransaction): void => {
21-
const { width, height } = getViewportDimension()
22-
transaction.add({
23-
data: {
24-
height,
25-
href: window.location.href,
26-
width,
27-
},
28-
type: RecordType.Meta,
29-
timestamp,
30-
})
31-
32-
transaction.add({
33-
data: {
34-
has_focus: document.hasFocus(),
35-
},
36-
type: RecordType.Focus,
37-
timestamp,
38-
})
39-
40-
transaction.add({
41-
data: {
42-
node: serializeDocument(document, transaction),
43-
initialOffset: {
44-
left: getScrollX(),
45-
top: getScrollY(),
46-
},
47-
},
48-
type: RecordType.FullSnapshot,
49-
timestamp,
50-
})
51-
52-
if (window.visualViewport) {
53-
transaction.add({
54-
data: getVisualViewport(window.visualViewport),
55-
type: RecordType.VisualViewport,
56-
timestamp,
57-
})
58-
}
59-
})
60-
}
61-
62-
takeFullSnapshot(timeStampNow(), SerializationKind.INITIAL_FULL_SNAPSHOT)
19+
takeFullSnapshot(timeStampNow(), SerializationKind.INITIAL_FULL_SNAPSHOT, emitRecord, emitStats, scope)
6320

6421
const { unsubscribe } = lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, (view) => {
6522
flushMutations()
66-
takeFullSnapshot(view.startClocks.timeStamp, SerializationKind.SUBSEQUENT_FULL_SNAPSHOT)
23+
takeFullSnapshot(
24+
view.startClocks.timeStamp,
25+
SerializationKind.SUBSEQUENT_FULL_SNAPSHOT,
26+
emitRecord,
27+
emitStats,
28+
scope
29+
)
6730
})
6831

6932
return {
7033
stop: unsubscribe,
7134
}
7235
}
36+
37+
export function takeFullSnapshot(
38+
timestamp: TimeStamp,
39+
kind: SerializationKind,
40+
emitRecord: EmitRecordCallback,
41+
emitStats: EmitStatsCallback,
42+
scope: RecordingScope
43+
): void {
44+
serializeInTransaction(kind, emitRecord, emitStats, scope, (transaction: SerializationTransaction): void => {
45+
const { width, height } = getViewportDimension()
46+
transaction.add({
47+
data: {
48+
height,
49+
href: window.location.href,
50+
width,
51+
},
52+
type: RecordType.Meta,
53+
timestamp,
54+
})
55+
56+
transaction.add({
57+
data: {
58+
has_focus: document.hasFocus(),
59+
},
60+
type: RecordType.Focus,
61+
timestamp,
62+
})
63+
64+
transaction.add({
65+
data: {
66+
node: serializeDocument(document, transaction),
67+
initialOffset: {
68+
left: getScrollX(),
69+
top: getScrollY(),
70+
},
71+
},
72+
type: RecordType.FullSnapshot,
73+
timestamp,
74+
})
75+
76+
if (window.visualViewport) {
77+
transaction.add({
78+
data: getVisualViewport(window.visualViewport),
79+
type: RecordType.VisualViewport,
80+
timestamp,
81+
})
82+
}
83+
})
84+
}

packages/rum/src/entries/internal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ export {
1515

1616
export * from '../types'
1717

18-
export { serializeNode, serializeNode as serializeNodeWithId } from '../domain/record'
18+
export { takeFullSnapshot, takeNodeSnapshot, serializeNode as serializeNodeWithId } from '../domain/record'

0 commit comments

Comments
 (0)