diff --git a/.buildkite/basic/electron-pipeline.yml b/.buildkite/basic/electron-pipeline.yml index 7881c2685d..54b0dba696 100644 --- a/.buildkite/basic/electron-pipeline.yml +++ b/.buildkite/basic/electron-pipeline.yml @@ -21,7 +21,7 @@ steps: - "^28.0.0" - "^30.0.0" node_version: - - "18" + - "22" commands: - echo "Running on Node `node -v`" - npm install electron@${ELECTRON_VERSION} --no-audit --progress=false --no-save diff --git a/.buildkite/basic/node-pipeline.yml b/.buildkite/basic/node-pipeline.yml index 05b670496d..0d9c548496 100644 --- a/.buildkite/basic/node-pipeline.yml +++ b/.buildkite/basic/node-pipeline.yml @@ -35,6 +35,7 @@ steps: - 16 - 18 - 20 + - 22 plugins: docker-compose#v4.12.0: run: node-maze-runner diff --git a/.buildkite/basic/react-native-android-full-pipeline.yml b/.buildkite/basic/react-native-android-full-pipeline.yml index 7c0c1081fc..465391d534 100644 --- a/.buildkite/basic/react-native-android-full-pipeline.yml +++ b/.buildkite/basic/react-native-android-full-pipeline.yml @@ -80,7 +80,8 @@ steps: - "0.76" - "0.78" - "0.80" - - "0.81" + - "0.82" + - "0.83" reactnavigation: - "true" adjustments: @@ -93,10 +94,10 @@ steps: key: "build-react-native-navigation-android-fixture-old-arch" timeout_in_minutes: 30 agents: - queue: macos-node-18 + queue: macos-node-22 env: JAVA_VERSION: "17" - NODE_VERSION: "18" + NODE_VERSION: "22" RN_VERSION: "{{matrix}}" RCT_NEW_ARCH_ENABLED: "0" BUILD_ANDROID: "true" @@ -118,10 +119,10 @@ steps: key: "build-react-native-navigation-android-fixture-new-arch" timeout_in_minutes: 30 agents: - queue: macos-node-18 + queue: macos-node-22 env: JAVA_VERSION: "17" - NODE_VERSION: "18" + NODE_VERSION: "22" RN_VERSION: "{{matrix}}" RCT_NEW_ARCH_ENABLED: "1" BUILD_ANDROID: "true" @@ -187,7 +188,6 @@ steps: - "0.78" - "0.80" - "0.81" - # current latest version (v7.40.1) of react-native-navigation's autolinking tool doesn't support RN 0.73+, # causing a build failure - see https://github.com/wix/react-native-navigation/issues/7821 # TODO: Investigate and try to re-enable when we add tests for more recent React Native versions @@ -235,7 +235,8 @@ steps: - "0.76" - "0.78" - "0.80" - - "0.81" + - "0.82" + - "0.83" reactnavigation: - "true" adjustments: diff --git a/.buildkite/basic/react-native-android-pipeline.yml b/.buildkite/basic/react-native-android-pipeline.yml index 66acad308f..7a3382f5ea 100644 --- a/.buildkite/basic/react-native-android-pipeline.yml +++ b/.buildkite/basic/react-native-android-pipeline.yml @@ -13,6 +13,7 @@ steps: env: JAVA_VERSION: "17" RN_VERSION: "{{matrix}}" + NODE_VERSION: "22" RCT_NEW_ARCH_ENABLED: "1" BUILD_ANDROID: "true" REACT_NAVIGATION: "true" @@ -26,7 +27,7 @@ steps: - exit_status: "*" limit: 1 matrix: - - "0.82" + - "0.84" # # End-to-end tests @@ -69,5 +70,5 @@ steps: concurrency_group: "bitbar" concurrency_method: eager matrix: - - "0.82" + - "0.84" diff --git a/.buildkite/basic/react-native-cli-pipeline.yml b/.buildkite/basic/react-native-cli-pipeline.yml index 68a4ce8630..7b150a5cdb 100644 --- a/.buildkite/basic/react-native-cli-pipeline.yml +++ b/.buildkite/basic/react-native-cli-pipeline.yml @@ -25,9 +25,9 @@ steps: - "bundle install" - "bundle exec maze-runner features/build-app-tests/build-android-app.feature" matrix: + - "0.84" + - "0.83" - "0.82" - - "0.81" - - "0.80" retry: automatic: - exit_status: "*" @@ -54,9 +54,9 @@ steps: - "bundle install" - "bundle exec maze-runner features/build-app-tests/build-ios-app.feature" matrix: + - "0.84" + - "0.83" - "0.82" - - "0.81" - - "0.80" retry: automatic: - exit_status: "*" @@ -92,9 +92,9 @@ steps: concurrency_group: "browserstack-app" concurrency_method: eager matrix: + - "0.84" + - "0.83" - "0.82" - - "0.81" - - "0.80" retry: automatic: - exit_status: 103 # Appium session failed @@ -127,9 +127,9 @@ steps: concurrency_group: "browserstack-app" concurrency_method: eager matrix: + - "0.84" + - "0.83" - "0.82" - - "0.81" - - "0.80" retry: automatic: - exit_status: 103 # Appium session failed diff --git a/.buildkite/basic/react-native-ios-full-pipeline.yml b/.buildkite/basic/react-native-ios-full-pipeline.yml index ee4ca239e0..1ececafa64 100644 --- a/.buildkite/basic/react-native-ios-full-pipeline.yml +++ b/.buildkite/basic/react-native-ios-full-pipeline.yml @@ -35,6 +35,7 @@ steps: - "0.78" - "0.80" - "0.81" + node: - "22" adjustments: @@ -73,7 +74,8 @@ steps: - "0.76" - "0.78" - "0.80" - - "0.81" + - "0.82" + - "0.83" reactnavigation: - "true" adjustments: @@ -91,7 +93,7 @@ steps: agents: queue: "macos-15" env: - NODE_VERSION: "18" + NODE_VERSION: "22" RN_VERSION: "{{matrix}}" RCT_NEW_ARCH_ENABLED: "0" BUILD_IOS: "true" @@ -184,7 +186,7 @@ steps: - "0.78" - "0.80" - "0.81" - + - label: ":bitbar: :mac: RN {{matrix.reactnative}} iOS (New Arch) end-to-end tests" depends_on: "build-react-native-ios-fixture-new-arch-full" timeout_in_minutes: 60 @@ -228,7 +230,8 @@ steps: - "0.76" - "0.78" - "0.80" - - "0.81" + - "0.82" + - "0.83" reactnavigation: - "true" adjustments: diff --git a/.buildkite/basic/react-native-ios-pipeline.yml b/.buildkite/basic/react-native-ios-pipeline.yml index 612967d18c..4b0f50c3ae 100644 --- a/.buildkite/basic/react-native-ios-pipeline.yml +++ b/.buildkite/basic/react-native-ios-pipeline.yml @@ -16,6 +16,7 @@ steps: queue: "macos-15" env: RN_VERSION: "{{matrix}}" + NODE_VERSION: "22" RCT_NEW_ARCH_ENABLED: "1" BUILD_IOS: "true" XCODE_VERSION: "16.2.0" @@ -26,7 +27,7 @@ steps: - "bundle install" - "node scripts/generate-react-native-fixture.js" matrix: - - "0.82" + - "0.84" retry: automatic: - exit_status: "*" @@ -71,5 +72,5 @@ steps: concurrency_group: "bitbar" concurrency_method: eager matrix: - - "0.82" + - "0.84" diff --git a/.buildkite/package_manifest.json b/.buildkite/package_manifest.json index 63344e0083..cbd5decc6f 100644 --- a/.buildkite/package_manifest.json +++ b/.buildkite/package_manifest.json @@ -20,6 +20,7 @@ "packages/plugin-interaction-breadcrumbs", "packages/plugin-navigation-breadcrumbs", "packages/plugin-network-breadcrumbs", + "packages/plugin-network-instrumentation", "packages/plugin-simple-throttle", "packages/plugin-strip-query-string", "packages/plugin-window-onerror", @@ -97,6 +98,7 @@ "packages/delivery-react-native", "packages/plugin-console-breadcrumbs", "packages/plugin-network-breadcrumbs", + "packages/plugin-network-instrumentation", "packages/plugin-react", "packages/plugin-react-native-client-sync", "packages/plugin-react-native-event-sync", @@ -121,6 +123,7 @@ "packages/delivery-react-native", "packages/plugin-console-breadcrumbs", "packages/plugin-network-breadcrumbs", + "packages/plugin-network-instrumentation", "packages/plugin-react", "packages/plugin-react-native-client-sync", "packages/plugin-react-native-event-sync", diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index ca397c2ce0..1aa3750201 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -8,7 +8,7 @@ steps: - label: ":copyright: License Audit" timeout_in_minutes: 20 agents: - queue: "macos-node-18" + queue: "macos-node-22" command: scripts/license_finder.sh # @@ -26,9 +26,9 @@ steps: key: "publish-js" timeout_in_minutes: 10 agents: - queue: "macos-node-18" + queue: "macos-node-22" env: - NODE_VERSION: "18" + NODE_VERSION: "22" PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" command: - "bundle install" @@ -92,6 +92,6 @@ steps: # - label: ":git: Detect changed packages" agents: - queue: "macos-node-18" + queue: "macos-node-22" timeout_in_minutes: 5 command: node .buildkite/pipeline_trigger.js diff --git a/.github/workflows/aws-lambda.yml b/.github/workflows/aws-lambda.yml index e6bc3bcde7..da21a85bd9 100644 --- a/.github/workflows/aws-lambda.yml +++ b/.github/workflows/aws-lambda.yml @@ -21,17 +21,17 @@ jobs: - run: sam --version - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Ruby - uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 + uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0 with: ruby-version: '3.1' - name: Install Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 18 + node-version: 22 - name: Run tests run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index cda566a73d..003df38bfb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -43,11 +43,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -61,7 +61,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -74,6 +74,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pr-diff.yml b/.github/workflows/pr-diff.yml index f400672148..6d86405744 100644 --- a/.github/workflows/pr-diff.yml +++ b/.github/workflows/pr-diff.yml @@ -9,38 +9,44 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 18.x + node-version: 22 - name: Checkout base branch - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.base_ref }} - name: Record before stats env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + ELECTRON_DISABLE_SANDBOX: 1 run: | mkdir .diff npm ci npm run build + npm run test:coverage cat packages/browser/dist/bugsnag.min.js | wc -c > .diff/size-before-minified cat packages/browser/dist/bugsnag.min.js | gzip | wc -c > .diff/size-before-gzipped + cp coverage/coverage-summary.json .diff/coverage-before.json - name: Checkout PR branch - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: clean: false - name: Record after stats env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + ELECTRON_DISABLE_SANDBOX: 1 run: | npm ci npm run build + npm run test:coverage cat packages/browser/dist/bugsnag.min.js | wc -c > .diff/size-after-minified cat packages/browser/dist/bugsnag.min.js | gzip | wc -c > .diff/size-after-gzipped + cp coverage/coverage-summary.json .diff/coverage-after.json - name: Run danger uses: danger/danger-js@67ed2c1f42fd2fc198cc3c14b43c8f83351f4fe9 # 13.0.5 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e4264251c0..0b181a6441 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.sarif @@ -68,7 +68,7 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: sarif_file: results.sarif @@ -76,5 +76,5 @@ jobs: name: "Checksum validation of Gradle Wrappers" runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: gradle/actions/wrapper-validation@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1 diff --git a/.github/workflows/test-electron.yml b/.github/workflows/test-electron.yml index 2f80d22020..0d1026e634 100644 --- a/.github/workflows/test-electron.yml +++ b/.github/workflows/test-electron.yml @@ -10,12 +10,12 @@ jobs: strategy: matrix: electron: [ '^20.0.0', '^24.0.0', '^26.0.0', '^28.0.0', '^30.0.0' ] - node-version: [18] + node-version: [22] os: [ ubuntu-latest ] steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ matrix.node-version }} - name: (Act) install build tools and dependencies @@ -30,7 +30,7 @@ jobs: if: ${{ !env.ACT }} run: | echo "::set-output name=dir::$(npm config get cache)" - - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 id: npm-cache if: ${{ !env.ACT }} with: @@ -66,7 +66,7 @@ jobs: START_LOCAL_NPM: 1 VERBOSE: 1 ELECTRON_VERSION: ${{ matrix.electron }} - - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: cucumber-failures diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index bc833af7f8..673052eae6 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -28,7 +28,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REVIEWER: gingerbenw steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: next @@ -40,7 +40,7 @@ jobs: - run: git submodule update --init --recursive - name: Install ruby - uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c # v1.269.0 + uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # v1.299.0 with: ruby-version: 2.7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4705c45e..81c59aa24a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,55 @@ # Changelog +## [8.9.0] - 2026-04-08 + +### Added + +- (delivery-react-native) Handle request and response parameters [#2667](https://github.com/bugsnag/bugsnag-js/pull/2667) + +### Changed + +- (plugin-inline-script-content) Add support for additional event target constructors including MediaSource, MediaRecorder, ServiceWorker, RTCPeerConnection, and others [#2700](https://github.com/bugsnag/bugsnag-js/pull/2700) +- (plugin-network-instrumentation) Manually parse URLs to improve React Native compatibility [#2674](https://github.com/bugsnag/bugsnag-js/pull/2674) +- (plugin-network-instrumentation) Refactor URL parsing to improve performance [#2667](https://github.com/bugsnag/bugsnag-js/pull/2667) + +### Fixed + +- (plugin-network-instrumentation) Report HTTP Errors as handled [#2662](https://github.com/bugsnag/bugsnag-js/pull/2662) +- (plugin-network-instrumentation) Omit stacktraces from HTTP Error events [#2684](https://github.com/bugsnag/bugsnag-js/pull/2684) +- (react-native) Report error cause with native stacktrace for Turbo Module exceptions [#2686] (https://github.com/bugsnag/bugsnag-js/pull/2686) + +### Dependencies + +- Update @bugsnag/cuid to [v3.2.1] (https://github.com/bugsnag/cuid/releases/tag/v3.2.1) [#2706](https://github.com/bugsnag/bugsnag-js/pull/2706) +- Update bugsnag-cocoa to [v6.35.0](https//github.com/bugsnag/bugsnag-cocoa/releases/tag/v6.35.0) [#2663](https://github.com/bugsnag/bugsnag-js/pull/2663) +- Update bugsnag-android to [v6.22.0](https//github.com/bugsnag/bugsnag-android/releases/tag/v6.22.0) [#2656](https://github.com/bugsnag/bugsnag-js/pull/2656) +- Update bugsnag-android to [v6.23.0](https//github.com/bugsnag/bugsnag-android/releases/tag/v6.23.0) [#2673](https://github.com/bugsnag/bugsnag-js/pull/2673) +- Update bugsnag-android to [v6.25.0](https//github.com/bugsnag/bugsnag-android/releases/tag/v6.25.0) [#2689](https://github.com/bugsnag/bugsnag-js/pull/2689) +- Update bugsnag-cocoa to [v6.36.0](https//github.com/bugsnag/bugsnag-cocoa/releases/tag/v6.36.0) [#2707](https://github.com/bugsnag/bugsnag-js/pull/2707) + ## [8.8.1] - 2026-01-26 ### Fixed -(plugin-network-breadcrumbs, plugin-network-instrumentation) Ensure XMLHttpRequest response types are handled gracefully [#2660](https://github.com/bugsnag/bugsnag-js/pull/2660) +- (plugin-network-breadcrumbs, plugin-network-instrumentation) Ensure XMLHttpRequest response types are handled gracefully [#2660](https://github.com/bugsnag/bugsnag-js/pull/2660) ## [8.8.0] - 2026-01-20 This release adds support for notitfying failed network requests using the new `plugin-network-instrumentation` package - ### Added -(plugin-network-instrumentation) Add new plugin to notify failed network requests [#2647](https://github.com/bugsnag/bugsnag-js/pull/2647) -(plugin-cloudflare-workers): Add initial support for Cloudflare Workers [#2643](https://github.com/bugsnag/bugsnag-js/pull/2643) [#2644](https://github.com/bugsnag/bugsnag-js/pull/2644) -(plugin-contextualize) Return callback value from contextualize plugin [#2654](https://github.com/bugsnag/bugsnag-js/pull/2654) +- (plugin-network-instrumentation) Add new plugin to notify failed network requests [#2647](https://github.com/bugsnag/bugsnag-js/pull/2647) +- (plugin-cloudflare-workers): Add initial support for Cloudflare Workers [#2643](https://github.com/bugsnag/bugsnag-js/pull/2643) [#2644](https://github.com/bugsnag/bugsnag-js/pull/2644) +- (plugin-contextualize) Return callback value from contextualize plugin [#2654](https://github.com/bugsnag/bugsnag-js/pull/2654) ### Fixed -(plugin-server-session) Delay session tracker initialization until first use [#2642](https://github.com/bugsnag/bugsnag-js/pull/2642) +- (plugin-server-session) Delay session tracker initialization until first use [#2642](https://github.com/bugsnag/bugsnag-js/pull/2642) ### Dependencies -Update bugsnag-cocoa to [6.34.1](https//github.com/bugsnag/bugsnag-cocoa/releases/tag/6.34.1) [#2606](https://github.com/bugsnag/bugsnag-js/pull/2606) -Update bugsnag-android to [v6.20.0](https//github.com/bugsnag/bugsnag-android/releases/tag/v6.20.0) [#2625](https://github.com/bugsnag/bugsnag-js/pull/2625) +- Update bugsnag-cocoa to [6.34.1](https//github.com/bugsnag/bugsnag-cocoa/releases/tag/6.34.1) [#2606](https://github.com/bugsnag/bugsnag-js/pull/2606) +- Update bugsnag-android to [v6.20.0](https//github.com/bugsnag/bugsnag-android/releases/tag/v6.20.0) [#2625](https://github.com/bugsnag/bugsnag-js/pull/2625) ## [8.7.0] - 2025-10-13 diff --git a/dangerfile.js b/dangerfile.js index 5c5ef0ffe6..832d254630 100644 --- a/dangerfile.js +++ b/dangerfile.js @@ -1,15 +1,18 @@ /* global markdown */ const { readFileSync } = require('fs') +const coverageDiff = require('coverage-diff') const before = { minified: parseInt(readFileSync(`${__dirname}/.diff/size-before-minified`, 'utf8').trim()), - gzipped: parseInt(readFileSync(`${__dirname}/.diff/size-before-gzipped`, 'utf8').trim()) + gzipped: parseInt(readFileSync(`${__dirname}/.diff/size-before-gzipped`, 'utf8').trim()), + coverage: JSON.parse(readFileSync(`${__dirname}/.diff/coverage-before.json`, 'utf8')) } const after = { minified: parseInt(readFileSync(`${__dirname}/.diff/size-after-minified`, 'utf8').trim()), - gzipped: parseInt(readFileSync(`${__dirname}/.diff/size-after-gzipped`, 'utf8').trim()) + gzipped: parseInt(readFileSync(`${__dirname}/.diff/size-after-gzipped`, 'utf8').trim()), + coverage: JSON.parse(readFileSync(`${__dirname}/.diff/coverage-after.json`, 'utf8')) } const formatKbs = (n) => `${(n / 1000).toFixed(2)} kB` @@ -33,5 +36,5 @@ markdown(` ### code coverage diff -<_temporarily disabled_> +${coverageDiff.diff(before.coverage, after.coverage).results} `) diff --git a/docker-compose.yml b/docker-compose.yml index fbd9a2a5df..4c45dd265a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,7 +114,7 @@ services: - ./reports/:/app/test/node/reports/ react-native-maze-runner: - image: 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v10-cli + image: 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v11-cli environment: <<: *common-environment BITBAR_USERNAME: @@ -139,7 +139,7 @@ services: - ./reports/:/app/reports react-native-cli-maze-runner: - image: 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v10-cli + image: 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v11-cli environment: <<: *common-environment BITBAR_USERNAME: diff --git a/dockerfiles/Dockerfile.browser b/dockerfiles/Dockerfile.browser index 2eb7d7da54..d5f64fed54 100644 --- a/dockerfiles/Dockerfile.browser +++ b/dockerfiles/Dockerfile.browser @@ -1,5 +1,5 @@ # CI test image for unit/lint/type tests -FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 AS browser-feature-builder +FROM node:22-alpine@sha256:4d64b49e6c891c8fc821007cb1cdc6c0db7773110ac2c34bf2e6960adef62ed3 AS browser-feature-builder RUN apk add --update bash python3 make gcc g++ musl-dev xvfb-run curl @@ -55,7 +55,7 @@ RUN find . -name package.json -type f -mindepth 2 -maxdepth 3 ! -path "./node_mo RUN rm -fr **/*/node_modules/ # The maze-runner browser tests (W3C protocol) -FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:v10.10.1-cli AS browser-maze-runner +FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v11-cli AS browser-maze-runner COPY --from=browser-feature-builder /app/test/browser /app/test/browser/ WORKDIR /app/test/browser diff --git a/dockerfiles/Dockerfile.ci b/dockerfiles/Dockerfile.ci index a141b16590..112d858bec 100644 --- a/dockerfiles/Dockerfile.ci +++ b/dockerfiles/Dockerfile.ci @@ -1,5 +1,5 @@ # CI test image for unit/lint/type tests -FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 +FROM node:22-alpine@sha256:4d64b49e6c891c8fc821007cb1cdc6c0db7773110ac2c34bf2e6960adef62ed3 RUN apk add --update bash python3 make gcc g++ musl-dev xvfb-run curl diff --git a/dockerfiles/Dockerfile.node b/dockerfiles/Dockerfile.node index 8d88d98c83..479984bb7e 100644 --- a/dockerfiles/Dockerfile.node +++ b/dockerfiles/Dockerfile.node @@ -1,5 +1,5 @@ # CI test image for unit/lint/type tests -FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 AS node-feature-builder +FROM node:22-alpine@sha256:4d64b49e6c891c8fc821007cb1cdc6c0db7773110ac2c34bf2e6960adef62ed3 AS node-feature-builder RUN apk add --update bash python3 make gcc g++ musl-dev xvfb-run curl @@ -22,7 +22,7 @@ RUN npm pack --verbose packages/plugin-restify/ RUN npm pack --verbose packages/plugin-hono/ # The maze-runner node tests -FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:v10.10.1-cli AS node-maze-runner +FROM 855461928731.dkr.ecr.us-west-1.amazonaws.com/maze-runner-releases:latest-v11-cli AS node-maze-runner WORKDIR /app/ COPY packages/node/ . COPY test/node/features test/node/features diff --git a/dockerfiles/Dockerfile.release b/dockerfiles/Dockerfile.release index 6927d72e21..1c59a1fb2f 100644 --- a/dockerfiles/Dockerfile.release +++ b/dockerfiles/Dockerfile.release @@ -1,4 +1,4 @@ -FROM node:18-alpine@sha256:974afb6cbc0314dc6502b14243b8a39fbb2d04d975e9059dd066be3e274fbb25 +FROM node:22-alpine@sha256:4d64b49e6c891c8fc821007cb1cdc6c0db7773110ac2c34bf2e6960adef62ed3 COPY ./zscaler-root-ca.crt* /usr/local/share/ca-certificates/ RUN if [ -f /usr/local/share/ca-certificates/zscaler-root-ca.crt ]; then cat /usr/local/share/ca-certificates/zscaler-root-ca.crt >> /etc/ssl/certs/ca-certificates.crt; fi diff --git a/jest.config.js b/jest.config.js index 358588715a..a54329a23d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -53,7 +53,8 @@ module.exports = { 'plugin-browser-session', 'plugin-network-instrumentation' ], { - testEnvironment: '/jest/FixJSDOMEnvironment.js' + testEnvironment: '/jest/FixJSDOMEnvironment.js', + modulePathIgnorePatterns: ['.verdaccio', 'dist', 'examples', 'fixtures'] }), project('react native', [ 'react-native', diff --git a/package-lock.json b/package-lock.json index b75e7d4e1a..9c910e8918 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2627,11 +2627,6 @@ "resolved": "packages/core", "link": true }, - "node_modules/@bugsnag/cuid": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.1.1.tgz", - "integrity": "sha512-d2z4b0rEo3chI07FNN1Xds8v25CNeekecU6FC/2Fs9MxY2EipkZTThVcV2YinMn8dvRUlViKOyC50evoUxg8tw==" - }, "node_modules/@bugsnag/delivery-electron": { "resolved": "packages/delivery-electron", "link": true @@ -46675,13 +46670,19 @@ "version": "8.8.0", "license": "MIT", "dependencies": { - "@bugsnag/cuid": "^3.0.0", + "@bugsnag/cuid": "^3.2.1", "@bugsnag/safe-json-stringify": "^6.0.0", "error-stack-parser": "^2.0.3", "iserror": "^0.0.2", "stack-generator": "^2.0.3" } }, + "packages/core/node_modules/@bugsnag/cuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.2.1.tgz", + "integrity": "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==", + "license": "MIT" + }, "packages/delivery-electron": { "name": "@bugsnag/delivery-electron", "version": "8.8.0", @@ -46818,7 +46819,7 @@ "version": "8.8.0", "license": "MIT", "dependencies": { - "@bugsnag/cuid": "^3.0.0" + "@bugsnag/cuid": "^3.2.1" }, "devDependencies": { "@bugsnag/core": "^8.8.0" @@ -46827,6 +46828,12 @@ "@bugsnag/core": "^8.0.0" } }, + "packages/in-flight/node_modules/@bugsnag/cuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.2.1.tgz", + "integrity": "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==", + "license": "MIT" + }, "packages/js": { "name": "@bugsnag/js", "version": "8.8.1", @@ -52954,7 +52961,7 @@ "version": "8.8.0", "license": "MIT", "dependencies": { - "@bugsnag/cuid": "^3.0.0" + "@bugsnag/cuid": "^3.2.1" }, "devDependencies": { "@bugsnag/core": "^8.8.0" @@ -52963,6 +52970,12 @@ "@bugsnag/core": "^8.0.0" } }, + "packages/plugin-browser-device/node_modules/@bugsnag/cuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.2.1.tgz", + "integrity": "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==", + "license": "MIT" + }, "packages/plugin-browser-request": { "name": "@bugsnag/plugin-browser-request", "version": "8.8.0", @@ -53037,6 +53050,7 @@ "packages/plugin-electron-app": { "name": "@bugsnag/plugin-electron-app", "version": "8.8.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "bindings": "^1.5.0" @@ -53078,6 +53092,7 @@ "packages/plugin-electron-client-state-persistence": { "name": "@bugsnag/plugin-electron-client-state-persistence", "version": "8.8.0", + "hasInstallScript": true, "license": "MIT", "dependencies": { "bindings": "^1.5.0" @@ -55675,18 +55690,20 @@ "@bugsnag/core": { "version": "file:packages/core", "requires": { - "@bugsnag/cuid": "^3.0.0", + "@bugsnag/cuid": "^3.2.1", "@bugsnag/safe-json-stringify": "^6.0.0", "error-stack-parser": "^2.0.3", "iserror": "^0.0.2", "stack-generator": "^2.0.3" + }, + "dependencies": { + "@bugsnag/cuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.2.1.tgz", + "integrity": "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==" + } } }, - "@bugsnag/cuid": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.1.1.tgz", - "integrity": "sha512-d2z4b0rEo3chI07FNN1Xds8v25CNeekecU6FC/2Fs9MxY2EipkZTThVcV2YinMn8dvRUlViKOyC50evoUxg8tw==" - }, "@bugsnag/delivery-electron": { "version": "file:packages/delivery-electron", "requires": { @@ -55780,7 +55797,14 @@ "version": "file:packages/in-flight", "requires": { "@bugsnag/core": "^8.8.0", - "@bugsnag/cuid": "^3.0.0" + "@bugsnag/cuid": "^3.2.1" + }, + "dependencies": { + "@bugsnag/cuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.2.1.tgz", + "integrity": "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==" + } } }, "@bugsnag/js": { @@ -59758,7 +59782,14 @@ "version": "file:packages/plugin-browser-device", "requires": { "@bugsnag/core": "^8.8.0", - "@bugsnag/cuid": "^3.0.0" + "@bugsnag/cuid": "^3.2.1" + }, + "dependencies": { + "@bugsnag/cuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@bugsnag/cuid/-/cuid-3.2.1.tgz", + "integrity": "sha512-zpvN8xQ5rdRWakMd/BcVkdn2F8HKlDSbM3l7duueK590WmI1T0ObTLc1V/1e55r14WNjPd5AJTYX4yPEAFVi+Q==" + } } }, "@bugsnag/plugin-browser-request": { diff --git a/package.json b/package.json index 84964698f9..760986226d 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "test:build-node-container": "docker compose up --build minimal-packager && docker compose build --pull node-maze-runner", "test:build-react-native-maze-runner": "docker-compose build --pull react-native-maze-runner", "test:node": "npm run test:build-node-container && docker compose run --use-aliases node-maze-runner", - "local-npm:start": "verdaccio --config test/electron/local-npm-config.yml --listen 0.0.0.0:5539", + "local-npm:start": "verdaccio --config test/local-npm-config.yml --listen 0.0.0.0:5539", "local-npm:publish-all": "lerna publish \"$VERSION_IDENTIFIER\" --yes --force-publish --exact --no-push --no-git-reset --no-git-tag-version --registry 'http://0.0.0.0:5539'", "local-npm:publish-all-win32": "lerna publish %VERSION_IDENTIFIER% --yes --force-publish --exact --no-push --no-git-reset --no-git-tag-version --registry 'http://0.0.0.0:5539'", "test:electron": "xvfb-maybe --auto-servernum -- cucumber-js test/electron/features --retry 3 --retry-tag-filter @flaky" diff --git a/packages/core/package.json b/packages/core/package.json index 2743f6d6a6..00f3e176be 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,7 +20,7 @@ "author": "Bugsnag", "license": "MIT", "dependencies": { - "@bugsnag/cuid": "^3.0.0", + "@bugsnag/cuid": "^3.2.1", "@bugsnag/safe-json-stringify": "^6.0.0", "error-stack-parser": "^2.0.3", "iserror": "^0.0.2", diff --git a/packages/delivery-react-native/delivery.js b/packages/delivery-react-native/delivery.js index 49561f9061..9f4f4a0215 100644 --- a/packages/delivery-react-native/delivery.js +++ b/packages/delivery-react-native/delivery.js @@ -3,12 +3,35 @@ const derecursify = require('@bugsnag/core/lib/derecursify') module.exports = (client, NativeClient) => ({ sendEvent: (payload, cb = () => {}) => { const event = payload.events[0] - let nativeStack + if (event.originalError) { + // extract native stacktrace from originalError if available + let nativeErrorMessage, nativeStack if (event.originalError.nativeStackIOS) { + // old arch ios + nativeErrorMessage = event.originalError.message nativeStack = event.originalError.nativeStackIOS } else if (event.originalError.nativeStackAndroid) { + // old arch android + nativeErrorMessage = event.originalError.message nativeStack = event.originalError.nativeStackAndroid + } else if (event.originalError.cause && event.originalError.cause.stackSymbols) { + // new arch ios + nativeErrorMessage = event.originalError.cause.message + nativeStack = event.originalError.cause.stackSymbols + } else if (event.originalError.cause && event.originalError.cause.stackElements) { + // new arch android + nativeErrorMessage = event.originalError.cause.message + nativeStack = event.originalError.cause.stackElements + } + + if (nativeErrorMessage && nativeStack) { + // add the native stack to the corresponding error in the event payload + const nativeError = event.errors.find(err => err.errorMessage === nativeErrorMessage) + + if (nativeError) { + nativeError.nativeStack = nativeStack + } } } @@ -25,12 +48,13 @@ module.exports = (client, NativeClient) => ({ breadcrumbs: derecursify(event.breadcrumbs), context: event.context, user: event._user, + request: event.request, + response: event.response, metadata: derecursify(event._metadata), groupingHash: event.groupingHash, groupingDiscriminator: event._groupingDiscriminator, apiKey: event.apiKey, featureFlags: event.toJSON().featureFlags, - nativeStack: nativeStack, correlation: event._correlation } diff --git a/packages/delivery-react-native/test/delivery.test.ts b/packages/delivery-react-native/test/delivery.test.ts index 27d847cc2d..e7b8c607d7 100644 --- a/packages/delivery-react-native/test/delivery.test.ts +++ b/packages/delivery-react-native/test/delivery.test.ts @@ -17,6 +17,8 @@ type NativeClientEvent = Pick { @@ -81,6 +91,8 @@ describe('delivery: react native', () => { expect(sent[0].context).toBe('test screen') expect(sent[0].user).toEqual({ id: '123', email: undefined, name: undefined }) expect(sent[0].metadata).toEqual({}) + expect(sent[0].request).toEqual({}) + expect(sent[0].response).toEqual({}) expect(sent[0].groupingHash).toEqual('ER_GRP_098') expect(sent[0].apiKey).toBe('abcdef123456abcdef123456abcdef123456') expect(sent[0].correlation).toEqual({ traceId: 'trace-id', spanId: 'span-id' }) @@ -111,7 +123,8 @@ describe('delivery: react native', () => { ] c.notify(error, (e) => {}, (err, event) => { expect(err).not.toBeTruthy() - expect(sent[0].nativeStack).toEqual(error.nativeStackIOS) + // @ts-expect-error nativeStack is added by the delivery module + expect(sent[0].errors[0].nativeStack).toEqual(error.nativeStackIOS) done() }) }) @@ -137,7 +150,96 @@ describe('delivery: react native', () => { ] c.notify(error, (e) => {}, (err, event) => { expect(err).not.toBeTruthy() - expect(sent[0].nativeStack).toBe(error.nativeStackAndroid) + // @ts-expect-error nativeStack is added by the delivery module + expect(sent[0].errors[0].nativeStack).toBe(error.nativeStackAndroid) + done() + }) + }) + + it('extracts iOS native error cause', done => { + const sent: NativeClientEvent[] = [] + const NativeClient = { + dispatchAsync: (event: NativeClientEvent) => { + sent.push(event) + return new Promise((resolve) => resolve(true)) + } + } + const c = new Client({ apiKey: 'api_key' }) + c._setDelivery(client => delivery(client, NativeClient)) + const error = new ReactNativeError('oh no') + const stackSymbols = [ + '0 ReactNativeTest 0x000000010fda7f1b RCTJSErrorFromCodeMessageAndNSError + 79', + '1 ReactNativeTest 0x000000010fd76897 __41-[RCTModuleMethod processMethodSignature]_block_invoke_2.103 + 97', + '2 ReactNativeTest 0x000000010fccd9c3 -[BenCrash asyncReject:rejecter:] + 106', + '3 CoreFoundation 0x00007fff23e44dec __invoking___ + 140', + '4 CoreFoundation 0x00007fff23e41fd1 -[NSInvocation invoke] + 321', + '5 CoreFoundation 0x00007fff23e422a4 -[NSInvocation invokeWithTarget:] + 68', + '6 ReactNativeTest 0x000000010fd76eae -[RCTModuleMethod invokeWithBridge:module:arguments:] + 578', + '7 ReactNativeTest 0x000000010fd79138 _ZN8facebook5reactL11invokeInnerEP9RCTBridgeP13RCTModuleDatajRKN5folly7dynamicE + 246' + ] + + error.cause = { + name: 'NativeiOSException', + message: 'Native iOS error occurred', + stackSymbols + } + + c.notify(error, (e) => {}, (err, event) => { + expect(err).not.toBeTruthy() + expect(sent[0].errors[0].errorMessage).toEqual(error.message) + // @ts-expect-error nativeStack is added by the delivery module + expect(sent[0].errors[0].nativeStack).toBeUndefined() + + expect(sent[0].errors[1].errorMessage).toBeDefined() + expect(sent[0].errors[1].errorMessage).toEqual(error.cause?.message) + expect(sent[0].errors[1].errorClass).toBeDefined() + expect(sent[0].errors[1].errorClass).toEqual(error.cause?.name) + + // @ts-expect-error nativeStack is added by the delivery module + expect(sent[0].errors[1].nativeStack).toEqual(error.cause.stackSymbols) + done() + }) + }) + + it('extracts Android native error cause', done => { + const sent: NativeClientEvent[] = [] + const NativeClient = { + dispatchAsync: (event: NativeClientEvent) => { + sent.push(event) + return new Promise((resolve) => resolve(true)) + } + } + const c = new Client({ apiKey: 'api_key' }) + c._setDelivery(client => delivery(client, NativeClient)) + const error = new ReactNativeError('oh no') + const stackElements = [ + { + class: 'com.testing.Blah', + lineNumber: 101, + file: 'app/com.testing.Blah.java', + methodName: 'crash()' + } + ] + + error.cause = { + name: 'NativeAndroidException', + message: 'Native Android error occurred', + stackElements + } + + c.notify(error, (e) => {}, (err, event) => { + expect(err).not.toBeTruthy() + expect(sent[0].errors[0].errorMessage).toEqual(error.message) + // @ts-expect-error nativeStack is added by the delivery module + expect(sent[0].errors[0].nativeStack).toBeUndefined() + + expect(sent[0].errors[1].errorMessage).toBeDefined() + expect(sent[0].errors[1].errorMessage).toEqual(error.cause?.message) + expect(sent[0].errors[1].errorClass).toBeDefined() + expect(sent[0].errors[1].errorClass).toEqual(error.cause?.name) + + // @ts-expect-error nativeStack is added by the delivery module + expect(sent[0].errors[1].nativeStack).toEqual(error.cause.stackElements) done() }) }) @@ -188,6 +290,8 @@ describe('delivery: react native', () => { expect(sent[0].context).toBe('test screen') expect(sent[0].user).toEqual({ id: '123', email: undefined, name: undefined }) expect(sent[0].metadata).toEqual({}) + expect(sent[0].request).toEqual({}) + expect(sent[0].response).toEqual({}) expect(sent[0].groupingHash).toEqual('ER_GRP_098') expect(sent[0].apiKey).toBe('abcdef123456abcdef123456abcdef123456') done() diff --git a/packages/in-flight/package.json b/packages/in-flight/package.json index 0153c00ffa..ce406b7ad6 100644 --- a/packages/in-flight/package.json +++ b/packages/in-flight/package.json @@ -19,7 +19,7 @@ "author": "Bugsnag", "license": "MIT", "dependencies": { - "@bugsnag/cuid": "^3.0.0" + "@bugsnag/cuid": "^3.2.1" }, "devDependencies": { "@bugsnag/core": "^8.8.0" diff --git a/packages/node/test/notifier.test.ts b/packages/node/test/notifier.test.ts index 1fa7cc4fed..e14d08f139 100644 --- a/packages/node/test/notifier.test.ts +++ b/packages/node/test/notifier.test.ts @@ -4,6 +4,7 @@ describe('node notifier', () => { beforeAll(() => { jest.spyOn(console, 'debug').mockImplementation(() => {}) jest.spyOn(console, 'warn').mockImplementation(() => {}) + jest.spyOn(console, 'log').mockImplementation(() => {}) }) beforeEach(() => { @@ -11,6 +12,15 @@ describe('node notifier', () => { Bugsnag._client = null }) + afterEach(() => { + // Clean up process listeners to prevent MaxListenersExceeded warning + // The unhandledRejection and uncaughtException plugins register listeners + // but don't have destroy methods called automatically + process.removeAllListeners('unhandledRejection') + process.removeAllListeners('uncaughtException') + jest.clearAllMocks() + }) + describe('isStarted()', () => { it('returns false when the notifier has not been initialised', () => { expect(Bugsnag.isStarted()).toBe(false) diff --git a/packages/plugin-aws-lambda/test/serverless-express.test.js b/packages/plugin-aws-lambda/test/serverless-express.test.js index 0212eca34e..a39b3a30a2 100644 --- a/packages/plugin-aws-lambda/test/serverless-express.test.js +++ b/packages/plugin-aws-lambda/test/serverless-express.test.js @@ -14,6 +14,10 @@ let sentEvents let sentSessions describe('serverless express', function () { + beforeAll(() => { + jest.spyOn(console, 'debug').mockImplementation(() => {}) + }) + beforeEach(function () { sentEvents = [] sentSessions = [] diff --git a/packages/plugin-browser-device/device.js b/packages/plugin-browser-device/device.js index bd1437b970..d1d60f75d8 100644 --- a/packages/plugin-browser-device/device.js +++ b/packages/plugin-browser-device/device.js @@ -1,5 +1,6 @@ const assign = require('@bugsnag/core/lib/es-utils/assign') const BUGSNAG_ANONYMOUS_ID_KEY = 'bugsnag-anonymous-id' +const cuid = require('@bugsnag/cuid') const getDeviceId = (win) => { try { @@ -7,13 +8,11 @@ const getDeviceId = (win) => { let id = storage.getItem(BUGSNAG_ANONYMOUS_ID_KEY) - // If we get an ID, make sure it looks like a valid cuid. The length can - // fluctuate slightly, so some leeway is built in - if (id && /^c[a-z0-9]{20,32}$/.test(id)) { + // If we get an ID, make sure it looks like a valid cuid + if (id && cuid.isCuid(id)) { return id } - const cuid = require('@bugsnag/cuid') id = cuid() storage.setItem(BUGSNAG_ANONYMOUS_ID_KEY, id) diff --git a/packages/plugin-browser-device/package.json b/packages/plugin-browser-device/package.json index f2711a1b7b..fcf8019eff 100644 --- a/packages/plugin-browser-device/package.json +++ b/packages/plugin-browser-device/package.json @@ -17,7 +17,7 @@ "author": "Bugsnag", "license": "MIT", "dependencies": { - "@bugsnag/cuid": "^3.0.0" + "@bugsnag/cuid": "^3.2.1" }, "devDependencies": { "@bugsnag/core": "^8.8.0" diff --git a/packages/plugin-electron-ipc/preload.js b/packages/plugin-electron-ipc/preload.js index c62e0f6981..926bca4ff0 100644 --- a/packages/plugin-electron-ipc/preload.js +++ b/packages/plugin-electron-ipc/preload.js @@ -1,26 +1,28 @@ +(function () { // preloads run in devtools panes too, but we don't want to run there -if (document.location.protocol === 'devtools:') return + if (document.location.protocol === 'devtools:') return -const { ipcRenderer, contextBridge } = require('electron') -const BugsnagIpcRenderer = require('./bugsnag-ipc-renderer') -const { CHANNEL_CONFIG } = require('./lib/constants') + const { ipcRenderer, contextBridge } = require('electron') + const BugsnagIpcRenderer = require('./bugsnag-ipc-renderer') + const { CHANNEL_CONFIG } = require('./lib/constants') -// one sync call is required on startup to get the main process config -const config = ipcRenderer.sendSync(CHANNEL_CONFIG) -if (!config) throw new Error('Bugsnag was not started in the main process before browser windows were created') + // one sync call is required on startup to get the main process config + const config = ipcRenderer.sendSync(CHANNEL_CONFIG) + if (!config) throw new Error('Bugsnag was not started in the main process before browser windows were created') -// attach config to the exposed interface -BugsnagIpcRenderer.config = JSON.parse(config) + // attach config to the exposed interface + BugsnagIpcRenderer.config = JSON.parse(config) -// attach process info to the exposed interface -const { isMainFrame, sandboxed, type } = process -BugsnagIpcRenderer.process = { isMainFrame, sandboxed, type } + // attach process info to the exposed interface + const { isMainFrame, sandboxed, type } = process + BugsnagIpcRenderer.process = { isMainFrame, sandboxed, type } -// expose Bugsnag as a global object for the browser -try { + // expose Bugsnag as a global object for the browser + try { // assume contextIsolation=true - contextBridge.exposeInMainWorld('__bugsnag_ipc__', BugsnagIpcRenderer) -} catch (e) {} + contextBridge.exposeInMainWorld('__bugsnag_ipc__', BugsnagIpcRenderer) + } catch (e) {} -// expose for other preload scripts to use, this also covers contextIsolation=false -window.__bugsnag_ipc__ = BugsnagIpcRenderer + // expose for other preload scripts to use, this also covers contextIsolation=false + window.__bugsnag_ipc__ = BugsnagIpcRenderer +})() diff --git a/packages/plugin-inline-script-content/inline-script-content.js b/packages/plugin-inline-script-content/inline-script-content.js index 26749c6a2f..f654229738 100644 --- a/packages/plugin-inline-script-content/inline-script-content.js +++ b/packages/plugin-inline-script-content/inline-script-content.js @@ -111,7 +111,10 @@ module.exports = (doc = document, win = window) => ({ 'CryptoOperation', 'EventSource', 'FileReader', 'HTMLUnknownElement', 'IDBDatabase', 'IDBRequest', 'IDBTransaction', 'KeyOperation', 'MediaController', 'MessagePort', 'ModalWindow', 'Notification', 'SVGElementInstance', 'Screen', 'TextTrack', 'TextTrackCue', 'TextTrackList', - 'WebSocket', 'WebSocketWorker', 'Worker', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload' + 'WebSocket', 'WebSocketWorker', 'Worker', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload', + 'MediaSource', 'MediaRecorder', 'MediaStream', 'ServiceWorker', 'ServiceWorkerContainer', + 'ServiceWorkerRegistration', 'BroadcastChannel', 'RTCPeerConnection', 'RTCDataChannel', 'AbortSignal', + 'MediaQueryList', 'ShadowRoot', 'FontFaceSet', 'Animation', 'PermissionStatus', 'PaymentRequest', 'VideoTrackList' ], o => { if (!win[o] || !win[o].prototype || !Object.prototype.hasOwnProperty.call(win[o].prototype, 'addEventListener')) return __proxy(win[o].prototype, 'addEventListener', original => diff --git a/packages/plugin-network-instrumentation/lib/extract-domain.js b/packages/plugin-network-instrumentation/lib/extract-domain.js deleted file mode 100644 index fd290943b5..0000000000 --- a/packages/plugin-network-instrumentation/lib/extract-domain.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Extract domain from URL - * @param {string} url - URL string - * @returns {string} Domain - */ -module.exports = function (url) { - try { - const urlObj = new URL(url) - return urlObj.host - } catch (e) { - return 'unknown' - } -} diff --git a/packages/plugin-network-instrumentation/lib/parse-query-params.js b/packages/plugin-network-instrumentation/lib/parse-query-params.js deleted file mode 100644 index e9dc18a054..0000000000 --- a/packages/plugin-network-instrumentation/lib/parse-query-params.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Parse query parameters from URL - * @param {string} url - URL string - * @returns {Object} Parsed query parameters - */ -module.exports = function (url) { - try { - const urlObj = new URL(url) - const params = {} - urlObj.searchParams.forEach((value, key) => { - params[key] = value - }) - return params - } catch (e) { - return {} - } -} diff --git a/packages/plugin-network-instrumentation/lib/parse-query-string.js b/packages/plugin-network-instrumentation/lib/parse-query-string.js new file mode 100644 index 0000000000..165c030db0 --- /dev/null +++ b/packages/plugin-network-instrumentation/lib/parse-query-string.js @@ -0,0 +1,19 @@ +/** + * Parse a query string into an object + * @param {string} queryString - Query string (e.g., "key=value&foo=bar") + * @returns {Object} Parsed query parameters as key-value pairs + */ +module.exports = function (queryString) { + const params = {} + if (!queryString) { + return params + } + + const pairs = queryString.split('&').filter(pair => pair.length > 0) + pairs.forEach(pair => { + const [key, value] = pair.split('=') + params[decodeURIComponent(key)] = decodeURIComponent(value || '') + }) + + return params +} diff --git a/packages/plugin-network-instrumentation/lib/parse-url.js b/packages/plugin-network-instrumentation/lib/parse-url.js new file mode 100644 index 0000000000..e3b453929f --- /dev/null +++ b/packages/plugin-network-instrumentation/lib/parse-url.js @@ -0,0 +1,64 @@ +/** + * Parse a URL in a single pass to extract domain, clean URL, and query string + * @param {string} url - URL string + * @returns {{ domain: string, cleanUrl: string, queryString: string }} Object with domain, cleanUrl, and queryString + */ +module.exports = function (url) { + try { + const isAbsolute = /^https?:\/\//i.test(url) + + // Extract query string from the full URL + const queryStart = url.indexOf('?') + const hashStart = url.indexOf('#') + + let queryString = '' + if (queryStart !== -1) { + const queryEnd = hashStart !== -1 && hashStart > queryStart ? hashStart : url.length + queryString = url.substring(queryStart + 1, queryEnd) + } + + // Extract domain + let domain = 'unknown' + if (isAbsolute) { + const urlWithoutProtocol = url.replace(/^https?:\/\//i, '') + + // Find the earliest occurrence of '/', '?', or '#' to determine the domain boundary + const slashIndex = urlWithoutProtocol.indexOf('/') + const domainQueryIndex = urlWithoutProtocol.indexOf('?') + const domainHashIndex = urlWithoutProtocol.indexOf('#') + let endIndex = urlWithoutProtocol.length + if (slashIndex !== -1 && slashIndex < endIndex) { + endIndex = slashIndex + } + if (domainQueryIndex !== -1 && domainQueryIndex < endIndex) { + endIndex = domainQueryIndex + } + if (domainHashIndex !== -1 && domainHashIndex < endIndex) { + endIndex = domainHashIndex + } + + domain = urlWithoutProtocol.substring(0, endIndex) + } + + // Strip query string while preserving hash + const hash = hashStart !== -1 ? url.substring(hashStart) : '' + let urlWithoutHash = queryStart !== -1 ? url.substring(0, queryStart) : url + if (hashStart !== -1 && queryStart === -1) { + // If there's a hash but no query string, remove the hash first + urlWithoutHash = url.substring(0, hashStart) + } + const cleanUrl = urlWithoutHash + hash + + return { + domain, + cleanUrl, + queryString + } + } catch (e) { + return { + domain: 'unknown', + cleanUrl: url, + queryString: '' + } + } +} diff --git a/packages/plugin-network-instrumentation/lib/redact-query-parameters.js b/packages/plugin-network-instrumentation/lib/redact-query-parameters.js deleted file mode 100644 index 3c69f97e08..0000000000 --- a/packages/plugin-network-instrumentation/lib/redact-query-parameters.js +++ /dev/null @@ -1,38 +0,0 @@ -const redactValues = require('./redact-values') - -function isAbsoluteURL (url) { - try { - // eslint-disable-next-line no-new - new URL(url) - return true - } catch (e) { - return false - } -} - -module.exports = function (url, redactedKeys) { - const isAbsolute = isAbsoluteURL(url) - const base = isAbsolute ? undefined : 'http://localhost' - - // Parse the URL - use a base only for relative URLs - const urlObj = new URL(url, base) - const params = new URLSearchParams(urlObj.search) - - // Convert URLSearchParams to object without using Object.fromEntries() - const paramsObject = {} - params.forEach((value, key) => { - paramsObject[key] = value - }) - - const redactedParams = redactValues(paramsObject, redactedKeys) - urlObj.search = new URLSearchParams(redactedParams).toString() - - // Return appropriate format based on original URL type - if (isAbsolute) { - return decodeURI(urlObj.toString()) - } - - // For relative URLs, return only the path + search + hash components - const relativePart = urlObj.pathname + urlObj.search + urlObj.hash - return decodeURI(relativePart) -} diff --git a/packages/plugin-network-instrumentation/network-instrumentation.js b/packages/plugin-network-instrumentation/network-instrumentation.js index f0ac3b6812..b28d777515 100644 --- a/packages/plugin-network-instrumentation/network-instrumentation.js +++ b/packages/plugin-network-instrumentation/network-instrumentation.js @@ -3,9 +3,9 @@ * A plugin to automatically capture and report HTTP errors */ -const extractDomain = require('./lib/extract-domain') -const parseQueryParams = require('./lib/parse-query-params') -const redactQueryParameters = require('./lib/redact-query-parameters') +const parseUrl = require('./lib/parse-url') +const parseQueryString = require('./lib/parse-query-string') +const redactValues = require('./lib/redact-values') const shouldCaptureStatusCode = require('./lib/should-capture-status-code') const truncate = require('./lib/truncate') @@ -84,22 +84,25 @@ module.exports = (config = {}, global = window) => { try { // Extract request information - const url = startContext.url - const requestParams = parseQueryParams(url) + const originalUrl = startContext.url + const { domain, cleanUrl, queryString } = parseUrl(originalUrl) + const url = cleanUrl const method = startContext.method - const domain = extractDomain(url) + + // Parse query string into object + const requestParams = parseQueryString(queryString) // Create request and response objects for callback const requestObj = { - url: startContext.url, - httpMethod: startContext.method, + url, + httpMethod: method, headers: startContext.headers, - params: requestParams + params: redactValues(requestParams, client._config.redactedKeys), + bodyLength: startContext.body ? startContext.body.length : undefined } const responseObj = { statusCode: endContext.status, headers: endContext.headers, - body: endContext.body, bodyLength: endContext.body ? endContext.body.length : undefined } @@ -116,27 +119,20 @@ module.exports = (config = {}, global = window) => { // Truncate request body if (maxRequestSize > 0 && startContext.body) { requestObj.body = truncate(startContext.body, maxRequestSize) - requestObj.bodyLength = startContext.body.length } // Truncate response body - XHR only if (maxResponseSize > 0 && endContext.body) { responseObj.body = truncate(endContext.body, maxResponseSize) - responseObj.bodyLength = endContext.body.length - } - - // Strip query parameters from URL - if (requestObj.url !== '[REDACTED]') { - requestObj.url = redactQueryParameters(requestObj.url, client._config.redactedKeys) } // Create error and notify - const error = new Error(`${responseObj.statusCode}: ${requestObj.url}`) + const error = new Error(`${responseObj.statusCode}: ${url}`) error.name = 'HTTPError' const handledState = { severity: 'error', - unhandled: true, + unhandled: false, severityReason: { type: 'httpError' } } @@ -148,6 +144,7 @@ module.exports = (config = {}, global = window) => { 0 ) + event.errors[0].stacktrace = [] // Omit the stacktrace for HTTP errors event.request = requestObj event.response = responseObj event.context = `${method} ${domain}` diff --git a/packages/plugin-network-instrumentation/test/network-instrumentation-xhr.test.ts b/packages/plugin-network-instrumentation/test/network-instrumentation-xhr.test.ts index b06796d394..f65931ff9c 100644 --- a/packages/plugin-network-instrumentation/test/network-instrumentation-xhr.test.ts +++ b/packages/plugin-network-instrumentation/test/network-instrumentation-xhr.test.ts @@ -133,9 +133,10 @@ describe('plugin-network-instrumentation', () => { // Verify error details expect(event.exceptions[0].errorClass).toBe('HTTPError') expect(event.exceptions[0].errorMessage).toBe('404: https://api.example.com/users/123') + expect(event.exceptions[0].stacktrace).toEqual([]) // Stacktrace should be empty for HTTP errors expect(event.context).toBe('POST api.example.com') expect(event.severity).toBe('error') - expect(event.unhandled).toBe(true) + expect(event.unhandled).toBe(false) expect(event.severityReason.type).toBe('httpError') // Verify request metadata @@ -209,32 +210,5 @@ describe('plugin-network-instrumentation', () => { // since they don't have status codes, but let's verify the behavior expect(notifyCallbacks.length).toBe(0) }) - - it('should redact specified query parameters in XHR URLs', async () => { - const notifyCallbacks: Event[] = [] - - plugin = createPlugin({ - httpErrorCodes: { min: 400, max: 499 } - }) - - const client = new Client({ apiKey: 'api_key', plugins: [plugin], redactedKeys: ['token', 'userId'] }) - client._setDelivery(createMockDelivery(notifyCallbacks)) - - const xhr = new XMLHttpRequest() as any - xhr.status = 403 - xhr.statusText = 'Forbidden' - xhr.response = 'Forbidden' - - xhr.open('GET', 'https://api.example.com/data?userId=42') - xhr.send() - - await new Promise(resolve => setTimeout(resolve, 20)) - - expect(notifyCallbacks.length).toBe(1) - const event = notifyCallbacks[0].toJSON() - - // Verify that sensitive query parameters are redacted - expect(event.request.url).toBe('https://api.example.com/data?userId=[REDACTED]') - }) }) }) diff --git a/packages/plugin-network-instrumentation/test/network-instrumentation.test.ts b/packages/plugin-network-instrumentation/test/network-instrumentation.test.ts index 5d3407aed9..33cb2fa696 100644 --- a/packages/plugin-network-instrumentation/test/network-instrumentation.test.ts +++ b/packages/plugin-network-instrumentation/test/network-instrumentation.test.ts @@ -55,6 +55,45 @@ describe('plugin-network-instrumentation', () => { }) }) + describe('config.redactedKeys', () => { + it('should redact specified query parameters from request.params', async () => { + const notifyCallbacks: Event[] = [] + + plugin = createPlugin() + + const client = new Client({ apiKey: 'api_key', plugins: [plugin], redactedKeys: ['password', 'token'] }) + client._setDelivery(createMockDelivery(notifyCallbacks)) + + mockFetch.mockResolvedValue(createMockResponse({ + ok: false, + status: 404, + statusText: 'Not Found', + url: 'https://example.com/api/users', + headers: new Headers({ 'content-type': 'application/json' }), + text: async () => '{"error": "User not found"}' + })) + + await fetch('https://example.com/api/users?page=1&limit=10&password=secret&token=abc123') + + // Wait for async processing + await new Promise(resolve => setTimeout(resolve, 10)) + + expect(notifyCallbacks.length).toBe(1) + const event = notifyCallbacks[0].toJSON() + + expect(event.exceptions[0].errorClass).toBe('HTTPError') + expect(event.exceptions[0].errorMessage).toBe('404: https://example.com/api/users') + expect(event.exceptions[0].stacktrace).toEqual([]) // Stacktrace should be empty for HTTP errors + expect(event.context).toBe('GET example.com') + expect(event.request.params).toStrictEqual({ + page: '1', + limit: '10', + password: '[REDACTED]', + token: '[REDACTED]' + }) + }) + }) + describe('config.httpErrorCodes - single range', () => { it('should capture 4xx errors when configured with single range', async () => { const notifyCallbacks: Event[] = [] @@ -85,9 +124,10 @@ describe('plugin-network-instrumentation', () => { expect(event.exceptions[0].errorClass).toBe('HTTPError') expect(event.exceptions[0].errorMessage).toBe('404: https://example.com/api/users') + expect(event.exceptions[0].stacktrace).toEqual([]) // Stacktrace should be empty for HTTP errors expect(event.context).toBe('GET example.com') expect(event.severity).toBe('error') - expect(event.unhandled).toBe(true) + expect(event.unhandled).toBe(false) expect(event.severityReason.type).toBe('httpError') expect(event.request.url).toBe('https://example.com/api/users') expect(event.request.httpMethod).toBe('GET') @@ -312,7 +352,7 @@ describe('plugin-network-instrumentation', () => { const requestMetadata = event.request expect(requestMetadata.body).toBeUndefined() - expect(requestMetadata.bodyLength).toBeUndefined() + expect(requestMetadata.bodyLength).toBe(10000) }) }) @@ -554,7 +594,7 @@ describe('plugin-network-instrumentation', () => { expect(notifyCallbacks.length).toBe(1) const event = notifyCallbacks[0].toJSON() - expect(event.request.url).toBe('https://example.com/api/users?page=1&limit=10') + expect(event.request.url).toBe('https://example.com/api/users') expect(event.request.httpMethod).toBe('POST') expect(event.request.headers).toBeDefined() expect(event.request.headers?.['content-type']).toBe('application/json') diff --git a/packages/plugin-network-instrumentation/test/parse-query-string.test.ts b/packages/plugin-network-instrumentation/test/parse-query-string.test.ts new file mode 100644 index 0000000000..b5c61a928f --- /dev/null +++ b/packages/plugin-network-instrumentation/test/parse-query-string.test.ts @@ -0,0 +1,85 @@ +import parseQueryString from '../lib/parse-query-string' + +describe('parseQueryString', () => { + it('should parse a query string into an object', () => { + const queryString = 'token=abc123&user=john&active=true' + const result = parseQueryString(queryString) + + expect(result).toEqual({ + token: 'abc123', + user: 'john', + active: 'true' + }) + }) + + it('should handle empty query string', () => { + const result = parseQueryString('') + + expect(result).toEqual({}) + }) + + it('should handle null/undefined gracefully', () => { + // @ts-expect-error + expect(parseQueryString(null)).toEqual({}) + // @ts-expect-error + expect(parseQueryString(undefined)).toEqual({}) + }) + + it('should decode URI components', () => { + const queryString = 'email=test%40example.com&name=John%20Doe&path=%2Fhome%2Fuser' + const result = parseQueryString(queryString) + + expect(result).toEqual({ + email: 'test@example.com', + name: 'John Doe', + path: '/home/user' + }) + }) + + it('should handle empty parameter values', () => { + const queryString = 'flag&value=test&empty=' + const result = parseQueryString(queryString) + + expect(result).toEqual({ + flag: '', + value: 'test', + empty: '' + }) + }) + + it('should handle single parameter', () => { + const queryString = 'id=123' + const result = parseQueryString(queryString) + + expect(result).toEqual({ id: '123' }) + }) + + it('should skip empty pairs', () => { + const queryString = 'key=value&&&another=test' + const result = parseQueryString(queryString) + + expect(result).toEqual({ + key: 'value', + another: 'test' + }) + }) + + it('should preserve special characters in values', () => { + const queryString = 'data={"key":"value"}&text=hello%20world' + const result = parseQueryString(queryString) + + expect(result).toEqual({ + data: '{"key":"value"}', + text: 'hello world' + }) + }) + + it('should handle duplicate parameter names (last one wins)', () => { + const queryString = 'key=first&key=second&key=third' + const result = parseQueryString(queryString) + + expect(result).toEqual({ + key: 'third' + }) + }) +}) diff --git a/packages/plugin-network-instrumentation/test/parse-url.test.ts b/packages/plugin-network-instrumentation/test/parse-url.test.ts new file mode 100644 index 0000000000..65975cc54b --- /dev/null +++ b/packages/plugin-network-instrumentation/test/parse-url.test.ts @@ -0,0 +1,117 @@ +import parseUrl from '../lib/parse-url' + +describe('parseUrl', () => { + it('should extract domain, clean URL, and query string from absolute URL', () => { + const url = 'https://api.example.com/path?token=abc123&user=john' + const result = parseUrl(url) + + expect(result.domain).toBe('api.example.com') + expect(result.cleanUrl).toBe('https://api.example.com/path') + expect(result.queryString).toBe('token=abc123&user=john') + }) + + it('should handle URLs without query strings', () => { + const url = 'https://example.com/path' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.cleanUrl).toBe('https://example.com/path') + expect(result.queryString).toBe('') + }) + + it('should preserve hash fragments', () => { + const url = 'https://example.com/path?key=value#section' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.cleanUrl).toBe('https://example.com/path#section') + expect(result.queryString).toBe('key=value') + }) + + it('should handle hash without query string', () => { + const url = 'https://example.com/path#section' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.cleanUrl).toBe('https://example.com/path#section') + expect(result.queryString).toBe('') + }) + + it('should preserve encoded URI components in query string', () => { + const url = 'https://example.com?email=test%40example.com&name=John%20Doe' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.queryString).toBe('email=test%40example.com&name=John%20Doe') + }) + + it('should handle empty query parameter values', () => { + const url = 'https://example.com?flag&value=test' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.queryString).toBe('flag&value=test') + }) + + it('should handle multiple slashes in path', () => { + const url = 'https://example.com/api/v1/users/profile?id=123' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.cleanUrl).toBe('https://example.com/api/v1/users/profile') + expect(result.queryString).toBe('id=123') + }) + + it('should handle ports in domain', () => { + const url = 'https://example.com:8080/path?query=value' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com:8080') + expect(result.cleanUrl).toBe('https://example.com:8080/path') + expect(result.queryString).toBe('query=value') + }) + + it('should return unknown domain for relative URLs', () => { + const url = '/api/endpoint?param=value' + const result = parseUrl(url) + + expect(result.domain).toBe('unknown') + expect(result.cleanUrl).toBe('/api/endpoint') + expect(result.queryString).toBe('param=value') + }) + + it('should handle http protocol', () => { + const url = 'http://example.com/path?key=value' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.cleanUrl).toBe('http://example.com/path') + expect(result.queryString).toBe('key=value') + }) + + it('should be case-insensitive for protocol matching', () => { + const url = 'HTTPS://example.com/path?key=value' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.cleanUrl).toBe('HTTPS://example.com/path') + expect(result.queryString).toBe('key=value') + }) + + it('should handle special characters in query string', () => { + const url = 'https://example.com?data={"key":"value"}&text=hello%20world' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.queryString).toBe('data={"key":"value"}&text=hello%20world') + }) + + it('should handle malformed query strings', () => { + const url = 'https://example.com/path?key=value&&&another=test' + const result = parseUrl(url) + + expect(result.domain).toBe('example.com') + expect(result.cleanUrl).toBe('https://example.com/path') + expect(result.queryString).toBe('key=value&&&another=test') + }) +}) diff --git a/packages/plugin-network-instrumentation/test/redact-query-parameters.test.ts b/packages/plugin-network-instrumentation/test/redact-query-parameters.test.ts deleted file mode 100644 index 1e5889df21..0000000000 --- a/packages/plugin-network-instrumentation/test/redact-query-parameters.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import redactQueryParameters from '../lib/redact-query-parameters' - -describe('redact-query-parameters', () => { - it('redacts specified query parameters in a URL', () => { - const url = 'http://example.com/path?token=abc123&userId=42&status=active' - const redactedKeys = ['token', 'userId'] - const redactedUrl = redactQueryParameters(url, redactedKeys) - expect(redactedUrl).toBe('http://example.com/path?token=[REDACTED]&userId=[REDACTED]&status=active') - }) - - it('handles URLs with no query parameters', () => { - const url = 'http://example.com/path' - const redactedKeys = ['token'] - const redactedUrl = redactQueryParameters(url, redactedKeys) - expect(redactedUrl).toBe('http://example.com/path') - }) - - it('handles relative URLs', () => { - const url = '/path?token=abc123&userId=42' - const redactedKeys = ['token', 'userId'] - const redactedUrl = redactQueryParameters(url, redactedKeys) - expect(redactedUrl).toBe('/path?token=[REDACTED]&userId=[REDACTED]') - }) -}) diff --git a/packages/react-native/CONTRIBUTING.md b/packages/react-native/CONTRIBUTING.md index 30ce0823ca..42117060e4 100644 --- a/packages/react-native/CONTRIBUTING.md +++ b/packages/react-native/CONTRIBUTING.md @@ -37,46 +37,43 @@ To solve this problem we publish to a local npm clone, which proxies requests fo #### Prerequisites -The proxy of choice is [verdaccio](https://verdaccio.org/): +The proxy of choice is [verdaccio](https://verdaccio.org/). This is already included as a dev dependency in the bugsnag-js repository, along with a config file at `test/local-npm.config.yml`. -```sh -# install it globally on your system -npm i -g verdaccio +To start the verdaccio server, run the `local-npm:start` npm script from the repo root: -# starts the on the default port -verdaccio - -# log in to the registry -# (you can enter anything, just be sure to remember them when -# your session times out and you need to "sign in" again) -npm adduser --registry http://localhost:4873 ``` +npm run local-npm:start +``` + +This will start verdaccio running on port `5539`. You will need to keep this running for the following steps. -On the project you want to install the development notifier, create an a `.npmrc` file at the project root alongside `package.json`: +In the project where you want to install the development notifier, create an a `.npmrc` file at the project root alongside `package.json` and set the local registry URL: ``` -registry=http://localhost:4873 +registry=http://localhost:5539 ``` -Alternatively you can just supply the `--registry=http://localhost:4873` to each npm/yarn command you issue. +Alternatively you can just supply the `--registry=http://localhost:5539` to each npm/yarn command you issue. -#### Installing the development notifier on a React Native project +#### Installing the development notifier in a React Native project 1. Make changes. -2. Run the following command to publish to the local registry: +2. In a new terminal window, from the repo root, run the `local-npm:publish-all` npm script to publish to the local registry: ``` - npx lerna publish v99.99.99-canary.`git rev-parse HEAD` --no-push --exact --no-git-tag-version --registry http://localhost:4873/ + VERSION_IDENTIFIER=8.99.99 npm run local-npm:publish-all ``` - This should prompt you for each module that has changed since the last proper publish. + This will publish all of the packages in the repo to verdaccio with the specified version. + + Note: You'll need to ensure you publish using the same major version as is currently in the repository. This is because some packages declare a peer dependency on `@bugsnag/core`, and lerna does not update peer dependencies when versioning, so changing the major version will mean the packages fail to install (since the peer dependency cannot be resolved from the local registry). -4. Reset the changes that were made to `lerna.json` and `package-lock.json`s `git reset --hard HEAD` (we don't want to store these throwaway versions) +4. Reset the changes that were made to `package.json`, `lerna.json` and `package-lock.json` files with `git reset --hard HEAD` (we don't want to commit these throwaway versions) -On the project you want to install `@bugsnag/react-native` substitute the version's output from above: +In the project where you want to install `@bugsnag/react-native` substitute the version's output from above: ``` -yarn add @bugsnag/react-native@99.99.99-canary. +yarn add @bugsnag/react-native@8.99.99 # or -npm i @bugsnag/react-native@99.99.99-canary. +npm i @bugsnag/react-native@8.99.99 ``` diff --git a/packages/react-native/android/build.gradle b/packages/react-native/android/build.gradle index beb3d75f1a..fdf534e4a1 100644 --- a/packages/react-native/android/build.gradle +++ b/packages/react-native/android/build.gradle @@ -45,8 +45,8 @@ android { } dependencies { - api "com.bugsnag:bugsnag-android:6.20.0" - api "com.bugsnag:bugsnag-plugin-react-native:6.20.0" + api "com.bugsnag:bugsnag-android:6.25.0" + api "com.bugsnag:bugsnag-plugin-react-native:6.25.0" implementation 'com.facebook.react:react-native:+' testImplementation "junit:junit:4.12" diff --git a/packages/react-native/ios/BugsnagReactNative/BugsnagEventDeserializer.m b/packages/react-native/ios/BugsnagReactNative/BugsnagEventDeserializer.m index 38547b6f4e..93ce2e30bb 100644 --- a/packages/react-native/ios/BugsnagReactNative/BugsnagEventDeserializer.m +++ b/packages/react-native/ios/BugsnagReactNative/BugsnagEventDeserializer.m @@ -9,6 +9,7 @@ #import "BugsnagEventDeserializer.h" #import "BugsnagInternals.h" +#import @implementation BugsnagEventDeserializer @@ -25,10 +26,12 @@ - (BugsnagEvent *)deserializeEvent:(NSDictionary *)payload { user:[[BugsnagUser alloc] initWithDictionary:user] metadata:metadata breadcrumbs:[self deserializeBreadcrumbs:payload[@"breadcrumbs"]] - errors:@[[BugsnagError new]] + errors:[self deserializeErrors:payload[@"errors"]] threads:[self deserializeThreads:payload[@"threads"]] session:nil /* set by -[BugsnagClient notifyInternal:block:] */]; event.context = payload[@"context"]; + event.request = [self deserializeRequest:payload[@"request"]]; + event.response = [self deserializeResponse:payload[@"response"]]; event.groupingHash = payload[@"groupingHash"]; event.groupingDiscriminator = payload[@"groupingDiscriminator"]; @@ -49,13 +52,17 @@ - (BugsnagEvent *)deserializeEvent:(NSDictionary *)payload { } } - NSDictionary *error = payload[@"errors"][0]; + return event; +} - if (error != nil) { - event.errors[0].errorClass = error[@"errorClass"]; - event.errors[0].errorMessage = error[@"errorMessage"]; +- (NSArray *)deserializeErrors:(NSArray *)errors { + NSMutableArray *array = [NSMutableArray new]; + for (NSDictionary *error in errors) { + BugsnagError *errorObj = [BugsnagError new]; + errorObj.errorClass = error[@"errorClass"]; + errorObj.errorMessage = error[@"errorMessage"]; NSArray *stacktrace = error[@"stacktrace"]; - NSArray *nativeStack = payload[@"nativeStack"]; + NSArray *nativeStack = error[@"nativeStack"]; if (nativeStack) { NSMutableArray *mixedStack = [NSMutableArray array]; for (BugsnagStackframe *frame in [BugsnagStackframe stackframesWithCallStackSymbols:nativeStack]) { @@ -66,10 +73,13 @@ - (BugsnagEvent *)deserializeEvent:(NSDictionary *)payload { [mixedStack addObjectsFromArray:stacktrace]; stacktrace = mixedStack; } - [event attachCustomStacktrace:stacktrace withType:@"reactnativejs"]; + + errorObj.stacktrace = [BugsnagStacktrace stacktraceFromJson:stacktrace].trace; + errorObj.type = BSGErrorTypeReactNativeJs; + [array addObject:errorObj]; } - return event; + return array; } - (NSArray *)deserializeBreadcrumbs:(NSArray *)crumbs { @@ -124,4 +134,18 @@ - (BugsnagHandledState *)deserializeHandledState:(NSDictionary *)payload { return array; } +- (BugsnagHttpRequest *)deserializeRequest:(NSDictionary *)request { + if (request != nil) { + return [BugsnagHttpRequest requestFromJson:request]; + } + return nil; +} + +- (BugsnagHttpResponse *)deserializeResponse:(NSDictionary *)response { + if (response != nil) { + return [BugsnagHttpResponse responseFromJson:response]; + } + return nil; +} + @end diff --git a/packages/react-native/ios/vendor/bugsnag-cocoa b/packages/react-native/ios/vendor/bugsnag-cocoa index f4348c29cc..7f57276b28 160000 --- a/packages/react-native/ios/vendor/bugsnag-cocoa +++ b/packages/react-native/ios/vendor/bugsnag-cocoa @@ -1 +1 @@ -Subproject commit f4348c29cc881ce80e4e22a46f29d7f5a18e924b +Subproject commit 7f57276b282dde418e602a8cc02a6a1e22993aae diff --git a/packages/react-native/prepare-android-vendor.config b/packages/react-native/prepare-android-vendor.config index d8e74d5e8f..5d36d1f248 100644 --- a/packages/react-native/prepare-android-vendor.config +++ b/packages/react-native/prepare-android-vendor.config @@ -1,2 +1,2 @@ version -6.20.0 +6.25.0 diff --git a/packages/react-native/test/index.test.ts b/packages/react-native/test/index.test.ts index 857b848c08..cbef6133b3 100644 --- a/packages/react-native/test/index.test.ts +++ b/packages/react-native/test/index.test.ts @@ -125,4 +125,20 @@ describe('react native notifier', () => { expect(Bugsnag.isStarted()).toBe(true) }) }) + + it('calls addOnError callback for all errors', (done) => { + Bugsnag.start({}) + const addOnErrorCb = jest.fn((event) => { + event.addMetadata('addonError', { called: true }) + return true + }) + Bugsnag.addOnError(addOnErrorCb) + const error = new Error('addonError test') + Bugsnag.notify(error, undefined, (err, event) => { + if (err) return done(err) + expect(addOnErrorCb).toHaveBeenCalledWith(expect.any(Object)) + expect(event.getMetadata('addonError')).toEqual({ called: true }) + done() + }) + }) }) diff --git a/scripts/generate-react-native-fixture.js b/scripts/generate-react-native-fixture.js index c4b79085c6..a151a271df 100644 --- a/scripts/generate-react-native-fixture.js +++ b/scripts/generate-react-native-fixture.js @@ -40,7 +40,8 @@ const replacementFilesDir = resolve(ROOT_DIR, 'test/react-native/features/fixtur const INTERNAL_DEPENDENCIES = [ '@bugsnag/react-native', - '@bugsnag/request-tracker' + '@bugsnag/request-tracker', + '@bugsnag/plugin-network-instrumentation' ] // make sure we install a compatible versions of peer dependencies diff --git a/test/aws-lambda/Gemfile b/test/aws-lambda/Gemfile index 3be2e6f61c..ad600ed83d 100644 --- a/test/aws-lambda/Gemfile +++ b/test/aws-lambda/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'bugsnag-maze-runner', '~>10.0' +gem 'bugsnag-maze-runner', '~>11.0' # Use a branch of Maze Runner # gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'tms/use-maze-check' diff --git a/test/browser/features/http_errors.feature b/test/browser/features/http_errors.feature index fef4138ee3..075a42926b 100644 --- a/test/browser/features/http_errors.feature +++ b/test/browser/features/http_errors.feature @@ -10,13 +10,14 @@ Feature: HTTP Errors Then the error is a valid browser payload for the error reporting API And I define "expected.context" as "GET " - And I define "expected.exception.message" as "401: /reflect?status=401&userId=[REDACTED]" - And I define "expected.request.url" as "/reflect?status=401&userId=[REDACTED]" + And I define "expected.exception.message" as "401: /reflect" + And I define "expected.request.url" as "/reflect" And the exception "errorClass" equals "HTTPError" And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message" + And the error payload field "events.0.exceptions.0.stacktrace" is an array with 0 elements And the event "severity" equals "error" - And the event "unhandled" is true + And the event "unhandled" is false And the event "severityReason.type" equals "httpError" And the error payload field "events.0.context" equals the stored value "expected.context" @@ -40,13 +41,14 @@ Feature: HTTP Errors Then the error is a valid browser payload for the error reporting API And I define "expected.context" as "POST " - And I define "expected.exception.message" as "408: /reflect?status=408&userId=[REDACTED]" - And I define "expected.request.url" as "/reflect?status=408&userId=[REDACTED]" + And I define "expected.exception.message" as "408: /reflect" + And I define "expected.request.url" as "/reflect" And the exception "errorClass" equals "HTTPError" And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message" + And the error payload field "events.0.exceptions.0.stacktrace" is an array with 0 elements And the event "severity" equals "error" - And the event "unhandled" is true + And the event "unhandled" is false And the event "severityReason.type" equals "httpError" And the error payload field "events.0.context" equals the stored value "expected.context" @@ -72,13 +74,14 @@ Feature: HTTP Errors Then the error is a valid browser payload for the error reporting API And I define "expected.context" as "GET " - And I define "expected.exception.message" as "404: /reflect?status=404&userId=[REDACTED]" - And I define "expected.request.url" as "/reflect?status=404&userId=[REDACTED]" + And I define "expected.exception.message" as "404: /reflect" + And I define "expected.request.url" as "/reflect" And the exception "errorClass" equals "HTTPError" And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message" + And the error payload field "events.0.exceptions.0.stacktrace" is an array with 0 elements And the event "severity" equals "error" - And the event "unhandled" is true + And the event "unhandled" is false And the event "severityReason.type" equals "httpError" And the error payload field "events.0.context" equals the stored value "expected.context" @@ -103,13 +106,14 @@ Feature: HTTP Errors Then the error is a valid browser payload for the error reporting API And I define "expected.context" as "POST " - And I define "expected.exception.message" as "403: /reflect?status=403&userId=[REDACTED]" - And I define "expected.request.url" as "/reflect?status=403&userId=[REDACTED]" + And I define "expected.exception.message" as "403: /reflect" + And I define "expected.request.url" as "/reflect" And the exception "errorClass" equals "HTTPError" And the error payload field "events.0.exceptions.0.message" equals the stored value "expected.exception.message" + And the error payload field "events.0.exceptions.0.stacktrace" is an array with 0 elements And the event "severity" equals "error" - And the event "unhandled" is true + And the event "unhandled" is false And the event "severityReason.type" equals "httpError" And the error payload field "events.0.context" equals the stored value "expected.context" diff --git a/test/cloudflare-workers/Gemfile b/test/cloudflare-workers/Gemfile index 3be2e6f61c..ad600ed83d 100644 --- a/test/cloudflare-workers/Gemfile +++ b/test/cloudflare-workers/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'bugsnag-maze-runner', '~>10.0' +gem 'bugsnag-maze-runner', '~>11.0' # Use a branch of Maze Runner # gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'tms/use-maze-check' diff --git a/test/electron/local-npm-config.yml b/test/local-npm-config.yml similarity index 90% rename from test/electron/local-npm-config.yml rename to test/local-npm-config.yml index 4ae36c6d2b..fb30d19cb4 100644 --- a/test/electron/local-npm-config.yml +++ b/test/local-npm-config.yml @@ -13,6 +13,9 @@ packages: '@bugsnag/cuid': access: $anonymous proxy: npmjs + '@bugsnag/cli': + access: $anonymous + proxy: npmjs 'bugsnag-expo-cli': access: $anonymous publish: $anonymous diff --git a/test/node/Gemfile b/test/node/Gemfile index a911459674..8a54de6914 100644 --- a/test/node/Gemfile +++ b/test/node/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'bugsnag-maze-runner', '~>10.0' +gem 'bugsnag-maze-runner', '~>11.0' # Use a branch of Maze Runner #gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'tms/use-maze-check' diff --git a/test/node/features/steps/server_fixture_request_steps.rb b/test/node/features/steps/server_fixture_request_steps.rb index 28adb78fe2..657576014e 100644 --- a/test/node/features/steps/server_fixture_request_steps.rb +++ b/test/node/features/steps/server_fixture_request_steps.rb @@ -5,7 +5,9 @@ # @step_input reqbody [String] urlencoded data to send. # @step_input url [String] The URL to post data to. When("I POST the data {string} to the URL {string}") do |reqbody, url| - Net::HTTP.post(URI(url), reqbody) + Net::HTTP.post(URI(url), + reqbody, + 'Content-Type' => 'application/x-www-form-urlencoded') end When('I open the URL {string} tolerating any error') do |url| diff --git a/test/react-native-cli/Gemfile b/test/react-native-cli/Gemfile index 44c25e95eb..76244fdbe4 100644 --- a/test/react-native-cli/Gemfile +++ b/test/react-native-cli/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' gem 'cocoapods' gem 'xcodeproj', '< 1.26.0' -gem 'bugsnag-maze-runner', '~>10.0' +gem 'bugsnag-maze-runner', '~>11.0' # Use a branch of Maze Runner #gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'tms/use-maze-check' diff --git a/test/react-native/Gemfile b/test/react-native/Gemfile index 2c9de72294..c0d9580be5 100644 --- a/test/react-native/Gemfile +++ b/test/react-native/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' gem 'cocoapods' gem 'xcodeproj', '< 1.26.0' -gem 'bugsnag-maze-runner', '~>10.0' +gem 'bugsnag-maze-runner', '~>11.0' # Use a branch of Maze Runner #gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'tms/use-maze-check' diff --git a/test/react-native/features/breadcrumbs.feature b/test/react-native/features/breadcrumbs.feature index 7274fd6a9c..13388a2cc5 100644 --- a/test/react-native/features/breadcrumbs.feature +++ b/test/react-native/features/breadcrumbs.feature @@ -47,3 +47,10 @@ Scenario: Manual breadcrumbs (Native) | ios | NSException | And the exception "message" equals "BreadcrumbsNativeManualScenario" And the event contains a breadcrumb matching the JSON fixture in "features/fixtures/expected_breadcrumbs/NativeManualScenario.json" + +Scenario: Network breadcrumbs (JS) + When I run "NetworkBreadcrumbsJsScenario" + Then I wait to receive an error + And the exception "errorClass" equals "Error" + And the exception "message" equals "NetworkBreadcrumbsJsScenario" + And the event contains a breadcrumb matching the JSON fixture in "features/fixtures/expected_breadcrumbs/NetworkJsScenario.json" \ No newline at end of file diff --git a/test/react-native/features/fixtures/expected_breadcrumbs/NetworkJsScenario.json b/test/react-native/features/fixtures/expected_breadcrumbs/NetworkJsScenario.json new file mode 100644 index 0000000000..79b9c3026e --- /dev/null +++ b/test/react-native/features/fixtures/expected_breadcrumbs/NetworkJsScenario.json @@ -0,0 +1,11 @@ +{ + "type": "request", + "name": "XMLHttpRequest succeeded", + "timestamp": "^\\d{4}\\-\\d{2}\\-\\d{2}T\\d{2}:\\d{2}:[\\d\\.]+Z?$", + "metaData": { + "status": "NUMBER", + "method": "GET", + "url": "^http:\/\/[0-9.:]*\/reflect$", + "duration": "NUMBER" + } +} \ No newline at end of file diff --git a/test/react-native/features/fixtures/expected_http_errors/401/exceptions.json b/test/react-native/features/fixtures/expected_http_errors/401/exceptions.json new file mode 100644 index 0000000000..e0a16a6cef --- /dev/null +++ b/test/react-native/features/fixtures/expected_http_errors/401/exceptions.json @@ -0,0 +1,8 @@ +[ + { + "message": "^401: http(s)?:\/\/.*\/reflect$", + "errorClass": "HTTPError", + "type": "reactnativejs", + "stacktrace": "IGNORE" + } +] diff --git a/test/react-native/features/fixtures/expected_http_errors/401/request.json b/test/react-native/features/fixtures/expected_http_errors/401/request.json new file mode 100644 index 0000000000..0b0c889c72 --- /dev/null +++ b/test/react-native/features/fixtures/expected_http_errors/401/request.json @@ -0,0 +1,8 @@ +{ + "url": "^http(s)?:\/\/.*\/reflect$", + "headers": {}, + "params": { + "status": "401" + }, + "httpMethod": "GET" +} diff --git a/test/react-native/features/fixtures/expected_http_errors/401/response.json b/test/react-native/features/fixtures/expected_http_errors/401/response.json new file mode 100644 index 0000000000..6cae68527b --- /dev/null +++ b/test/react-native/features/fixtures/expected_http_errors/401/response.json @@ -0,0 +1,6 @@ +{ + "body": "[Binary Data]", + "statusCode": 401, + "bodyLength": 13, + "headers": "IGNORE" +} diff --git a/test/react-native/features/fixtures/expected_http_errors/500/exceptions.json b/test/react-native/features/fixtures/expected_http_errors/500/exceptions.json new file mode 100644 index 0000000000..35c000eeac --- /dev/null +++ b/test/react-native/features/fixtures/expected_http_errors/500/exceptions.json @@ -0,0 +1,8 @@ +[ + { + "message": "^500: http(s)?:\/\/.*\/reflect$", + "errorClass": "HTTPError", + "type": "reactnativejs", + "stacktrace": "IGNORE" + } +] diff --git a/test/react-native/features/fixtures/expected_http_errors/500/request.json b/test/react-native/features/fixtures/expected_http_errors/500/request.json new file mode 100644 index 0000000000..9f7a3f5e65 --- /dev/null +++ b/test/react-native/features/fixtures/expected_http_errors/500/request.json @@ -0,0 +1,8 @@ +{ + "url": "^http(s)?:\/\/.*\/reflect$", + "headers": {}, + "params": { + "status": "500" + }, + "httpMethod": "GET" +} diff --git a/test/react-native/features/fixtures/expected_http_errors/500/response.json b/test/react-native/features/fixtures/expected_http_errors/500/response.json new file mode 100644 index 0000000000..3b7aa2d949 --- /dev/null +++ b/test/react-native/features/fixtures/expected_http_errors/500/response.json @@ -0,0 +1,6 @@ +{ + "body": "[Binary Data]", + "statusCode": 500, + "bodyLength": 13, + "headers": "IGNORE" +} \ No newline at end of file diff --git a/test/react-native/features/fixtures/scenario-launcher/scenarios/core/AddOnErrorCallbackScenario.js b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/AddOnErrorCallbackScenario.js new file mode 100644 index 0000000000..5710d5e26a --- /dev/null +++ b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/AddOnErrorCallbackScenario.js @@ -0,0 +1,13 @@ + +import Scenario from './Scenario' +import Bugsnag from '@bugsnag/react-native' + +export class AddOnErrorCallbackScenario extends Scenario { + run () { + Bugsnag.addOnError(event => { + event.addMetadata('addonError', { scenario: true }) + return true + }) + Bugsnag.notify(new Error('addonError scenario test')) + } +} diff --git a/test/react-native/features/fixtures/scenario-launcher/scenarios/core/NetworkBreadcrumbsJsScenario.js b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/NetworkBreadcrumbsJsScenario.js new file mode 100644 index 0000000000..a781356bc7 --- /dev/null +++ b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/NetworkBreadcrumbsJsScenario.js @@ -0,0 +1,15 @@ +import Scenario from './Scenario' +import Bugsnag from '@bugsnag/react-native' + +export class NetworkBreadcrumbsJsScenario extends Scenario { + constructor (nativeConfig, jsConfig, scenarioData) { + super() + this.reflectEndpoint = nativeConfig.endpoints.notify.replace('/notify', '/reflect') + } + + run () { + fetch(this.reflectEndpoint).then(() => { + Bugsnag.notify(new Error('NetworkBreadcrumbsJsScenario')) + }) + } +} diff --git a/test/react-native/features/fixtures/scenario-launcher/scenarios/core/NetworkRequestScenario.js b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/NetworkRequestScenario.js new file mode 100644 index 0000000000..2411f8ab0a --- /dev/null +++ b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/NetworkRequestScenario.js @@ -0,0 +1,25 @@ +import Scenario from './Scenario' +import Bugsnag from '@bugsnag/react-native' +import BugsnagPluginNetworkInstrumentation from '@bugsnag/plugin-network-instrumentation' + +export class NetworkRequestScenario extends Scenario { + constructor (nativeConfig, jsConfig, scenarioData) { + super() + this.reflectEndpoint = nativeConfig.endpoints.notify.replace('/notify', '/reflect') + this.statusCode = scenarioData + + const plugin = BugsnagPluginNetworkInstrumentation({ + maxRequestSize: 1024, + maxResponseSize: 1024 + }) + + jsConfig.plugins = jsConfig.plugins || [] + jsConfig.plugins.push(plugin) + } + + run () { + fetch(`${this.reflectEndpoint}?status=${this.statusCode}`).catch((err) => { + Bugsnag.notify(err) + }) + } +} diff --git a/test/react-native/features/fixtures/scenario-launcher/scenarios/core/OnErrorCallbackScenario.js b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/OnErrorCallbackScenario.js new file mode 100644 index 0000000000..db60113d05 --- /dev/null +++ b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/OnErrorCallbackScenario.js @@ -0,0 +1,10 @@ +import Scenario from './Scenario' +import Bugsnag from '@bugsnag/react-native' + +export class OnErrorCallbackScenario extends Scenario { + run () { + Bugsnag.notify(new Error('addonError scenario test'),event=>{ + event.addMetadata('onError', { scenario: true }) + }) + } +} diff --git a/test/react-native/features/fixtures/scenario-launcher/scenarios/core/index.js b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/index.js index 988e1529a8..31aedacfad 100644 --- a/test/react-native/features/fixtures/scenario-launcher/scenarios/core/index.js +++ b/test/react-native/features/fixtures/scenario-launcher/scenarios/core/index.js @@ -35,6 +35,7 @@ export { BreadcrumbsAutomaticErrorScenario } from './BreadcrumbsAutomaticErrorSc export { BreadcrumbsJsManualScenario } from './BreadcrumbsJsManualScenario' export { BreadcrumbsNativeManualScenario } from './BreadcrumbsNativeManualScenario' export { BreadcrumbsNullEnabledBreadcrumbTypesScenario } from './BreadcrumbsNullEnabledBreadcrumbTypesScenario' +export { NetworkBreadcrumbsJsScenario } from './NetworkBreadcrumbsJsScenario' // device.feature export { DeviceJsHandledScenario } from './DeviceJsHandledScenario' @@ -80,3 +81,10 @@ export { ReactNativeErrorBoundaryScenario } from './ReactNativeErrorBoundaryScen // grouping-discriminator.feature export { GroupingDiscriminatorScenario } from './GroupingDiscriminatorScenario' export { GroupingDiscriminatorNativeScenario } from './GroupingDiscriminatorNativeScenario' + +// http_errors.feature +export { NetworkRequestScenario } from './NetworkRequestScenario' + +export { AddOnErrorCallbackScenario } from './AddOnErrorCallbackScenario' + +export { OnErrorCallbackScenario } from './OnErrorCallbackScenario' \ No newline at end of file diff --git a/test/react-native/features/http_errors.feature b/test/react-native/features/http_errors.feature new file mode 100644 index 0000000000..a87388a67c --- /dev/null +++ b/test/react-native/features/http_errors.feature @@ -0,0 +1,21 @@ +Feature: HTTP Errors + +Scenario Outline: Error is reported for network requests with error status code + When I run "NetworkRequestScenario" with data "" + And I wait to receive an error + Then the error payload field "events.0.context" matches the regex "^GET [0-9.]*:[0-9]{4}$" + And the error payload field "events.0.exceptions" matches the JSON fixture in "features/fixtures/expected_http_errors//exceptions.json" + And the error payload field "events.0.request" matches the JSON fixture in "features/fixtures/expected_http_errors//request.json" + And the error payload field "events.0.response" matches the JSON fixture in "features/fixtures/expected_http_errors//response.json" + Examples: + | status_code | + | 401 | + | 500 | + +Scenario Outline: Error is not reported for successful network requests + When I run "NetworkRequestScenario" with data "" + Then I should receive no errors + Examples: + | status_code | + | 200 | + | 307 | \ No newline at end of file diff --git a/test/react-native/features/native-stack.feature b/test/react-native/features/native-stack-android.feature similarity index 51% rename from test/react-native/features/native-stack.feature rename to test/react-native/features/native-stack-android.feature index f2e6651b1e..8d5cb09177 100644 --- a/test/react-native/features/native-stack.feature +++ b/test/react-native/features/native-stack-android.feature @@ -1,7 +1,8 @@ +@android_only Feature: Native stacktrace is parsed for promise rejections # Skipped on New Arch below 0.74 - see PLAT-12193 -@android_only @skip_new_arch_below_074 +@skip_new_arch_below_074 Scenario: Handled native promise rejection with native stacktrace When I run "NativePromiseRejectionHandledScenario" Then I wait to receive an error @@ -11,7 +12,6 @@ Scenario: Handled native promise rejection with native stacktrace # On 0.75+ the Error name is set to the native exception class And the event "exceptions.0.errorClass" equals the version-dependent string: | arch | version | value | - | new | 0.72 | Error | | new | 0.74 | Error | | new | default | java.lang.RuntimeException | | old | 0.68 | Error | @@ -41,14 +41,14 @@ Scenario: Handled native promise rejection with native stacktrace | runScenario | # the javascript part follows - # On 0.74+ New Arch there is no JS stacktrace - see PLAT-12193 + # On New Arch there is no JS stacktrace - see PLAT-12193 And the stacktrace contains "file" equal to the version-dependent string: | arch | version | value | | new | default | @skip | | old | default | index.android.bundle | # Skipped on New Arch below 0.74 - see PLAT-12193 -@android_only @skip_new_arch_below_074 +@skip_new_arch_below_074 Scenario: Unhandled native promise rejection with native stacktrace When I run "NativePromiseRejectionUnhandledScenario" Then I wait to receive an error @@ -57,7 +57,6 @@ Scenario: Unhandled native promise rejection with native stacktrace # On 0.75+ the Error name is set to the native exception class And the event "exceptions.0.errorClass" equals the version-dependent string: | arch | version | value | - | new | 0.72 | Error | | new | 0.74 | Error | | new | default | java.lang.RuntimeException | | old | 0.68 | Error | @@ -94,7 +93,7 @@ Scenario: Unhandled native promise rejection with native stacktrace | runScenario | # the javascript part follows - # On 0.74+ New Arch there is no JS stacktrace - see PLAT-12193 + # On New Arch there is no JS stacktrace - see PLAT-12193 And the stacktrace contains "file" equal to the version-dependent string: | arch | version | value | | new | default | @skip | @@ -105,93 +104,47 @@ Scenario: Unhandled native promise rejection with native stacktrace # And the error payload field "events.0.exceptions.1.stacktrace.1.lineNumber" equals 1 # And the error payload field "events.0.exceptions.1.stacktrace.2.lineNumber" equals 2 -# Skipped on New Arch below 0.74 - see PLAT-12193 -@ios_only @skip_new_arch_below_074 -Scenario: Handled native promise rejection with native stacktrace - When I run "NativePromiseRejectionHandledScenario" +@skip_old_arch @skip_new_arch_below_074 +Scenario: Unhandled exception in synchronous turbo module method with native stacktrace + When I run "UnhandledNativeErrorSyncScenario" and relaunch the crashed app + And I configure Bugsnag for "UnhandledNativeErrorSyncScenario" Then I wait to receive an error - And the event "unhandled" is false - And the error payload field "events.0.exceptions" is an array with 1 elements + And the event "unhandled" is true + + # First exception is the JS Error with JS stacktrace And the event "exceptions.0.errorClass" equals "Error" - And the event "exceptions.0.message" equals "NativePromiseRejectionHandledScenario" + And the event "exceptions.0.message" equals "Exception in HostFunction: UnhandledNativeErrorScenario" And the event "exceptions.0.type" equals "reactnativejs" - And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array - - # the native part of the stack comes first - And the error payload field "events.0.exceptions.0.stacktrace.0.frameAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.machoFile" equals "reactnative" - And the error payload field "events.0.exceptions.0.stacktrace.0.machoLoadAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.machoUUID" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.machoVMAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.method" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.symbolAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.type" equals "cocoa" - - # the javascript part follows - # On 0.74+ New Arch there is no JS stacktrace - see PLAT-12193 - # We're check the method: asyncGeneratorStep - And the event "exceptions.0.stacktrace.21.columnNumber" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @not_null | - | new | default | @skip | - | old | default | @not_null | - And the event "exceptions.0.stacktrace.21.file" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @not_null | - | new | default | @skip | - | old | default | @not_null | - And the event "exceptions.0.stacktrace.21.lineNumber" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @not_null | - | new | default | @skip | - | old | default | @not_null | - And the event "exceptions.0.stacktrace.21.type" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @null | - | new | default | @skip | - | old | default | @null | - -# Skipped on New Arch below 0.74 - see PLAT-12193 -@ios_only @skip_new_arch_below_074 -Scenario: Unhandled native promise rejection with native stacktrace - When I run "NativePromiseRejectionUnhandledScenario" + And the event "exceptions.0.stacktrace.0.method" equals "runScenarioSync" + And the event "exceptions.0.stacktrace.0.file" equals "(native)" + And the event "exceptions.0.stacktrace.1.method" equals "run" + And the event "exceptions.0.stacktrace.1.file" equals "index.android.bundle" + + # Second exception (cause) is the native exception with native stacktrace + And the event "exceptions.1.errorClass" equals "java.lang.RuntimeException" + And the event "exceptions.1.message" equals "UnhandledNativeErrorScenario" + And the event "exceptions.1.type" equals "reactnativejs" + And the event "exceptions.1.stacktrace.0.method" equals "com.reactnative.scenarios.Scenario.generateException" + And the event "exceptions.1.stacktrace.0.file" equals "Scenario.kt" + And the event "exceptions.1.stacktrace.0.type" equals "android" + +@skip_old_arch @skip_new_arch_below_074 +Scenario: Unhandled exception in asynchronous turbo module method with native stacktrace + When I run "UnhandledNativeErrorScenario" and relaunch the crashed app + And I configure Bugsnag for "UnhandledNativeErrorScenario" Then I wait to receive an error And the event "unhandled" is true + + # First exception is the JS Error, however there is no JS stacktrace And the event "exceptions.0.errorClass" equals "Error" - And the event "exceptions.0.message" equals "NativePromiseRejectionUnhandledScenario" + And the event "exceptions.0.message" equals "Exception in HostFunction: UnhandledNativeErrorScenario" And the event "exceptions.0.type" equals "reactnativejs" - And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array - - # the native part of the stack comes first - And the error payload field "events.0.exceptions.0.stacktrace.0.frameAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.machoFile" equals "reactnative" - And the error payload field "events.0.exceptions.0.stacktrace.0.machoLoadAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.machoUUID" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.machoVMAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.method" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.symbolAddress" is not null - And the error payload field "events.0.exceptions.0.stacktrace.0.type" equals "cocoa" - - # the javascript part follows - # On 0.74+ New Arch there is no JS stacktrace - see PLAT-12193 - # We're check the method: asyncGeneratorStep - And the event "exceptions.0.stacktrace.21.columnNumber" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @not_null | - | new | default | @skip | - | old | default | @not_null | - And the event "exceptions.0.stacktrace.21.file" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @not_null | - | new | default | @skip | - | old | default | @not_null | - And the event "exceptions.0.stacktrace.21.lineNumber" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @not_null | - | new | default | @skip | - | old | default | @not_null | - And the event "exceptions.0.stacktrace.21.type" equals the version-dependent string: - | arch | version | value | - | new | 0.72 | @null | - | new | default | @skip | - | old | default | @null | + And the error payload field "events.0.exceptions.0.stacktrace" is an array with 0 elements + + # Second exception (cause) is the native exception with native stacktrace + And the event "exceptions.1.errorClass" equals "java.lang.RuntimeException" + And the event "exceptions.1.message" equals "UnhandledNativeErrorScenario" + And the event "exceptions.1.type" equals "reactnativejs" + And the event "exceptions.1.stacktrace.0.method" equals "com.reactnative.scenarios.Scenario.generateException" + And the event "exceptions.1.stacktrace.0.file" equals "Scenario.kt" + And the event "exceptions.1.stacktrace.0.type" equals "android" diff --git a/test/react-native/features/native-stack-ios.feature b/test/react-native/features/native-stack-ios.feature new file mode 100644 index 0000000000..186e640f02 --- /dev/null +++ b/test/react-native/features/native-stack-ios.feature @@ -0,0 +1,120 @@ +@ios_only +Feature: Native stacktrace is parsed for promise rejections + +# Skipped on New Arch below 0.74 - see PLAT-12193 +@skip_new_arch_below_074 +Scenario: Handled native promise rejection with native stacktrace + When I run "NativePromiseRejectionHandledScenario" + Then I wait to receive an error + And the event "unhandled" is false + And the error payload field "events.0.exceptions" is an array with 1 elements + And the event "exceptions.0.errorClass" equals "Error" + And the event "exceptions.0.message" equals "NativePromiseRejectionHandledScenario" + And the event "exceptions.0.type" equals "reactnativejs" + And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array + + # the native part of the stack comes first + And the error payload field "events.0.exceptions.0.stacktrace.0.frameAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.machoFile" equals one of: + | reactnative | + | React | + And the error payload field "events.0.exceptions.0.stacktrace.0.machoLoadAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.machoUUID" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.machoVMAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.method" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.symbolAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.type" equals "cocoa" + + # the javascript part follows + # On New Arch there is no JS stacktrace - see PLAT-12193 + # We're check the method: asyncGeneratorStep + And the event "exceptions.0.stacktrace.21.columnNumber" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @not_null | + And the event "exceptions.0.stacktrace.21.file" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @not_null | + And the event "exceptions.0.stacktrace.21.lineNumber" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @not_null | + And the event "exceptions.0.stacktrace.21.type" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @null | + +# Skipped on New Arch below 0.74 - see PLAT-12193 +@skip_new_arch_below_074 +Scenario: Unhandled native promise rejection with native stacktrace + When I run "NativePromiseRejectionUnhandledScenario" + Then I wait to receive an error + And the event "unhandled" is true + And the event "exceptions.0.errorClass" equals "Error" + And the event "exceptions.0.message" equals "NativePromiseRejectionUnhandledScenario" + And the event "exceptions.0.type" equals "reactnativejs" + And the error payload field "events.0.exceptions.0.stacktrace" is a non-empty array + + # the native part of the stack comes first + And the error payload field "events.0.exceptions.0.stacktrace.0.frameAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.machoFile" equals one of: + | reactnative | + | React | + And the error payload field "events.0.exceptions.0.stacktrace.0.machoLoadAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.machoUUID" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.machoVMAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.method" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.symbolAddress" is not null + And the error payload field "events.0.exceptions.0.stacktrace.0.type" equals "cocoa" + + # the javascript part follows + # On New Arch there is no JS stacktrace - see PLAT-12193 + # We're check the method: asyncGeneratorStep + And the event "exceptions.0.stacktrace.21.columnNumber" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @not_null | + And the event "exceptions.0.stacktrace.21.file" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @not_null | + And the event "exceptions.0.stacktrace.21.lineNumber" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @not_null | + And the event "exceptions.0.stacktrace.21.type" equals the version-dependent string: + | arch | version | value | + | new | default | @skip | + | old | default | @null | + +@skip_old_arch @skip_new_arch_below_074 +Scenario: Unhandled synchronous turbo module exception with native stacktrace + When I run "UnhandledNativeErrorSyncScenario" and relaunch the crashed app + And I configure Bugsnag for "UnhandledNativeErrorSyncScenario" + Then I wait to receive an error + And the event "unhandled" is true + + # First exception is the JS Error with JS stacktrace + And the event "exceptions.0.errorClass" equals "Error" + And the event "exceptions.0.message" equals the version-dependent string: + | arch | version | value | + | new | 0.74 | Exception in HostFunction: UnhandledNativeErrorScenario | + | new | default | BugsnagTestInterface.runScenarioSync raised an exception: UnhandledNativeErrorScenario | + + And the event "exceptions.0.type" equals "reactnativejs" + And the event "exceptions.0.stacktrace.0.method" equals "runScenarioSync" + And the event "exceptions.0.stacktrace.0.file" equals "(native)" + And the event "exceptions.0.stacktrace.1.method" equals "run" + And the error payload field "events.0.exceptions.0.stacktrace.1.file" matches the regex "main\.jsbundle$" + + # Second exception (cause) is the native exception with native stacktrace + And the event "exceptions.1.errorClass" equals "NSException" + And the event "exceptions.1.message" equals "UnhandledNativeErrorScenario" + And the event "exceptions.1.type" equals "reactnativejs" + And each element in error payload field "events.0.exceptions.1.stacktrace" has "frameAddress" + And each element in error payload field "events.0.exceptions.1.stacktrace" has "machoFile" + And each element in error payload field "events.0.exceptions.1.stacktrace" has "machoLoadAddress" + And each element in error payload field "events.0.exceptions.1.stacktrace" has "machoUUID" + And each element in error payload field "events.0.exceptions.1.stacktrace" has "machoVMAddress" + And each element in error payload field "events.0.exceptions.1.stacktrace" has "symbolAddress" diff --git a/test/react-native/features/onerror_callback.feature b/test/react-native/features/onerror_callback.feature new file mode 100644 index 0000000000..f0d02698cc --- /dev/null +++ b/test/react-native/features/onerror_callback.feature @@ -0,0 +1,11 @@ +Feature: React Native addOnError & onError callbacks + +Scenario: Event is modified by addOnError callback + When I run "AddOnErrorCallbackScenario" + Then I wait to receive an error + And the event "metaData.addonError.scenario" is true + + Scenario: Event is modified by onError callback + When I run "OnErrorCallbackScenario" + Then I wait to receive an error + And the event "metaData.onError.scenario" is true \ No newline at end of file