-
Notifications
You must be signed in to change notification settings - Fork 170
✨[PANA-5156] Expose better session replay internal API #4018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { NodeType, RecordType } from '../../types' | ||
| import { appendElement } from '../../../../rum-core/test' | ||
| import { takeFullSnapshot, takeNodeSnapshot } from './internalApi' | ||
|
|
||
| describe('takeFullSnapshot', () => { | ||
| it('should produce Meta, Focus, and FullSnapshot records', () => { | ||
| expect(takeFullSnapshot()).toEqual( | ||
| jasmine.arrayContaining([ | ||
| { | ||
| data: { | ||
| height: jasmine.any(Number), | ||
| href: window.location.href, | ||
| width: jasmine.any(Number), | ||
| }, | ||
| type: RecordType.Meta, | ||
| timestamp: jasmine.any(Number), | ||
| }, | ||
| { | ||
| data: { | ||
| has_focus: document.hasFocus(), | ||
| }, | ||
| type: RecordType.Focus, | ||
| timestamp: jasmine.any(Number), | ||
| }, | ||
| { | ||
| data: { | ||
| node: jasmine.any(Object), | ||
| initialOffset: { | ||
| left: jasmine.any(Number), | ||
| top: jasmine.any(Number), | ||
| }, | ||
| }, | ||
| type: RecordType.FullSnapshot, | ||
| timestamp: jasmine.any(Number), | ||
| }, | ||
| ]) | ||
| ) | ||
| }) | ||
|
|
||
| it('should produce VisualViewport records when supported', () => { | ||
| if (!window.visualViewport) { | ||
| pending('visualViewport not supported') | ||
| } | ||
|
|
||
| expect(takeFullSnapshot()).toEqual( | ||
| jasmine.arrayContaining([ | ||
| { | ||
| data: jasmine.any(Object), | ||
| type: RecordType.VisualViewport, | ||
| timestamp: jasmine.any(Number), | ||
| }, | ||
| ]) | ||
| ) | ||
| }) | ||
| }) | ||
|
|
||
| describe('takeNodeSnapshot', () => { | ||
| it('should serialize nodes', () => { | ||
| const node = appendElement('<div>Hello <b>world</b></div>', document.body) | ||
| expect(takeNodeSnapshot(node)).toEqual({ | ||
| type: NodeType.Element, | ||
| id: 0, | ||
| tagName: 'div', | ||
| isSVG: undefined, | ||
| attributes: {}, | ||
| childNodes: [ | ||
| { | ||
| type: NodeType.Text, | ||
| id: 1, | ||
| textContent: 'Hello ', | ||
| }, | ||
| { | ||
| type: NodeType.Element, | ||
| id: 2, | ||
| tagName: 'b', | ||
| isSVG: undefined, | ||
| attributes: {}, | ||
| childNodes: [ | ||
| { | ||
| type: NodeType.Text, | ||
| id: 3, | ||
| textContent: 'world', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }) | ||
| }) | ||
|
|
||
| it('should serialize shadow hosts', () => { | ||
| const node = appendElement('<div>Hello</div>', document.body) | ||
| const shadowRoot = node.attachShadow({ mode: 'open' }) | ||
| shadowRoot.appendChild(document.createTextNode('world')) | ||
| expect(takeNodeSnapshot(node)).toEqual({ | ||
| type: NodeType.Element, | ||
| id: 0, | ||
| tagName: 'div', | ||
| isSVG: undefined, | ||
| attributes: {}, | ||
| childNodes: [ | ||
| { | ||
| type: NodeType.Text, | ||
| id: 1, | ||
| textContent: 'Hello', | ||
| }, | ||
| { | ||
| type: NodeType.DocumentFragment, | ||
| id: 2, | ||
| isShadowRoot: true, | ||
| adoptedStyleSheets: undefined, | ||
| childNodes: [ | ||
| { | ||
| type: NodeType.Text, | ||
| id: 3, | ||
| textContent: 'world', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { noop, timeStampNow } from '@datadog/browser-core' | ||
| import type { RumConfiguration } from '@datadog/browser-rum-core' | ||
| import { getNodePrivacyLevel, NodePrivacyLevel } from '@datadog/browser-rum-core' | ||
| import type { BrowserRecord, SerializedNodeWithId } from '../../types' | ||
| import { takeFullSnapshot as doTakeFullSnapshot } from './startFullSnapshots' | ||
| import type { ShadowRootsController } from './shadowRootsController' | ||
| import type { RecordingScope } from './recordingScope' | ||
| import { createRecordingScope } from './recordingScope' | ||
| import { createElementsScrollPositions } from './elementsScrollPositions' | ||
| import { createEventIds } from './eventIds' | ||
| import { createNodeIds } from './nodeIds' | ||
| import type { EmitRecordCallback } from './record.types' | ||
| import type { SerializationTransaction } from './serialization' | ||
| import { SerializationKind, serializeInTransaction, serializeNode } from './serialization' | ||
|
|
||
| /** | ||
| * Take a full snapshot of the document, generating the same records that the browser SDK | ||
| * would generate. | ||
| * | ||
| * This is an internal API function. Be sure to update Datadog-internal callers if you | ||
| * change its signature or behavior. | ||
| */ | ||
| export function takeFullSnapshot({ | ||
| configuration, | ||
| }: { configuration?: Partial<RumConfiguration> } = {}): BrowserRecord[] { | ||
| const records: BrowserRecord[] = [] | ||
| const emitRecord: EmitRecordCallback = (record: BrowserRecord) => { | ||
| records.push(record) | ||
| } | ||
|
|
||
| doTakeFullSnapshot( | ||
| timeStampNow(), | ||
| SerializationKind.INITIAL_FULL_SNAPSHOT, | ||
| emitRecord, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We must be sure that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed! In the near term, for better or worse, I think we'll be stuck with that limitation anyway, because if we yield, the DOM might mutate out from under us. (It'd be interesting to investigate a full snapshot algorithm that could tolerate that, though...) |
||
| noop, | ||
| createTemporaryRecordingScope(configuration) | ||
| ) | ||
|
|
||
| return records | ||
| } | ||
|
|
||
| /** | ||
| * Take a snapshot of a DOM node, generating the serialized representation that the | ||
| * browser SDK would generate. | ||
| * | ||
| * This is an internal API function. Be sure to update Datadog-internal callers if you | ||
| * change its signature or behavior. | ||
| */ | ||
| export function takeNodeSnapshot( | ||
| node: Node, | ||
| { configuration }: { configuration?: Partial<RumConfiguration> } = {} | ||
| ): SerializedNodeWithId | null { | ||
| let serializedNode: SerializedNodeWithId | null = null | ||
|
|
||
| serializeInTransaction( | ||
| SerializationKind.INITIAL_FULL_SNAPSHOT, | ||
| noop, | ||
| noop, | ||
| createTemporaryRecordingScope(configuration), | ||
| (transaction: SerializationTransaction): void => { | ||
| const privacyLevel = getNodePrivacyLevel(node, transaction.scope.configuration.defaultPrivacyLevel) | ||
| if (privacyLevel === NodePrivacyLevel.HIDDEN || privacyLevel === NodePrivacyLevel.IGNORE) { | ||
| return | ||
| } | ||
| serializedNode = serializeNode(node, privacyLevel, transaction) | ||
| } | ||
| ) | ||
|
|
||
| return serializedNode | ||
| } | ||
|
|
||
| function createTemporaryRecordingScope(configuration?: Partial<RumConfiguration>): RecordingScope { | ||
| return createRecordingScope( | ||
| { | ||
| defaultPrivacyLevel: NodePrivacyLevel.ALLOW, | ||
| ...configuration, | ||
| } as RumConfiguration, | ||
| createElementsScrollPositions(), | ||
| createEventIds(), | ||
| createNodeIds(), | ||
| { | ||
| addShadowRoot: noop, | ||
| removeShadowRoot: noop, | ||
| flush: noop, | ||
| stop: noop, | ||
| } as ShadowRootsController | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,4 +15,4 @@ export { | |
|
|
||
| export * from '../types' | ||
|
|
||
| export { serializeNode, serializeNode as serializeNodeWithId } from '../domain/record' | ||
| export { takeFullSnapshot, takeNodeSnapshot, serializeNode as serializeNodeWithId } from '../domain/record' | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m a bit confused. Why is there so much logic tied to internalAPI? FMU, internal.ts was meant to expose the underlying APIs as-is, not to add internal-specific code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the downside of dependency injection; we need to configure all the dependencies for this code! All of the logic is just calls to factory functions, or direct construction of "noop" versions of dependencies in cases where we don't need the full versions.