-
Notifications
You must be signed in to change notification settings - Fork 1
[RUM-15529] Integrate Session Replay #131
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
Open
cdn34dd
wants to merge
11
commits into
main
Choose a base branch
from
carlosnogueira/RUM-15529/integrate-session-replay
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
fb1f12c
Refactor transport to make BatchProducer/Consumer extensible for new …
cdn34dd 004d737
Integrate session replay into the transport pipeline
cdn34dd 354b94b
Update .gitignore rules to allow for yarn patches
cdn34dd b64b210
Patch dd-trace preload to enable session replay and fix config timing
cdn34dd bf10a7e
Switch CONFIG_CHANNEL handler from ipcMain.on to ipcMain.handle
cdn34dd 3783730
Add unit and E2E test coverage for session replay
cdn34dd 9418900
Review fixes
cdn34dd d52bd0b
Review fixes
cdn34dd d1ae8af
Review fixes
cdn34dd 71fef85
Review fixes
cdn34dd 5182d1b
Replace yarn patches for dd-trace with a best-effort postinstall script
cdn34dd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| 'use strict'; | ||
|
|
||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| const PRELOAD_REL = 'packages/datadog-instrumentations/src/electron/preload.js'; | ||
|
|
||
| // The replacement preload — identical to upstream except getCapabilities() | ||
| // returns '["records"]' to enable session replay recording. | ||
| const NEW_PRELOAD = `'use strict'; | ||
|
|
||
| // eslint-disable-next-line n/no-missing-require | ||
| const { contextBridge, ipcRenderer } = require('electron'); | ||
|
|
||
| const BRIDGE_CHANNEL = 'datadog:bridge-send'; | ||
| const CONFIG_CHANNEL = 'datadog:bridge-config'; | ||
|
|
||
| // Privacy levels matching @datadog/browser-core DefaultPrivacyLevel | ||
| const MASK = 'mask'; | ||
|
|
||
| const config = ipcRenderer.sendSync(CONFIG_CHANNEL); | ||
|
|
||
| const defaultPrivacyLevel = config?.defaultPrivacyLevel ?? MASK; | ||
| const configuredHosts = config?.allowedWebViewHosts ?? []; | ||
| // eslint-disable-next-line no-undef | ||
| const allowedHosts = [...new Set([location.hostname, ...configuredHosts])]; | ||
|
|
||
| const bridge = { | ||
| getCapabilities() { | ||
| return '["records"]'; | ||
| }, | ||
| getPrivacyLevel() { | ||
| return defaultPrivacyLevel; | ||
| }, | ||
| getAllowedWebViewHosts() { | ||
| return JSON.stringify(allowedHosts); | ||
| }, | ||
| send(msg) { | ||
| ipcRenderer.send(BRIDGE_CHANNEL, msg); | ||
| }, | ||
| }; | ||
|
|
||
| // Support both contextIsolation enabled (default) and disabled | ||
|
|
||
| window.DatadogEventBridge = bridge; | ||
|
|
||
| try { | ||
| contextBridge.exposeInMainWorld('DatadogEventBridge', bridge); | ||
| } catch { | ||
| // exposeInMainWorld throws when contextIsolation is disabled | ||
| } | ||
| `; | ||
|
|
||
| function findPreloadIn(dir) { | ||
| const p = path.join(dir, 'node_modules', 'dd-trace', PRELOAD_REL); | ||
| return fs.existsSync(p) ? p : null; | ||
| } | ||
|
|
||
| /** Walk up from dir looking for a package.json with a workspaces field. */ | ||
| function findMonorepoRoot(dir) { | ||
| let current = dir; | ||
| while (true) { | ||
| const parent = path.dirname(current); | ||
| if (parent === current) return null; | ||
| const pkgPath = path.join(parent, 'package.json'); | ||
| if (fs.existsSync(pkgPath)) { | ||
| try { | ||
| const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); | ||
| if (pkg.workspaces) return parent; | ||
| } catch {} | ||
| } | ||
| current = parent; | ||
| } | ||
| } | ||
|
|
||
| const cwd = process.cwd(); | ||
|
|
||
| let preloadPath = findPreloadIn(cwd); | ||
|
|
||
| if (!preloadPath) { | ||
| const root = findMonorepoRoot(cwd); | ||
| if (root) preloadPath = findPreloadIn(root); | ||
| } | ||
|
|
||
| if (!preloadPath) { | ||
| console.log('[datadog] dd-trace preload.js not found — skipping patch'); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| try { | ||
| fs.writeFileSync(preloadPath, NEW_PRELOAD, 'utf8'); | ||
| console.log('[datadog] Patched', path.relative(cwd, preloadPath)); | ||
| } catch (err) { | ||
| console.warn('[datadog] Could not patch dd-trace preload:', err.message); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { test, expect } from '../lib/helpers'; | ||
|
|
||
| /** | ||
| * Session replay E2E scenarios. | ||
| * | ||
| * These tests verify that: | ||
| * 1. The dd-trace preload exposes "records" capability so the browser RUM SDK | ||
| * starts emitting BrowserRecord events through the bridge. | ||
| * 2. ReplayCollection buffers those records and emits a compressed segment. | ||
| * 3. ReplayBatchConsumer uploads the segment as multipart/form-data and the | ||
| * mock intake receives it with correct metadata. | ||
| */ | ||
|
|
||
| test.describe('session replay', () => { | ||
|
Collaborator
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. 💬 suggestion: we should add a scenario to exercise the mask configuration |
||
| test('replay segment arrives at the intake after opening a bridge window', async ({ | ||
| electronApp, | ||
| mainPage, | ||
| intake, | ||
| }) => { | ||
| // Open a bridge window — the browser RUM SDK initialises and, because the | ||
| // dd-trace preload advertises "records" capability, starts recording rrweb events. | ||
| const bridgeWindow = await mainPage.openBridgeFileWindow(electronApp); | ||
|
|
||
| // Give the renderer time to produce at least one full-snapshot record. | ||
| await bridgeWindow.page.waitForTimeout(2000); | ||
|
|
||
| // Flush forces ReplayCollection to flush the current segment, compress it, | ||
| // and push it through the batch pipeline to the intake. | ||
| await mainPage.flushTransport(); | ||
|
|
||
| const segment = await intake.waitForReplaySegment({ timeout: 20_000 }); | ||
|
|
||
| // The segment metadata should be populated with main-process context | ||
| expect(segment.metadata['session']).toBeDefined(); | ||
| expect(segment.metadata['application']).toBeDefined(); | ||
| expect(segment.metadata['view']).toBeDefined(); | ||
| expect(segment.metadata['records_count']).toBeGreaterThan(0); | ||
| expect(segment.metadata['source']).toBe('browser'); | ||
| }); | ||
|
|
||
| test('view event includes has_replay: true after a replay segment is sent', async ({ | ||
| electronApp, | ||
| mainPage, | ||
| intake, | ||
| }) => { | ||
| // Open bridge window and wait for recording to produce data | ||
| const bridgeWindow = await mainPage.openBridgeFileWindow(electronApp); | ||
| await bridgeWindow.page.waitForTimeout(2000); | ||
|
|
||
| // First flush — sends the replay segment to the intake | ||
| await mainPage.flushTransport(); | ||
| await intake.waitForReplaySegment({ timeout: 20_000 }); | ||
|
|
||
| // Trigger renderer activity so browser-rum emits a fresh view update. | ||
| // generateError works in file:// context (unlike fetch-based generateResource). | ||
| // Assembly will enrich the resulting view update with has_replay: true now | ||
| // that replay stats exist for this view. | ||
| await bridgeWindow.generateError('replay-trigger'); | ||
| await mainPage.flushTransport(); | ||
|
|
||
| const viewEvents = await intake.waitForEventCount('view', 1, { | ||
| timeout: 20_000, | ||
| predicate: (e) => { | ||
| const body = e.body as Record<string, unknown>; | ||
| const session = body['session'] as Record<string, unknown> | undefined; | ||
| return session?.['has_replay'] === true; | ||
| }, | ||
| }); | ||
|
|
||
| expect(viewEvents.length).toBeGreaterThanOrEqual(1); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
💬 suggestion: those two are not used in bridge mode