From 2f3898bc2ae958203bdcf64fef63b4cdbe7e511a Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Mon, 2 Dec 2024 17:52:53 +0000 Subject: [PATCH 1/8] Add workflow to sign release assets --- .github/workflows/signing.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/signing.yml diff --git a/.github/workflows/signing.yml b/.github/workflows/signing.yml new file mode 100644 index 000000000..199601bdf --- /dev/null +++ b/.github/workflows/signing.yml @@ -0,0 +1,30 @@ +name: Sign release assets + +on: + release: + types: [released] + workflow_dispatch: + inputs: + tag: + description: 'Tag to sign' + required: true + type: string +jobs: + sign-assets: + runs-on: ubuntu-latest + steps: + - name: Install gpg + run: | + sudo apt-get update + sudo apt-get install -y gnupg + - name: Import GPG key + run: | + echo "${{ secrets.PLATFORMS_GPG_KEY_BASE64 }}" | base64 --decode | gpg --batch --import + - name: Sign assets + uses: bugsnag/platforms-release-signer@main + with: + github_token: ${{ secrets.PLATFORMS_SIGNING_GITHUB_TOKEN }} + full_repository: ${{ github.repository }} + release_tag: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.event.release.tag_name }} + key_id: ${{ secrets.PLATFORMS_GPG_KEY_ID }} + key_passphrase: ${{ secrets.PLATFORMS_GPG_KEY_PASSPHRASE }} From 3d29865f587872aea8d2ed58823d92398d99d15f Mon Sep 17 00:00:00 2001 From: Steve Kirkland Date: Mon, 2 Dec 2024 18:37:03 +0000 Subject: [PATCH 2/8] Buildkite pipeline tidy-up [full ci] --- .buildkite/block.full.yml | 3 +++ .buildkite/browser-pipeline.full.yml | 6 ++++++ .buildkite/browser-pipeline.yml | 5 ++++- .buildkite/pipeline.full.yml | 6 ++++++ .buildkite/pipeline.yml | 10 +++++++++- .../react-native-navigation-pipeline.full.yml | 20 +++++++++---------- .../react-native-navigation-pipeline.yml | 18 +++++++++-------- .buildkite/react-native-pipeline.full.yml | 19 ++++++++++-------- .buildkite/react-native-pipeline.yml | 11 ++++++---- 9 files changed, 66 insertions(+), 32 deletions(-) diff --git a/.buildkite/block.full.yml b/.buildkite/block.full.yml index 443182410..ca0e9fa32 100644 --- a/.buildkite/block.full.yml +++ b/.buildkite/block.full.yml @@ -4,4 +4,7 @@ steps: - label: 'Upload the full test pipeline' depends_on: 'trigger-full-build' + agents: + queue: macos + timeout_in_minutes: 2 command: buildkite-agent pipeline upload .buildkite/pipeline.full.yml diff --git a/.buildkite/browser-pipeline.full.yml b/.buildkite/browser-pipeline.full.yml index 5af8affe4..d9dc879b1 100644 --- a/.buildkite/browser-pipeline.full.yml +++ b/.buildkite/browser-pipeline.full.yml @@ -1,3 +1,6 @@ +agents: + queue: "opensource" + steps: - label: ":docker: Build BrowserStack Maze Runner image" key: "browser-maze-runner-bs" @@ -43,5 +46,8 @@ steps: concurrency_group: "browserstack" - label: ":pipeline_upload: Basic browser pipeline with CDN build" + agents: + queue: "macos" + timeout_in_minutes: 2 commands: - USE_CDN_BUILD=1 EXTRA_STEP_LABEL=" (CDN)" buildkite-agent pipeline upload .buildkite/browser-pipeline.yml diff --git a/.buildkite/browser-pipeline.yml b/.buildkite/browser-pipeline.yml index a5f33f6f9..ab0b6a907 100644 --- a/.buildkite/browser-pipeline.yml +++ b/.buildkite/browser-pipeline.yml @@ -1,3 +1,6 @@ +agents: + queue: "opensource" + steps: - group: "Browser Tests" steps: @@ -126,4 +129,4 @@ steps: matrix: setup: browser: [safari] - version: [11] \ No newline at end of file + version: [11] diff --git a/.buildkite/pipeline.full.yml b/.buildkite/pipeline.full.yml index f8490a5b4..39ff8f42f 100644 --- a/.buildkite/pipeline.full.yml +++ b/.buildkite/pipeline.full.yml @@ -1,16 +1,22 @@ +agents: + queue: macos + steps: # # Upload all full pipelines # - label: ":pipeline_upload: Full browser pipeline" + timeout_in_minutes: 2 commands: - buildkite-agent pipeline upload .buildkite/browser-pipeline.full.yml - label: ":pipeline_upload: Full react native pipeline" + timeout_in_minutes: 2 commands: - buildkite-agent pipeline upload .buildkite/react-native-pipeline.full.yml - label: ":pipeline_upload: Full react native navigation pipeline" + timeout_in_minutes: 2 commands: - buildkite-agent pipeline upload .buildkite/react-native-navigation-pipeline.full.yml diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 7fae1479e..5497623bb 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,9 +1,14 @@ +agents: + queue: macos + steps: # # License audit # - label: ":copyright: License Audit" + agents: + queue: opensource timeout_in_minutes: 20 plugins: - docker-compose#v4.12.0: @@ -13,14 +18,17 @@ steps: # Upload each basic pipeline # - label: ":pipeline_upload: Basic browser pipeline" + timeout_in_minutes: 2 commands: - buildkite-agent pipeline upload .buildkite/browser-pipeline.yml - label: ":pipeline_upload: React Native pipeline" + timeout_in_minutes: 2 commands: - buildkite-agent pipeline upload .buildkite/react-native-pipeline.yml - label: ":pipeline_upload: React Native Navigation pipeline" + timeout_in_minutes: 2 commands: - buildkite-agent pipeline upload .buildkite/react-native-navigation-pipeline.yml @@ -28,5 +36,5 @@ steps: # Conditionally trigger full pipeline # - label: 'Conditionally trigger full set of tests' - timeout_in_minutes: 30 + timeout_in_minutes: 2 command: sh -c .buildkite/pipeline_trigger.sh diff --git a/.buildkite/react-native-navigation-pipeline.full.yml b/.buildkite/react-native-navigation-pipeline.full.yml index 867d3693b..0fd1d621c 100644 --- a/.buildkite/react-native-navigation-pipeline.full.yml +++ b/.buildkite/react-native-navigation-pipeline.full.yml @@ -1,5 +1,7 @@ # This pipeline file builds runs the end-to-end tests for # React Native Navigation on BitBar. +agents: + queue: opensource steps: - group: "React Native Navigation Tests" @@ -9,7 +11,7 @@ steps: # ------------------------------ - label: ":building_construction: :android: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (Old Arch)" key: "build-react-native-navigation-android-fixture-old-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: macos-12-arm env: @@ -35,7 +37,7 @@ steps: - label: ":building_construction: :android: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (New Arch)" key: "build-react-native-navigation-android-fixture-new-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: macos-12-arm env: @@ -64,7 +66,7 @@ steps: # ------------------------------ - label: ':building_construction: :mac: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (Old Arch)' key: "build-react-native-navigation-ios-fixture-old-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: "macos-12-arm" env: @@ -90,7 +92,7 @@ steps: - label: ':building_construction: :mac: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (New Arch)' key: "build-react-native-navigation-ios-fixture-new-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: "macos-12-arm" env: @@ -119,7 +121,7 @@ steps: # ------------------------------ - label: ":bitbar: :android: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (Old Arch) Android {{matrix.android_version}}" depends_on: "build-react-native-navigation-android-fixture-old-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" plugins: @@ -160,7 +162,7 @@ steps: - label: ":bitbar: :android: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (New Arch) Android {{matrix.android_version}}" depends_on: "build-react-native-navigation-android-fixture-new-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" RCT_NEW_ARCH_ENABLED: "true" @@ -204,7 +206,7 @@ steps: # ------------------------------ - label: ":bitbar: :mac: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (Old Arch) iOS {{matrix.ios_version}}" depends_on: "build-react-native-navigation-ios-fixture-old-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" plugins: @@ -222,7 +224,6 @@ steps: - --device=IOS_{{matrix.ios_version}} - --a11y-locator - --fail-fast - - --appium-version=1.22 - --no-tunnel - --aws-public-ip test-collector#v1.10.2: @@ -245,7 +246,7 @@ steps: - label: ":bitbar: :mac: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (New Arch) iOS {{matrix.ios_version}}" depends_on: "build-react-native-navigation-ios-fixture-new-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" RCT_NEW_ARCH_ENABLED: "true" @@ -264,7 +265,6 @@ steps: - --device=IOS_{{matrix.ios_version}} - --a11y-locator - --fail-fast - - --appium-version=1.22 - --no-tunnel - --aws-public-ip test-collector#v1.10.2: diff --git a/.buildkite/react-native-navigation-pipeline.yml b/.buildkite/react-native-navigation-pipeline.yml index 9dee1f1cd..8272f1913 100644 --- a/.buildkite/react-native-navigation-pipeline.yml +++ b/.buildkite/react-native-navigation-pipeline.yml @@ -1,5 +1,7 @@ # This pipeline file builds runs the end-to-end tests for # React Native Navigation on BitBar. +agents: + queue: opensource steps: - group: "React Native Navigation Tests" @@ -8,7 +10,7 @@ steps: # ------------------------------ - label: ":building_construction: :android: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (Old Arch)" key: "build-react-native-navigation-android-fixture-old-arch" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: macos-12-arm env: @@ -34,7 +36,7 @@ steps: - label: ":building_construction: :android: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (New Arch)" key: "build-react-native-navigation-android-fixture-new-arch" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: macos-12-arm env: @@ -63,7 +65,7 @@ steps: # ------------------------------ - label: ":building_construction: :mac: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (Old Arch)" key: "build-react-native-navigation-ios-fixture-old-arch" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: "macos-12-arm" env: @@ -89,7 +91,7 @@ steps: - label: ":building_construction: :mac: Build react-native-navigation test fixture - RN {{matrix.rn_version}} (New Arch)" key: "build-react-native-navigation-ios-fixture-new-arch" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: "macos-12-arm" env: @@ -118,7 +120,7 @@ steps: # ------------------------------ - label: ":bitbar: :android: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (Old Arch) / Android {{matrix.android_version}} " depends_on: "build-react-native-navigation-android-fixture-old-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" plugins: @@ -159,7 +161,7 @@ steps: - label: ":bitbar: :android: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (New Arch) / Android {{matrix.android_version}}" depends_on: "build-react-native-navigation-android-fixture-new-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" RCT_NEW_ARCH_ENABLED: "true" @@ -203,7 +205,7 @@ steps: # ------------------------------ - label: ":bitbar: :mac: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (Old Arch) / iOS {{matrix.ios_version}}" depends_on: "build-react-native-navigation-ios-fixture-old-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" plugins: @@ -244,7 +246,7 @@ steps: - label: ":bitbar: :mac: react-native-navigation end-to-end tests - RN {{matrix.rn_version}} (New Arch) / iOS {{matrix.ios_version}} react-native-navigation end-to-end tests" depends_on: "build-react-native-navigation-ios-fixture-new-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 30 env: REACT_NATIVE_NAVIGATION: "true" RCT_NEW_ARCH_ENABLED: "true" diff --git a/.buildkite/react-native-pipeline.full.yml b/.buildkite/react-native-pipeline.full.yml index d2e205d06..69df54337 100644 --- a/.buildkite/react-native-pipeline.full.yml +++ b/.buildkite/react-native-pipeline.full.yml @@ -1,3 +1,6 @@ +agents: + queue: "opensource" + steps: - group: "React Native Tests" steps: @@ -6,7 +9,7 @@ steps: # - label: ':android: Build RN {{matrix.reactnative}} test fixture APK (Old Arch)' key: "build-react-native-android-fixture-old-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: macos-14 env: @@ -39,7 +42,7 @@ steps: - label: ':android: Build RN {{matrix}} test fixture APK (New Arch)' key: "build-react-native-android-fixture-new-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: macos-14 env: @@ -65,7 +68,7 @@ steps: - label: ':mac: Build RN {{matrix}} test fixture ipa (Old Arch)' key: "build-react-native-ios-fixture-old-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: "macos-14" env: @@ -92,7 +95,7 @@ steps: - label: ':mac: Build RN {{matrix}} test fixture ipa (New Arch)' key: "build-react-native-ios-fixture-new-arch-full" - timeout_in_minutes: 30 + timeout_in_minutes: 15 agents: queue: "macos-14" env: @@ -122,7 +125,7 @@ steps: # - label: ":bitbar: :android: RN {{matrix}} Android 12 (Old Arch) end-to-end tests" depends_on: "build-react-native-android-fixture-old-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/old-arch/{{matrix}}/reactnative.apk" @@ -160,7 +163,7 @@ steps: - label: ":bitbar: :android: RN {{matrix}} Android 12 (New Arch) end-to-end tests" depends_on: "build-react-native-android-fixture-new-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/new-arch/{{matrix}}/reactnative.apk" @@ -199,7 +202,7 @@ steps: - label: ":bitbar: :mac: RN {{matrix}} iOS 14 (Old Arch) end-to-end tests" depends_on: "build-react-native-ios-fixture-old-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/old-arch/{{matrix}}/output/reactnative.ipa" @@ -237,7 +240,7 @@ steps: - label: ":bitbar: :mac: RN {{matrix}} iOS 14 (New Arch) end-to-end tests" depends_on: "build-react-native-ios-fixture-new-arch-full" - timeout_in_minutes: 60 + timeout_in_minutes: 30 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/new-arch/{{matrix}}/output/reactnative.ipa" diff --git a/.buildkite/react-native-pipeline.yml b/.buildkite/react-native-pipeline.yml index 2fd319af9..c7dc7dcab 100644 --- a/.buildkite/react-native-pipeline.yml +++ b/.buildkite/react-native-pipeline.yml @@ -1,3 +1,6 @@ +agents: + queue: "opensource" + steps: - group: "React Native Tests" steps: @@ -107,7 +110,7 @@ steps: # - label: ":bitbar: :android: RN {{matrix}} Android 12 (Old Arch) end-to-end tests" depends_on: "build-react-native-android-fixture-old-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 20 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/old-arch/{{matrix}}/reactnative.apk" @@ -141,7 +144,7 @@ steps: - label: ":bitbar: :android: RN {{matrix}} Android 12 (New Arch) end-to-end tests" depends_on: "build-react-native-android-fixture-new-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 20 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/new-arch/{{matrix}}/reactnative.apk" @@ -177,7 +180,7 @@ steps: - label: ":bitbar: :mac: RN {{matrix}} iOS 14 (Old Arch) end-to-end tests" depends_on: "build-react-native-ios-fixture-old-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 20 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/old-arch/{{matrix}}/output/reactnative.ipa" @@ -211,7 +214,7 @@ steps: - label: ":bitbar: :mac: RN {{matrix}} iOS 14 (New Arch) end-to-end tests" depends_on: "build-react-native-ios-fixture-new-arch" - timeout_in_minutes: 60 + timeout_in_minutes: 20 plugins: artifacts#v1.9.0: download: "test/react-native/features/fixtures/generated/new-arch/{{matrix}}/output/reactnative.ipa" From 17de1ca1bc2cd1eb760d9672edda058aeaeac84a Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Tue, 10 Dec 2024 11:28:43 +0000 Subject: [PATCH 3/8] test: :white_check_mark: update react-router and react-dom dependencies --- .../fixtures/packages/react-router/package.json | 3 ++- .../packages/route-change-spans/package.json | 2 +- .../packages/route-change-spans/src/app.jsx | 15 ++++++--------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/test/browser/features/fixtures/packages/react-router/package.json b/test/browser/features/fixtures/packages/react-router/package.json index 9a5be155a..ae6330347 100644 --- a/test/browser/features/fixtures/packages/react-router/package.json +++ b/test/browser/features/fixtures/packages/react-router/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "react": "^18.2.0", - "react-router-dom": "^6.14.2" + "react-dom": "^18.2.0", + "react-router-dom": "~6.28.0" } } diff --git a/test/browser/features/fixtures/packages/route-change-spans/package.json b/test/browser/features/fixtures/packages/route-change-spans/package.json index 1c8de9192..431235d6a 100644 --- a/test/browser/features/fixtures/packages/route-change-spans/package.json +++ b/test/browser/features/fixtures/packages/route-change-spans/package.json @@ -4,7 +4,7 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^5.0.0", + "react-router-dom": "^6.28.0", "@babel/core": "^7.21.3", "@babel/preset-react": "^7.18.6", "@rollup/plugin-commonjs": "^25.0.0", diff --git a/test/browser/features/fixtures/packages/route-change-spans/src/app.jsx b/test/browser/features/fixtures/packages/route-change-spans/src/app.jsx index e8b95b96a..397451ce3 100644 --- a/test/browser/features/fixtures/packages/route-change-spans/src/app.jsx +++ b/test/browser/features/fixtures/packages/route-change-spans/src/app.jsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react" import { createRoot } from 'react-dom/client' import { BrowserRouter as Router, - Switch, + Routes, Route, Link } from "react-router-dom" @@ -26,14 +26,11 @@ function App() { return ( - - - - - - - - + + } /> + } /> + } /> + ) } From c610eed8a46362034a5bb10c01f5ac960154d072 Mon Sep 17 00:00:00 2001 From: Tom Longridge Date: Wed, 18 Dec 2024 11:40:53 +0000 Subject: [PATCH 4/8] build: bump upload-artifact action to v4.5.0 and pin dependencies --- .github/workflows/compare-pr-stats.yml | 4 ++-- .github/workflows/node-ci.yml | 12 ++++++------ .github/workflows/record-pr-stats.yml | 12 ++++++------ .github/workflows/signing.yml | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/compare-pr-stats.yml b/.github/workflows/compare-pr-stats.yml index ec669ad4d..94816d313 100644 --- a/.github/workflows/compare-pr-stats.yml +++ b/.github/workflows/compare-pr-stats.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 with: node-version: 18 cache: 'npm' @@ -33,7 +33,7 @@ jobs: - run: npm ci - name: Leave comment - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: script: | const leaveComment = require('./.github/leave-pr-stats-comment.js') diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index a26012a31..1aa53d4ed 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -9,8 +9,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 with: node-version: 18 cache: 'npm' @@ -21,8 +21,8 @@ jobs: linting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 with: node-version: 18 cache: 'npm' @@ -33,8 +33,8 @@ jobs: unit_tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 with: node-version: 18 cache: 'npm' diff --git a/.github/workflows/record-pr-stats.yml b/.github/workflows/record-pr-stats.yml index 9b51f0818..37aef9cb4 100644 --- a/.github/workflows/record-pr-stats.yml +++ b/.github/workflows/record-pr-stats.yml @@ -35,11 +35,11 @@ jobs: code-coverage: ${{ steps.code-coverage.outputs.result }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.ref }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2 with: node-version: 18 cache: 'npm' @@ -49,7 +49,7 @@ jobs: - run: npm run test:coverage - id: package - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: script: | let json = '' @@ -85,18 +85,18 @@ jobs: echo "size=$SIZE" >> $GITHUB_OUTPUT - id: code-coverage - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: script: | return require('./coverage/coverage-summary.json') - - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: packages-${{ inputs.ref }} path: | bugsnag-*.tgz - - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: bundles-${{ inputs.ref }} path: | diff --git a/.github/workflows/signing.yml b/.github/workflows/signing.yml index 199601bdf..ed1818205 100644 --- a/.github/workflows/signing.yml +++ b/.github/workflows/signing.yml @@ -21,7 +21,7 @@ jobs: run: | echo "${{ secrets.PLATFORMS_GPG_KEY_BASE64 }}" | base64 --decode | gpg --batch --import - name: Sign assets - uses: bugsnag/platforms-release-signer@main + uses: uses: bugsnag/platforms-release-signer@4d88944b11e503624f8a511cf6d0fa2901822b60 with: github_token: ${{ secrets.PLATFORMS_SIGNING_GITHUB_TOKEN }} full_repository: ${{ github.repository }} From ec555844b3a4428d8691b125ff54897afe64a0f7 Mon Sep 17 00:00:00 2001 From: Tom Longridge Date: Wed, 18 Dec 2024 11:42:55 +0000 Subject: [PATCH 5/8] build: add dependabot config for GHA --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..d7007f466 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly \ No newline at end of file From a25d5eb7450a9fe2e3312c9e93326603152129d5 Mon Sep 17 00:00:00 2001 From: Daria Bialobrzeska <88393714+DariaKunoichi@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:37:06 +0100 Subject: [PATCH 6/8] Replace react-native-file-access module with native implementation (#561) --- package-lock.json | 16 +- .../react-native/__mocks__/file-native.ts | 102 ++++++++ .../__mocks__/react-native-file-access.ts | 228 ------------------ .../react-native/__mocks__/react-native.ts | 19 +- .../NativeBugsnagPerformanceImpl.java | 115 ++++++++- .../BugsnagReactNativePerformance.java | 40 +++ .../BugsnagReactNativePerformancePackage.java | 2 +- .../BugsnagReactNativePerformance.java | 40 +++ .../ios/BugsnagReactNativePerformance.mm | 125 +++++++++- .../lib/NativeBugsnagPerformance.ts | 16 +- packages/platforms/react-native/lib/client.ts | 6 +- .../react-native/lib/id-generator.ts | 15 +- packages/platforms/react-native/lib/native.ts | 18 +- .../lib/persistence/file-native.ts | 30 +++ .../react-native/lib/persistence/file.ts | 4 +- .../react-native/lib/persistence/index.ts | 4 +- .../react-native/lib/retry-queue/directory.ts | 2 +- packages/platforms/react-native/package.json | 6 +- .../platforms/react-native/rollup.config.mjs | 1 - .../react-native/tests/id-generator.test.ts | 48 ++-- .../react-native/tests/native.test.ts | 53 ++++ .../tests/persistence/file-based.test.ts | 4 +- .../tests/persistence/file.test.ts | 4 +- .../tests/persistence/index.test.ts | 4 +- .../tests/resource-attributes-source.test.ts | 6 +- 25 files changed, 598 insertions(+), 310 deletions(-) create mode 100644 packages/platforms/react-native/__mocks__/file-native.ts delete mode 100644 packages/platforms/react-native/__mocks__/react-native-file-access.ts create mode 100644 packages/platforms/react-native/lib/persistence/file-native.ts create mode 100644 packages/platforms/react-native/tests/native.test.ts diff --git a/package-lock.json b/package-lock.json index f116e74de..0dc5e2bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23993,16 +23993,6 @@ } } }, - "node_modules/react-native-file-access": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.1.0.tgz", - "integrity": "sha512-wOpfKpJ8s4Csfjcvf7H4L1EtmejM07HQpndzMRWAianLC50EsPc78iV8TQaw5yI7j18rh9fWMqpevz8f5a1rsA==", - "dev": true, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-navigation": { "version": "7.37.2", "resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-7.37.2.tgz", @@ -28581,14 +28571,12 @@ "devDependencies": { "@react-native-community/netinfo": "^9.4.1", "@types/react": "^18.2.24", - "metro-react-native-babel-preset": "^0.77.0", - "react-native-file-access": ">=1.7.1 <4.0.0" + "metro-react-native-babel-preset": "^0.77.0" }, "peerDependencies": { "@react-native-community/netinfo": ">= 9.4.1", "react": "*", - "react-native": "*", - "react-native-file-access": ">=1.7.1 <4.0.0" + "react-native": "*" } }, "packages/plugin-react-native-navigation": { diff --git a/packages/platforms/react-native/__mocks__/file-native.ts b/packages/platforms/react-native/__mocks__/file-native.ts new file mode 100644 index 000000000..46f6f1b9a --- /dev/null +++ b/packages/platforms/react-native/__mocks__/file-native.ts @@ -0,0 +1,102 @@ +/** + * MIT License + * + * Copyright (c) 2020 alpha0010 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * https://github.com/alpha0010/react-native-file-access/blob/7179426e701fa6e54bda6c2a753cfe31a4a08293/LICENSE + */ + +// Copied from v3.0.4: +// https://github.com/alpha0010/react-native-file-access/blob/7179426e701fa6e54bda6c2a753cfe31a4a08293/jest/react-native-file-access.ts + +/* eslint-disable */ +/* global jest */ + +export const Dirs = { + CacheDir: '/mock/CacheDir', + DocumentDir: '/mock/DocumentDir', +}; + +const DIR_MARKER = '__dir__'; + +class FileSystemMock { + /** + * Data store for mock filesystem. + */ + public filesystem = new Map(); + + /** + * Check if a path exists. + */ + public exists = jest.fn(async (path: string) => this.filesystem.has(path)); + + /** + * Check if a path is a directory. + */ + public isDir = jest.fn(async (path: string) => !this.filesystem.has(path)); + + /** + * List files in a directory. + */ + public ls = jest.fn(async (_path: string) => ['file1', 'file2']); + + /** + * Make a directory. + * + * This is a noop as the mock file system does not differentiate between files + * and directories + * + * NOTE: this method was added by bugsnag + */ + public mkdir = jest.fn(async (path: string) => { + this.filesystem.set(path, DIR_MARKER) + return path + }) + + /** + * Read the content of a file. + */ + public readFile = jest.fn(async (path: string) => this.getFileOrThrow(path)); + + /** + * Delete a file. + */ + public unlink = jest.fn(async (path: string) => { + this.filesystem.delete(path); + }); + + /** + * Write content to a file. + */ + public writeFile = jest.fn(async (path: string, data: string) => { + this.filesystem.set(path, data); + }); + + private getFileOrThrow(path: string): string { + const data = this.filesystem.get(path); + if (data == null) { + throw new Error(`File ${path} not found`); + } + return data; + } +} + +export const FileSystem = new FileSystemMock(); diff --git a/packages/platforms/react-native/__mocks__/react-native-file-access.ts b/packages/platforms/react-native/__mocks__/react-native-file-access.ts deleted file mode 100644 index ba417cb94..000000000 --- a/packages/platforms/react-native/__mocks__/react-native-file-access.ts +++ /dev/null @@ -1,228 +0,0 @@ -/** - * MIT License - * - * Copyright (c) 2020 alpha0010 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * https://github.com/alpha0010/react-native-file-access/blob/7179426e701fa6e54bda6c2a753cfe31a4a08293/LICENSE - */ - -// Copied from v3.0.4: -// https://github.com/alpha0010/react-native-file-access/blob/7179426e701fa6e54bda6c2a753cfe31a4a08293/jest/react-native-file-access.ts - -/* eslint-disable */ -/* global jest */ - -import { Platform } from 'react-native'; -import type { - ExternalDir, - FetchResult, - FileStat, - FsStat, - HashAlgorithm, -} from 'react-native-file-access'; - -export const Dirs = { - CacheDir: '/mock/CacheDir', - DatabaseDir: '/mock/DatabaseDir', - DocumentDir: '/mock/DocumentDir', - LibraryDir: '/mock/LibraryDir', - MainBundleDir: '/mock/MainBundleDir', -}; - -class FileSystemMock { - /** - * Data store for mock filesystem. - */ - public filesystem = new Map(); - - /** - * Append content to a file. - */ - public appendFile = jest.fn(async (path: string, data: string) => { - this.filesystem.set(path, (this.filesystem.get(path) ?? '') + data); - }); - - /** - * Append a file to another file. - * - * Returns number of bytes written. - */ - public concatFiles = jest.fn(async (source: string, target: string) => { - const data = this.getFileOrThrow(source); - this.filesystem.set(target, (this.filesystem.get(target) ?? '') + data); - return data.length; - }); - - /** - * Copy a file. - */ - public cp = jest.fn(async (source: string, target: string) => { - this.filesystem.set(target, this.getFileOrThrow(source)); - }); - - /** - * Copy a file to external storage - */ - public cpExternal = jest.fn( - async (source: string, targetName: string, dir: ExternalDir) => { - this.filesystem.set(`/${dir}/${targetName}`, this.getFileOrThrow(source)); - } - ); - - /** - * Copy a bundled asset file. - */ - public cpAsset = jest.fn(async (asset: string, target: string) => { - this.filesystem.set(target, `[Mock asset data for '${asset}']`); - }); - - /** - * Check device available space. - */ - public df = jest.fn, []>(async () => ({ - internal_free: 100, - internal_total: 200, - })); - - /** - * Check if a path exists. - */ - public exists = jest.fn(async (path: string) => this.filesystem.has(path)); - - /** - * Save a network request to a file. - */ - public fetch = jest.fn( - async ( - resource: string, - init: { - body?: string; - headers?: { [key: string]: string }; - method?: string; - path?: string; - } - ): Promise => { - if (init.path != null) { - this.filesystem.set(init.path, `[Mock fetch data for '${resource}']`); - } - return { - headers: {}, - ok: true, - redirected: false, - status: 200, - statusText: 'OK', - url: resource, - }; - } - ); - - /** - * Return the local storage directory for app groups. - * - * This is an Apple only feature. - */ - public getAppGroupDir = jest.fn((groupName: string) => { - if (Platform.OS !== 'ios' && Platform.OS !== 'macos') { - throw new Error('AppGroups are available on Apple devices only'); - } - return `${Dirs.DocumentDir}/shared/AppGroup/${groupName}`; - }); - - /** - * Hash the file content. - */ - public hash = jest.fn(async (path: string, algorithm: HashAlgorithm) => { - if (!this.filesystem.has(path)) { - throw new Error(`File ${path} not found`); - } - return `[${algorithm} hash of '${path}']`; - }); - - /** - * Check if a path is a directory. - */ - public isDir = jest.fn(async (path: string) => !this.filesystem.has(path)); - - /** - * List files in a directory. - */ - public ls = jest.fn(async (_path: string) => ['file1', 'file2']); - - /** - * Make a directory. - * - * This is a noop as the mock file system does not differentiate between files - * and directories - * - * NOTE: this method was added by bugsnag - */ - public mkdir = jest.fn(async () => {}) - - /** - * Move a file. - */ - public mv = jest.fn(async (source: string, target: string) => { - this.filesystem.set(target, this.getFileOrThrow(source)); - this.filesystem.delete(source); - }); - - /** - * Read the content of a file. - */ - public readFile = jest.fn(async (path: string) => this.getFileOrThrow(path)); - - /** - * Read file metadata. - */ - public stat = jest.fn( - async (path: string): Promise => ({ - filename: path.substring(path.lastIndexOf('/')), - lastModified: 1, - path: path, - size: this.getFileOrThrow(path).length, - type: 'file', - }) - ); - - /** - * Delete a file. - */ - public unlink = jest.fn(async (path: string) => { - this.filesystem.delete(path); - }); - - /** - * Write content to a file. - */ - public writeFile = jest.fn(async (path: string, data: string) => { - this.filesystem.set(path, data); - }); - - private getFileOrThrow(path: string): string { - const data = this.filesystem.get(path); - if (data == null) { - throw new Error(`File ${path} not found`); - } - return data; - } -} - -export const FileSystem = new FileSystemMock(); diff --git a/packages/platforms/react-native/__mocks__/react-native.ts b/packages/platforms/react-native/__mocks__/react-native.ts index 67717109a..cf29a7310 100644 --- a/packages/platforms/react-native/__mocks__/react-native.ts +++ b/packages/platforms/react-native/__mocks__/react-native.ts @@ -133,7 +133,20 @@ const BugsnagReactNativePerformance = { }), requestEntropyAsync: jest.fn(() => { return Promise.resolve(createPool()) - }) + }), + getNativeConstants () { + return { + CacheDir: '/mock/CacheDir', + DocumentDir: '/mock/DocumentDir' + } + }, + exists: jest.fn(), + isDir: jest.fn(), + ls: jest.fn(), + mkdir: jest.fn(), + readFile: jest.fn(), + unlink: jest.fn(), + writeFile: jest.fn() } export const TurboModuleRegistry = { @@ -148,6 +161,10 @@ export const TurboModuleRegistry = { } } +export const NativeModules = { + BugsnagReactNativePerformance +} + export type AppStateStatus = 'active' | 'inactive' | 'background' type AppStateChangeCallback = (status: AppStateStatus) => void diff --git a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java index 6c10268eb..62644fb92 100644 --- a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java +++ b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java @@ -12,6 +12,13 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import java.security.SecureRandom; +import java.io.File; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.io.FileInputStream; +import java.io.FileOutputStream; class NativeBugsnagPerformanceImpl { @@ -25,7 +32,7 @@ public NativeBugsnagPerformanceImpl(ReactApplicationContext reactContext) { this.reactContext = reactContext; } - public WritableMap getDeviceInfo() { + WritableMap getDeviceInfo() { WritableMap map = Arguments.createMap(); try { String bundleIdentifier = this.reactContext.getPackageName(); @@ -55,7 +62,7 @@ public WritableMap getDeviceInfo() { return map; } - public String requestEntropy() { + String requestEntropy() { byte[] bytes = new byte[1024]; random.nextBytes(bytes); @@ -70,10 +77,112 @@ public String requestEntropy() { return hex.toString(); } - public void requestEntropyAsync(Promise promise) { + void requestEntropyAsync(Promise promise) { promise.resolve(requestEntropy()); } + WritableMap getNativeConstants() { + WritableMap map = Arguments.createMap(); + map.putString("CacheDir", this.reactContext.getCacheDir().getAbsolutePath()); + map.putString("DocumentDir", this.reactContext.getFilesDir().getAbsolutePath()); + + return map; + } + + void exists(String path, Promise promise) { + try { + boolean result = new File(path).exists(); + promise.resolve(result); + } catch(Exception e) { + promise.reject(e); + } + } + + void isDir(String path, Promise promise) { + try { + boolean result = new File(path).isDirectory(); + promise.resolve(result); + } catch(Exception e) { + promise.reject(e); + } + } + + void ls(String path, Promise promise) { + try { + String[] files = new File(path).list(); + WritableArray resultArray = Arguments.createArray(); + for (String file : files) { + resultArray.pushString(file); + } + + promise.resolve(resultArray); + } catch(Exception e) { + promise.reject(e); + } + } + + void mkdir(String path, Promise promise) { + try { + File file = new File(path); + if (file.exists()) { + promise.reject("EEXIST", new Exception("Already exists.")); + return; + } + + boolean result = file.mkdirs(); + if (result) { + promise.resolve(path); + } else { + promise.reject("EPERM", new Exception("Failed to create directory")); + } + } catch(Exception e) { + promise.reject(e); + } + } + + void readFile(String path, String encoding, Promise promise) { + File file = new File(path); + StringBuilder fileContent = new StringBuilder((int) file.length()); + try( + FileInputStream fin = new FileInputStream(file); + InputStreamReader isr = new InputStreamReader(fin, encoding); + ) { + char[] buffer = new char[4096]; + int charsRead = 0; + while ((charsRead = isr.read(buffer)) != -1) { + fileContent.append(buffer, 0, charsRead); + } + promise.resolve(fileContent.toString()); + } catch (Exception e) { + promise.reject(e); + } + } + + void unlink(String path, Promise promise) { + try { + boolean result = new File(path).delete(); + if (result) { + promise.resolve(null); + } else { + promise.reject(new Exception("Failed to delete file/directory")); + } + } catch(Exception e) { + promise.reject(e); + } + } + + void writeFile(String path, String data, String encoding, Promise promise){ + try( + FileOutputStream fout = new FileOutputStream(path); + Writer w = new OutputStreamWriter(fout, encoding); + ) { + w.write(data); + promise.resolve(null); + } catch (Exception e) { + promise.reject(e); + } + } + @Nullable private String abiToArchitecture(@Nullable String abi) { if (abi == null) { diff --git a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java index 31e8aea46..7eb0fa218 100644 --- a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java +++ b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java @@ -34,5 +34,45 @@ public String requestEntropy() { public void requestEntropyAsync(Promise promise) { impl.requestEntropyAsync(promise); } + + @Override + public WritableMap getNativeConstants() { + return impl.getNativeConstants(); + } + + @Override + public void exists(String path, Promise promise) { + impl.exists(path, promise); + } + + @Override + public void isDir(String path, Promise promise) { + impl.isDir(path, promise); + } + + @Override + public void ls(String path, Promise promise) { + impl.ls(path, promise); + } + + @Override + public void mkdir(String path, Promise promise) { + impl.mkdir(path, promise); + } + + @Override + public void readFile(String path, String encoding, Promise promise) { + impl.readFile(path, encoding, promise); + } + + @Override + public void unlink(String path, Promise promise) { + impl.unlink(path, promise); + } + + @Override + public void writeFile(String path, String data, String encoding, Promise promise){ + impl.writeFile(path, data, encoding, promise); + } } diff --git a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformancePackage.java b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformancePackage.java index a214ca5c3..178f422cb 100644 --- a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformancePackage.java +++ b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformancePackage.java @@ -34,7 +34,7 @@ public Map getReactModuleInfos() { NativeBugsnagPerformanceImpl.MODULE_NAME, false, // canOverrideExistingModule true, // needsEagerInit - false, // hasConstants + true, // hasConstants false, // isCxxModule true // isTurboModule ) diff --git a/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java b/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java index d54f3d6a2..e376b2aab 100644 --- a/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java +++ b/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java @@ -36,4 +36,44 @@ public String requestEntropy() { public void requestEntropyAsync(Promise promise) { impl.requestEntropyAsync(promise); } + + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableMap getNativeConstants() { + return impl.getNativeConstants(); + } + + @ReactMethod + public void exists(String path, Promise promise) { + impl.exists(path, promise); + } + + @ReactMethod + public void isDir(String path, Promise promise) { + impl.isDir(path, promise); + } + + @ReactMethod + public void ls(String path, Promise promise) { + impl.ls(path, promise); + } + + @ReactMethod + public void mkdir(String path, Promise promise) { + impl.mkdir(path, promise); + } + + @ReactMethod + public void readFile(String path, String encoding, Promise promise) { + impl.readFile(path, encoding, promise); + } + + @ReactMethod + public void unlink(String path, Promise promise) { + impl.unlink(path, promise); + } + + @ReactMethod + public void writeFile(String path, String data, String encoding, Promise promise) { + impl.writeFile(path, data, encoding, promise); + } } diff --git a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm index f23d83ec2..21e44e1c3 100644 --- a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm +++ b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm @@ -82,12 +82,125 @@ @implementation BugsnagReactNativePerformance return hexStr; } - RCT_EXPORT_METHOD(requestEntropyAsync:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) - { - NSString *hexStr = getRandomBytes(); - resolve(hexStr); - } +RCT_EXPORT_METHOD(requestEntropyAsync:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *hexStr = getRandomBytes(); + resolve(hexStr); +} + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getNativeConstants) { + NSMutableDictionary *nativeDirs = [NSMutableDictionary new]; + NSArray *caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true); + if (caches != nil && [caches count] != 0 ) { + nativeDirs[@"CacheDir"] = caches[0]; + } + NSArray *documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true); + if (documents != nil && [documents count] != 0 ) { + nativeDirs[@"DocumentDir"] = documents[0]; + } + + return nativeDirs; +} + +RCT_EXPORT_METHOD(exists:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:path]; + resolve(@(exists)); +} + +RCT_EXPORT_METHOD(isDir:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + BOOL isDir; + BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDir]; + resolve(@(exists && isDir)); +} + +RCT_EXPORT_METHOD(ls:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSError *error; + NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtPath:path error:&error]; + if (error != nil) { + reject(@"ENOENT", @"Directory does not exist", error); + } else { + resolve(contents); + } +} + +RCT_EXPORT_METHOD(mkdir:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSError *error; + BOOL result = [NSFileManager.defaultManager createDirectoryAtPath:path withIntermediateDirectories:true attributes:nil error:&error]; + if (error != nil) { + reject(@"EIO", @"Failed to create directory", error); + } else { + resolve(@(result)); + } +} + +RCT_EXPORT_METHOD(readFile:(NSString *)path + encoding:(NSString *)encoding + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSError *error; + if ([encoding isEqualToString:@"utf8"]) { + NSString *fileString = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) { + reject(@"EIO", @"Failed to read file", error); + } else { + resolve(fileString); + } + } else if ([encoding isEqualToString:@"base64"]) { + NSData *fileData = [[NSData alloc] initWithContentsOfFile:path]; + if (fileData != nil) { + resolve([fileData base64EncodedStringWithOptions:0]); + } else { + reject(@"ERR", @"Failed to read file, invalid base64", nil); + } + } +} + +RCT_EXPORT_METHOD(unlink:(NSString *)path + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + NSError *error; + BOOL result = [NSFileManager.defaultManager removeItemAtPath:path error:&error]; + if (error != nil) { + reject(@"EIO", @"Failed to remove file", error); + } else if (result) { + resolve(nil); + } else { + reject(@"ENOENT", @"Failed to delete file/directory", nil); + } +} + +RCT_EXPORT_METHOD(writeFile:(NSString *)path + data:(NSString *)data + encoding:(NSString *)encoding + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + if ([encoding isEqualToString:@"utf8"]) { + NSError *error; + [data writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:&error]; + if (error != nil) { + reject(@"EIO", @"Failed to write file", error); + } else { + resolve(nil); + } + } else if ([encoding isEqualToString:@"base64"]) { + NSURL *fileURL = [NSURL fileURLWithPath:path]; + NSData *nsData = [[NSData alloc] initWithBase64EncodedString:data options:NSDataBase64DecodingIgnoreUnknownCharacters]; + if (nsData != nil) { + [nsData writeToURL:fileURL atomically:NO]; + resolve(nil); + } else { + reject(@"ERR", @"Failed to write to '\(path)', invalid base64.", nil); + } + } +} #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: diff --git a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts index 9bde1be17..13d163c7d 100644 --- a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts +++ b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts @@ -10,10 +10,24 @@ export type DeviceInfo = { bundleIdentifier: string | undefined } +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type NativeDirs = { + CacheDir: string // Temporary files. System/user may delete these if device storage is low. + DocumentDir: string // Persistent data. Generally user created content. +} + export interface Spec extends TurboModule { - getDeviceInfo: () => DeviceInfo + getDeviceInfo: () => DeviceInfo | undefined requestEntropy: () => string requestEntropyAsync: () => Promise + getNativeConstants: () => NativeDirs + exists: (path: string) => Promise + isDir: (path: string) => Promise + ls: (path: string) => Promise + mkdir: (path: string) => Promise + readFile: (path: string, encoding: string) => Promise + unlink: (path: string) => Promise + writeFile: (path: string, data: string, encoding: string) => Promise } export default TurboModuleRegistry.get( diff --git a/packages/platforms/react-native/lib/client.ts b/packages/platforms/react-native/lib/client.ts index 1b786be97..0ef48220a 100644 --- a/packages/platforms/react-native/lib/client.ts +++ b/packages/platforms/react-native/lib/client.ts @@ -2,7 +2,7 @@ import { createClient } from '@bugsnag/core-performance' import createFetchDeliveryFactory from '@bugsnag/delivery-fetch-performance' import { createXmlHttpRequestTracker } from '@bugsnag/request-tracker-performance' import { AppRegistry, AppState } from 'react-native' -import { FileSystem } from 'react-native-file-access' +import { FileSystem } from './persistence/file-native' import { AppStartPlugin, NetworkRequestPlugin } from './auto-instrumentation' import createClock from './clock' import createSchema from './config' @@ -27,7 +27,7 @@ const clock = createClock(performance) const appStartTime = clock.now() const deliveryFactory = createFetchDeliveryFactory(fetch, clock) const spanAttributesSource = createSpanAttributesSource() -const deviceInfo = NativeBugsnagPerformance && !isDebuggingRemotely ? NativeBugsnagPerformance.getDeviceInfo() : undefined +const deviceInfo = !isDebuggingRemotely ? NativeBugsnagPerformance.getDeviceInfo() : undefined const persistence = persistenceFactory(FileSystem, deviceInfo) const resourceAttributesSource = resourceAttributesSourceFactory(persistence, deviceInfo) const backgroundingListener = createBrowserBackgroundingListener(AppState) @@ -35,7 +35,7 @@ const backgroundingListener = createBrowserBackgroundingListener(AppState) // React Native's fetch polyfill uses xhr under the hood, so we only track xhr requests const xhrRequestTracker = createXmlHttpRequestTracker(XMLHttpRequest, clock) -const idGenerator = createIdGenerator(NativeBugsnagPerformance, isDebuggingRemotely) +const idGenerator = createIdGenerator(isDebuggingRemotely) const BugsnagPerformance = createClient({ backgroundingListener, diff --git a/packages/platforms/react-native/lib/id-generator.ts b/packages/platforms/react-native/lib/id-generator.ts index e076a07a9..eee351de7 100644 --- a/packages/platforms/react-native/lib/id-generator.ts +++ b/packages/platforms/react-native/lib/id-generator.ts @@ -1,16 +1,10 @@ import type { BitLength, IdGenerator } from '@bugsnag/core-performance' -import type { Spec as NativeBugsnag } from './NativeBugsnagPerformance' +import NativeBugsnagPerformance from './native' const POOL_SIZE = 1024 const CALLS_BEFORE_POOL_REFRESH = 1000 -const isNativeModuleEnabled = (nativeModule: NativeBugsnag | null): nativeModule is NativeBugsnag => { - return nativeModule !== null && - typeof nativeModule.requestEntropy === 'function' && - typeof nativeModule.requestEntropyAsync === 'function' -} - export function toHex (value: number): string { const hex = value.toString(16) @@ -32,19 +26,18 @@ export function createRandomString (): string { return random } -function createIdGenerator (NativeBugsnagPerformance: NativeBugsnag | null, isDebuggingRemotely = false): IdGenerator { +function createIdGenerator (isDebuggingRemotely = false): IdGenerator { // If the native module is not available or remote debugging is enabled, fall back to a JS implementation - const requestEntropy = isNativeModuleEnabled(NativeBugsnagPerformance) && !isDebuggingRemotely + const requestEntropy = !isDebuggingRemotely ? NativeBugsnagPerformance.requestEntropy : createRandomString - const requestEntropyAsync = isNativeModuleEnabled(NativeBugsnagPerformance) ? NativeBugsnagPerformance.requestEntropyAsync : async () => createRandomString() // initialise the pool synchronously const randomValues = requestEntropy() let randomPool = randomValues.length > 0 ? randomValues : createRandomString() const regeneratePool = async () => { - const randomValues = await requestEntropyAsync() + const randomValues = await NativeBugsnagPerformance.requestEntropyAsync() randomPool = randomValues.length > 0 ? randomValues : createRandomString() } diff --git a/packages/platforms/react-native/lib/native.ts b/packages/platforms/react-native/lib/native.ts index 27ab1663e..7194e1b0f 100644 --- a/packages/platforms/react-native/lib/native.ts +++ b/packages/platforms/react-native/lib/native.ts @@ -7,8 +7,22 @@ declare const global: { const isTurboModuleEnabled = () => global.__turboModuleProxy != null -export const NativeBugsnagPerformance = isTurboModuleEnabled() +const NativeBsgModule = isTurboModuleEnabled() ? TurboModuleRegistry.get('BugsnagReactNativePerformance') : NativeModules.BugsnagReactNativePerformance -export default NativeBugsnagPerformance as Spec | null +const NativeBugsnagPerformance = NativeBsgModule || { + getDeviceInfo: () => undefined, + requestEntropy: () => '', + requestEntropyAsync: async () => '', + getNativeConstants: () => ({ CacheDir: '', DocumentDir: '' }), + exists: async (path: string) => false, + isDir: async (path: string) => false, + ls: async (path: string) => [], + mkdir: async (path: string) => '', + readFile: async (path: string, encoding: string) => '', + unlink: async (path: string) => { }, + writeFile: async (path: string, data: string, encoding: string) => { } +} + +export default NativeBugsnagPerformance as Spec diff --git a/packages/platforms/react-native/lib/persistence/file-native.ts b/packages/platforms/react-native/lib/persistence/file-native.ts new file mode 100644 index 000000000..127d50c85 --- /dev/null +++ b/packages/platforms/react-native/lib/persistence/file-native.ts @@ -0,0 +1,30 @@ +import NativeBugsnagPerformance from '../native' + +export const Dirs: { + CacheDir: string + DocumentDir: string +} = NativeBugsnagPerformance.getNativeConstants() + +export const FileSystem = { + exists (path: string) { + return NativeBugsnagPerformance.exists(path) + }, + isDir (path: string) { + return NativeBugsnagPerformance.isDir(path) + }, + ls (path: string) { + return NativeBugsnagPerformance.ls(path) + }, + mkdir (path: string) { + return NativeBugsnagPerformance.mkdir(path) + }, + readFile (path: string, encoding: string = 'utf8') { + return NativeBugsnagPerformance.readFile(path, encoding) + }, + unlink (path: string) { + return NativeBugsnagPerformance.unlink(path) + }, + writeFile (path: string, data: string, encoding: string = 'utf8') { + return NativeBugsnagPerformance.writeFile(path, data, encoding) + } +} diff --git a/packages/platforms/react-native/lib/persistence/file.ts b/packages/platforms/react-native/lib/persistence/file.ts index ffa11d4c5..7baf716e7 100644 --- a/packages/platforms/react-native/lib/persistence/file.ts +++ b/packages/platforms/react-native/lib/persistence/file.ts @@ -1,5 +1,5 @@ import { isObject } from '@bugsnag/core-performance' -import type { FileSystem } from 'react-native-file-access' +import type { FileSystem } from './file-native' import { Util } from './file-utils' export interface ReadableFile { @@ -13,7 +13,7 @@ export interface WritableFile { export type ReadWriteFile = ReadableFile & WritableFile /** - * A wrapper around 'react-native-file-access' that allows reading from a + * A wrapper around react-native file i/o that allows reading from a * specific file without having to specify the path every time it's used */ export class ReadOnlyFile implements ReadableFile { diff --git a/packages/platforms/react-native/lib/persistence/index.ts b/packages/platforms/react-native/lib/persistence/index.ts index 7d56fba79..2231243b2 100644 --- a/packages/platforms/react-native/lib/persistence/index.ts +++ b/packages/platforms/react-native/lib/persistence/index.ts @@ -1,8 +1,8 @@ import { Platform } from 'react-native' -import type { FileSystem } from 'react-native-file-access' -import { Dirs } from 'react-native-file-access' import type { DeviceInfo } from '../NativeBugsnagPerformance' import { File, NullFile, ReadOnlyFile } from './file' +import { Dirs } from './file-native' +import type { FileSystem } from './file-native' import FileBasedPersistence from './file-based' export { Util } from './file-utils' diff --git a/packages/platforms/react-native/lib/retry-queue/directory.ts b/packages/platforms/react-native/lib/retry-queue/directory.ts index 191ec336d..6da83468f 100644 --- a/packages/platforms/react-native/lib/retry-queue/directory.ts +++ b/packages/platforms/react-native/lib/retry-queue/directory.ts @@ -1,5 +1,5 @@ import { isObject } from '@bugsnag/core-performance' -import type { FileSystem } from 'react-native-file-access' +import type { FileSystem } from '../persistence/file-native' import { Util } from '../persistence' import timestampFromFilename from './timestamp-from-filename' diff --git a/packages/platforms/react-native/package.json b/packages/platforms/react-native/package.json index ce89b2c1d..ec7b992ec 100644 --- a/packages/platforms/react-native/package.json +++ b/packages/platforms/react-native/package.json @@ -23,14 +23,12 @@ "devDependencies": { "@react-native-community/netinfo": "^9.4.1", "@types/react": "^18.2.24", - "metro-react-native-babel-preset": "^0.77.0", - "react-native-file-access": ">=1.7.1 <4.0.0" + "metro-react-native-babel-preset": "^0.77.0" }, "peerDependencies": { "@react-native-community/netinfo": ">= 9.4.1", "react": "*", - "react-native": "*", - "react-native-file-access": ">=1.7.1 <4.0.0" + "react-native": "*" }, "scripts": { "build": "rollup --config", diff --git a/packages/platforms/react-native/rollup.config.mjs b/packages/platforms/react-native/rollup.config.mjs index 868a60140..f4d53f224 100644 --- a/packages/platforms/react-native/rollup.config.mjs +++ b/packages/platforms/react-native/rollup.config.mjs @@ -25,7 +25,6 @@ const config = createRollupConfig({ '@react-native-community/netinfo', 'react', 'react-native', - 'react-native-file-access', ] }) diff --git a/packages/platforms/react-native/tests/id-generator.test.ts b/packages/platforms/react-native/tests/id-generator.test.ts index 4564775f5..ada740c38 100644 --- a/packages/platforms/react-native/tests/id-generator.test.ts +++ b/packages/platforms/react-native/tests/id-generator.test.ts @@ -1,6 +1,5 @@ import createIdGenerator, { toHex, createRandomString } from '../lib/id-generator' -import type { IdGenerator } from '@bugsnag/core-performance' -import NativeBugsnagPerformance from '../lib/NativeBugsnagPerformance' +import NativeBugsnagPerformance from '../lib/native' jest.useFakeTimers() @@ -28,15 +27,8 @@ describe('React Native ID generator', () => { }) }) - let idGenerator: IdGenerator - beforeAll(() => { - idGenerator = createIdGenerator(NativeBugsnagPerformance) - }) - describe('idGenerator', () => { - // @ts-expect-error NativeBugsnagPerformance is possibly null const requestEntropy = NativeBugsnagPerformance.requestEntropy as jest.MockedFunction - // @ts-expect-error NativeBugsnagPerformance is possibly null const requestEntropyAsync = NativeBugsnagPerformance.requestEntropyAsync as jest.MockedFunction beforeEach(() => { @@ -45,27 +37,27 @@ describe('React Native ID generator', () => { }) it('generates random 64 bit ID', () => { - const idGenerator = createIdGenerator(NativeBugsnagPerformance) + const idGenerator = createIdGenerator() const id = idGenerator.generate(64) expect(id).toMatch(/^[a-f0-9]{16}$/) }) it('generates random 128 bit ID', () => { - idGenerator = createIdGenerator(NativeBugsnagPerformance) + const idGenerator = createIdGenerator() const id = idGenerator.generate(128) expect(id).toMatch(/^[a-f0-9]{32}$/) }) it('initialises the pool from the synchronous native entropy source', () => { - createIdGenerator(NativeBugsnagPerformance) + createIdGenerator() expect(requestEntropy).toHaveBeenCalled() expect(requestEntropyAsync).not.toHaveBeenCalled() }) it('regenerates the pool after 1000 calls', async () => { - const idGenerator = createIdGenerator(NativeBugsnagPerformance) + const idGenerator = createIdGenerator() for (let i = 0; i < 999; i++) { const id = idGenerator.generate(64) @@ -78,17 +70,11 @@ describe('React Native ID generator', () => { expect(requestEntropyAsync).toHaveBeenCalled() }) - it('falls back to JS entropy source if native module is null', () => { - const idGenerator = createIdGenerator(null) - const id = idGenerator.generate(64) - expect(id).toMatch(/^[a-f0-9]{16}$/) - }) - it('falls back to JS entropy source if native module returns an empty string', async () => { requestEntropy.mockReturnValueOnce('') requestEntropyAsync.mockResolvedValueOnce('') - const idGenerator = createIdGenerator(NativeBugsnagPerformance) + const idGenerator = createIdGenerator() expect(requestEntropy).toHaveBeenCalled() for (let i = 0; i < 1000; i++) { @@ -106,10 +92,30 @@ describe('React Native ID generator', () => { }) it('falls back to JS entropy source if remote debugging is enabled', () => { - const idGenerator = createIdGenerator(NativeBugsnagPerformance, true) + // ensuring the native module is "loaded" and returns some not empty value + requestEntropy.mockReturnValue('a3b9c8d7e6f5g4h2') + requestEntropyAsync.mockResolvedValue('a3b9c8d7e6f5g4h2') + + const idGenerator = createIdGenerator(true) expect(requestEntropy).not.toHaveBeenCalled() const id = idGenerator.generate(64) expect(id).toMatch(/^[a-f0-9]{16}$/) }) }) + + // Test written here so it does not clash with native module jest override + describe('React Native turbomodule is not null so implementation uses turbomodule', () => { + it('getDeviceInfo returns expected values', () => { + expect(NativeBugsnagPerformance.getDeviceInfo()).toStrictEqual({ + arch: 'arm64', + model: 'iPhone14,1', + bundleVersion: '12345', + bundleIdentifier: 'my.cool.app' + }) + }) + + it('getNativeConstants returns expected values', () => { + expect(NativeBugsnagPerformance.getNativeConstants()).toStrictEqual({ CacheDir: '/mock/CacheDir', DocumentDir: '/mock/DocumentDir' }) + }) + }) }) diff --git a/packages/platforms/react-native/tests/native.test.ts b/packages/platforms/react-native/tests/native.test.ts new file mode 100644 index 000000000..ed2b07625 --- /dev/null +++ b/packages/platforms/react-native/tests/native.test.ts @@ -0,0 +1,53 @@ +import NativeBugsnagPerformance from '../lib/native' + +jest.mock('react-native', () => { + return { + NativeModules: {}, + TurboModuleRegistry: { + get () { + return null + } + } + } +}) + +describe('React Native turbomodule is null so implementation falls back to stub functions', () => { + afterAll(() => { + jest.resetModules() + }) + it('getDeviceInfo returns undefined', () => { + expect(NativeBugsnagPerformance.getDeviceInfo()).toBeUndefined() + }) + + it('requestEntropy returns an empty string', () => { + expect(NativeBugsnagPerformance.requestEntropy()).toBe('') + }) + + it('requestEntropyAsync returns an empty string', async () => { + expect(await NativeBugsnagPerformance.requestEntropyAsync()).toBe('') + }) + + it('getNativeConstants returns an empty string', () => { + expect(NativeBugsnagPerformance.getNativeConstants()).toStrictEqual({ CacheDir: '', DocumentDir: '' }) + }) + + it('exists returns false', async () => { + expect(await NativeBugsnagPerformance.exists('')).toBe(false) + }) + + it('isDir returns false', async () => { + expect(await NativeBugsnagPerformance.isDir('')).toBe(false) + }) + + it('ls returns an empty array', async () => { + expect(await NativeBugsnagPerformance.ls('')).toStrictEqual([]) + }) + + it('mkdir returns an empty string', async () => { + expect(await NativeBugsnagPerformance.mkdir('')).toBe('') + }) + + it('readFile returns an empty string', async () => { + expect(await NativeBugsnagPerformance.readFile('', '')).toBe('') + }) +}) diff --git a/packages/platforms/react-native/tests/persistence/file-based.test.ts b/packages/platforms/react-native/tests/persistence/file-based.test.ts index e89d34be0..0f7ecbf2a 100644 --- a/packages/platforms/react-native/tests/persistence/file-based.test.ts +++ b/packages/platforms/react-native/tests/persistence/file-based.test.ts @@ -1,7 +1,8 @@ import { File, ReadOnlyFile } from '../../lib/persistence/file' import FileBasedPersistence from '../../lib/persistence/file-based' -import { FileSystem } from 'react-native-file-access' import { Platform } from 'react-native' +// eslint-disable-next-line jest/no-mocks-import +import { FileSystem } from '../../__mocks__/file-native' const PATH = '/test/path/persistent-state.json' const NATIVE_DEVICE_ID_PATH_IOS = '/mock/CacheDir/bugsnag-shared-my.cool.app/device-id.json' @@ -16,7 +17,6 @@ describe('FileBasedPersistence', () => { beforeEach(() => { // reset the FileSystem mock between tests, otherwise they will interfere // with each other - // @ts-expect-error this exists on 'FileSystemMock' (see '__mocks__') FileSystem.filesystem = new Map() }) diff --git a/packages/platforms/react-native/tests/persistence/file.test.ts b/packages/platforms/react-native/tests/persistence/file.test.ts index f52c5b75f..8e905151c 100644 --- a/packages/platforms/react-native/tests/persistence/file.test.ts +++ b/packages/platforms/react-native/tests/persistence/file.test.ts @@ -1,10 +1,10 @@ import { File, ReadOnlyFile, NullFile } from '../../lib/persistence/file' -import { FileSystem } from 'react-native-file-access' +// eslint-disable-next-line jest/no-mocks-import +import { FileSystem } from '../../__mocks__/file-native' beforeEach(() => { // reset the FileSystem mock between tests, otherwise they will interfere // with each other - // @ts-expect-error this exists on 'FileSystemMock' (see '__mocks__') FileSystem.filesystem = new Map() }) diff --git a/packages/platforms/react-native/tests/persistence/index.test.ts b/packages/platforms/react-native/tests/persistence/index.test.ts index df7bcf1a0..997bfe7e0 100644 --- a/packages/platforms/react-native/tests/persistence/index.test.ts +++ b/packages/platforms/react-native/tests/persistence/index.test.ts @@ -1,6 +1,7 @@ import persistenceFactory from '../../lib/persistence' import FileBasedPersistence from '../../lib/persistence/file-based' -import { FileSystem } from 'react-native-file-access' +// eslint-disable-next-line jest/no-mocks-import +import { FileSystem } from '../../__mocks__/file-native' const EXPECTED_PATH = '/mock/CacheDir/bugsnag-performance-react-native/v1/persisted-state.json' @@ -8,7 +9,6 @@ describe('persistenceFactory', () => { beforeEach(() => { // reset the FileSystem mock between tests, otherwise they will interfere // with each other - // @ts-expect-error this exists on 'FileSystemMock' (see '__mocks__') FileSystem.filesystem = new Map() }) diff --git a/packages/platforms/react-native/tests/resource-attributes-source.test.ts b/packages/platforms/react-native/tests/resource-attributes-source.test.ts index c63914ef7..38bb6682c 100644 --- a/packages/platforms/react-native/tests/resource-attributes-source.test.ts +++ b/packages/platforms/react-native/tests/resource-attributes-source.test.ts @@ -3,12 +3,12 @@ import { createConfiguration } from '@bugsnag/js-performance-test-utilities' import { Platform } from 'react-native' import type { ReactNativeConfiguration } from '../lib/config' import resourceAttributesSourceFactory from '../lib/resource-attributes-source' -import NativeBugsnagPerformance from '../lib/NativeBugsnagPerformance' +import NativeBugsnagPerformance from '../lib/native' describe('resourceAttributesSource', () => { it('includes all expected attributes (iOS)', async () => { const configuration = createConfiguration({ releaseStage: 'test', appVersion: '1.0.0', codeBundleId: '12345678' }) - const deviceInfo = NativeBugsnagPerformance?.getDeviceInfo() + const deviceInfo = NativeBugsnagPerformance.getDeviceInfo() const resourceAttributesSource = resourceAttributesSourceFactory(new InMemoryPersistence(), deviceInfo) const resourceAttributes = await resourceAttributesSource(configuration) const jsonAttributes = resourceAttributes.toJson() @@ -39,7 +39,7 @@ describe('resourceAttributesSource', () => { // our Platform mock (see '__mocks__/react-native.ts') await Platform.bugsnagWithTestPlatformSetTo('android', async () => { const configuration = createConfiguration({ releaseStage: 'test', appVersion: '1.0.0', codeBundleId: '12345678' }) - const deviceInfo = NativeBugsnagPerformance?.getDeviceInfo() + const deviceInfo = NativeBugsnagPerformance.getDeviceInfo() const resourceAttributesSource = resourceAttributesSourceFactory(new InMemoryPersistence(), deviceInfo) const resourceAttributes = await resourceAttributesSource(configuration) const jsonAttributes = resourceAttributes.toJson() From 2b4e6c567de6b72154aa27c527e18249988833b8 Mon Sep 17 00:00:00 2001 From: Yousif Ahmed Date: Tue, 14 Jan 2025 10:59:43 +0000 Subject: [PATCH 7/8] chore(react-native): add missing java import --- .../android/performance/NativeBugsnagPerformanceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java index c0ed6b625..1368c5681 100644 --- a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java +++ b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java @@ -13,6 +13,7 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import java.io.File; From b021ccab60e715daf5cad95203bb4067aac377f9 Mon Sep 17 00:00:00 2001 From: Yousif Ahmed Date: Tue, 14 Jan 2025 10:48:38 +0000 Subject: [PATCH 8/8] chore(react-native): update native module fallback and tests --- packages/platforms/react-native/lib/native.ts | 8 +++- .../react-native/tests/native.test.ts | 37 ++++++------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/packages/platforms/react-native/lib/native.ts b/packages/platforms/react-native/lib/native.ts index 7194e1b0f..cd58f0be5 100644 --- a/packages/platforms/react-native/lib/native.ts +++ b/packages/platforms/react-native/lib/native.ts @@ -22,7 +22,13 @@ const NativeBugsnagPerformance = NativeBsgModule || { mkdir: async (path: string) => '', readFile: async (path: string, encoding: string) => '', unlink: async (path: string) => { }, - writeFile: async (path: string, data: string, encoding: string) => { } + writeFile: async (path: string, data: string, encoding: string) => { }, + isNativePerformanceAvailable: () => false, + attachToNativeSDK: () => null, + startNativeSpan: (name: string, options: object) => ({ name, id: '', traceId: '', startTime: 0, parentSpanId: '' }), + endNativeSpan: async (spanId: string, traceId: string, endTime: number, attributes: object) => { }, + markNativeSpanEndTime: (spanId: string, traceId: string, endTime: number) => { }, + discardNativeSpan: async (spanId: string, traceId: string) => { } } export default NativeBugsnagPerformance as Spec diff --git a/packages/platforms/react-native/tests/native.test.ts b/packages/platforms/react-native/tests/native.test.ts index ed2b07625..384ee8676 100644 --- a/packages/platforms/react-native/tests/native.test.ts +++ b/packages/platforms/react-native/tests/native.test.ts @@ -11,43 +11,28 @@ jest.mock('react-native', () => { } }) -describe('React Native turbomodule is null so implementation falls back to stub functions', () => { +describe('NativeBugsnagPerformance', () => { afterAll(() => { jest.resetModules() }) - it('getDeviceInfo returns undefined', () => { - expect(NativeBugsnagPerformance.getDeviceInfo()).toBeUndefined() - }) - it('requestEntropy returns an empty string', () => { + it('falls back to stub functions when native module is null', async () => { + expect(NativeBugsnagPerformance.getDeviceInfo()).toBeUndefined() expect(NativeBugsnagPerformance.requestEntropy()).toBe('') - }) - - it('requestEntropyAsync returns an empty string', async () => { expect(await NativeBugsnagPerformance.requestEntropyAsync()).toBe('') - }) - - it('getNativeConstants returns an empty string', () => { expect(NativeBugsnagPerformance.getNativeConstants()).toStrictEqual({ CacheDir: '', DocumentDir: '' }) - }) - - it('exists returns false', async () => { expect(await NativeBugsnagPerformance.exists('')).toBe(false) - }) - - it('isDir returns false', async () => { expect(await NativeBugsnagPerformance.isDir('')).toBe(false) - }) - - it('ls returns an empty array', async () => { expect(await NativeBugsnagPerformance.ls('')).toStrictEqual([]) - }) - - it('mkdir returns an empty string', async () => { expect(await NativeBugsnagPerformance.mkdir('')).toBe('') - }) - - it('readFile returns an empty string', async () => { expect(await NativeBugsnagPerformance.readFile('', '')).toBe('') + await expect(NativeBugsnagPerformance.unlink('')).resolves.toBeUndefined() + await expect(NativeBugsnagPerformance.writeFile('', '', '')).resolves.toBeUndefined() + expect(NativeBugsnagPerformance.isNativePerformanceAvailable()).toBe(false) + expect(NativeBugsnagPerformance.attachToNativeSDK()).toBeNull() + expect(NativeBugsnagPerformance.startNativeSpan('', {})).toStrictEqual({ name: '', id: '', traceId: '', startTime: 0, parentSpanId: '' }) + await expect(NativeBugsnagPerformance.endNativeSpan('', '', 0, {})).resolves.toBeUndefined() + await expect(NativeBugsnagPerformance.discardNativeSpan('', '')).resolves.toBeUndefined() + expect(() => { NativeBugsnagPerformance.markNativeSpanEndTime('', '', 0) }).not.toThrow() }) })