Skip to content

feat: Support Electron #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 12, 2025
Merged
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
54 changes: 49 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ jobs:

- name: Build TypeScript
run: yarn build:lib

- name: Extract Prebuilt Binaries
uses: actions/download-artifact@v4
with:
Expand All @@ -271,8 +271,8 @@ jobs:
retention-days: 90
path: ${{ github.workspace }}/*.tgz

job_test:
name: Test (v${{ matrix.node }}) ${{ matrix.os }}
job_test_bindings:
name: Test Bindings (v${{ matrix.node }}) ${{ matrix.os }}
needs: [job_build]
runs-on: ${{ matrix.os }}
strategy:
Expand Down Expand Up @@ -302,11 +302,55 @@ jobs:
with:
name: ${{ github.sha }}
- name: Run tests
run: yarn test
run: yarn test:bindings

job_test_electron:
name: Test Electron ${{ matrix.os }}
needs: [job_build]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
steps:
- name: Check out current commit
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
- name: Install dependencies
run: yarn install --ignore-engines --ignore-scripts --frozen-lockfile
- name: Download Tarball
uses: actions/download-artifact@v4
with:
name: ${{ github.sha }}
- name: Run tests
run: yarn test:electron

job_test_bundling:
name: Test Bundling
needs: [job_build]
runs-on: ubuntu-latest
steps:
- name: Check out current commit
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
- name: Install dependencies
run: yarn install --ignore-engines --ignore-scripts --frozen-lockfile
- name: Download Tarball
uses: actions/download-artifact@v4
with:
name: ${{ github.sha }}
- name: Run tests
run: yarn test:bundling

job_required_jobs_passed:
name: All required jobs passed
needs: [job_test, job_lint]
needs: [job_lint, job_test_bindings, job_test_electron, job_test_bundling]
# Always run this, even if a dependent job failed
if: always()
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

build/*
lib/*
test/package.json
test/yarn.lock
test/**/yarn.lock
test/**/package.json

### Node ###
# Logs
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
"build:bindings:arm64": "node-gyp build --arch=arm64 && node scripts/copy-target.js",
"build:dev": "yarn clean && yarn build:bindings:configure && yarn build",
"build:tarball": "npm pack",
"test": "node ./test/prepare.js && vitest run --testTimeout 60000"
"test": "yarn test:bindings && yarn test:bundling && yarn test:electron",
"test:bundling": "node test/prepare.mjs bundler && vitest run --testTimeout 60000 ./test/bundler",
"test:electron": "node test/prepare.mjs electron && vitest run --testTimeout 120000 ./test/electron",
"test:bindings": "node test/prepare.mjs bindings && vitest run --testTimeout 60000 ./test/bindings"
},
"dependencies": {
"detect-libc": "^2.0.3",
Expand All @@ -55,10 +58,7 @@
"eslint": "^7.0.0",
"node-gyp": "^9.4.1",
"typescript": "^5.7.3",
"vitest": "^3.0.4",
"webpack": "^5.97.1",
"node-loader": "^2.1.0",
"esbuild": "^0.24.2"
"vitest": "^3.0.5"
},
"sideEffects": false,
"volta": {
Expand Down
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings {
return require(binaryPath);
}

if (process.versions.electron) {
try {
return require('../build/Release/sentry_cpu_profiler.node');
} catch (e) {
console.warn(`The '@sentry/profiling-node' binary could not be found. Use '@electron/rebuild' to ensure the native module is built for Electron.`);
throw e;
}
}

// We need the fallthrough so that in the end, we can fallback to the dynamic require.
// This is for cases where precompiled binaries were not provided, but may have been compiled from source.
if (platform === 'darwin') {
Expand Down
File renamed without changes.
7 changes: 7 additions & 0 deletions test/bindings/package.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "node-cpu-profiler-test",
"license": "MIT",
"dependencies": {
"@sentry-internal/node-cpu-profiler": "{{path}}"
}
}
File renamed without changes.
5 changes: 2 additions & 3 deletions test/bundle.mjs → test/bundler/bundle.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { inspect } from 'node:util';

import { CpuProfilerBindings, ProfileFormat } from '@sentry-internal/node-cpu-profiler';

CpuProfilerBindings.startProfiling('test');

setTimeout(() => {
const report = CpuProfilerBindings.stopProfiling('test', ProfileFormat.THREAD);
console.log(inspect(report, false, null, true));
console.assert(report);
process.exit(0);
}, 5000);
4 changes: 3 additions & 1 deletion test/bundler.test.ts → test/bundler/bundler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// eslint-disable-next-line import/no-unresolved
import esbuild from 'esbuild';
import * as path from 'path';
import { describe, expect, test } from "vitest";
// eslint-disable-next-line import/no-unresolved
import webpack from 'webpack';

const entry = path.resolve(__dirname, 'bundle.mjs');
Expand Down Expand Up @@ -28,7 +30,7 @@ describe('Bundler tests', () => {
rules: [
{
test: /\.node$/,
loader: 'node-loader',
loader: require.resolve('node-loader'),
},
],
},
Expand Down
10 changes: 10 additions & 0 deletions test/bundler/package.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "node-cpu-profiler-bundler-test",
"license": "MIT",
"dependencies": {
"webpack": "^5.97.1",
"node-loader": "^2.1.0",
"esbuild": "^0.24.2",
"@sentry-internal/node-cpu-profiler": "{{path}}"
}
}
27 changes: 27 additions & 0 deletions test/electron/electron.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { SpawnSyncReturns } from 'child_process';
import { execSync, spawnSync } from 'child_process';
// eslint-disable-next-line import/no-unresolved
import { default as getElectronPath } from 'electron';
import { describe, expect, test } from "vitest";

function runElectron(): SpawnSyncReturns<Buffer> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - The Electron types don't detail the default export outside
// of an Electron process.
return spawnSync(getElectronPath, [__dirname]);
}

describe('Electron', () => {

test('should work', () => {
const result1 = runElectron();

expect(result1.stderr.toString()).toContain('binary could not be found')
expect(result1.status).toBe(1);

execSync('yarn rebuild', { cwd: __dirname });

const result2 = runElectron();
expect(result2.status).toBe(0);
});
});
18 changes: 18 additions & 0 deletions test/electron/main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
process.on('unhandledRejection', (err) => {
console.error(err);
process.exit(1);
});
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});

const { CpuProfilerBindings, ProfileFormat } = await import('@sentry-internal/node-cpu-profiler');

CpuProfilerBindings.startProfiling('test');

setTimeout(() => {
const report = CpuProfilerBindings.stopProfiling('test', ProfileFormat.THREAD);
console.assert(report);
process.exit(0);
}, 5000);
13 changes: 13 additions & 0 deletions test/electron/package.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "node-cpu-profiler-test",
"license": "MIT",
"main": "main.mjs",
"scripts": {
"rebuild": "electron-rebuild"
},
"dependencies": {
"electron": "^34.1.0",
"@electron/rebuild": "^3.7.1",
"@sentry-internal/node-cpu-profiler": "{{path}}"
}
}
42 changes: 0 additions & 42 deletions test/prepare.js

This file was deleted.

45 changes: 45 additions & 0 deletions test/prepare.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { execSync, spawnSync } from 'node:child_process';
import {readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);

function prepareTest(root) {
const pkgJson = require('../package.json');
const normalizedName = pkgJson.name.replace('@', '').replace('/', '-');

const tarball = join(__dirname, '..', `${normalizedName}-${pkgJson.version}.tgz`);

if (!existsSync(tarball)) {
console.error(`Tarball not found: '${tarball}'`);
console.error(`Run 'yarn build && yarn build:tarball' first`);
process.exit(1);
}

const tarballRelative = relative(root, tarball);

console.log('Clearing node_modules...');
rmSync(join(root, 'node_modules'), { recursive: true, force: true });
console.log('Clearing yarn.lock...');
rmSync(join(root, 'yarn.lock'), { force: true });

console.log('Clearing yarn cache...');
spawnSync(`yarn cache clean ${pkgJson.name}`, { shell: true, stdio: 'inherit' });
// Yarn has a bug where 'yarn cache clean X' does not remove the temp directory where the tgz is unpacked to.
// This means installing from local tgz does not update when src changes are made https://github.com/yarnpkg/yarn/issues/5357
const dirResult = spawnSync('yarn cache dir', { shell: true });
const tmpDir = join(dirResult.output.toString().replace(/[,\n\r]/g, ''), '.tmp');
rmSync(tmpDir, { recursive: true, force: true });

const pkg = readFileSync(join(root, 'package.json.template'), 'utf-8');
const modified = pkg.replace(/"{{path}}"/, JSON.stringify(`file:${tarballRelative}`));
writeFileSync(join(root, 'package.json'), modified);

console.log('Installing dependencies...');
execSync('yarn install', { cwd: root });
}

prepareTest(join(__dirname, process.argv[2]));
Loading