Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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();
}
}
69 changes: 69 additions & 0 deletions e2e/scenarios/process.scenario.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { test, expect } from '../lib/helpers';

interface ProcessEvent {
type: 'process';
process: {
id: string;
role: 'main' | 'renderer';
pid: number;
name?: string;
duration?: number;
exit_reason?: string;
};
_dd: { document_version: number };
}

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

expect(events.length).toBeGreaterThanOrEqual(1);
const mainEvent = events.find((e) => (e.body as ProcessEvent).process.role === 'main');
expect(mainEvent).toBeDefined();

const body = mainEvent!.body as ProcessEvent;
expect(body.process.role).toBe('main');
expect(body.process.pid).toBeGreaterThan(0);
expect(body._dd.document_version).toBe(1);
expect(body.process.duration).toBeUndefined();
expect(body.process.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 process.id and process.role', 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 processCtx = view['process'] as { id: string; role: string } | undefined;
expect(processCtx).toBeDefined();
expect(processCtx!.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
expect(processCtx!.role).toBe('main');
});

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

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

const afterOpen = await intake.getEventsByType('process');
const rendererStart = afterOpen.slice(before).find((e) => (e.body as ProcessEvent).process.role === 'renderer');
expect(rendererStart).toBeDefined();

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

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

const afterClose = await intake.getEventsByType('process');
const rendererEnd = afterClose
.slice(afterOpen.length)
.find((e) => (e.body as ProcessEvent).process.id === rendererId);
expect(rendererEnd).toBeDefined();
expect((rendererEnd!.body as ProcessEvent)._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