Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ae6c169
⬆️ switch rum-events-format submodule to process-event draft PR branch
bcaudan Jul 22, 2026
890b35c
✨ add webContentsId to RumAssembleParams for renderer process enrichment
bcaudan Jul 22, 2026
58874ac
✨ add RawRumProcess type and simplify RawRumView (remove counters)
bcaudan Jul 22, 2026
4453216
👌 fix stale JSDoc and make RUM_EVENT_TYPES exhaustive against RumEven…
bcaudan Jul 22, 2026
e28ff61
✨ replace main-process view with fake view (session.id, electron://fa…
bcaudan Jul 22, 2026
8743813
✨ add ProcessContext for main and renderer process enrichment
bcaudan Jul 22, 2026
57cd955
✨ add ProcessCollection to track main and renderer process lifecycle
bcaudan Jul 22, 2026
a4a3752
🐛 clear main process timer on SESSION_EXPIRED
bcaudan Jul 22, 2026
45cc86f
✨ enrich RUM events with process context via hook
bcaudan Jul 22, 2026
b51d07b
✨ wire ProcessCollection and process enrichment hook in SDK init
bcaudan Jul 22, 2026
782dda3
✅ update view E2E and add process event E2E scenario
bcaudan Jul 22, 2026
29afb8e
✨ add process lifecycle controls to playground
bcaudan Jul 22, 2026
2ce56f9
🐛 fix process event assembly path and clean up unused scaffolding
bcaudan Jul 22, 2026
72f9d0f
✅ add playground scenario to generate process events and send to staging
bcaudan Jul 27, 2026
d1e9458
♻️ [RUM-17561] rename process event and enrichment to execution_context
bcaudan Aug 4, 2026
70fbd86
♻️ [RUM-17561] replace execution_context.pid with instance_id
bcaudan Aug 5, 2026
a71a2e4
✨ log execution_context events at send time
bcaudan Aug 12, 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
22 changes: 22 additions & 0 deletions e2e/app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {

const isDebugMode = process.env.PWDEBUG === '1';
let mainWindow: BrowserWindow | null = null;
let testRendererWindow: BrowserWindow | null = null;
let rendererHttpServer: http.Server | null = null;

const noop = () => undefined;
Expand Down Expand Up @@ -247,6 +248,27 @@ void app.whenReady().then(async () => {

ipcMain.handle('ping', () => 'pong');

ipcMain.handle('openRendererProcess', () => {
testRendererWindow = new BrowserWindow({
width: 400,
height: 300,
show: isDebugMode,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
},
});
void testRendererWindow.loadURL('about:blank');
testRendererWindow.on('closed', () => {
testRendererWindow = null;
});
});

ipcMain.handle('closeRendererProcess', () => {
testRendererWindow?.close();
testRendererWindow = null;
});

ipcMain.on('mainFireAndForget', (event) => {
event.sender.send('mainFireAndForgetAck');
});
Expand Down
2 changes: 2 additions & 0 deletions e2e/app/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
openBridgeFileWindowNoIsolation: () => ipcRenderer.invoke('openBridgeFileWindowNoIsolation'),
openBridgeHttpWindow: () => ipcRenderer.invoke('openBridgeHttpWindow'),
openBridgeAppProtocolWindow: () => ipcRenderer.invoke('openBridgeAppProtocolWindow'),
openRendererProcess: () => ipcRenderer.invoke('openRendererProcess'),
closeRendererProcess: () => ipcRenderer.invoke('closeRendererProcess'),
});
12 changes: 12 additions & 0 deletions e2e/lib/mainPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ interface ElectronAppWindow {
openBridgeFileWindowNoIsolation: () => Promise<void>;
openBridgeHttpWindow: () => Promise<void>;
openBridgeAppProtocolWindow: () => Promise<void>;
openRendererProcess: () => Promise<void>;
closeRendererProcess: () => Promise<void>;
};
}

Expand Down Expand Up @@ -242,4 +244,14 @@ export class MainPage {
);
return await BridgeWindowPage.waitForReady(electronApp);
}

async openRendererProcess(): Promise<void> {
await this.page.evaluate(() => (globalThis as unknown as ElectronAppWindow).electronAPI.openRendererProcess());
await this.waitForIpcPropagation();
}

async closeRendererProcess(): Promise<void> {
await this.page.evaluate(() => (globalThis as unknown as ElectronAppWindow).electronAPI.closeRendererProcess());
await this.waitForIpcPropagation();
}
}
71 changes: 71 additions & 0 deletions e2e/scenarios/execution-context.scenario.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { test, expect } from '../lib/helpers';

interface ExecutionContextEvent {
type: 'execution_context';
execution_context: {
id: string;
type: 'main-process' | 'renderer-process';
instance_id: string;
name?: string;
duration?: number;
exit_reason?: string;
};
_dd: { document_version: number };
}

test('emits a main execution context start event on SDK init', async ({ mainPage, intake }) => {
await mainPage.flushTransport();
const events = await intake.getEventsByType('execution_context');

expect(events.length).toBeGreaterThanOrEqual(1);
const mainEvent = events.find((e) => (e.body as ExecutionContextEvent).execution_context.type === 'main-process');
expect(mainEvent).toBeDefined();

const body = mainEvent!.body as ExecutionContextEvent;
expect(body.execution_context.type).toBe('main-process');
expect(body.execution_context.instance_id).toMatch(/^\d+$/);
expect(body._dd.document_version).toBe(1);
expect(body.execution_context.duration).toBeUndefined();
expect(body.execution_context.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
});

test('all main-process events carry execution_context.id and execution_context.type', async ({ mainPage, intake }) => {
await mainPage.flushTransport();
const viewEvents = await intake.getEventsByType('view');
expect(viewEvents.length).toBeGreaterThanOrEqual(1);

const view = viewEvents[0].body as Record<string, unknown>;
const executionContext = view['execution_context'] as { id: string; type: string } | undefined;
expect(executionContext).toBeDefined();
expect(executionContext!.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
expect(executionContext!.type).toBe('main-process');
});

test('emits start and end execution_context events for a renderer window lifecycle', async ({ mainPage, intake }) => {
await mainPage.flushTransport();
const before = (await intake.getEventsByType('execution_context')).length;

await mainPage.openRendererProcess();
await mainPage.flushTransport();

const afterOpen = await intake.getEventsByType('execution_context');
const rendererStart = afterOpen
.slice(before)
.find((e) => (e.body as ExecutionContextEvent).execution_context.type === 'renderer-process');
expect(rendererStart).toBeDefined();

const body = rendererStart!.body as ExecutionContextEvent;
expect(body._dd.document_version).toBe(1);
expect(body.execution_context.duration).toBeUndefined();
const rendererId = body.execution_context.id;

await mainPage.closeRendererProcess();
await mainPage.flushTransport();

const afterClose = await intake.getEventsByType('execution_context');
const rendererEnd = afterClose
.slice(afterOpen.length)
.find((e) => (e.body as ExecutionContextEvent).execution_context.id === rendererId);
expect(rendererEnd).toBeDefined();
expect((rendererEnd!.body as ExecutionContextEvent)._dd.document_version).toBeGreaterThan(1);
});
23 changes: 4 additions & 19 deletions e2e/scenarios/view.scenario.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { test, expect } from '../lib/helpers';
import type { RumViewEvent } from '@datadog/electron-sdk';

const isMainProcessView = (event: { body: unknown }) =>
(event.body as RumViewEvent).view.url === 'electron://main-process';
const isMainProcessView = (event: { body: unknown }) => (event.body as RumViewEvent).view.url === 'electron://fake';

test('emits an initial active view event on SDK init', async ({ mainPage, intake }) => {
await mainPage.flushTransport();
Expand Down Expand Up @@ -30,12 +29,10 @@ test('emits an initial active view event on SDK init', async ({ mainPage, intake
expect(headers['dd-evp-origin']).toBe('electron');
expect(headers['dd-evp-origin-version']).toMatch(/^\d+\.\d+\.\d+$/);
expect(headers['dd-request-id']).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
expect(view.view.name).toBe('main process');
expect(view.view.url).toBe('electron://main-process');
expect(view.view.url).toBe('electron://fake');
expect((view.view as unknown as { is_fake?: boolean }).is_fake).toBe(true);
expect((view.view as unknown as { name?: string }).name).toBeUndefined();
expect(view.view.is_active).toBe(true);
expect(view.view.action.count).toBe(0);
expect(view.view.error.count).toBe(0);
expect(view.view.resource.count).toBe(0);
expect(view._dd.document_version).toBe(1);
expect(view.view.id).toBeDefined();
expect(view.view.time_spent).toBeGreaterThanOrEqual(0);
Expand Down Expand Up @@ -73,15 +70,3 @@ test.describe('session renewal via user activity', () => {
expect(newView._dd.document_version).toBe(1);
});
});

test('increments view error count after an uncaught exception', async ({ mainPage, intake }) => {
await mainPage.generateUncaughtException();
await mainPage.flushTransport();

await intake.getEventsByType('error');
const viewEvents = await intake.waitForEventCount('view', 2);
const updatedView = viewEvents[1].body as RumViewEvent;

expect(updatedView.view.error.count).toBe(1);
expect(updatedView._dd.document_version).toBeGreaterThan(1);
});
5 changes: 3 additions & 2 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
"description": "Playground for testing @datadog/electron-sdk",
"main": "./dist/main.js",
"scripts": {
"build": "tsc && yarn build:renderer && cp src/index.html dist/index.html",
"build": "tsc && yarn build:renderer && yarn build:secondary-renderer && cp src/index.html dist/index.html && cp src/secondary.html dist/secondary.html",
"build:renderer": "esbuild src/renderer.ts --bundle --format=esm --outfile=dist/renderer.js --sourcemap",
"build:watch": "concurrently \"tsc --watch\" \"yarn build:renderer --watch\"",
"build:secondary-renderer": "esbuild src/secondary-renderer.ts --bundle --format=esm --outfile=dist/secondary-renderer.js --sourcemap",
"build:watch": "concurrently \"tsc --watch\" \"yarn build:renderer --watch\" \"yarn build:secondary-renderer --watch\"",
"start": "yarn build && electron .",
"dev": "yarn build && concurrently \"yarn build:watch\" \"electron .\"",
"test": "yarn build && playwright test -c test/playwright.config.ts"
Expand Down
5 changes: 5 additions & 0 deletions playground/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ <h2>Session ID:</h2>
<button id="op-parallel-uploads">Run 5 parallel uploads</button>
</div>

<div class="section-label">Process Lifecycle</div>
<div class="button-group">
<button id="open-secondary-window">Open Secondary Window</button>
</div>

<div class="session-section">
<h2>IPC Activity Log:</h2>
<div id="ipc-log"></div>
Expand Down
18 changes: 18 additions & 0 deletions playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { buildRumExplorerUrl } from './main/utils';
const isTestMode = process.env.DD_TEST_MODE === '1';

let mainWindow: BrowserWindow | null = null;
let secondaryWindow: BrowserWindow | null = null;

// Serving the renderer over a custom scheme (instead of file://) lets us attach the `Document-Policy: js-profiling`
// response header, which is required to enable the JS Self-Profiling API. The scheme must be registered as
Expand Down Expand Up @@ -232,6 +233,23 @@ ipcMain.handle('open-rum-explorer', () => {
void shell.openExternal(buildRumExplorerUrl(CONF[ACTIVE_ENV], ctx.session_id));
});

ipcMain.handle('main:open-secondary-window', () => {
if (secondaryWindow) return;
secondaryWindow = new BrowserWindow({
width: 500,
height: 400,
title: 'Secondary Renderer Process',
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
},
});
void secondaryWindow.loadURL('app://app/secondary.html');
secondaryWindow.on('closed', () => {
secondaryWindow = null;
});
});

void app.whenReady().then(async () => {
// Initialize SDK on app ready (before window creation)
console.log('Initializing SDK from main process...');
Expand Down
1 change: 1 addition & 0 deletions playground/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
mainFetchApiNet: () => ipcRenderer.invoke('main:fetch-api-net'),
openRumExplorer: () => ipcRenderer.invoke('open-rum-explorer'),
flushTransport: () => ipcRenderer.invoke('flush-transport'),
openSecondaryWindow: () => ipcRenderer.invoke('main:open-secondary-window'),
setUserInfo: () => ipcRenderer.invoke('main:set-user-info'),
addUserExtraInfo: () => ipcRenderer.invoke('main:add-user-extra-info'),
clearUserInfo: () => ipcRenderer.invoke('main:clear-user-info'),
Expand Down
3 changes: 3 additions & 0 deletions playground/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ interface ElectronAPI {
mainFetchApiNet: () => Promise<unknown>;
openRumExplorer: () => Promise<void>;
flushTransport: () => Promise<void>;
openSecondaryWindow: () => Promise<void>;
setUserInfo: () => Promise<void>;
addUserExtraInfo: () => Promise<void>;
clearUserInfo: () => Promise<void>;
Expand Down Expand Up @@ -348,3 +349,5 @@ if (parallelBtn) {
});
});
}

setupDemoButton('open-secondary-window', 'main:open-secondary-window', () => window.electronAPI.openSecondaryWindow());
38 changes: 38 additions & 0 deletions playground/src/secondary-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { datadogRum } from '@datadog/browser-rum';

datadogRum.init({
applicationId: '6efd3722-af0a-4070-994c-0e87076d4814',
clientToken: 'pub2a7307cdec74934cacb411a193f632f8',
site: 'datad0g.com',
service: 'electron-playground',
env: 'dev',
sessionSampleRate: 100,
trackResources: true,
trackLongTasks: true,
trackUserInteractions: true,
});

const status = document.getElementById('status') as HTMLElement;

function setStatus(msg: string) {
status.textContent = msg;
}

const fetchBtn = document.getElementById('fetch-btn') as HTMLButtonElement;
fetchBtn.addEventListener('click', () => {
fetchBtn.disabled = true;
setStatus('Fetching…');
fetch('https://httpbin.org/json')
.then((res) => res.json())
.then(() => setStatus('Fetch done'))
.catch((err) => setStatus(`Fetch error: ${String(err)}`))
.finally(() => {
fetchBtn.disabled = false;
});
});

const errorBtn = document.getElementById('error-btn') as HTMLButtonElement;
errorBtn.addEventListener('click', () => {
setStatus('Error thrown');
throw new Error('test error from secondary renderer');
});
Loading