Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c32d541
📝 add child process monitoring landscape survey and prototype stubs
bcaudan Apr 14, 2026
bc5448c
📝 document lifecycle events prototype findings
bcaudan Apr 14, 2026
20a100a
📝 document spawn/exec/execFile prototype findings
bcaudan Apr 14, 2026
3368860
📝 document utilityProcess prototype findings
bcaudan Apr 14, 2026
eab7d07
📝 add instrumentation strategy diagrams to each prototype
bcaudan Apr 14, 2026
49fbe4c
📝 document parentPort piggyback reliability assessment
bcaudan Apr 14, 2026
43d730c
📝 update research doc with validated feasibility findings
bcaudan Apr 14, 2026
9c9d074
📝 add AI usage log for session 1
bcaudan Apr 14, 2026
be24b5b
📝 document RUM concept mapping for child process telemetry
bcaudan Apr 15, 2026
87eae59
📝 add AI usage log for session 2
bcaudan Apr 15, 2026
d095d65
⚗️ setup playground test harness with Playwright + mock intake
bcaudan Apr 15, 2026
ce73a8e
⚗️ instrument child_process spawn/exec as RUM resource events
bcaudan Apr 15, 2026
effc238
⚗️ model utility processes as RUM views
bcaudan Apr 15, 2026
86a3cf1
⚗️ add performance metrics polling for utility process views
bcaudan Apr 15, 2026
2b4e830
⚗️ add renderer process views with container hierarchy
bcaudan Apr 15, 2026
91cf017
🐛 fix duplicate child_process resource events
bcaudan Apr 15, 2026
ece84aa
🐛 fix main view date using assembly time instead of view start time
bcaudan Apr 15, 2026
13cbd9e
♻️ detect renderer processes eagerly via web-contents-created
bcaudan Apr 15, 2026
2c9f84a
👌 playground improvements
bcaudan Apr 15, 2026
f9dad0a
📝 document prototype findings for child process RUM telemetry
bcaudan Apr 15, 2026
2d3b26a
♻️ move memory metrics to view schema fields, document CPU gap
bcaudan Apr 15, 2026
c734736
⚗️ sanitize app paths in events, use method-specific resource URLs
bcaudan Apr 15, 2026
7e452df
👌 use page title for renderer view name, remove pid from name
bcaudan Apr 15, 2026
486b6c7
👌 fix renderer view title and date accuracy
bcaudan Apr 15, 2026
d08ddf9
👌 strip app path from events instead of replacing with placeholder
bcaudan Apr 15, 2026
f23651a
📝 update findings with path sanitization learnings
bcaudan Apr 15, 2026
0aea34d
📝 review findings doc, remove debug log
bcaudan Apr 16, 2026
38a70cd
📝 add AI usage log for session 3
bcaudan Apr 16, 2026
bd1d300
📝 add conclusion doc for child process monitoring prototype
bcaudan Apr 16, 2026
31b0fcf
📝 add AI usage log for session 4
bcaudan Apr 16, 2026
e531e83
📝 add AI usage overview with key insights across sessions
bcaudan Apr 16, 2026
c155329
📝 add demo scenario and slides
bcaudan Apr 16, 2026
c599166
🐛 fix utility process events attributed to main view
bcaudan Apr 16, 2026
01274ce
🔧 tweak playground UI for demo
bcaudan Apr 16, 2026
18ba43d
👌 simplify demo
bcaudan Apr 17, 2026
386a671
⚗️ prototype utility process uncaught error capture
bcaudan Apr 23, 2026
048cb9b
📝 add architecture schemas for current state and child process monito…
bcaudan Apr 23, 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
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"types": "./dist/preload.d.ts",
"require": "./dist/preload-auto.cjs",
"import": "./dist/preload.mjs"
},
"./utility": {
"require": "./dist/utility-preload.cjs"
}
},
"typesVersions": {
Expand All @@ -40,6 +43,8 @@
"dist/preload.d.ts",
"dist/preload.mjs",
"dist/preload.mjs.map",
"dist/utility-preload.cjs",
"dist/utility-preload.cjs.map",
"README.md",
"LICENSE",
"LICENSE-3rdparty.csv"
Expand All @@ -56,6 +61,7 @@
"typecheck": "tsc -b --noEmit",
"dev:init": "cd playground && yarn install",
"dev": "scripts/cli dev",
"playground:test": "yarn build && cd playground && yarn test",
"test": "vitest",
"test:unit": "vitest run --coverage",
"test:e2e:init": "yarn build && cd e2e/app && yarn install && yarn build",
Expand Down
36 changes: 36 additions & 0 deletions playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,42 @@ This will:
- Reload Electron when files change
- Open DevTools by default

## Testing

Automated Playwright tests validate playground scenarios against a mock intake server.

### Run Tests

```bash
# From playground/
yarn test

# Or from root
yarn playground:test
```

### Creating a Scenario

1. **Add a button + IPC handler** in `src/main.ts` and `src/preload.ts` for the feature you want to test
2. **Write a test** in `test/<name>.scenario.ts`:

```typescript
import { test, expect } from './helpers';

test('description', async ({ window, intake }) => {
// Click a button in the playground
await window.click('#my-button');

// Assert events arrived at the mock intake
const events = await intake.getEventsByType('resource', 10_000);
expect(events.length).toBeGreaterThanOrEqual(1);
});
```

3. **Run** `yarn test` and iterate

The test harness auto-launches the playground app with SDK events routed to a mock intake server via the `DD_SDK_PROXY` env var.

## Development with SDK Changes

From the root directory, run:
Expand Down
3 changes: 2 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"build:renderer": "esbuild src/renderer.ts --bundle --format=esm --outfile=dist/renderer.js --sourcemap",
"build:watch": "concurrently \"tsc --watch\" \"yarn build:renderer --watch\"",
"start": "yarn build && electron .",
"dev": "yarn build && concurrently \"yarn build:watch\" \"electron .\""
"dev": "yarn build && concurrently \"yarn build:watch\" \"electron .\"",
"test": "yarn build && playwright test -c test/playwright.config.ts"
},
"dependencies": {
"@datadog/browser-rum": "6.30.1",
Expand Down
24 changes: 24 additions & 0 deletions playground/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ <h1>Electron SDK Playground</h1>
<div class="session-section">
<h2>Session File Content:</h2>
<pre id="session-content">Loading...</pre>
<button id="copy-session-id" style="margin-top: 8px; padding: 8px 16px; font-size: 13px">
Copy Session ID
</button>
</div>

<div class="section-label">SDK Actions</div>
Expand All @@ -184,6 +187,27 @@ <h2>Session File Content:</h2>
<button id="crash-btn">Crash</button>
</div>

<div class="section-label">Child Process</div>
<div class="button-group">
<button id="spawn-ls">Spawn ls</button>
<button id="exec-echo">Exec echo</button>
<button id="spawn-fail">Spawn fail</button>
<button id="exec-timeout">Exec timeout</button>
</div>

<div class="section-label">Utility Process</div>
<div class="button-group">
<button id="fork-utility">Fork utility</button>
<button id="send-message">Send message</button>
<button id="crash-utility">Crash utility</button>
<button id="throw-utility-error">Throw error</button>
</div>

<div class="section-label">Renderer Process</div>
<div class="button-group">
<button id="crash-renderer">Crash renderer</button>
</div>

<div class="section-label">Fetch</div>
<div class="button-group">
<button id="demo-renderer-fetch">Renderer Fetch</button>
Expand Down
124 changes: 122 additions & 2 deletions playground/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { app, BrowserWindow, ipcMain } from 'electron';
import { app, BrowserWindow, ipcMain, utilityProcess } from 'electron';
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as https from 'node:https';
import { init, stopSession, _generateActivity, _generateTelemetryError } from '@datadog/electron-sdk';
import * as childProcess from 'node:child_process';
import { init, stopSession, _generateActivity, _generateTelemetryError, _flushTransport } from '@datadog/electron-sdk';
import { loadWindowState, saveWindowState } from './main/windowState';
import { setupHotReload } from './main/hotReload';

const isTestMode = process.env.DD_TEST_MODE === '1';
let mainWindow: BrowserWindow | null = null;

function getSessionFilePath(): string {
Expand All @@ -20,6 +22,7 @@ function createWindow() {
height: savedState?.height ?? 768,
x: savedState?.x,
y: savedState?.y,
show: !isTestMode,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
Expand Down Expand Up @@ -56,6 +59,10 @@ ipcMain.handle('get-session-file', () => {
}
});

ipcMain.handle('flushTransport', async () => {
await _flushTransport();
});

ipcMain.handle('stop-session', () => {
stopSession();
});
Expand Down Expand Up @@ -102,6 +109,117 @@ ipcMain.handle('crash', () => {
process.crash();
});

// --- Child process demo handlers ---

ipcMain.handle('child-process:spawn-ls', () => {
return new Promise<string>((resolve, reject) => {
const child = childProcess.spawn('ls', ['-la']);
let stdout = '';
child.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});
child.on('close', () => resolve(stdout));
child.on('error', reject);
});
});

ipcMain.handle('child-process:exec-echo', () => {
return new Promise<string>((resolve, reject) => {
childProcess.exec('echo hello world', (error, stdout) => {
if (error) reject(error);
else resolve(stdout.trim());
});
});
});

ipcMain.handle('child-process:spawn-fail', () => {
return new Promise<string>((resolve) => {
const child = childProcess.spawn('nonexistent-command-xyz');
child.on('error', (err) => resolve(`Error: ${err.message}`));
child.on('close', (code) => resolve(`Exited with code ${code}`));
});
});

ipcMain.handle('child-process:exec-timeout', () => {
return new Promise<string>((resolve) => {
childProcess.exec('sleep 10', { timeout: 100 }, (error) => {
resolve(error ? `Timeout: ${error.message}` : 'Completed');
});
});
});

// --- Utility process demo handlers ---

const WORKER_PATH = path.join(__dirname, 'workers', 'demo-worker.js');

ipcMain.handle('utility-process:fork', () => {
return new Promise<string>((resolve) => {
const child = utilityProcess.fork(WORKER_PATH, [], { serviceName: 'dd-demo-fork' });
child.once('message', (msg: { ready?: boolean }) => {
if (msg.ready) resolve(`Worker forked, pid: ${child.pid}`);
});
child.once('exit', (code: number) => resolve(`Worker exited with code ${code}`));
});
});

ipcMain.handle('utility-process:send-message', () => {
return new Promise<string>((resolve) => {
const child = utilityProcess.fork(WORKER_PATH, [], { serviceName: 'dd-demo-message' });
child.on('message', (msg: { ready?: boolean; reply?: string }) => {
if (msg.ready) {
child.postMessage({ action: 'ping' });
} else if (msg.reply) {
resolve(`Reply: ${msg.reply}`);
child.kill();
}
});
child.once('exit', () => resolve('Worker exited'));
});
});

ipcMain.handle('utility-process:crash', () => {
return new Promise<string>((resolve) => {
const child = utilityProcess.fork(WORKER_PATH, [], { serviceName: 'dd-demo-crash-worker' });
child.once('message', (msg: { ready?: boolean }) => {
if (msg.ready) {
child.postMessage({ action: 'crash' });
}
});
child.once('exit', (code: number) => resolve(`Worker crashed, exit code: ${code}`));
});
});

ipcMain.handle('utility-process:throw-error', () => {
return new Promise<string>((resolve) => {
const child = utilityProcess.fork(WORKER_PATH, [], { serviceName: 'dd-demo-throw-worker' });
child.once('message', (msg: { ready?: boolean }) => {
if (msg.ready) child.postMessage({ action: 'throw-error' });
});
child.once('exit', (code: number) => resolve(`Worker exited with code: ${code}`));
});
});

// --- Renderer process demo handlers ---

ipcMain.handle('renderer-process:crash', () => {
return new Promise<string>((resolve) => {
const win = new BrowserWindow({
width: 400,
height: 300,
show: !isTestMode,
webPreferences: { contextIsolation: true, nodeIntegration: false },
});
void win.loadURL('about:blank');
win.webContents.once('did-finish-load', () => {
// Wait for at least one renderer poll cycle so the process is tracked
setTimeout(() => {
win.webContents.forcefullyCrashRenderer();
resolve('Renderer crashed');
}, 3000);
});
});
});

void app.whenReady().then(async () => {
// Initialize SDK on app ready (before window creation)
console.log('Initializing SDK from main process...');
Expand All @@ -121,6 +239,8 @@ void app.whenReady().then(async () => {
...CONF.staging,
service: 'electron-playground',
env: 'dev',
// Allow tests to redirect events to a mock intake server
...(process.env.DD_SDK_PROXY ? { proxy: process.env.DD_SDK_PROXY } : {}),
});
console.log('SDK init result:', result);

Expand Down
10 changes: 10 additions & 0 deletions playground/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
generateUnhandledRejection: () => ipcRenderer.invoke('generateUnhandledRejection'),
crash: () => ipcRenderer.invoke('crash'),
mainFetchApi: () => ipcRenderer.invoke('main:fetch-api'),
flushTransport: () => ipcRenderer.invoke('flushTransport'),
spawnLs: () => ipcRenderer.invoke('child-process:spawn-ls'),
execEcho: () => ipcRenderer.invoke('child-process:exec-echo'),
spawnFail: () => ipcRenderer.invoke('child-process:spawn-fail'),
execTimeout: () => ipcRenderer.invoke('child-process:exec-timeout'),
forkUtility: () => ipcRenderer.invoke('utility-process:fork'),
sendMessage: () => ipcRenderer.invoke('utility-process:send-message'),
crashUtility: () => ipcRenderer.invoke('utility-process:crash'),
throwUtilityError: () => ipcRenderer.invoke('utility-process:throw-error'),
crashRenderer: () => ipcRenderer.invoke('renderer-process:crash'),
});
39 changes: 38 additions & 1 deletion playground/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ datadogRum.init({
sessionSampleRate: 100,
trackResources: true,
trackLongTasks: true,
trackUserInteractions: true,
trackUserInteractions: false,
});

// Type definition for the exposed API
Expand All @@ -26,6 +26,16 @@ interface ElectronAPI {
generateUnhandledRejection: () => Promise<void>;
crash: () => Promise<void>;
mainFetchApi: () => Promise<unknown>;
flushTransport: () => Promise<void>;
forkUtility: () => Promise<string>;
sendMessage: () => Promise<string>;
crashUtility: () => Promise<string>;
throwUtilityError: () => Promise<string>;
crashRenderer: () => Promise<string>;
spawnLs: () => Promise<string>;
execEcho: () => Promise<string>;
spawnFail: () => Promise<string>;
execTimeout: () => Promise<string>;
}

declare global {
Expand Down Expand Up @@ -68,6 +78,18 @@ async function refreshSessionDisplay() {
}
}

const copySessionIdBtn = document.getElementById('copy-session-id') as HTMLButtonElement;
copySessionIdBtn.addEventListener('click', () => {
try {
const parsed = JSON.parse(sessionContent.textContent ?? '');
if (parsed.id) {
void navigator.clipboard.writeText(parsed.id as string);
}
} catch {
// Session content not valid JSON
}
});

if (stopBtn && sessionContent) {
// Load session file content on page load
void refreshSessionDisplay();
Expand Down Expand Up @@ -200,3 +222,18 @@ if (rendererFetchBtn) {
}

setupDemoButton('main-fetch', 'main:fetch-api', () => window.electronAPI.mainFetchApi());

// Utility process buttons
setupDemoButton('fork-utility', 'utility-process:fork', () => window.electronAPI.forkUtility());
setupDemoButton('send-message', 'utility-process:send-message', () => window.electronAPI.sendMessage());
setupDemoButton('crash-utility', 'utility-process:crash', () => window.electronAPI.crashUtility());
setupDemoButton('throw-utility-error', 'utility-process:throw-error', () => window.electronAPI.throwUtilityError());

// Renderer process buttons
setupDemoButton('crash-renderer', 'renderer-process:crash', () => window.electronAPI.crashRenderer());

// Child process buttons
setupDemoButton('spawn-ls', 'child-process:spawn-ls', () => window.electronAPI.spawnLs());
setupDemoButton('exec-echo', 'child-process:exec-echo', () => window.electronAPI.execEcho());
setupDemoButton('spawn-fail', 'child-process:spawn-fail', () => window.electronAPI.spawnFail());
setupDemoButton('exec-timeout', 'child-process:exec-timeout', () => window.electronAPI.execTimeout());
27 changes: 27 additions & 0 deletions playground/src/workers/demo-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Demo utility process worker for playground testing.
* Receives messages via parentPort and responds.
*/

// Enable Datadog SDK error forwarding for this utility process
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('@datadog/electron-sdk/utility');

process.parentPort.on('message', (e: Electron.MessageEvent) => {
const { action } = e.data as { action: string };

if (action === 'ping') {
process.parentPort.postMessage({ reply: 'pong' });
} else if (action === 'crash') {
process.crash();
} else if (action === 'exit-clean') {
process.exit(0);
} else if (action === 'exit-error') {
process.exit(1);
} else if (action === 'throw-error') {
throw new Error('Uncaught error in utility process');
}
});

// Signal that the worker is ready
process.parentPort.postMessage({ ready: true });
Loading
Loading