Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
33e6077
✨ replace IPC span creation with RUM event handler callback
bcaudan Aug 14, 2026
61f96ae
feat: add IpcResourceCollector emitting RUM ipc resource events (prot…
bcaudan Aug 14, 2026
acef5b6
feat: add renderer-side IPC preload module with DatadogIpcBridge (pro…
bcaudan Aug 14, 2026
adbe9dc
fix: restore ipc.id in stop-event context.ipc for cross-process corre…
bcaudan Aug 14, 2026
75378cd
chore: point playground at local browser-sdk worktree for the IPC pro…
bcaudan Aug 14, 2026
f32e534
feat: add request/response IPC scenario to playground
bcaudan Aug 14, 2026
a32f0ab
✨ add fire-and-forget IPC scenario to playground
bcaudan Aug 14, 2026
5adc237
✨ add fire-and-forget main→renderer IPC scenario with 2 listeners
bcaudan Aug 14, 2026
3b472c1
✨ add nested IPC scenario with progress feedback
bcaudan Aug 14, 2026
718adf8
✨ Add broadcast IPC scenario and wire renderer bridge to RUM resources
bcaudan Aug 14, 2026
17c7f67
fix: skip datadog: internal channels in patchIpcRenderer to prevent f…
bcaudan Aug 14, 2026
76e4d15
🐛 share ipc event handler across bundles via globalThis symbol
bcaudan Aug 14, 2026
eb59501
✅ add E2E scenarios validating IPC RUM event correlation
bcaudan Aug 14, 2026
77eb4ac
fix: satisfy public browser-rum API types in playground IPC bridge wi…
bcaudan Aug 14, 2026
f9b0bc8
feat: add error and vital events alongside IPC scenarios in playground
bcaudan Aug 17, 2026
d1fb028
fix: restore/add a real fetch per IPC scenario alongside error/vital …
bcaudan Aug 17, 2026
77c7d6d
feat: add @datadog/electron-sdk/renderer entry to auto-wire IPC resou…
bcaudan Aug 17, 2026
c8de0c1
feat: expose the IPC resource bridge as a RumPlugin, not a plain func…
bcaudan Aug 18, 2026
d641f25
chore: approve git-based transitive dependency for yarn install
bcaudan Aug 18, 2026
2f8767f
fix: hardcode-exclude get-internal-context from IPC RUM events
bcaudan Aug 18, 2026
2d5c490
fix: await hidden broadcast windows finishing load before relaying
bcaudan Aug 18, 2026
012d3b3
feat: propagate ipc.id ancestry as context.ipc.parent_ids
bcaudan Aug 19, 2026
2ddd682
fix: reword broadcast button label, stale after window-open split
bcaudan Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
"require": "./dist/esbuild-plugin.cjs",
"import": "./dist/esbuild-plugin.mjs"
},
"./preload": "./dist/preload.js"
"./preload": "./dist/preload.js",
"./renderer": {
"types": "./dist/renderer.d.ts",
"require": "./dist/renderer.cjs",
"import": "./dist/renderer.mjs"
}
},
"typesVersions": {
"*": {
Expand All @@ -47,6 +52,9 @@
],
"esbuild-plugin": [
"./dist/esbuild-plugin.d.ts"
],
"renderer": [
"./dist/renderer.d.ts"
]
}
},
Expand Down Expand Up @@ -81,6 +89,11 @@
"dist/esbuild-plugin.mjs.map",
"dist/esbuild-plugin.d.ts",
"dist/preload.js",
"dist/renderer.cjs",
"dist/renderer.cjs.map",
"dist/renderer.mjs",
"dist/renderer.mjs.map",
"dist/renderer.d.ts",
"README.md",
"LICENSE",
"LICENSE-3rdparty.csv"
Expand Down
4 changes: 3 additions & 1 deletion playground/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# Align with Renovate's minimumReleaseAge of 7 days for defense-in-depth
approvedGitRepositories:
- '**'

npmMinimalAgeGate: 7d
2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"test": "yarn build && playwright test -c test/playwright.config.ts"
},
"dependencies": {
"@datadog/browser-rum": "7.5.0",
"@datadog/browser-rum": "portal:/Users/bastien.caudan/go/src/github.com/DataDog/browser-sdk/.worktrees/bcaudan/browser-ipc/packages/browser-rum",
"@datadog/electron-sdk": "portal:.."
},
"devDependencies": {
Expand Down
10 changes: 10 additions & 0 deletions playground/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ <h2>Session ID:</h2>
<button id="main-fetch-net">Main Fetch (net)</button>
</div>

<div class="section-label">IPC Scenarios (prototype)</div>
<div class="button-group">
<button id="ipc-get-profile">Request/Response (get-profile)</button>
<button id="ipc-ping-main">Fire-and-forget: renderer → main (2 listeners)</button>
<button id="ipc-ping-renderer">Fire-and-forget: main → renderer (2 listeners)</button>
<button id="ipc-nested-profile">Nested IPC (progress + fetch)</button>
<button id="ipc-open-broadcast-windows">Open broadcast windows</button>
<button id="ipc-broadcast">Broadcast to opened windows</button>
</div>

<div class="section-label">Custom Duration Vitals (main process)</div>
<div class="button-group">
<button id="vital-add">Add 1.5s "database.migration"</button>
Expand Down
66 changes: 65 additions & 1 deletion playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
_flushTransport,
getInternalContext,
_generateTelemetryError,
addError,
addDurationVital,
startDurationVital,
stopDurationVital,
Expand Down Expand Up @@ -145,6 +146,69 @@ ipcMain.handle('main:fetch-api-net', async () => {
return (await res.json()) as unknown;
});

ipcMain.handle('ipc-demo:get-profile', async () => {
const res = await fetch('https://httpbin.org/json');
return (await res.json()) as unknown;
});

ipcMain.handle('ipc-demo:get-profile-with-progress', async () => {
mainWindow?.webContents.send('ipc-demo:profile-progress', { status: 'fetching' });
startDurationVital('profile.fetch');
const res = await fetch('https://httpbin.org/json');
stopDurationVital('profile.fetch');
return (await res.json()) as unknown;
});

ipcMain.on('ipc-demo:ping-main', () => {
void fetch('https://httpbin.org/json').catch(() => undefined);
});
ipcMain.on('ipc-demo:ping-main', () => {
void fetch('https://httpbin.org/uuid').catch(() => undefined);
addError(new Error('ping-main listener #2 failed'), { context: { scenario: 'ipc-demo:ping-main' } });
});

ipcMain.handle('ipc-demo:trigger-ping-renderer', () => {
void fetch('https://httpbin.org/json').catch(() => undefined);
mainWindow?.webContents.send('ipc-demo:ping-renderer', { from: 'main' });
});

let broadcastWindows: BrowserWindow[] = [];

// Opening the helper windows is its own explicit user action (the "Open broadcast windows" button),
// separate from actually broadcasting. This keeps the broadcast handler itself fully synchronous — no
// await between receiving the trigger and relaying to each window — which matters for two reasons:
// (1) by the time a user can click "Broadcast", the windows the earlier click already opened are
// guaranteed loaded (`loadURL` resolves once the page has finished loading, by which point the
// renderer's synchronous top-level script — including its ipcRenderer.on registration — has already
// run), so Electron never silently drops a send aimed at a not-yet-registered listener; and (2) a
// synchronous handler keeps the relay sends within the same ambient IPC context as the trigger call,
// so they correctly inherit it as their parent (see src/domain/tracing/ipcParentContext.ts) — an
// `await` between the trigger and the relay would clear that context before the sends fire.
ipcMain.handle('ipc-demo:open-broadcast-windows', async () => {
if (broadcastWindows.length > 0) return; // already opened, idempotent
broadcastWindows = [0, 1].map(
() =>
new BrowserWindow({
width: 400,
height: 300,
show: !isTestMode,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js'),
},
})
);
await Promise.all(broadcastWindows.map((win) => win.loadURL('app://app/')));
});

ipcMain.handle('ipc-demo:broadcast', (_event, data: unknown) => {
void fetch('https://httpbin.org/json').catch(() => undefined);
for (const win of broadcastWindows) {
win.webContents.send('ipc-demo:broadcast-received', data);
}
});

// IPC handler to crash the main process
ipcMain.handle('crash', () => {
process.crash();
Expand Down Expand Up @@ -207,7 +271,7 @@ ipcMain.handle(
}
);

const ACTIVE_ENV = 'staging';
const ACTIVE_ENV = 'prod';
const CONF = {
staging: {
applicationId: '6efd3722-af0a-4070-994c-0e87076d4814',
Expand Down
15 changes: 15 additions & 0 deletions playground/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke('main:fail-operation', name, failureReason, options),
mainFetchApiFetch: () => ipcRenderer.invoke('main:fetch-api-fetch'),
mainFetchApiNet: () => ipcRenderer.invoke('main:fetch-api-net'),
getProfile: () => ipcRenderer.invoke('ipc-demo:get-profile'),
getProfileWithProgress: () => ipcRenderer.invoke('ipc-demo:get-profile-with-progress'),
onProfileProgress: (callback: (data: unknown) => void) => {
ipcRenderer.on('ipc-demo:profile-progress', (_event, data) => callback(data));
},
pingMain: () => ipcRenderer.send('ipc-demo:ping-main'),
openRumExplorer: () => ipcRenderer.invoke('open-rum-explorer'),
flushTransport: () => ipcRenderer.invoke('flush-transport'),
setUserInfo: () => ipcRenderer.invoke('main:set-user-info'),
Expand All @@ -43,4 +49,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
setAccountInfo: () => ipcRenderer.invoke('main:set-account-info'),
addAccountExtraInfo: () => ipcRenderer.invoke('main:add-account-extra-info'),
clearAccountInfo: () => ipcRenderer.invoke('main:clear-account-info'),
triggerPingRenderer: () => ipcRenderer.invoke('ipc-demo:trigger-ping-renderer'),
onPingFromMain: (callback: (data: unknown) => void) => {
ipcRenderer.on('ipc-demo:ping-renderer', (_event, data) => callback(data));
},
openBroadcastWindows: () => ipcRenderer.invoke('ipc-demo:open-broadcast-windows'),
broadcast: (data: unknown) => ipcRenderer.invoke('ipc-demo:broadcast', data),
onBroadcastReceived: (callback: (data: unknown) => void) => {
ipcRenderer.on('ipc-demo:broadcast-received', (_event, data) => callback(data));
},
});
45 changes: 45 additions & 0 deletions playground/src/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { datadogRum } from '@datadog/browser-rum';
import { datadogRendererPlugin } from '@datadog/electron-sdk/renderer';

interface DurationVitalOptions {
vitalKey?: string;
Expand Down Expand Up @@ -26,6 +27,7 @@ datadogRum.init({
trackResources: true,
trackLongTasks: true,
trackUserInteractions: true,
plugins: [datadogRendererPlugin()],
});

// Type definition for the exposed API
Expand All @@ -49,6 +51,10 @@ interface ElectronAPI {
) => Promise<void>;
mainFetchApiFetch: () => Promise<unknown>;
mainFetchApiNet: () => Promise<unknown>;
getProfile: () => Promise<unknown>;
getProfileWithProgress: () => Promise<unknown>;
onProfileProgress: (callback: (data: unknown) => void) => void;
pingMain: () => void;
openRumExplorer: () => Promise<void>;
flushTransport: () => Promise<void>;
setUserInfo: () => Promise<void>;
Expand All @@ -57,6 +63,11 @@ interface ElectronAPI {
setAccountInfo: () => Promise<void>;
addAccountExtraInfo: () => Promise<void>;
clearAccountInfo: () => Promise<void>;
triggerPingRenderer: () => Promise<void>;
onPingFromMain: (callback: (data: unknown) => void) => void;
openBroadcastWindows: () => Promise<void>;
broadcast: (data: unknown) => Promise<void>;
onBroadcastReceived: (callback: (data: unknown) => void) => void;
}

declare global {
Expand Down Expand Up @@ -261,6 +272,40 @@ setupDemoButton('clear-account-info', 'main:clear-account-info', () => window.el
setupDemoButton('main-fetch', 'main:fetch-api', () => window.electronAPI.mainFetchApi());
setupDemoButton('main-fetch-fetch', 'main:fetch-api-fetch', () => window.electronAPI.mainFetchApiFetch());
setupDemoButton('main-fetch-net', 'main:fetch-api-net', () => window.electronAPI.mainFetchApiNet());
setupDemoButton('ipc-get-profile', 'ipc-demo:get-profile', () => window.electronAPI.getProfile());
setupDemoButton('ipc-ping-main', 'ipc-demo:ping-main', () => {
window.electronAPI.pingMain();
return Promise.resolve();
});

// Register two separate listeners for ping-renderer to demonstrate multiplicity
window.electronAPI.onPingFromMain((data) => logIpcCall('ipc-demo:ping-renderer#1', 'done', 0, JSON.stringify(data)));
window.electronAPI.onPingFromMain((data) => {
logIpcCall('ipc-demo:ping-renderer#2', 'done', 0, JSON.stringify(data));
datadogRum.addDurationVital('ping-renderer.handling', {
startTime: Date.now() - 50,
duration: 50,
context: { scenario: 'ipc-demo:ping-renderer' },
});
});

setupDemoButton('ipc-ping-renderer', 'ipc-demo:trigger-ping-renderer', () => window.electronAPI.triggerPingRenderer());

window.electronAPI.onProfileProgress((data) =>
logIpcCall('ipc-demo:profile-progress', 'done', 0, JSON.stringify(data))
);
setupDemoButton('ipc-nested-profile', 'ipc-demo:get-profile-with-progress', () =>
window.electronAPI.getProfileWithProgress()
);

window.electronAPI.onBroadcastReceived((data) => {
logIpcCall('ipc-demo:broadcast-received', 'done', 0, JSON.stringify(data));
datadogRum.addError(new Error('broadcast-received handling failed'), { scenario: 'ipc-demo:broadcast-received' });
});
setupDemoButton('ipc-open-broadcast-windows', 'ipc-demo:open-broadcast-windows', () =>
window.electronAPI.openBroadcastWindows()
);
setupDemoButton('ipc-broadcast', 'ipc-demo:broadcast', () => window.electronAPI.broadcast({ from: 'main-window' }));

// --- Custom duration vital demo buttons ---

Expand Down
Loading