From ecb8217041697ef8115841bb6cfebf79e715d226 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 2 Jun 2025 23:19:08 +0100 Subject: [PATCH 1/5] ci: Build and test in CI --- .github/workflows/ci.yml | 346 +++++++++++++++++++++++++++++++++++++++ package.json | 6 +- test/e2e.test.mjs | 31 ++-- test/prepare.mjs | 8 +- test/yarn.lock | 16 +- yarn.lock | 12 +- 6 files changed, 387 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..894cfd2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,346 @@ + +name: "CI: Build & Test" +on: + push: + branches: + - main + - release/** + pull_request: + +jobs: + job_lint: + name: Lint + 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: Lint + run: yarn lint + + job_compile: + name: Compile Binary (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} + runs-on: ${{ matrix.os }} + container: + image: ${{ matrix.container }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + # x64 glibc + - os: ubuntu-22.04 + node: 18 + binary: linux-x64-glibc-108 + - os: ubuntu-22.04 + node: 20 + binary: linux-x64-glibc-115 + - os: ubuntu-22.04 + node: 22 + binary: linux-x64-glibc-127 + - os: ubuntu-22.04 + node: 24 + binary: linux-x64-glibc-137 + + # x64 musl + - os: ubuntu-22.04 + container: node:18-alpine3.17 + node: 18 + binary: linux-x64-musl-108 + - os: ubuntu-22.04 + container: node:20-alpine3.17 + node: 20 + binary: linux-x64-musl-115 + - os: ubuntu-22.04 + container: node:22-alpine3.18 + node: 22 + binary: linux-x64-musl-127 + - os: ubuntu-22.04 + container: node:24-alpine3.20 + node: 24 + binary: linux-x64-musl-137 + + # arm64 glibc + - os: ubuntu-22.04 + arch: arm64 + node: 18 + binary: linux-arm64-glibc-108 + - os: ubuntu-22.04 + arch: arm64 + node: 20 + binary: linux-arm64-glibc-115 + - os: ubuntu-22.04 + arch: arm64 + node: 22 + binary: linux-arm64-glibc-127 + - os: ubuntu-22.04 + arch: arm64 + node: 24 + binary: linux-arm64-glibc-137 + + # arm64 musl + - os: ubuntu-22.04 + arch: arm64 + container: node:18-alpine3.17 + node: 18 + binary: linux-arm64-musl-108 + - os: ubuntu-22.04 + arch: arm64 + container: node:20-alpine3.17 + node: 20 + binary: linux-arm64-musl-115 + - os: ubuntu-22.04 + arch: arm64 + container: node:22-alpine3.18 + node: 22 + binary: linux-arm64-musl-127 + - os: ubuntu-22.04 + arch: arm64 + container: node:24-alpine3.20 + node: 24 + binary: linux-arm64-musl-137 + + # macos x64 + - os: macos-13 + node: 18 + arch: x64 + binary: darwin-x64-108 + - os: macos-13 + node: 20 + arch: x64 + binary: darwin-x64-115 + - os: macos-13 + node: 22 + arch: x64 + binary: darwin-x64-127 + - os: macos-13 + node: 24 + arch: x64 + binary: darwin-x64-137 + + # macos arm64 + - os: macos-13 + arch: arm64 + node: 18 + target_platform: darwin + binary: darwin-arm64-108 + - os: macos-13 + arch: arm64 + node: 20 + target_platform: darwin + binary: darwin-arm64-115 + - os: macos-13 + arch: arm64 + node: 22 + target_platform: darwin + binary: darwin-arm64-127 + - os: macos-13 + arch: arm64 + node: 24 + target_platform: darwin + binary: darwin-arm64-137 + + # windows x64 + - os: windows-2022 + node: 18 + arch: x64 + binary: win32-x64-108 + - os: windows-2022 + node: 20 + arch: x64 + binary: win32-x64-115 + - os: windows-2022 + node: 22 + arch: x64 + binary: win32-x64-127 + - os: windows-2022 + node: 24 + arch: x64 + binary: win32-x64-137 + + steps: + - name: Setup (alpine) + if: contains(matrix.container, 'alpine') + run: | + apk add --no-cache build-base git g++ make curl python3 + ln -sf python3 /usr/bin/python + + - name: Check out current commit + uses: actions/checkout@v4 + + # Note: On alpine images, this does nothing + # The node version will be the one that is installed in the image + # If you want to change the node version, you need to change the image + # For non-alpine images, this will install the correct version of node + - name: Setup Node + uses: actions/setup-node@v4 + if: contains(matrix.container, 'alpine') == false + with: + node-version: ${{ matrix.node }} + + - name: Increase yarn network timeout on Windows + if: contains(matrix.os, 'windows') + run: yarn config set network-timeout 600000 -g + + - name: Install dependencies + run: yarn install --ignore-engines --ignore-scripts --frozen-lockfile + + - name: Configure safe directory + run: | + git config --global --add safe.directory "*" + + - name: Setup python + uses: actions/setup-python@v5 + if: ${{ !contains(matrix.container, 'alpine') }} + id: python-setup + with: + python-version: "3.9.13" + + - name: Setup (arm64| ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' + run: | + sudo apt-get update + sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + + - name: Setup Musl + if: contains(matrix.container, 'alpine') + run: | + curl -OL https://musl.cc/aarch64-linux-musl-cross.tgz + tar -xzvf aarch64-linux-musl-cross.tgz + $(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc --version + + # configure node-gyp + - name: Configure node-gyp + if: matrix.arch != 'arm64' + run: yarn build:bindings:configure + + - name: Configure node-gyp (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && matrix.target_platform != 'darwin' + run: yarn build:bindings:configure:arm64 + + - name: Configure node-gyp (arm64, darwin) + if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin' + run: yarn build:bindings:configure:arm64 + + # build bindings + - name: Build Bindings + if: matrix.arch != 'arm64' + run: | + yarn build:bindings + + - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' + run: | + CC=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc \ + CXX=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-g++ \ + BUILD_ARCH=arm64 \ + yarn build:bindings + + - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) + if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' + run: | + CC=aarch64-linux-gnu-gcc \ + CXX=aarch64-linux-gnu-g++ \ + BUILD_ARCH=arm64 \ + yarn build:bindings:arm64 + + - name: Build Bindings (arm64, darwin) + if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin' + run: | + BUILD_PLATFORM=darwin \ + BUILD_ARCH=arm64 \ + yarn build:bindings:arm64 + + - name: Build + run: yarn build:lib + + - name: Archive Binary + uses: actions/upload-artifact@v4 + with: + name: stack-trace-${{ matrix.binary }} + path: ${{ github.workspace }}/lib/stack-trace-${{matrix.binary}}.node + if-no-files-found: error + + job_build: + name: Build Package + needs: [job_compile] + 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: Build TypeScript + run: yarn build:lib + + - name: Extract Prebuilt Binaries + uses: actions/download-artifact@v4 + with: + pattern: stack-trace-* + path: ${{ github.workspace }}/lib/ + merge-multiple: true + + - name: Pack tarball + run: yarn build:tarball + + - name: Archive artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ github.sha }} + retention-days: 90 + path: ${{ github.workspace }}/*.tgz + + job_test_bindings: + name: Test (v${{ matrix.node }}) ${{ matrix.os }} + needs: [job_build] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ + ubuntu-24.04, + ubuntu-22.04, + ubuntu-22.04-arm, + macos-latest, # macOS arm64 + macos-13, # macOS x64 + windows-latest, + ] + node: [18, 20, 22, 24] + steps: + - name: Check out current commit + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - 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 + + job_required_jobs_passed: + name: All required jobs passed + needs: [job_lint, job_test_bindings] + # Always run this, even if a dependent job failed + if: always() + runs-on: ubuntu-latest + steps: + - name: Check for failures + if: contains(needs.*.result, 'failure') + run: | + echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1 diff --git a/package.json b/package.json index 21325d8..5ae2ea3 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,14 @@ "build:dev": "yarn clean && yarn build:bindings:configure && yarn build", "build:tarball": "npm pack", "clean": "node-gyp clean && rm -rf lib && rm -rf build", - "test": "vitest run --silent=false --disable-console-intercept" + "test": "node ./test/prepare.mjs && vitest run --silent=false --disable-console-intercept" }, "volta": { "node": "24.1.0" }, "dependencies": { "detect-libc": "^2.0.4", - "node-abi": "^4.9.0" + "node-abi": "^3.73.0" }, "devDependencies": { "@sentry-internal/eslint-config-sdk": "^9.22.0", @@ -49,4 +49,4 @@ "/scripts/check-build.mjs", "/scripts/copy-target.mjs" ] -} +} \ No newline at end of file diff --git a/test/e2e.test.mjs b/test/e2e.test.mjs index fba60ee..82bf050 100644 --- a/test/e2e.test.mjs +++ b/test/e2e.test.mjs @@ -1,22 +1,23 @@ import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; -import { beforeAll, describe, expect, test } from 'vitest'; -import { installTarballAsDependency } from './prepare.mjs'; +import { describe, expect, test } from 'vitest'; const __dirname = import.meta.dirname || new URL('.', import.meta.url).pathname; describe('e2e Tests', { timeout: 20000 }, () => { - beforeAll(() => { - installTarballAsDependency(__dirname); - }); - test('Capture stack trace from multiple threads', () => { const testFile = join(__dirname, 'stack-traces.js'); const result = spawnSync('node', [testFile]) - expect(result.status).toBe(0); - - const stacks = JSON.parse(result.stdout.toString()); + let stacks; + try { + stacks = JSON.parse(result.stdout.toString()); + } catch (e) { + console.log('status', result.status); + console.log('stdout', result.stdout.toString()); + console.log('stderr', result.stderr.toString()); + throw e; + } expect(stacks['0']).toEqual(expect.arrayContaining([ { @@ -65,9 +66,15 @@ describe('e2e Tests', { timeout: 20000 }, () => { const testFile = join(__dirname, 'stalled.js'); const result = spawnSync('node', [testFile]); - expect(result.status).toBe(0); - - const stacks = JSON.parse(result.stdout.toString()); + let stacks; + try { + stacks = JSON.parse(result.stdout.toString()); + } catch (e) { + console.log('status', result.status); + console.log('stdout', result.stdout.toString()); + console.log('stderr', result.stderr.toString()); + throw e; + } expect(stacks['0']).toEqual(expect.arrayContaining([ { diff --git a/test/prepare.mjs b/test/prepare.mjs index e146c5f..eb2ca66 100644 --- a/test/prepare.mjs +++ b/test/prepare.mjs @@ -9,7 +9,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const env = {...process.env, NODE_OPTIONS: '--no-deprecation'}; -export function installTarballAsDependency(root) { +function installTarballAsDependency(root) { const pkgJson = require('../package.json'); const normalizedName = pkgJson.name.replace('@', '').replace('/', '-'); @@ -17,7 +17,7 @@ export function installTarballAsDependency(root) { if (!existsSync(tarball)) { console.error(`Tarball not found: '${tarball}'`); - console.error(`Run 'yarn build && yarn build:tarball' first`); + console.error('Run \'yarn build && yarn build:tarball\' first'); process.exit(1); } @@ -41,5 +41,7 @@ export function installTarballAsDependency(root) { writeFileSync(join(root, 'package.json'), modified); console.log('Installing dependencies...'); - execSync('yarn install', { cwd: root }); + execSync('yarn install', { cwd: root, stdio: 'inherit' }); } + +installTarballAsDependency(__dirname); diff --git a/test/yarn.lock b/test/yarn.lock index 1779b92..10be108 100644 --- a/test/yarn.lock +++ b/test/yarn.lock @@ -4,24 +4,24 @@ "@sentry-internal/node-native-stacktrace@file:../sentry-internal-node-native-stacktrace-0.1.0.tgz": version "0.1.0" - resolved "file:../sentry-internal-node-native-stacktrace-0.1.0.tgz#9f528856b2eecdefad847e171435f0d33d2375f5" + resolved "file:../sentry-internal-node-native-stacktrace-0.1.0.tgz#9c96a02e580c905dcdc99058b4ac067b02d8468e" dependencies: detect-libc "^2.0.4" - node-abi "^4.9.0" + node-abi "^3.73.0" detect-libc@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== -node-abi@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-4.9.0.tgz#ca6dabf7991e54bf3ba6d8d32641e1b84f305263" - integrity sha512-0isb3h+AXUblx5Iv0mnYy2WsErH+dk2e9iXJXdKAtS076Q5hP+scQhp6P4tvDeVlOBlG3ROKvkpQHtbORllq2A== +node-abi@^3.73.0: + version "3.75.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.75.0.tgz#2f929a91a90a0d02b325c43731314802357ed764" + integrity sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg== dependencies: - semver "^7.6.3" + semver "^7.3.5" -semver@^7.6.3: +semver@^7.3.5: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== diff --git a/yarn.lock b/yarn.lock index 4f2c2ad..3b5dba9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2284,12 +2284,12 @@ negotiator@^1.0.0: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== -node-abi@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-4.9.0.tgz#ca6dabf7991e54bf3ba6d8d32641e1b84f305263" - integrity sha512-0isb3h+AXUblx5Iv0mnYy2WsErH+dk2e9iXJXdKAtS076Q5hP+scQhp6P4tvDeVlOBlG3ROKvkpQHtbORllq2A== +node-abi@^3.73.0: + version "3.75.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.75.0.tgz#2f929a91a90a0d02b325c43731314802357ed764" + integrity sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg== dependencies: - semver "^7.6.3" + semver "^7.3.5" node-gyp@^11.2.0: version "11.2.0" @@ -2656,7 +2656,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4, semver@^7.6.3: +semver@^7.2.1, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== From aa570936b1c40696fb484a079d44defe32439cc6 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 3 Jun 2025 01:33:04 +0200 Subject: [PATCH 2/5] Use my CF proxy --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 894cfd2..9bc66ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,7 +210,7 @@ jobs: - name: Setup Musl if: contains(matrix.container, 'alpine') run: | - curl -OL https://musl.cc/aarch64-linux-musl-cross.tgz + curl -OL https://musl.cc.timfish.dev//aarch64-linux-musl-cross.tgz tar -xzvf aarch64-linux-musl-cross.tgz $(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc --version From a91dfdc1f119735accd2c784fae6f00e37f190cc Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 3 Jun 2025 01:36:43 +0200 Subject: [PATCH 3/5] oops url --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bc66ba..19f4066 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,7 +210,7 @@ jobs: - name: Setup Musl if: contains(matrix.container, 'alpine') run: | - curl -OL https://musl.cc.timfish.dev//aarch64-linux-musl-cross.tgz + curl -OL https://musl.cc.timfish.dev/aarch64-linux-musl-cross.tgz tar -xzvf aarch64-linux-musl-cross.tgz $(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc --version From fe775820a8b0fcedada80734064f464802fbd194 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 3 Jun 2025 11:59:03 +0200 Subject: [PATCH 4/5] expect status to be 0 --- test/e2e.test.mjs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/test/e2e.test.mjs b/test/e2e.test.mjs index 82bf050..7b23c92 100644 --- a/test/e2e.test.mjs +++ b/test/e2e.test.mjs @@ -9,15 +9,9 @@ describe('e2e Tests', { timeout: 20000 }, () => { const testFile = join(__dirname, 'stack-traces.js'); const result = spawnSync('node', [testFile]) - let stacks; - try { - stacks = JSON.parse(result.stdout.toString()); - } catch (e) { - console.log('status', result.status); - console.log('stdout', result.stdout.toString()); - console.log('stderr', result.stderr.toString()); - throw e; - } + expect(result.status).toEqual(0); + + const stacks = JSON.parse(result.stdout.toString()); expect(stacks['0']).toEqual(expect.arrayContaining([ { @@ -66,15 +60,9 @@ describe('e2e Tests', { timeout: 20000 }, () => { const testFile = join(__dirname, 'stalled.js'); const result = spawnSync('node', [testFile]); - let stacks; - try { - stacks = JSON.parse(result.stdout.toString()); - } catch (e) { - console.log('status', result.status); - console.log('stdout', result.stdout.toString()); - console.log('stderr', result.stderr.toString()); - throw e; - } + expect(result.status).toEqual(0); + + const stacks = JSON.parse(result.stdout.toString()); expect(stacks['0']).toEqual(expect.arrayContaining([ { From 9cff6f0672894afbebed1ea841a01fdeffc6b2eb Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 3 Jun 2025 12:23:11 +0200 Subject: [PATCH 5/5] fix: Don't send objects across isolates --- .gitignore | 1 + README.md | 82 ++++++++++++++++--------------------------- module.cc | 88 +++++++++++++++++++++-------------------------- package.json | 1 + src/index.ts | 11 ++---- test/e2e.test.mjs | 32 +++++++++++++++-- test/yarn.lock | 27 --------------- yarn.lock | 5 +++ 8 files changed, 106 insertions(+), 141 deletions(-) delete mode 100644 test/yarn.lock diff --git a/.gitignore b/.gitignore index cf7a18c..9e91cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ build/ lib/ /*.tgz +test/yarn.lock diff --git a/README.md b/README.md index 7abdbc5..e5c1011 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ registerThread(); Watchdog thread: ```ts -const { captureStackTrace } = require("@sentry-internal/node-native-stacktrace"); +const { captureStackTrace } = require( + "@sentry-internal/node-native-stacktrace", +); const stacks = captureStackTrace(); console.log(stacks); @@ -26,58 +28,32 @@ Results in: ```js { - '0': [ - { - function: 'from', - filename: 'node:buffer', - lineno: 298, - colno: 28 - }, - { - function: 'pbkdf2Sync', - filename: 'node:internal/crypto/pbkdf2', - lineno: 78, - colno: 17 - }, - { - function: 'longWork', - filename: '/app/test.js', - lineno: 20, - colno: 29 - }, - { - function: '?', - filename: '/app/test.js', - lineno: 24, - colno: 1 - } - ], - '2': [ - { - function: 'from', - filename: 'node:buffer', - lineno: 298, - colno: 28 - }, - { - function: 'pbkdf2Sync', - filename: 'node:internal/crypto/pbkdf2', - lineno: 78, - colno: 17 - }, - { - function: 'longWork', - filename: '/app/worker.js', - lineno: 10, - colno: 29 - }, - { - function: '?', - filename: '/app/worker.js', - lineno: 14, - colno: 1 - } - ] + '0': ' at from (node:buffer:299:28)\n' + + ' at pbkdf2Sync (node:internal/crypto/pbkdf2:78:17)\n' + + ' at longWork (/Users/tim/test/test/long-work.js:6:25)\n' + + ' at ? (/Users/tim/test/test/stack-traces.js:11:1)\n' + + ' at ? (node:internal/modules/cjs/loader:1734:14)\n' + + ' at ? (node:internal/modules/cjs/loader:1899:10)\n' + + ' at ? (node:internal/modules/cjs/loader:1469:32)\n' + + ' at ? (node:internal/modules/cjs/loader:1286:12)\n' + + ' at traceSync (node:diagnostics_channel:322:14)\n' + + ' at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)\n' + + ' at executeUserEntryPoint (node:internal/modules/run_main:152:5)\n' + + ' at ? (node:internal/main/run_main_module:33:47)', + '2': ' at from (node:buffer:299:28)\n' + + ' at pbkdf2Sync (node:internal/crypto/pbkdf2:78:17)\n' + + ' at longWork (/Users/tim/test/test/long-work.js:6:25)\n' + + ' at ? (/Users/tim/test/test/worker.js:6:1)\n' + + ' at ? (node:internal/modules/cjs/loader:1734:14)\n' + + ' at ? (node:internal/modules/cjs/loader:1899:10)\n' + + ' at ? (node:internal/modules/cjs/loader:1469:32)\n' + + ' at ? (node:internal/modules/cjs/loader:1286:12)\n' + + ' at traceSync (node:diagnostics_channel:322:14)\n' + + ' at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)\n' + + ' at executeUserEntryPoint (node:internal/modules/run_main:152:5)\n' + + ' at ? (node:internal/main/worker_thread:212:26)\n' + + ' at [nodejs.internal.kHybridDispatch] (node:internal/event_target:827:20)\n' + + ' at ? (node:internal/per_context/messageport:23:28)' } ``` diff --git a/module.cc b/module.cc index f1536f9..f9983d6 100644 --- a/module.cc +++ b/module.cc @@ -2,6 +2,7 @@ #include #include #include +#include using namespace v8; using namespace node; @@ -23,76 +24,61 @@ static std::unordered_map threads = {}; // Function to be called when an isolate's execution is interrupted static void ExecutionInterrupted(Isolate *isolate, void *data) { - auto promise = static_cast> *>(data); + auto promise = static_cast *>(data); auto stack = StackTrace::CurrentStackTrace(isolate, kMaxStackFrames, StackTrace::kDetailed); if (stack.IsEmpty()) { - promise->set_value(Array::New(isolate, 0)); + promise->set_value(""); return; } - auto frames = Array::New(isolate, stack->GetFrameCount()); + std::ostringstream stack_stream; for (int i = 0; i < stack->GetFrameCount(); i++) { auto frame = stack->GetFrame(isolate, i); auto fn_name = frame->GetFunctionName(); + // Build stack trace line in JavaScript format: + // " at functionName(filename:line:column)" + stack_stream << " at "; if (frame->IsEval()) { - fn_name = - String::NewFromUtf8(isolate, "[eval]", NewStringType::kInternalized) - .ToLocalChecked(); + stack_stream << "[eval]"; } else if (fn_name.IsEmpty() || fn_name->Length() == 0) { - fn_name = String::NewFromUtf8(isolate, "?", NewStringType::kInternalized) - .ToLocalChecked(); + stack_stream << "?"; } else if (frame->IsConstructor()) { - fn_name = String::NewFromUtf8(isolate, "[constructor]", - NewStringType::kInternalized) - .ToLocalChecked(); + stack_stream << "[constructor]"; + } else { + v8::String::Utf8Value utf8_fn(isolate, fn_name); + stack_stream << (*utf8_fn ? *utf8_fn : "?"); } - auto frame_obj = Object::New(isolate); - frame_obj - ->Set(isolate->GetCurrentContext(), - String::NewFromUtf8(isolate, "function", - NewStringType::kInternalized) - .ToLocalChecked(), - fn_name) - .Check(); - - frame_obj - ->Set(isolate->GetCurrentContext(), - String::NewFromUtf8(isolate, "filename", - NewStringType::kInternalized) - .ToLocalChecked(), - frame->GetScriptName()) - .Check(); - - frame_obj - ->Set( - isolate->GetCurrentContext(), - String::NewFromUtf8(isolate, "lineno", NewStringType::kInternalized) - .ToLocalChecked(), - Integer::New(isolate, frame->GetLineNumber())) - .Check(); + stack_stream << " ("; - frame_obj - ->Set( - isolate->GetCurrentContext(), - String::NewFromUtf8(isolate, "colno", NewStringType::kInternalized) - .ToLocalChecked(), - Integer::New(isolate, frame->GetColumn())) - .Check(); + auto script_name = frame->GetScriptName(); + if (!script_name.IsEmpty()) { + v8::String::Utf8Value utf8_filename(isolate, script_name); + stack_stream << (*utf8_filename ? *utf8_filename : ""); + } else { + stack_stream << ""; + } - frames->Set(isolate->GetCurrentContext(), i, frame_obj).Check(); + int line_number = frame->GetLineNumber(); + int column_number = frame->GetColumn(); + + stack_stream << ":" << line_number << ":" << column_number << ")"; + + if (i < stack->GetFrameCount() - 1) { + stack_stream << "\n"; + } } - promise->set_value(frames); + promise->set_value(stack_stream.str()); } // Function to capture the stack trace of a single isolate -Local CaptureStackTrace(Isolate *isolate) { - std::promise> promise; +std::string CaptureStackTrace(Isolate *isolate) { + std::promise promise; auto future = promise.get_future(); // The v8 isolate must be interrupted to capture the stack trace @@ -105,7 +91,7 @@ Local CaptureStackTrace(Isolate *isolate) { void CaptureStackTraces(const FunctionCallbackInfo &args) { auto capture_from_isolate = args.GetIsolate(); - using ThreadResult = std::tuple>; + using ThreadResult = std::tuple; std::vector> futures; // We collect the futures into a vec so they can be processed in parallel @@ -128,13 +114,17 @@ void CaptureStackTraces(const FunctionCallbackInfo &args) { // JavaScript object Local result = Object::New(capture_from_isolate); for (auto &future : futures) { - auto [thread_name, frames] = future.get(); + auto [thread_name, stack_string] = future.get(); auto key = String::NewFromUtf8(capture_from_isolate, thread_name.c_str(), NewStringType::kNormal) .ToLocalChecked(); - result->Set(capture_from_isolate->GetCurrentContext(), key, frames).Check(); + auto value = String::NewFromUtf8(capture_from_isolate, stack_string.c_str(), + NewStringType::kNormal) + .ToLocalChecked(); + + result->Set(capture_from_isolate->GetCurrentContext(), key, value).Check(); } args.GetReturnValue().Set(result); diff --git a/package.json b/package.json index 21325d8..49bbae1 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@sentry-internal/eslint-config-sdk": "^9.22.0", + "@sentry/core": "^9.22.0", "@types/node": "^18.19.1", "@types/node-abi": "^3.0.3", "clang-format": "^1.8.0", diff --git a/src/index.ts b/src/index.ts index e21d3da..314656b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,16 +11,9 @@ const arch = process.env['BUILD_ARCH'] || _arch(); const abi = getAbi(versions.node, 'node'); const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-'); -type StackFrame = { - function: string; - filename: string; - lineno: number; - colno: number; -}; - interface Native { registerThread(threadName: string): void; - captureStackTrace(): Record; + captureStackTrace(): Record; getThreadsLastSeen(): Record; } @@ -180,7 +173,7 @@ export function registerThread(threadName: string = String(threadId)): void { /** * Captures stack traces for all registered threads. */ -export function captureStackTrace(): Record { +export function captureStackTrace(): Record { return native.captureStackTrace(); } diff --git a/test/e2e.test.mjs b/test/e2e.test.mjs index fba60ee..61ec149 100644 --- a/test/e2e.test.mjs +++ b/test/e2e.test.mjs @@ -1,9 +1,17 @@ import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; +import { createStackParser, nodeStackLineParser } from '@sentry/core'; import { beforeAll, describe, expect, test } from 'vitest'; import { installTarballAsDependency } from './prepare.mjs'; const __dirname = import.meta.dirname || new URL('.', import.meta.url).pathname; +const defaultStackParser = createStackParser(nodeStackLineParser()); + +function parseStacks(stacks) { + return Object.fromEntries( + Object.entries(stacks).map(([id, stack]) => [id, defaultStackParser(stack)]), + ); +} describe('e2e Tests', { timeout: 20000 }, () => { beforeAll(() => { @@ -16,7 +24,7 @@ describe('e2e Tests', { timeout: 20000 }, () => { expect(result.status).toBe(0); - const stacks = JSON.parse(result.stdout.toString()); + const stacks = parseStacks(JSON.parse(result.stdout.toString())); expect(stacks['0']).toEqual(expect.arrayContaining([ { @@ -24,18 +32,24 @@ describe('e2e Tests', { timeout: 20000 }, () => { filename: expect.any(String), lineno: expect.any(Number), colno: expect.any(Number), + in_app: false, + module: undefined, }, { function: 'longWork', filename: expect.stringMatching(/long-work.js$/), lineno: expect.any(Number), colno: expect.any(Number), + in_app: true, + module: undefined, }, { function: '?', filename: expect.stringMatching(/stack-traces.js$/), lineno: expect.any(Number), colno: expect.any(Number), + in_app: true, + module: undefined, }, ])); @@ -45,29 +59,35 @@ describe('e2e Tests', { timeout: 20000 }, () => { filename: expect.any(String), lineno: expect.any(Number), colno: expect.any(Number), + in_app: false, + module: undefined, }, { function: 'longWork', filename: expect.stringMatching(/long-work.js$/), lineno: expect.any(Number), colno: expect.any(Number), + in_app: true, + module: undefined, }, { function: '?', filename: expect.stringMatching(/worker.js$/), lineno: expect.any(Number), colno: expect.any(Number), + in_app: true, + module: undefined, }, ])); }); - test('detect stalled thread', { timeout: 20000 }, () => { + test('Detect stalled thread', { timeout: 20000 }, () => { const testFile = join(__dirname, 'stalled.js'); const result = spawnSync('node', [testFile]); expect(result.status).toBe(0); - const stacks = JSON.parse(result.stdout.toString()); + const stacks = parseStacks(JSON.parse(result.stdout.toString())); expect(stacks['0']).toEqual(expect.arrayContaining([ { @@ -75,18 +95,24 @@ describe('e2e Tests', { timeout: 20000 }, () => { filename: expect.any(String), lineno: expect.any(Number), colno: expect.any(Number), + in_app: false, + module: undefined, }, { function: 'longWork', filename: expect.stringMatching(/long-work.js$/), lineno: expect.any(Number), colno: expect.any(Number), + in_app: true, + module: undefined, }, { function: '?', filename: expect.stringMatching(/stalled.js$/), lineno: expect.any(Number), colno: expect.any(Number), + in_app: true, + module: undefined, }, ])); diff --git a/test/yarn.lock b/test/yarn.lock deleted file mode 100644 index 1779b92..0000000 --- a/test/yarn.lock +++ /dev/null @@ -1,27 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@sentry-internal/node-native-stacktrace@file:../sentry-internal-node-native-stacktrace-0.1.0.tgz": - version "0.1.0" - resolved "file:../sentry-internal-node-native-stacktrace-0.1.0.tgz#9f528856b2eecdefad847e171435f0d33d2375f5" - dependencies: - detect-libc "^2.0.4" - node-abi "^4.9.0" - -detect-libc@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" - integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== - -node-abi@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-4.9.0.tgz#ca6dabf7991e54bf3ba6d8d32641e1b84f305263" - integrity sha512-0isb3h+AXUblx5Iv0mnYy2WsErH+dk2e9iXJXdKAtS076Q5hP+scQhp6P4tvDeVlOBlG3ROKvkpQHtbORllq2A== - dependencies: - semver "^7.6.3" - -semver@^7.6.3: - version "7.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== diff --git a/yarn.lock b/yarn.lock index 4f2c2ad..a44e7de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -388,6 +388,11 @@ resolved "https://registry.yarnpkg.com/@sentry-internal/typescript/-/typescript-9.22.0.tgz#7bce764807c0bb122126f29af74fc43bf863039c" integrity sha512-pqeMOKuzUwpMXh12ONbWUtpJ9Tey+UYnLgqPVXZEnB4vpZfJ/y4mdHsNaEXxgGLWZ+wvvSdYJB9GsGBl0iSJrQ== +"@sentry/core@^9.22.0": + version "9.25.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.25.0.tgz#354b9d500075b0db1decc533c16fe0566a30c21b" + integrity sha512-k0AgzR6RIf6OEwkVz09zer8GcK1s7RothlS1R6Z4x1wAJ+brtx4HqWnbLp05LDNDNrjTzK30HXvuCGGusnZuig== + "@types/estree@1.0.7", "@types/estree@^1.0.0": version "1.0.7" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"