Skip to content

Commit 76d1a63

Browse files
authored
feat: Support Electron (#9)
1 parent 6f82262 commit 76d1a63

16 files changed

+250
-600
lines changed

.github/workflows/build.yml

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ jobs:
253253

254254
- name: Build TypeScript
255255
run: yarn build:lib
256-
256+
257257
- name: Extract Prebuilt Binaries
258258
uses: actions/download-artifact@v4
259259
with:
@@ -271,8 +271,8 @@ jobs:
271271
retention-days: 90
272272
path: ${{ github.workspace }}/*.tgz
273273

274-
job_test:
275-
name: Test (v${{ matrix.node }}) ${{ matrix.os }}
274+
job_test_bindings:
275+
name: Test Bindings (v${{ matrix.node }}) ${{ matrix.os }}
276276
needs: [job_build]
277277
runs-on: ${{ matrix.os }}
278278
strategy:
@@ -302,11 +302,55 @@ jobs:
302302
with:
303303
name: ${{ github.sha }}
304304
- name: Run tests
305-
run: yarn test
305+
run: yarn test:bindings
306+
307+
job_test_electron:
308+
name: Test Electron ${{ matrix.os }}
309+
needs: [job_build]
310+
runs-on: ${{ matrix.os }}
311+
strategy:
312+
fail-fast: false
313+
matrix:
314+
os: [macos-latest, windows-latest]
315+
steps:
316+
- name: Check out current commit
317+
uses: actions/checkout@v4
318+
- name: Set up Node
319+
uses: actions/setup-node@v4
320+
with:
321+
node-version-file: 'package.json'
322+
- name: Install dependencies
323+
run: yarn install --ignore-engines --ignore-scripts --frozen-lockfile
324+
- name: Download Tarball
325+
uses: actions/download-artifact@v4
326+
with:
327+
name: ${{ github.sha }}
328+
- name: Run tests
329+
run: yarn test:electron
330+
331+
job_test_bundling:
332+
name: Test Bundling
333+
needs: [job_build]
334+
runs-on: ubuntu-latest
335+
steps:
336+
- name: Check out current commit
337+
uses: actions/checkout@v4
338+
- name: Set up Node
339+
uses: actions/setup-node@v4
340+
with:
341+
node-version-file: 'package.json'
342+
- name: Install dependencies
343+
run: yarn install --ignore-engines --ignore-scripts --frozen-lockfile
344+
- name: Download Tarball
345+
uses: actions/download-artifact@v4
346+
with:
347+
name: ${{ github.sha }}
348+
- name: Run tests
349+
run: yarn test:bundling
306350

307351
job_required_jobs_passed:
308352
name: All required jobs passed
309-
needs: [job_test, job_lint]
353+
needs: [job_lint, job_test_bindings, job_test_electron, job_test_bundling]
310354
# Always run this, even if a dependent job failed
311355
if: always()
312356
runs-on: ubuntu-latest

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
build/*
55
lib/*
6-
test/package.json
7-
test/yarn.lock
6+
test/**/yarn.lock
7+
test/**/package.json
88

99
### Node ###
1010
# Logs

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
"build:bindings:arm64": "node-gyp build --arch=arm64 && node scripts/copy-target.js",
4141
"build:dev": "yarn clean && yarn build:bindings:configure && yarn build",
4242
"build:tarball": "npm pack",
43-
"test": "node ./test/prepare.js && vitest run --testTimeout 60000"
43+
"test": "yarn test:bindings && yarn test:bundling && yarn test:electron",
44+
"test:bundling": "node test/prepare.mjs bundler && vitest run --testTimeout 60000 ./test/bundler",
45+
"test:electron": "node test/prepare.mjs electron && vitest run --testTimeout 120000 ./test/electron",
46+
"test:bindings": "node test/prepare.mjs bindings && vitest run --testTimeout 60000 ./test/bindings"
4447
},
4548
"dependencies": {
4649
"detect-libc": "^2.0.3",
@@ -55,10 +58,7 @@
5558
"eslint": "^7.0.0",
5659
"node-gyp": "^9.4.1",
5760
"typescript": "^5.7.3",
58-
"vitest": "^3.0.4",
59-
"webpack": "^5.97.1",
60-
"node-loader": "^2.1.0",
61-
"esbuild": "^0.24.2"
61+
"vitest": "^3.0.5"
6262
},
6363
"sideEffects": false,
6464
"volta": {

src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings {
3737
return require(binaryPath);
3838
}
3939

40+
if (process.versions.electron) {
41+
try {
42+
return require('../build/Release/sentry_cpu_profiler.node');
43+
} catch (e) {
44+
console.warn(`The '@sentry/profiling-node' binary could not be found. Use '@electron/rebuild' to ensure the native module is built for Electron.`);
45+
throw e;
46+
}
47+
}
48+
4049
// We need the fallthrough so that in the end, we can fallback to the dynamic require.
4150
// This is for cases where precompiled binaries were not provided, but may have been compiled from source.
4251
if (platform === 'darwin') {
File renamed without changes.

test/bindings/package.json.template

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "node-cpu-profiler-test",
3+
"license": "MIT",
4+
"dependencies": {
5+
"@sentry-internal/node-cpu-profiler": "{{path}}"
6+
}
7+
}
File renamed without changes.
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { inspect } from 'node:util';
2-
31
import { CpuProfilerBindings, ProfileFormat } from '@sentry-internal/node-cpu-profiler';
42

53
CpuProfilerBindings.startProfiling('test');
64

75
setTimeout(() => {
86
const report = CpuProfilerBindings.stopProfiling('test', ProfileFormat.THREAD);
9-
console.log(inspect(report, false, null, true));
7+
console.assert(report);
8+
process.exit(0);
109
}, 5000);

test/bundler.test.ts renamed to test/bundler/bundler.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
// eslint-disable-next-line import/no-unresolved
12
import esbuild from 'esbuild';
23
import * as path from 'path';
34
import { describe, expect, test } from "vitest";
5+
// eslint-disable-next-line import/no-unresolved
46
import webpack from 'webpack';
57

68
const entry = path.resolve(__dirname, 'bundle.mjs');
@@ -28,7 +30,7 @@ describe('Bundler tests', () => {
2830
rules: [
2931
{
3032
test: /\.node$/,
31-
loader: 'node-loader',
33+
loader: require.resolve('node-loader'),
3234
},
3335
],
3436
},

test/bundler/package.json.template

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "node-cpu-profiler-bundler-test",
3+
"license": "MIT",
4+
"dependencies": {
5+
"webpack": "^5.97.1",
6+
"node-loader": "^2.1.0",
7+
"esbuild": "^0.24.2",
8+
"@sentry-internal/node-cpu-profiler": "{{path}}"
9+
}
10+
}

test/electron/electron.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { SpawnSyncReturns } from 'child_process';
2+
import { execSync, spawnSync } from 'child_process';
3+
// eslint-disable-next-line import/no-unresolved
4+
import { default as getElectronPath } from 'electron';
5+
import { describe, expect, test } from "vitest";
6+
7+
function runElectron(): SpawnSyncReturns<Buffer> {
8+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
9+
// @ts-ignore - The Electron types don't detail the default export outside
10+
// of an Electron process.
11+
return spawnSync(getElectronPath, [__dirname]);
12+
}
13+
14+
describe('Electron', () => {
15+
16+
test('should work', () => {
17+
const result1 = runElectron();
18+
19+
expect(result1.stderr.toString()).toContain('binary could not be found')
20+
expect(result1.status).toBe(1);
21+
22+
execSync('yarn rebuild', { cwd: __dirname });
23+
24+
const result2 = runElectron();
25+
expect(result2.status).toBe(0);
26+
});
27+
});

test/electron/main.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
process.on('unhandledRejection', (err) => {
2+
console.error(err);
3+
process.exit(1);
4+
});
5+
process.on('uncaughtException', (err) => {
6+
console.error(err);
7+
process.exit(1);
8+
});
9+
10+
const { CpuProfilerBindings, ProfileFormat } = await import('@sentry-internal/node-cpu-profiler');
11+
12+
CpuProfilerBindings.startProfiling('test');
13+
14+
setTimeout(() => {
15+
const report = CpuProfilerBindings.stopProfiling('test', ProfileFormat.THREAD);
16+
console.assert(report);
17+
process.exit(0);
18+
}, 5000);

test/electron/package.json.template

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "node-cpu-profiler-test",
3+
"license": "MIT",
4+
"main": "main.mjs",
5+
"scripts": {
6+
"rebuild": "electron-rebuild"
7+
},
8+
"dependencies": {
9+
"electron": "^34.1.0",
10+
"@electron/rebuild": "^3.7.1",
11+
"@sentry-internal/node-cpu-profiler": "{{path}}"
12+
}
13+
}

test/prepare.js

Lines changed: 0 additions & 42 deletions
This file was deleted.

test/prepare.mjs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { execSync, spawnSync } from 'node:child_process';
2+
import {readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
3+
import { createRequire } from 'node:module';
4+
import { dirname, join, relative } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
7+
const __dirname = dirname(fileURLToPath(import.meta.url));
8+
const require = createRequire(import.meta.url);
9+
10+
function prepareTest(root) {
11+
const pkgJson = require('../package.json');
12+
const normalizedName = pkgJson.name.replace('@', '').replace('/', '-');
13+
14+
const tarball = join(__dirname, '..', `${normalizedName}-${pkgJson.version}.tgz`);
15+
16+
if (!existsSync(tarball)) {
17+
console.error(`Tarball not found: '${tarball}'`);
18+
console.error(`Run 'yarn build && yarn build:tarball' first`);
19+
process.exit(1);
20+
}
21+
22+
const tarballRelative = relative(root, tarball);
23+
24+
console.log('Clearing node_modules...');
25+
rmSync(join(root, 'node_modules'), { recursive: true, force: true });
26+
console.log('Clearing yarn.lock...');
27+
rmSync(join(root, 'yarn.lock'), { force: true });
28+
29+
console.log('Clearing yarn cache...');
30+
spawnSync(`yarn cache clean ${pkgJson.name}`, { shell: true, stdio: 'inherit' });
31+
// Yarn has a bug where 'yarn cache clean X' does not remove the temp directory where the tgz is unpacked to.
32+
// This means installing from local tgz does not update when src changes are made https://github.com/yarnpkg/yarn/issues/5357
33+
const dirResult = spawnSync('yarn cache dir', { shell: true });
34+
const tmpDir = join(dirResult.output.toString().replace(/[,\n\r]/g, ''), '.tmp');
35+
rmSync(tmpDir, { recursive: true, force: true });
36+
37+
const pkg = readFileSync(join(root, 'package.json.template'), 'utf-8');
38+
const modified = pkg.replace(/"{{path}}"/, JSON.stringify(`file:${tarballRelative}`));
39+
writeFileSync(join(root, 'package.json'), modified);
40+
41+
console.log('Installing dependencies...');
42+
execSync('yarn install', { cwd: root });
43+
}
44+
45+
prepareTest(join(__dirname, process.argv[2]));

0 commit comments

Comments
 (0)