packages = new PackageList(this).getPackages();
28 | packages.add(new RNSentryPackage());
29 | return packages;
30 | }
31 |
32 | @Override
33 | protected String getJSMainModuleName() {
34 | return "index";
35 | }
36 | };
37 |
38 | @Override
39 | public ReactNativeHost getReactNativeHost() {
40 | return mReactNativeHost;
41 | }
42 |
43 | @Override
44 | public void onCreate() {
45 | super.onCreate();
46 | SoLoader.init(this, /* native exopackage */ false);
47 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
48 | }
49 |
50 | /**
51 | * Loads Flipper in React Native templates. Call this in the onCreate method
52 | * with something like initializeFlipper(this,
53 | * getReactNativeHost().getReactInstanceManager());
54 | *
55 | * @param context
56 | * @param reactInstanceManager
57 | */
58 | private static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
59 | if (BuildConfig.DEBUG) {
60 | try {
61 | /*
62 | * We use reflection here to pick up the class that initializes Flipper, since
63 | * Flipper library is not available in release mode
64 | */
65 | Class> aClass = Class.forName("io.sentry.sample.ReactNativeFlipper");
66 | aClass.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class).invoke(null, context,
67 | reactInstanceManager);
68 | } catch (ClassNotFoundException e) {
69 | e.printStackTrace();
70 | } catch (NoSuchMethodException e) {
71 | e.printStackTrace();
72 | } catch (IllegalAccessException e) {
73 | e.printStackTrace();
74 | } catch (InvocationTargetException e) {
75 | e.printStackTrace();
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: weekly
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: "@sentry/core"
10 | versions:
11 | - 6.2.1
12 | - 6.2.2
13 | - 6.2.3
14 | - 6.2.5
15 | - 6.3.1
16 | - dependency-name: "@sentry/browser"
17 | versions:
18 | - 6.0.3
19 | - 6.1.0
20 | - 6.2.2
21 | - 6.2.3
22 | - 6.2.5
23 | - 6.3.1
24 | - dependency-name: "@sentry/tracing"
25 | versions:
26 | - 6.0.3
27 | - 6.1.0
28 | - 6.2.1
29 | - 6.2.2
30 | - 6.2.3
31 | - 6.2.5
32 | - 6.3.1
33 | - dependency-name: "@types/react-native"
34 | versions:
35 | - 0.63.48
36 | - 0.63.49
37 | - 0.63.50
38 | - 0.63.51
39 | - 0.64.0
40 | - 0.64.4
41 | - dependency-name: "@sentry/hub"
42 | versions:
43 | - 6.0.3
44 | - 6.1.0
45 | - 6.2.1
46 | - 6.2.2
47 | - 6.2.3
48 | - 6.3.1
49 | - dependency-name: "@sentry/types"
50 | versions:
51 | - 6.0.3
52 | - 6.1.0
53 | - 6.2.1
54 | - 6.2.2
55 | - 6.2.3
56 | - 6.3.1
57 | - dependency-name: eslint
58 | versions:
59 | - 7.19.0
60 | - 7.20.0
61 | - 7.21.0
62 | - 7.22.0
63 | - 7.23.0
64 | - 7.24.0
65 | - 7.25.0
66 | - dependency-name: "@sentry-internal/eslint-config-sdk"
67 | versions:
68 | - 6.0.3
69 | - 6.2.1
70 | - 6.2.2
71 | - 6.2.3
72 | - 6.3.1
73 | - dependency-name: "@sentry/utils"
74 | versions:
75 | - 6.1.0
76 | - 6.2.2
77 | - 6.2.3
78 | - 6.3.1
79 | - dependency-name: eslint-plugin-react
80 | versions:
81 | - 7.23.1
82 | - 7.23.2
83 | - dependency-name: "@sentry-internal/eslint-plugin-sdk"
84 | versions:
85 | - 6.0.3
86 | - 6.1.0
87 | - 6.2.1
88 | - 6.2.2
89 | - 6.2.3
90 | - 6.2.5
91 | - dependency-name: "@sentry/wizard"
92 | versions:
93 | - 1.2.3
94 | - dependency-name: "@sentry/integrations"
95 | versions:
96 | - 6.0.3
97 | - 6.1.0
98 | - 6.2.1
99 | - 6.2.2
100 | - 6.2.3
101 | - 6.2.5
102 | - dependency-name: "@types/jest"
103 | versions:
104 | - 26.0.21
105 | - 26.0.22
106 | - dependency-name: "@sentry/react"
107 | versions:
108 | - 6.0.3
109 | - 6.1.0
110 | - 6.2.3
111 | - dependency-name: "@types/react"
112 | versions:
113 | - 17.0.1
114 | - 17.0.2
115 | - 17.0.3
116 | - dependency-name: typescript
117 | versions:
118 | - 3.9.9
119 | - dependency-name: replace-in-file
120 | versions:
121 | - 6.2.0
122 | - package-ecosystem: gradle
123 | directory: "/android"
124 | schedule:
125 | interval: weekly
126 | open-pull-requests-limit: 10
127 | ignore:
128 | - dependency-name: io.sentry:sentry-android
129 | versions:
130 | - 4.2.0
131 |
--------------------------------------------------------------------------------
/test/integrations/release.test.ts:
--------------------------------------------------------------------------------
1 | import { addGlobalEventProcessor, getCurrentHub } from "@sentry/core";
2 | import { EventProcessor } from "@sentry/types";
3 |
4 | import { Release } from "../../src/js/integrations/release";
5 |
6 | jest.mock("@sentry/core", () => {
7 | const client = {
8 | getOptions: jest.fn(),
9 | };
10 |
11 | const hub = {
12 | getClient: () => client,
13 | getIntegration: () => Release,
14 | };
15 |
16 | return {
17 | addGlobalEventProcessor: jest.fn(),
18 | getCurrentHub: () => hub,
19 | };
20 | });
21 |
22 | jest.mock("../../src/js/wrapper", () => ({
23 | NATIVE: {
24 | fetchRelease: async () => ({
25 | build: "native_build",
26 | id: "native_id",
27 | version: "native_version",
28 | }),
29 | },
30 | }));
31 |
32 | describe("Tests the Release integration", () => {
33 | test("Uses release from native SDK if release/dist are not present in options.", async () => {
34 | const releaseIntegration = new Release();
35 |
36 | let eventProcessor: EventProcessor = () => null;
37 |
38 | // @ts-ignore Mock
39 | addGlobalEventProcessor.mockImplementation((e) => (eventProcessor = e));
40 | releaseIntegration.setupOnce();
41 |
42 | expect(addGlobalEventProcessor).toBeCalled();
43 |
44 | const client = getCurrentHub().getClient();
45 |
46 | // @ts-ignore Mock
47 | client.getOptions.mockImplementation(() => ({}));
48 |
49 | const event = await eventProcessor({});
50 |
51 | expect(event?.release).toBe(`native_id@native_version+native_build`);
52 | expect(event?.dist).toBe("native_build");
53 | });
54 |
55 | test("Uses release and dist from options", async () => {
56 | const releaseIntegration = new Release();
57 |
58 | let eventProcessor: EventProcessor = () => null;
59 |
60 | // @ts-ignore Mock
61 | addGlobalEventProcessor.mockImplementation((e) => (eventProcessor = e));
62 | releaseIntegration.setupOnce();
63 |
64 | expect(addGlobalEventProcessor).toBeCalled();
65 |
66 | const client = getCurrentHub().getClient();
67 |
68 | // @ts-ignore Mock
69 | client.getOptions.mockImplementation(() => ({
70 | dist: "options_dist",
71 | release: "options_release",
72 | }));
73 |
74 | const event = await eventProcessor({});
75 |
76 | expect(event?.release).toBe("options_release");
77 | expect(event?.dist).toBe("options_dist");
78 | });
79 |
80 | test("Uses __sentry_release and __sentry_dist over everything else.", async () => {
81 | const releaseIntegration = new Release();
82 |
83 | let eventProcessor: EventProcessor = () => null;
84 |
85 | // @ts-ignore Mock
86 | addGlobalEventProcessor.mockImplementation((e) => (eventProcessor = e));
87 | releaseIntegration.setupOnce();
88 |
89 | expect(addGlobalEventProcessor).toBeCalled();
90 |
91 | const client = getCurrentHub().getClient();
92 |
93 | // @ts-ignore Mock
94 | client.getOptions.mockImplementation(() => ({
95 | dist: "options_dist",
96 | release: "options_release",
97 | }));
98 |
99 | const event = await eventProcessor({
100 | extra: {
101 | __sentry_dist: "sentry_dist",
102 | __sentry_release: "sentry_release",
103 | },
104 | });
105 |
106 | expect(event?.release).toBe("sentry_release");
107 | expect(event?.dist).toBe("sentry_dist");
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/sample/android/app/src/debug/java/io/sentry/sample/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package io.sentry.sample;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | public class ReactNativeFlipper {
28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
29 | if (FlipperUtils.shouldEnableFlipper(context)) {
30 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
31 |
32 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
33 | client.addPlugin(new ReactFlipperPlugin());
34 | client.addPlugin(new DatabasesFlipperPlugin(context));
35 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
36 | client.addPlugin(CrashReporterPlugin.getInstance());
37 |
38 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
39 | NetworkingModule.setCustomClientBuilder(
40 | new NetworkingModule.CustomClientBuilder() {
41 | @Override
42 | public void apply(OkHttpClient.Builder builder) {
43 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
44 | }
45 | });
46 | client.addPlugin(networkFlipperPlugin);
47 | client.start();
48 |
49 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
50 | // Hence we run if after all native modules have been initialized
51 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
52 | if (reactContext == null) {
53 | reactInstanceManager.addReactInstanceEventListener(
54 | new ReactInstanceManager.ReactInstanceEventListener() {
55 | @Override
56 | public void onReactContextInitialized(ReactContext reactContext) {
57 | reactInstanceManager.removeReactInstanceEventListener(this);
58 | reactContext.runOnNativeModulesQueueThread(
59 | new Runnable() {
60 | @Override
61 | public void run() {
62 | client.addPlugin(new FrescoFlipperPlugin());
63 | }
64 | });
65 | }
66 | });
67 | } else {
68 | client.addPlugin(new FrescoFlipperPlugin());
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/sample/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto init
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto init
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :init
68 | @rem Get command-line arguments, handling Windows variants
69 |
70 | if not "%OS%" == "Windows_NT" goto win9xME_args
71 |
72 | :win9xME_args
73 | @rem Slurp the command line arguments.
74 | set CMD_LINE_ARGS=
75 | set _SKIP=2
76 |
77 | :win9xME_args_slurp
78 | if "x%~1" == "x" goto execute
79 |
80 | set CMD_LINE_ARGS=%*
81 |
82 | :execute
83 | @rem Setup the command line
84 |
85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
86 |
87 | @rem Execute Gradle
88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
89 |
90 | :end
91 | @rem End local scope for the variables with windows NT shell
92 | if "%ERRORLEVEL%"=="0" goto mainEnd
93 |
94 | :fail
95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
96 | rem the _cmd.exe /c_ return code!
97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
98 | exit /b 1
99 |
100 | :mainEnd
101 | if "%OS%"=="Windows_NT" endlocal
102 |
103 | :omega
104 |
--------------------------------------------------------------------------------
/sample/ios/sample.xcodeproj/xcshareddata/xcschemes/sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/sample/ios/sample.xcodeproj/xcshareddata/xcschemes/sample-tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sentry/react-native",
3 | "homepage": "https://github.com/getsentry/sentry-react-native",
4 | "repository": "https://github.com/getsentry/sentry-react-native",
5 | "version": "2.5.0-beta.1",
6 | "description": "Official Sentry SDK for react-native",
7 | "typings": "dist/js/index.d.ts",
8 | "types": "dist/js/index.d.ts",
9 | "typescript": {
10 | "definition": "dist/js/index.d.ts"
11 | },
12 | "main": "dist/js/index.js",
13 | "scripts": {
14 | "build": "tsc -p tsconfig.build.json",
15 | "build:watch": "tsc -p tsconfig.build.json -w --preserveWatchOutput",
16 | "clean": "rimraf dist coverage",
17 | "test": "jest",
18 | "lint": "eslint .",
19 | "test:watch": "jest --watch",
20 | "run-ios": "cd sample && yarn react-native run-ios",
21 | "run-android": "cd sample && yarn react-native run-android"
22 | },
23 | "keywords": [
24 | "react-native",
25 | "sentry",
26 | "crashreporting",
27 | "ios"
28 | ],
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "author": "Sentry",
33 | "license": "MIT",
34 | "//": [
35 | "React version to be compatible with RN",
36 | "When bumping make sure to update the version of react, i.e: https://github.com/facebook/react-native/blob/v0.56.0/package.json"
37 | ],
38 | "peerDependencies": {
39 | "react": ">=16.4.1",
40 | "react-native": ">=0.56.0"
41 | },
42 | "dependencies": {
43 | "@sentry/browser": "6.2.1",
44 | "@sentry/core": "6.2.1",
45 | "@sentry/hub": "6.2.1",
46 | "@sentry/integrations": "6.2.1",
47 | "@sentry/react": "6.2.1",
48 | "@sentry/tracing": "6.2.1",
49 | "@sentry/types": "6.2.1",
50 | "@sentry/utils": "6.2.1",
51 | "@sentry/wizard": "^1.2.2"
52 | },
53 | "devDependencies": {
54 | "@sentry-internal/eslint-config-sdk": "6.2.1",
55 | "@sentry-internal/eslint-plugin-sdk": "6.2.1",
56 | "@sentry/typescript": "^5.20.0",
57 | "@types/jest": "^26.0.15",
58 | "@types/react": "^16.9.49",
59 | "@types/react-native": "^0.64.2",
60 | "babel-jest": "^26.1.0",
61 | "eslint": "^7.6.0",
62 | "eslint-plugin-react": "^7.20.6",
63 | "eslint-plugin-react-native": "^3.8.1",
64 | "jest": "^24.9.0",
65 | "prettier": "^2.0.5",
66 | "react": ">=16.4.1",
67 | "react-native": ">=0.56.0",
68 | "replace-in-file": "^6.0.0",
69 | "rimraf": "^3.0.0",
70 | "ts-jest": "^24.3.0",
71 | "typescript": "^3.4.5"
72 | },
73 | "rnpm": {
74 | "commands": {
75 | "postlink": "node node_modules/@sentry/wizard/dist/bin.js -i reactNative -p ios android",
76 | "postunlink": "node node_modules/@sentry/wizard/dist/bin.js -i reactNative -p ios android --uninstall"
77 | },
78 | "android": {
79 | "packageInstance": "new RNSentryPackage()"
80 | },
81 | "ios": {
82 | "sharedLibraries": [
83 | "libz"
84 | ]
85 | }
86 | },
87 | "jest": {
88 | "collectCoverage": true,
89 | "preset": "react-native",
90 | "transform": {
91 | "^.+\\.(tsx)$": "/node_modules/react-native/jest/preprocessor.js",
92 | "^.+\\.(ts|tsx)$": "ts-jest"
93 | },
94 | "globals": {
95 | "__DEV__": true,
96 | "ts-jest": {
97 | "tsConfig": "./tsconfig.json",
98 | "diagnostics": false
99 | }
100 | },
101 | "moduleFileExtensions": [
102 | "ts",
103 | "tsx",
104 | "js"
105 | ],
106 | "testPathIgnorePatterns": [
107 | "/sample/"
108 | ],
109 | "testEnvironment": "node",
110 | "testMatch": [
111 | "**/*.test.(ts|tsx)"
112 | ]
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/sample/src/screens/TrackerScreen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {Button, View, StyleSheet, Text, ActivityIndicator} from 'react-native';
3 |
4 | import * as Sentry from '@sentry/react-native';
5 |
6 | /**
7 | * An example of how to add a Sentry Transaction to a React component manually.
8 | * So you can control all spans that belong to that one transaction.
9 | *
10 | * This screen calls an API to get the latest COVID-19 Data to display. We attach a span
11 | * to the fetch call and track the time it takes for Promise to resolve.
12 | */
13 | const TrackerScreen = () => {
14 | const [cases, setCases] = React.useState<{
15 | TotalConfirmed: number;
16 | TotalDeaths: number;
17 | TotalRecovered: number;
18 | } | null>(null);
19 |
20 | const loadData = () => {
21 | setCases(null);
22 |
23 | fetch('https://api.covid19api.com/summary', {
24 | method: 'GET',
25 | headers: {
26 | Accept: 'application/json',
27 | 'Content-Type': 'application/json',
28 | },
29 | })
30 | .then((response) => response.json())
31 | .then((json) => {
32 | setCases(json.Global);
33 | });
34 | };
35 |
36 | React.useEffect(() => {
37 | loadData();
38 | }, []);
39 |
40 | return (
41 |
42 |
43 | Global COVID19 Cases
44 |
45 |
46 | {cases ? (
47 | <>
48 |
53 |
58 |
63 | >
64 | ) : (
65 |
66 | )}
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default Sentry.withProfiler(TrackerScreen);
74 |
75 | const Statistic = (props: {
76 | title: string;
77 | count: number;
78 | textColor: string;
79 | }): React.ReactElement => {
80 | return (
81 |
82 | {props.title}
83 |
84 | {`${props.count}`.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')}
85 |
86 |
87 | );
88 | };
89 |
90 | const ProfiledStatistic = Sentry.withProfiler(Statistic);
91 |
92 | const styles = StyleSheet.create({
93 | screen: {
94 | flex: 1,
95 | padding: 16,
96 | },
97 | titleContainer: {
98 | paddingBottom: 12,
99 | },
100 | title: {
101 | fontSize: 24,
102 | fontWeight: '700',
103 | },
104 | card: {
105 | width: '100%',
106 | height: 240,
107 | padding: 12,
108 | borderWidth: 1,
109 | borderColor: '#79628C',
110 | borderRadius: 6,
111 | backgroundColor: '#F6F6F8',
112 | alignItems: 'center',
113 | justifyContent: 'center',
114 | },
115 | statisticContainer: {
116 | width: '100%',
117 | flex: 1,
118 | flexDirection: 'row',
119 | alignItems: 'center',
120 | justifyContent: 'space-between',
121 | },
122 | statisticTitle: {
123 | fontSize: 16,
124 | fontWeight: '500',
125 | },
126 | statisticCount: {
127 | fontSize: 16,
128 | fontWeight: '700',
129 | },
130 | });
131 |
--------------------------------------------------------------------------------
/sample/ios/sample/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/.github/workflows/buildandtest.yml:
--------------------------------------------------------------------------------
1 | name: Build & Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - trunk
7 | - release/**
8 | pull_request:
9 | branches:
10 | - "*"
11 |
12 | jobs:
13 | job_test:
14 | name: Test
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v2
18 | - uses: actions/setup-node@v1
19 |
20 | - uses: actions/cache@v2
21 | id: cache
22 | with:
23 | path: node_modules
24 | key: ${{ runner.os }}-${{ github.sha }}
25 | - name: Install Dependencies
26 | if: steps.cache.outputs['cache-hit'] != 'true'
27 | run: yarn install
28 | - name: Test
29 | run: yarn test
30 |
31 | job_lint:
32 | name: Lint
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v2
36 | - uses: actions/cache@v2
37 | id: cache
38 | with:
39 | path: node_modules
40 | key: ${{ runner.os }}-${{ github.sha }}
41 | - name: Install Dependencies
42 | if: steps.cache.outputs['cache-hit'] != 'true'
43 | run: yarn install
44 | - name: Lint
45 | run: yarn lint
46 |
47 | job_build:
48 | name: Build
49 | runs-on: ubuntu-latest
50 | steps:
51 | - uses: actions/checkout@v2
52 | - uses: actions/setup-node@v1
53 | - uses: actions/cache@v2
54 | id: cache
55 | with:
56 | path: node_modules
57 | key: ${{ runner.os }}-${{ github.sha }}
58 | - name: Install Dependencies
59 | if: steps.cache.outputs['cache-hit'] != 'true'
60 | run: yarn install
61 | - name: Build
62 | run: yarn build
63 | - name: Pack
64 | run: yarn pack
65 | - name: Archive Artifacts
66 | uses: actions/upload-artifact@v2
67 | with:
68 | name: ${{ github.sha }}
69 | path: |
70 | ${{ github.workspace }}/*.tgz
71 |
72 | job_bundle:
73 | name: Bundle
74 | runs-on: ubuntu-latest
75 | needs: [job_test, job_build]
76 | strategy:
77 | # we want that the matrix keeps running, default is to cancel them if it fails.
78 | fail-fast: false
79 | matrix:
80 | platform: ["ios", "android"]
81 | dev: [true, false]
82 | steps:
83 | - uses: actions/checkout@v2
84 | - uses: actions/setup-node@v1
85 | with:
86 | node-version: '12'
87 | - name: Cache Dependencies
88 | uses: actions/cache@v2
89 | id: cache
90 | with:
91 | path: node_modules
92 | key: ${{ runner.os }}-${{ github.sha }}
93 | - name: Cache Sample Dependencies
94 | uses: actions/cache@v2
95 | id: cache-sample
96 | with:
97 | path: sample/node_modules
98 | key: ${{ runner.os }}-${{ hashFiles('sample/yarn.lock') }}
99 | - name: Install Dependencies
100 | if: steps.cache.outputs['cache-hit'] != 'true'
101 | run: yarn install
102 | - name: Install Sample Dependencies
103 | if: steps.cache-sample.outputs['cache-hit'] != 'true'
104 | run: yarn install
105 | working-directory: sample
106 | - name: Build
107 | run: yarn build
108 | - name: Make Sample Build Directory
109 | run: mkdir build
110 | working-directory: sample
111 | - name: Bundle
112 | run: |
113 | yarn run react-native bundle \
114 | --entry-file index.js \
115 | --platform ${{ matrix.platform }} \
116 | --dev ${{ matrix.dev }} \
117 | --reset-cache \
118 | --bundle-output build/bundle.${{ matrix.platform }}.js \
119 | --sourcemap-output build/bundle.${{ matrix.platform }}.map
120 | working-directory: sample
121 |
--------------------------------------------------------------------------------
/sample/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {Provider} from 'react-redux';
3 | import {
4 | NavigationContainer,
5 | NavigationContainerRef,
6 | } from '@react-navigation/native';
7 | import {createStackNavigator} from '@react-navigation/stack';
8 |
9 | // Import the Sentry React Native SDK
10 | import * as Sentry from '@sentry/react-native';
11 |
12 | import HomeScreen from './screens/HomeScreen';
13 | import TrackerScreen from './screens/TrackerScreen';
14 | import ManualTrackerScreen from './screens/ManualTrackerScreen';
15 | import PerformanceTimingScreen from './screens/PerformanceTimingScreen';
16 | import EndToEndTestsScreen from './screens/EndToEndTestsScreen';
17 | import ReduxScreen from './screens/ReduxScreen';
18 |
19 | import {store} from './reduxApp';
20 | import {version as packageVersion} from '../../package.json';
21 | import {SENTRY_INTERNAL_DSN} from './dsn';
22 |
23 | const reactNavigationV5Instrumentation = new Sentry.ReactNavigationV5Instrumentation(
24 | {
25 | routeChangeTimeoutMs: 500, // How long it will wait for the route change to complete. Default is 1000ms
26 | },
27 | );
28 |
29 | Sentry.init({
30 | // Replace the example DSN below with your own DSN:
31 | dsn: SENTRY_INTERNAL_DSN,
32 | debug: true,
33 | beforeSend: (e) => {
34 | console.log('Event beforeSend:', e);
35 | return e;
36 | },
37 | // This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
38 | onReady: ({didCallNativeInit}) => {
39 | console.log('onReady called with didCallNativeInit:', didCallNativeInit);
40 | },
41 | maxBreadcrumbs: 150, // Extend from the default 100 breadcrumbs.
42 | integrations: [
43 | new Sentry.ReactNativeTracing({
44 | idleTimeout: 5000,
45 | routingInstrumentation: reactNavigationV5Instrumentation,
46 | tracingOrigins: ['localhost', /^\//, /^https:\/\//],
47 | beforeNavigate: (context: Sentry.ReactNavigationTransactionContext) => {
48 | // Example of not sending a transaction for the screen with the name "Manual Tracker"
49 | if (context.data.route.name === 'ManualTracker') {
50 | context.sampled = false;
51 | }
52 |
53 | return context;
54 | },
55 | }),
56 | ],
57 | enableAutoSessionTracking: true,
58 | // For testing, session close when 5 seconds (instead of the default 30) in the background.
59 | sessionTrackingIntervalMillis: 5000,
60 | // This will capture ALL TRACES and likely use up all your quota
61 | tracesSampleRate: 1.0,
62 | // Sets the `release` and `dist` on Sentry events. Make sure this matches EXACTLY with the values on your sourcemaps
63 | // otherwise they will not work.
64 | release: packageVersion,
65 | dist: `${packageVersion}.0`,
66 | });
67 |
68 | const Stack = createStackNavigator();
69 |
70 | const App = () => {
71 | const navigation = React.useRef();
72 |
73 | return (
74 |
75 | {
78 | reactNavigationV5Instrumentation.registerNavigationContainer(
79 | navigation,
80 | );
81 | }}>
82 |
83 |
84 |
85 |
86 |
90 |
91 |
92 |
93 |
94 |
95 | );
96 | };
97 |
98 | export default Sentry.withTouchEventBoundary(App, {
99 | ignoreNames: ['Provider', 'UselessName', /^SomeRegex/],
100 | });
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | _Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us [**Check out our open positions**](https://sentry.io/careers/)_
8 |
9 | Sentry SDK for React Native
10 | ===========================
11 |
12 | [](https://travis-ci.com/getsentry/sentry-react-native)
13 | [](https://github.com/getsentry/sentry-react-native/actions?query=workflow%3A"End-to-End%20Tests")
14 | [](https://www.npmjs.com/package/@sentry/react-native)
15 | [](https://www.npmjs.com/package/@sentry/react-native)
16 | [](https://www.npmjs.com/package/@sentry/react-native)
17 | [](https://discord.gg/PXa5Apfe7K)
18 |
19 | ## Requirements
20 |
21 | - `react-native >= 0.56.0`
22 |
23 | ## Features
24 |
25 | - Automatic JS Error Tracking (using [@sentry/browser](https://github.com/getsentry/sentry-javascript))
26 | - Automatic Native Crash Error Tracking (using [sentry-cocoa](https://github.com/getsentry/sentry-cocoa) & [sentry-android](https://github.com/getsentry/sentry-java) under the hood)
27 | - Offline storage of events
28 | - On Device symbolication for JS (in Debug)
29 | - [Autolinking](https://facebook.github.io/react-native/blog/2019/07/03/version-60#native-modules-are-now-autolinked)
30 | - Events with enriched device data
31 | - RAM bundle support
32 | - Hermes support
33 | - Expo support ([sentry-expo](https://github.com/expo/sentry-expo))
34 |
35 | ## Installation and Usage
36 |
37 | To install the package:
38 |
39 | ```sh
40 | npm install --save @sentry/react-native
41 | # OR
42 | yarn add @sentry/react-native
43 | ```
44 |
45 | If you are using a version of React Native <= 0.60.x link the package using `react-native`.
46 |
47 | ```sh
48 | react-native link @sentry/react-native
49 | # OR, if self hosting
50 | SENTRY_WIZARD_URL=http://sentry.acme.com/ react-native link @sentry/react-native
51 | ```
52 |
53 | How to use it:
54 |
55 | ```javascript
56 | import * as Sentry from "@sentry/react-native";
57 |
58 | Sentry.init({
59 | dsn: "__DSN__"
60 | });
61 |
62 | Sentry.setTag("myTag", "tag-value");
63 | Sentry.setExtra("myExtra", "extra-value");
64 | Sentry.addBreadcrumb({ message: "test" });
65 |
66 | Sentry.captureMessage("Hello Sentry!");
67 | ```
68 |
69 | ## Upgrade
70 |
71 | If you are coming from `react-native-sentry` which was our SDK `< 1.0` you should follow the [upgrade guide](https://docs.sentry.io/platforms/react-native/#upgrading-from-react-native-sentry) and then follow the [install steps](https://docs.sentry.io/platforms/react-native/#integrating-the-sdk).
72 |
73 | ## Blog posts
74 |
75 | [Performance Monitoring Support for React Native](https://blog.sentry.io/2021/03/11/performance-monitoring-support-for-react-native/?utm_source=github&utm_medium=readme&utm_campaign=sentry-react-native).
76 |
77 | ## Resources
78 |
79 | * [](https://docs.sentry.io/platforms/react-native/)
80 | * [](https://forum.sentry.io/c/sdks)
81 | * [](https://discord.gg/PXa5Apfe7K)
82 | * [](http://stackoverflow.com/questions/tagged/sentry)
83 | * [](https://github.com/getsentry/.github/blob/master/CODE_OF_CONDUCT.md)
84 | * [](https://twitter.com/intent/follow?screen_name=getsentry)
85 |
--------------------------------------------------------------------------------
/src/js/backend.ts:
--------------------------------------------------------------------------------
1 | import { BrowserBackend } from "@sentry/browser/dist/backend";
2 | import { BaseBackend, NoopTransport } from "@sentry/core";
3 | import { BrowserOptions, Transports } from "@sentry/react";
4 | import { Event, EventHint, Severity, Transport } from "@sentry/types";
5 | // @ts-ignore LogBox introduced in RN 0.63
6 | import { Alert, LogBox, YellowBox } from "react-native";
7 |
8 | import { ReactNativeOptions } from "./options";
9 | import { NativeTransport } from "./transports/native";
10 | import { NATIVE } from "./wrapper";
11 |
12 | /** The Sentry ReactNative SDK Backend. */
13 | export class ReactNativeBackend extends BaseBackend {
14 | private readonly _browserBackend: BrowserBackend;
15 |
16 | /** Creates a new ReactNative backend instance. */
17 | public constructor(protected readonly _options: ReactNativeOptions) {
18 | super(_options);
19 | this._browserBackend = new BrowserBackend(_options);
20 |
21 | // This is a workaround for now using fetch on RN, this is a known issue in react-native and only generates a warning
22 | // YellowBox deprecated and replaced with with LogBox in RN 0.63
23 | if (LogBox) {
24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
25 | LogBox.ignoreLogs(["Require cycle:"]);
26 | } else {
27 | // eslint-disable-next-line deprecation/deprecation
28 | YellowBox.ignoreWarnings(["Require cycle:"]);
29 | }
30 |
31 | void this._startWithOptions();
32 | }
33 |
34 | /**
35 | * If native client is available it will trigger a native crash.
36 | * Use this only for testing purposes.
37 | */
38 | public nativeCrash(): void {
39 | if (this._options.enableNative) {
40 | NATIVE.crash();
41 | }
42 | }
43 |
44 | /**
45 | * @inheritDoc
46 | */
47 | public eventFromException(
48 | exception: unknown,
49 | hint?: EventHint
50 | ): PromiseLike {
51 | return this._browserBackend.eventFromException(exception, hint);
52 | }
53 |
54 | /**
55 | * @inheritDoc
56 | */
57 | public eventFromMessage(
58 | message: string,
59 | level: Severity = Severity.Info,
60 | hint?: EventHint
61 | ): PromiseLike {
62 | return this._browserBackend.eventFromMessage(message, level, hint);
63 | }
64 |
65 | /**
66 | * @inheritDoc
67 | */
68 | protected _setupTransport(): Transport {
69 | if (!this._options.dsn) {
70 | // We return the noop transport here in case there is no Dsn.
71 | return new NoopTransport();
72 | }
73 |
74 | const transportOptions = {
75 | ...this._options.transportOptions,
76 | dsn: this._options.dsn,
77 | };
78 |
79 | if (this._options.transport) {
80 | return new this._options.transport(transportOptions);
81 | }
82 |
83 | if (this._isNativeTransportAvailable()) {
84 | return new NativeTransport();
85 | }
86 |
87 | return new Transports.FetchTransport(transportOptions);
88 | }
89 |
90 | /**
91 | * If true, native client is availabe and active
92 | */
93 | private _isNativeTransportAvailable(): boolean {
94 | return (
95 | this._options.enableNative === true &&
96 | NATIVE.isNativeClientAvailable() &&
97 | NATIVE.isNativeTransportAvailable()
98 | );
99 | }
100 |
101 | /**
102 | * Starts native client with dsn and options
103 | */
104 | private async _startWithOptions(): Promise {
105 | let didCallNativeInit = false;
106 |
107 | try {
108 | didCallNativeInit = await NATIVE.startWithOptions(this._options);
109 | } catch (_) {
110 | this._showCannotConnectDialog();
111 |
112 | this._options.onReady?.({ didCallNativeInit: false });
113 |
114 | return;
115 | }
116 |
117 | this._options.onReady?.({ didCallNativeInit });
118 | }
119 |
120 | /**
121 | * If the user is in development mode, and the native nagger is enabled then it will show an alert.
122 | */
123 | private _showCannotConnectDialog(): void {
124 | if (__DEV__ && this._options.enableNativeNagger) {
125 | Alert.alert(
126 | "Sentry",
127 | "Warning, could not connect to Sentry native SDK.\nIf you do not want to use the native component please pass `enableNative: false` in the options.\nVisit: https://docs.sentry.io/platforms/react-native/#linking for more details."
128 | );
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/sample/test/e2e.test.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable: no-unsafe-any
2 | import wd from 'wd';
3 |
4 | import {fetchEvent} from '../utils/fetchEvent';
5 |
6 | // 10 minutes timeout.
7 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 3e5;
8 | const PORT = 4723;
9 |
10 | const driver = wd.promiseChainRemote('localhost', PORT);
11 |
12 | // 20 min timeout why not
13 | jest.setTimeout(1.2e6);
14 |
15 | beforeAll(async () => {
16 | const config =
17 | process.env.PLATFORM === 'android'
18 | ? {
19 | platformName: 'Android',
20 |
21 | deviceName: 'Android Emulator',
22 |
23 | app: './android/app/build/outputs/apk/release/app-release.apk',
24 | newCommandTimeout: 600000,
25 | }
26 | : {
27 | app: 'io.sentry.sample',
28 | deviceName: 'iPhone 12',
29 | platformName: 'iOS',
30 | newCommandTimeout: 600000,
31 | automationName: 'XCUITest',
32 | };
33 |
34 | await driver.init(config);
35 | await driver.sleep(10000);
36 |
37 | expect(await driver.hasElementByAccessibilityId('openEndToEndTests')).toBe(
38 | true,
39 | );
40 | const element = await driver.elementByAccessibilityId('openEndToEndTests');
41 | await element.click();
42 | await driver.sleep(2000);
43 | });
44 |
45 | beforeEach(async () => {
46 | await driver.hasElementByAccessibilityId('clearEventId');
47 | const element = await driver.elementByAccessibilityId('clearEventId');
48 | await element.click();
49 | await driver.sleep(2000);
50 | });
51 |
52 | describe('End to end tests for common events', () => {
53 | test('captureMessage', async () => {
54 | expect(await driver.hasElementByAccessibilityId('captureMessage')).toBe(
55 | true,
56 | );
57 |
58 | const element = await driver.elementByAccessibilityId('captureMessage');
59 | await element.click();
60 |
61 | await driver.sleep(100);
62 |
63 | expect(await driver.hasElementByAccessibilityId('eventId')).toBe(true);
64 |
65 | const eventIdElement = await driver.elementByAccessibilityId('eventId');
66 | const eventId = await eventIdElement.text();
67 |
68 | await driver.sleep(10000);
69 |
70 | const sentryEvent = await fetchEvent(eventId);
71 |
72 | expect(sentryEvent.eventID).toMatch(eventId);
73 | });
74 |
75 | test('captureException', async () => {
76 | expect(await driver.hasElementByAccessibilityId('captureException')).toBe(
77 | true,
78 | );
79 |
80 | const element = await driver.elementByAccessibilityId('captureException');
81 | await element.click();
82 |
83 | await driver.sleep(100);
84 |
85 | expect(await driver.hasElementByAccessibilityId('eventId')).toBe(true);
86 |
87 | const eventIdElement = await driver.elementByAccessibilityId('eventId');
88 | const eventId = await eventIdElement.text();
89 |
90 | await driver.sleep(10000);
91 |
92 | const sentryEvent = await fetchEvent(eventId);
93 |
94 | expect(sentryEvent.eventID).toMatch(eventId);
95 | });
96 |
97 | test('unhandledPromiseRejection', async () => {
98 | expect(
99 | await driver.hasElementByAccessibilityId('unhandledPromiseRejection'),
100 | ).toBe(true);
101 |
102 | const element = await driver.elementByAccessibilityId(
103 | 'unhandledPromiseRejection',
104 | );
105 | await element.click();
106 |
107 | // Promises needs a while to fail
108 | await driver.sleep(5000);
109 |
110 | expect(await driver.hasElementByAccessibilityId('eventId')).toBe(true);
111 |
112 | const eventIdElement = await driver.elementByAccessibilityId('eventId');
113 | const eventId = await eventIdElement.text();
114 |
115 | await driver.sleep(10000);
116 |
117 | const sentryEvent = await fetchEvent(eventId);
118 |
119 | expect(sentryEvent.eventID).toMatch(eventId);
120 | });
121 |
122 | test('close', async () => {
123 | expect(await driver.hasElementByAccessibilityId('close')).toBe(true);
124 |
125 | const element = await driver.elementByAccessibilityId('close');
126 | await element.click();
127 |
128 | // Wait a while in case
129 | await driver.sleep(5000);
130 |
131 | // This time we don't expect an eventId
132 | expect(await driver.hasElementByAccessibilityId('eventId')).toBe(true);
133 | const eventIdElement = await driver.elementByAccessibilityId('eventId');
134 | const eventId = await eventIdElement.text();
135 |
136 | expect(eventId).toBe('');
137 | });
138 | });
139 |
--------------------------------------------------------------------------------
/sample/src/screens/ManualTrackerScreen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {Button, View, StyleSheet, Text, ActivityIndicator} from 'react-native';
3 |
4 | import * as Sentry from '@sentry/react-native';
5 |
6 | /**
7 | * An example of how to add a Sentry Transaction to a React component manually.
8 | * So you can control all spans that belong to that one transaction.
9 | *
10 | * This screen calls an API to get the latest COVID-19 Data to display. We attach a span
11 | * to the fetch call and track the time it takes for Promise to resolve.
12 | */
13 | const TrackerScreen = () => {
14 | const [cases, setCases] = React.useState<{
15 | TotalConfirmed: number;
16 | TotalDeaths: number;
17 | TotalRecovered: number;
18 | } | null>(null);
19 |
20 | const transaction = React.useRef(null);
21 |
22 | React.useEffect(() => {
23 | // Initialize the transaction for the screen.
24 | transaction.current = Sentry.startTransaction({
25 | name: 'Tracker Screen',
26 | op: 'navigation',
27 | });
28 |
29 | return () => {
30 | // Finishing the transaction triggers sending the data to Sentry.
31 | transaction.current?.finish();
32 | transaction.current = null;
33 | Sentry.configureScope((scope) => {
34 | scope.setSpan(undefined);
35 | });
36 | };
37 | }, []);
38 |
39 | const loadData = () => {
40 | setCases(null);
41 |
42 | // Create a child span for the API call.
43 | const span = transaction.current?.startChild({
44 | op: 'http',
45 | description: 'Fetch Covid19 data from API',
46 | });
47 |
48 | fetch('https://api.covid19api.com/summary', {
49 | method: 'GET',
50 | headers: {
51 | Accept: 'application/json',
52 | 'Content-Type': 'application/json',
53 | },
54 | })
55 | .then((response) => response.json())
56 | .then((json) => {
57 | setCases(json.Global);
58 |
59 | span?.setData('json', json);
60 | span?.finish();
61 | });
62 | };
63 |
64 | React.useEffect(() => {
65 | loadData();
66 | }, []);
67 |
68 | return (
69 |
70 |
71 | Global COVID19 Cases
72 |
73 |
74 | {cases ? (
75 | <>
76 |
81 |
86 |
91 | >
92 | ) : (
93 |
94 | )}
95 |
96 |
97 |
98 | );
99 | };
100 |
101 | export default Sentry.withProfiler(TrackerScreen);
102 |
103 | const Statistic = (props: {
104 | title: string;
105 | count: number;
106 | textColor: string;
107 | }): React.ReactElement => {
108 | return (
109 |
110 | {props.title}
111 |
112 | {`${props.count}`.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')}
113 |
114 |
115 | );
116 | };
117 |
118 | const styles = StyleSheet.create({
119 | screen: {
120 | flex: 1,
121 | padding: 16,
122 | },
123 | titleContainer: {
124 | paddingBottom: 12,
125 | },
126 | title: {
127 | fontSize: 24,
128 | fontWeight: '700',
129 | },
130 | card: {
131 | width: '100%',
132 | height: 240,
133 | padding: 12,
134 | borderWidth: 1,
135 | borderColor: '#79628C',
136 | borderRadius: 6,
137 | backgroundColor: '#F6F6F8',
138 | alignItems: 'center',
139 | justifyContent: 'center',
140 | },
141 | statisticContainer: {
142 | width: '100%',
143 | flex: 1,
144 | flexDirection: 'row',
145 | alignItems: 'center',
146 | justifyContent: 'space-between',
147 | },
148 | statisticTitle: {
149 | fontSize: 16,
150 | fontWeight: '500',
151 | },
152 | statisticCount: {
153 | fontSize: 16,
154 | fontWeight: '700',
155 | },
156 | });
157 |
--------------------------------------------------------------------------------
/test/backend.test.ts:
--------------------------------------------------------------------------------
1 | import * as RN from "react-native";
2 |
3 | import { ReactNativeBackend } from "../src/js/backend";
4 | import { NATIVE } from "../src/js/wrapper";
5 |
6 | const EXAMPLE_DSN =
7 | "https://6890c2f6677340daa4804f8194804ea2@o19635.ingest.sentry.io/148053";
8 |
9 | jest.mock(
10 | "react-native",
11 | () => ({
12 | NativeModules: {
13 | RNSentry: {
14 | crash: jest.fn(),
15 | nativeClientAvailable: true,
16 | nativeTransport: true,
17 |
18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
19 | startWithOptions: async (options: any): Promise => {
20 | if (typeof options.dsn !== "string") {
21 | throw new Error();
22 | }
23 | return true;
24 | },
25 | },
26 | },
27 | Platform: {
28 | OS: "mock",
29 | },
30 | LogBox: {
31 | ignoreLogs: jest.fn(),
32 | },
33 | YellowBox: {
34 | ignoreWarnings: jest.fn(),
35 | },
36 | }),
37 | /* virtual allows us to mock modules that aren't in package.json */
38 | { virtual: true }
39 | );
40 |
41 | afterEach(() => {
42 | jest.resetAllMocks();
43 | NATIVE.enableNative = true;
44 | });
45 |
46 | describe("Tests ReactNativeBackend", () => {
47 | describe("initializing the backend", () => {
48 | test("backend initializes", async () => {
49 | const backend = new ReactNativeBackend({
50 | dsn: EXAMPLE_DSN,
51 | enableNative: true,
52 | });
53 |
54 | await expect(backend.eventFromMessage("test")).resolves.toBeDefined();
55 | // @ts-ignore: Is Mocked
56 | // eslint-disable-next-line @typescript-eslint/unbound-method
57 | await expect(RN.LogBox.ignoreLogs).toBeCalled();
58 | });
59 |
60 | test("invalid dsn is thrown", () => {
61 | try {
62 | new ReactNativeBackend({
63 | dsn: "not a dsn",
64 | enableNative: true,
65 | });
66 | } catch (e) {
67 | expect(e.message).toBe("Invalid Dsn");
68 | }
69 | });
70 |
71 | test("undefined dsn doesn't crash", () => {
72 | expect(() => {
73 | const backend = new ReactNativeBackend({
74 | dsn: undefined,
75 | enableNative: true,
76 | });
77 |
78 | return expect(backend.eventFromMessage("test")).resolves.toBeDefined();
79 | }).not.toThrow();
80 | });
81 |
82 | test("falls back to YellowBox if no LogBox", async () => {
83 | // @ts-ignore: Is Mocked
84 | RN.LogBox = undefined;
85 |
86 | const backend = new ReactNativeBackend({
87 | dsn: EXAMPLE_DSN,
88 | enableNative: true,
89 | });
90 |
91 | await expect(backend.eventFromMessage("test")).resolves.toBeDefined();
92 | // eslint-disable-next-line deprecation/deprecation
93 | await expect(RN.YellowBox.ignoreWarnings).toBeCalled();
94 | });
95 | });
96 |
97 | describe("onReady", () => {
98 | test("calls onReady callback with true if Native SDK is initialized", (done) => {
99 | new ReactNativeBackend({
100 | dsn: EXAMPLE_DSN,
101 | enableNative: true,
102 | onReady: ({ didCallNativeInit }) => {
103 | expect(didCallNativeInit).toBe(true);
104 |
105 | done();
106 | },
107 | });
108 | });
109 |
110 | test("calls onReady callback with false if Native SDK was not initialized", (done) => {
111 | new ReactNativeBackend({
112 | dsn: EXAMPLE_DSN,
113 | enableNative: false,
114 | onReady: ({ didCallNativeInit }) => {
115 | expect(didCallNativeInit).toBe(false);
116 |
117 | done();
118 | },
119 | });
120 | });
121 |
122 | test("calls onReady callback with false if Native SDK failed to initialize", (done) => {
123 | const RN = require("react-native");
124 |
125 | RN.NativeModules.RNSentry.startWithOptions = async () => {
126 | throw new Error();
127 | };
128 |
129 | new ReactNativeBackend({
130 | dsn: EXAMPLE_DSN,
131 | enableNative: true,
132 | onReady: ({ didCallNativeInit }) => {
133 | expect(didCallNativeInit).toBe(false);
134 |
135 | done();
136 | },
137 | });
138 | });
139 | });
140 |
141 | describe("nativeCrash", () => {
142 | test("calls NativeModules crash", () => {
143 | const RN = require("react-native");
144 |
145 | const backend = new ReactNativeBackend({
146 | enableNative: true,
147 | });
148 | backend.nativeCrash();
149 |
150 | expect(RN.NativeModules.RNSentry.crash).toBeCalled();
151 | });
152 | });
153 | });
154 |
--------------------------------------------------------------------------------
/.github/workflows/e2e.yml:
--------------------------------------------------------------------------------
1 | name: End-to-End Tests
2 |
3 | on:
4 | push:
5 | branches: [trunk]
6 | pull_request:
7 | branches:
8 | - "*"
9 |
10 | env:
11 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
12 |
13 | jobs:
14 | android:
15 | # Android emulator said to perform best with macos HAXM
16 | runs-on: macos-latest
17 | env:
18 | PLATFORM: android
19 | steps:
20 | - uses: actions/checkout@v2
21 | - uses: actions/setup-node@v1
22 | with:
23 | node-version: '12'
24 | - name: Install Global Dependencies
25 | run: yarn global add react-native-cli @sentry/cli
26 | - uses: actions/cache@v2
27 | id: deps-cache
28 | with:
29 | path: |
30 | node_modules
31 | sample/node_modules
32 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
33 | - name: Install Dependencies
34 | if: steps.deps-cache.outputs['cache-hit'] != 'true'
35 | run: yarn install
36 | - name: Install Sample Dependencies
37 | if: steps.deps-cache.outputs['cache-hit'] != 'true'
38 | working-directory: ./sample
39 | run: yarn install
40 | - name: Build SDK
41 | run: yarn build
42 | - name: Start Appium Server
43 | working-directory: ./sample
44 | run: yarn run appium --log-timestamp --log-no-colors --allow-insecure chromedriver_autodownload > appium.log &
45 | - name: Run Android Emulator
46 | uses: reactivecircus/android-emulator-runner@v2
47 | with:
48 | api-level: 29
49 | # All the tests need to be run in this script as the emulator is killed after this command.
50 | script: cd sample && react-native run-android --variant=release && curl --output /dev/null --silent --head --fail http://127.0.0.1:4723/wd/hub/sessions && yarn test
51 | - name: Upload Appium logs
52 | # This condition is so it uploads the logs always regardless of whether the previous step succeeded or not
53 | # otherwise it would not run if the previous step failed
54 | if: ${{ always() }}
55 | uses: actions/upload-artifact@v1
56 | with:
57 | name: appium.android.log
58 | path: ./sample/appium.log
59 | ios:
60 | runs-on: macos-latest
61 | env:
62 | PLATFORM: ios
63 | steps:
64 | - uses: actions/checkout@v2
65 | - uses: actions/setup-node@v1
66 | with:
67 | node-version: '12'
68 | - name: Install Global Dependencies
69 | run: yarn global add react-native-cli @sentry/cli
70 | - uses: actions/cache@v2
71 | id: deps-cache
72 | with:
73 | path: |
74 | node_modules
75 | sample/node_modules
76 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
77 | - uses: actions/cache@v2
78 | id: pods-cache
79 | with:
80 | path: |
81 | sample/ios/Pods
82 | ~/Library/Developer/Xcode/DerivedData
83 | key: ${{ runner.os }}-${{ hashFiles('sample/ios/Podfile.lock') }}
84 | - name: Install Dependencies
85 | if: steps.deps-cache.outputs['cache-hit'] != 'true'
86 | run: yarn install
87 | - name: Install Sample Dependencies
88 | if: steps.deps-cache.outputs['cache-hit'] != 'true'
89 | working-directory: ./sample
90 | run: yarn install
91 | - name: Build SDK
92 | run: yarn build
93 | - name: Install iOS pods
94 | # Even though we cache the pods, we call it regardless
95 | working-directory: ./sample/ios
96 | run: pod install
97 | - name: Start Appium Server
98 | working-directory: ./sample
99 | run: yarn run appium --log-timestamp --log-no-colors --allow-insecure chromedriver_autodownload > appium.log &
100 | - name: Build and run iOS emulator
101 | working-directory: ./sample
102 | run: react-native run-ios --configuration Release --simulator "iPhone 12"
103 | # Ping the Appium server to make sure its running, this way if it does fail it'll be easy to tell that this step failed and not the tests
104 | - name: Ping Appium Server
105 | run: curl --output /dev/null --silent --head --fail http://127.0.0.1:4723/wd/hub/sessions
106 | - name: Run Tests
107 | working-directory: ./sample
108 | run: yarn test
109 | - name: Upload Appium logs
110 | # This condition is so it uploads the logs always regardless of whether the previous step succeeded or not
111 | # otherwise it would not run if the previous step failed
112 | if: ${{ always() }}
113 | uses: actions/upload-artifact@v1
114 | with:
115 | name: appium.ios.log
116 | path: ./sample/appium.log
117 |
--------------------------------------------------------------------------------
/src/js/sdk.ts:
--------------------------------------------------------------------------------
1 | import { initAndBind, setExtra } from "@sentry/core";
2 | import { Hub, makeMain } from "@sentry/hub";
3 | import { RewriteFrames } from "@sentry/integrations";
4 | import { defaultIntegrations, getCurrentHub } from "@sentry/react";
5 | import { StackFrame } from "@sentry/types";
6 | import { getGlobalObject, logger } from "@sentry/utils";
7 |
8 | import { ReactNativeClient } from "./client";
9 | import {
10 | DebugSymbolicator,
11 | DeviceContext,
12 | ReactNativeErrorHandlers,
13 | Release,
14 | } from "./integrations";
15 | import { ReactNativeOptions } from "./options";
16 | import { ReactNativeScope } from "./scope";
17 |
18 | const IGNORED_DEFAULT_INTEGRATIONS = [
19 | "GlobalHandlers", // We will use the react-native internal handlers
20 | "TryCatch", // We don't need this
21 | ];
22 | const DEFAULT_OPTIONS: ReactNativeOptions = {
23 | enableNative: true,
24 | enableNativeCrashHandling: true,
25 | enableNativeNagger: true,
26 | autoInitializeNativeSdk: true,
27 | };
28 |
29 | /**
30 | * Inits the SDK
31 | */
32 | export function init(passedOptions: ReactNativeOptions): void {
33 | const reactNativeHub = new Hub(undefined, new ReactNativeScope());
34 | makeMain(reactNativeHub);
35 |
36 | const options = {
37 | ...DEFAULT_OPTIONS,
38 | ...passedOptions,
39 | };
40 |
41 | if (options.defaultIntegrations === undefined) {
42 | options.defaultIntegrations = [
43 | new ReactNativeErrorHandlers(),
44 | new Release(),
45 | ...defaultIntegrations.filter(
46 | (i) => !IGNORED_DEFAULT_INTEGRATIONS.includes(i.name)
47 | ),
48 | ];
49 | if (__DEV__) {
50 | options.defaultIntegrations.push(new DebugSymbolicator());
51 | }
52 | options.defaultIntegrations.push(
53 | new RewriteFrames({
54 | iteratee: (frame: StackFrame) => {
55 | if (frame.filename) {
56 | frame.filename = frame.filename
57 | .replace(/^file:\/\//, "")
58 | .replace(/^address at /, "")
59 | .replace(/^.*\/[^.]+(\.app|CodePush|.*(?=\/))/, "");
60 |
61 | if (
62 | frame.filename !== "[native code]" &&
63 | frame.filename !== "native"
64 | ) {
65 | const appPrefix = "app://";
66 | // We always want to have a triple slash
67 | frame.filename =
68 | frame.filename.indexOf("/") === 0
69 | ? `${appPrefix}${frame.filename}`
70 | : `${appPrefix}/${frame.filename}`;
71 | }
72 | }
73 | return frame;
74 | },
75 | })
76 | );
77 | if (options.enableNative) {
78 | options.defaultIntegrations.push(new DeviceContext());
79 | }
80 | }
81 |
82 | initAndBind(ReactNativeClient, options);
83 |
84 | // set the event.origin tag.
85 | getCurrentHub().setTag("event.origin", "javascript");
86 |
87 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
88 | if (getGlobalObject().HermesInternal) {
89 | getCurrentHub().setTag("hermes", "true");
90 | }
91 | }
92 |
93 | /**
94 | * Deprecated. Sets the release on the event.
95 | * NOTE: Does not set the release on sessions.
96 | * @deprecated
97 | */
98 | export function setRelease(release: string): void {
99 | setExtra("__sentry_release", release);
100 | }
101 |
102 | /**
103 | * Deprecated. Sets the dist on the event.
104 | * NOTE: Does not set the dist on sessions.
105 | * @deprecated
106 | */
107 | export function setDist(dist: string): void {
108 | setExtra("__sentry_dist", dist);
109 | }
110 |
111 | /**
112 | * If native client is available it will trigger a native crash.
113 | * Use this only for testing purposes.
114 | */
115 | export function nativeCrash(): void {
116 | const client = getCurrentHub().getClient();
117 | if (client) {
118 | client.nativeCrash();
119 | }
120 | }
121 |
122 | /**
123 | * Flushes all pending events in the queue to disk.
124 | * Use this before applying any realtime updates such as code-push or expo updates.
125 | */
126 | export async function flush(): Promise {
127 | try {
128 | const client = getCurrentHub().getClient();
129 |
130 | if (client) {
131 | const result = await client.flush();
132 |
133 | return result;
134 | }
135 | // eslint-disable-next-line no-empty
136 | } catch (_) {}
137 |
138 | logger.error("Failed to flush the event queue.");
139 |
140 | return false;
141 | }
142 |
143 | /**
144 | * Closes the SDK, stops sending events.
145 | */
146 | export async function close(): Promise {
147 | try {
148 | const client = getCurrentHub().getClient();
149 |
150 | if (client) {
151 | await client.close();
152 | }
153 | } catch (e) {
154 | logger.error("Failed to close the SDK");
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/test/touchevents.test.tsx:
--------------------------------------------------------------------------------
1 | import * as core from "@sentry/core";
2 | import { Severity } from "@sentry/types";
3 |
4 | import { TouchEventBoundary } from "../src/js/touchevents";
5 |
6 | const addBreadcrumb = jest.spyOn(core, "addBreadcrumb");
7 |
8 | afterEach(() => {
9 | jest.resetAllMocks();
10 | });
11 |
12 | describe("TouchEventBoundary._onTouchStart", () => {
13 | it("tree without displayName", () => {
14 | const { defaultProps } = TouchEventBoundary;
15 | const boundary = new TouchEventBoundary(defaultProps);
16 |
17 | const event = {
18 | _targetInst: {
19 | elementType: {
20 | name: "View",
21 | },
22 | return: {
23 | elementType: {
24 | name: "Text",
25 | },
26 | return: {
27 | elementType: {
28 | name: "CoolComponent",
29 | },
30 | return: {
31 | elementType: {
32 | name: "Screen",
33 | },
34 | },
35 | },
36 | },
37 | },
38 | };
39 |
40 | // @ts-ignore Calling private member
41 | boundary._onTouchStart(event);
42 |
43 | expect(addBreadcrumb).toBeCalledWith({
44 | category: defaultProps.breadcrumbCategory,
45 | data: {
46 | componentTree: ["View", "Text", "CoolComponent", "Screen"],
47 | },
48 | level: Severity.Info,
49 | message: "Touch event within component tree",
50 | type: defaultProps.breadcrumbType,
51 | });
52 | });
53 |
54 | it("displayName is displayed", () => {
55 | const { defaultProps } = TouchEventBoundary;
56 | const boundary = new TouchEventBoundary(defaultProps);
57 |
58 | const event = {
59 | _targetInst: {
60 | elementType: {
61 | name: "View",
62 | },
63 | return: {
64 | elementType: {
65 | name: "Text",
66 | },
67 | return: {
68 | elementType: {
69 | displayName: "Connect(View)",
70 | },
71 | },
72 | },
73 | },
74 | };
75 |
76 | // @ts-ignore Calling private member
77 | boundary._onTouchStart(event);
78 |
79 | expect(addBreadcrumb).toBeCalledWith({
80 | category: defaultProps.breadcrumbCategory,
81 | data: {
82 | componentTree: ["View", "Text", "Connect(View)"],
83 | },
84 | level: Severity.Info,
85 | message: "Touch event within element: Connect(View)",
86 | type: defaultProps.breadcrumbType,
87 | });
88 | });
89 |
90 | it("ignoreNames", () => {
91 | const { defaultProps } = TouchEventBoundary;
92 | const boundary = new TouchEventBoundary({
93 | ...defaultProps,
94 | ignoreNames: ["View", /^Connect\(/, new RegExp("^Happy\\(")],
95 | });
96 |
97 | const event = {
98 | _targetInst: {
99 | elementType: {
100 | name: "View",
101 | },
102 | return: {
103 | elementType: {
104 | name: "Text",
105 | },
106 | return: {
107 | elementType: {
108 | displayName: "Connect(View)",
109 | },
110 | return: {
111 | elementType: {
112 | displayName: "Styled(View)",
113 | },
114 | return: {
115 | elementType: {
116 | displayName: "Happy(View)",
117 | },
118 | },
119 | },
120 | },
121 | },
122 | },
123 | };
124 |
125 | // @ts-ignore Calling private member
126 | boundary._onTouchStart(event);
127 |
128 | expect(addBreadcrumb).toBeCalledWith({
129 | category: defaultProps.breadcrumbCategory,
130 | data: {
131 | componentTree: ["Text", "Styled(View)"],
132 | },
133 | level: Severity.Info,
134 | message: "Touch event within element: Styled(View)",
135 | type: defaultProps.breadcrumbType,
136 | });
137 | });
138 |
139 | it("maxComponentTreeSize", () => {
140 | const { defaultProps } = TouchEventBoundary;
141 | const boundary = new TouchEventBoundary({
142 | ...defaultProps,
143 | maxComponentTreeSize: 3,
144 | });
145 |
146 | const event = {
147 | _targetInst: {
148 | elementType: {
149 | name: "View",
150 | },
151 | return: {
152 | elementType: {
153 | name: "Text",
154 | },
155 | return: {
156 | elementType: {
157 | displayName: "Connect(View)",
158 | },
159 | return: {
160 | elementType: {
161 | displayName: "Styled(View)",
162 | },
163 | return: {
164 | elementType: {
165 | displayName: "Happy(View)",
166 | },
167 | },
168 | },
169 | },
170 | },
171 | },
172 | };
173 |
174 | // @ts-ignore Calling private member
175 | boundary._onTouchStart(event);
176 |
177 | expect(addBreadcrumb).toBeCalledWith({
178 | category: defaultProps.breadcrumbCategory,
179 | data: {
180 | componentTree: ["View", "Text", "Connect(View)"],
181 | },
182 | level: Severity.Info,
183 | message: "Touch event within element: Connect(View)",
184 | type: defaultProps.breadcrumbType,
185 | });
186 | });
187 | });
188 |
--------------------------------------------------------------------------------
/src/js/integrations/reactnativeerrorhandlers.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentHub } from "@sentry/core";
2 | import { Integration, Severity } from "@sentry/types";
3 | import { getGlobalObject, logger } from "@sentry/utils";
4 |
5 | import { ReactNativeClient } from "../client";
6 |
7 | /** ReactNativeErrorHandlers Options */
8 | interface ReactNativeErrorHandlersOptions {
9 | onerror: boolean;
10 | onunhandledrejection: boolean;
11 | }
12 |
13 | declare const global: any;
14 |
15 | /** ReactNativeErrorHandlers Integration */
16 | export class ReactNativeErrorHandlers implements Integration {
17 | /**
18 | * @inheritDoc
19 | */
20 | public static id: string = "ReactNativeErrorHandlers";
21 |
22 | /**
23 | * @inheritDoc
24 | */
25 | public name: string = ReactNativeErrorHandlers.id;
26 |
27 | /** ReactNativeOptions */
28 | private readonly _options: ReactNativeErrorHandlersOptions;
29 |
30 | /** Constructor */
31 | public constructor(options?: ReactNativeErrorHandlersOptions) {
32 | this._options = {
33 | onerror: true,
34 | onunhandledrejection: true,
35 | ...options,
36 | };
37 | }
38 |
39 | /**
40 | * @inheritDoc
41 | */
42 | public setupOnce(): void {
43 | this._handleUnhandledRejections();
44 | this._handleOnError();
45 | }
46 |
47 | /**
48 | * Handle Promises
49 | */
50 | private _handleUnhandledRejections(): void {
51 | if (this._options.onunhandledrejection) {
52 | const tracking: {
53 | disable: () => void;
54 | enable: (arg: unknown) => void;
55 | // eslint-disable-next-line @typescript-eslint/no-var-requires,import/no-extraneous-dependencies
56 | } = require("promise/setimmediate/rejection-tracking");
57 |
58 | tracking.disable();
59 | tracking.enable({
60 | allRejections: true,
61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
62 | onUnhandled: (id: any, error: any) => {
63 | if (__DEV__) {
64 | // We mimic the behavior of unhandled promise rejections showing up as a warning.
65 | // eslint-disable-next-line no-console
66 | console.warn(id, error);
67 | }
68 | getCurrentHub().captureException(error, {
69 | data: { id },
70 | originalException: error,
71 | });
72 | },
73 | });
74 |
75 | /* eslint-disable
76 | @typescript-eslint/no-var-requires,
77 | import/no-extraneous-dependencies,
78 | @typescript-eslint/no-explicit-any,
79 | @typescript-eslint/no-unsafe-member-access
80 | */
81 | const Promise = require("promise/setimmediate/core");
82 | const _global = getGlobalObject();
83 |
84 | /* In newer RN versions >=0.63, the global promise is not the same reference as the one imported from the promise library.
85 | Due to this, we need to take the methods that tracking.enable sets, and then set them on the global promise.
86 | Note: We do not want to overwrite the whole promise in case there are extensions present.
87 |
88 | If the global promise is the same as the imported promise (expected in RN <0.63), we do nothing.
89 | */
90 | const _onHandle = Promise._onHandle ?? Promise._Y;
91 | const _onReject = Promise._onReject ?? Promise._Z;
92 |
93 | if (
94 | Promise !== _global.Promise &&
95 | typeof _onHandle !== "undefined" &&
96 | typeof _onReject !== "undefined"
97 | ) {
98 | if ("_onHandle" in _global.Promise && "_onReject" in _global.Promise) {
99 | _global.Promise._onHandle = _onHandle;
100 | _global.Promise._onReject = _onReject;
101 | } else if ("_Y" in _global.Promise && "_Z" in _global.Promise) {
102 | _global.Promise._Y = _onHandle;
103 | _global.Promise._Z = _onReject;
104 | }
105 | }
106 | /* eslint-enable
107 | @typescript-eslint/no-var-requires,
108 | import/no-extraneous-dependencies,
109 | @typescript-eslint/no-explicit-any,
110 | @typescript-eslint/no-unsafe-member-access
111 | */
112 | }
113 | }
114 | /**
115 | * Handle erros
116 | */
117 | private _handleOnError(): void {
118 | if (this._options.onerror) {
119 | let handlingFatal = false;
120 |
121 | const defaultHandler =
122 | ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler();
123 |
124 | ErrorUtils.setGlobalHandler((error: any, isFatal?: boolean) => {
125 | // We want to handle fatals, but only in production mode.
126 | const shouldHandleFatal = isFatal && !__DEV__;
127 | if (shouldHandleFatal) {
128 | if (handlingFatal) {
129 | logger.log(
130 | "Encountered multiple fatals in a row. The latest:",
131 | error
132 | );
133 | return;
134 | }
135 | handlingFatal = true;
136 | }
137 |
138 | getCurrentHub().withScope((scope) => {
139 | if (isFatal) {
140 | scope.setLevel(Severity.Fatal);
141 | }
142 | getCurrentHub().captureException(error, {
143 | originalException: error,
144 | });
145 | });
146 |
147 | const client = getCurrentHub().getClient();
148 | // If in dev, we call the default handler anyway and hope the error will be sent
149 | // Just for a better dev experience
150 | if (client && !__DEV__) {
151 | void client
152 | .flush(client.getOptions().shutdownTimeout || 2000)
153 | .then(() => {
154 | defaultHandler(error, isFatal);
155 | });
156 | } else {
157 | // If there is no client something is fishy, anyway we call the default handler
158 | defaultHandler(error, isFatal);
159 | }
160 | });
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/js/touchevents.tsx:
--------------------------------------------------------------------------------
1 | import { addBreadcrumb } from "@sentry/core";
2 | import { Severity } from "@sentry/types";
3 | import * as React from "react";
4 | import { StyleSheet, View } from "react-native";
5 |
6 | export type TouchEventBoundaryProps = {
7 | /**
8 | * The category assigned to the breadcrumb that is logged by the touch event.
9 | */
10 | breadcrumbCategory?: string;
11 | /**
12 | * The type assigned to the breadcrumb that is logged by the touch event.
13 | */
14 | breadcrumbType?: string;
15 | /**
16 | * The max number of components to display when logging a touch's component tree.
17 | */
18 | maxComponentTreeSize?: number;
19 | /**
20 | * Component name(s) to ignore when logging the touch event. This prevents unhelpful logs such as
21 | * "Touch event within element: View" where you still can't tell which View it occurred in.
22 | */
23 | ignoreNames?: Array;
24 | /**
25 | * Deprecated, use ignoreNames instead
26 | * @deprecated
27 | */
28 | ignoredDisplayNames?: Array;
29 | };
30 |
31 | const touchEventStyles = StyleSheet.create({
32 | wrapperView: {
33 | flex: 1,
34 | },
35 | });
36 |
37 | const DEFAULT_BREADCRUMB_CATEGORY = "touch";
38 | const DEFAULT_BREADCRUMB_TYPE = "user";
39 | const DEFAULT_MAX_COMPONENT_TREE_SIZE = 20;
40 |
41 | interface ElementInstance {
42 | elementType?: {
43 | displayName?: string;
44 | name?: string;
45 | };
46 | return?: ElementInstance;
47 | }
48 |
49 | /**
50 | * Boundary to log breadcrumbs for interaction events.
51 | */
52 | class TouchEventBoundary extends React.Component {
53 | public static displayName: string = "__Sentry.TouchEventBoundary";
54 | public static defaultProps: Partial = {
55 | breadcrumbCategory: DEFAULT_BREADCRUMB_CATEGORY,
56 | breadcrumbType: DEFAULT_BREADCRUMB_TYPE,
57 | ignoreNames: [],
58 | maxComponentTreeSize: DEFAULT_MAX_COMPONENT_TREE_SIZE,
59 | };
60 |
61 | /**
62 | *
63 | */
64 | public render(): React.ReactNode {
65 | return (
66 |
70 | {this.props.children}
71 |
72 | );
73 | }
74 |
75 | /**
76 | *
77 | */
78 | private _logTouchEvent(
79 | componentTreeNames: string[],
80 | displayName: string | null
81 | ): void {
82 | addBreadcrumb({
83 | category: this.props.breadcrumbCategory,
84 | data: { componentTree: componentTreeNames },
85 | level: Severity.Info,
86 | message: displayName
87 | ? `Touch event within element: ${displayName}`
88 | : `Touch event within component tree`,
89 | type: this.props.breadcrumbType,
90 | });
91 | }
92 |
93 | private _isNameIgnored = (name: string): boolean => {
94 | let ignoreNames = this.props.ignoreNames || [];
95 | // eslint-disable-next-line deprecation/deprecation
96 | if (this.props.ignoredDisplayNames) {
97 | // This is to make it compatible with prior version.
98 | // eslint-disable-next-line deprecation/deprecation
99 | ignoreNames = [...ignoreNames, ...this.props.ignoredDisplayNames];
100 | }
101 |
102 | return ignoreNames.some(
103 | (ignoreName: string | RegExp) =>
104 | (typeof ignoreName === "string" && name === ignoreName) ||
105 | (ignoreName instanceof RegExp && name.match(ignoreName))
106 | );
107 | };
108 |
109 | // Originally was going to clean the names of any HOCs as well but decided that it might hinder debugging effectively. Will leave here in case
110 | // private readonly _cleanName = (name: string): string =>
111 | // name.replace(/.*\(/g, "").replace(/\)/g, "");
112 |
113 | private _onTouchStart = (e: { _targetInst?: ElementInstance }): void => {
114 | if (e._targetInst) {
115 | let currentInst: ElementInstance | undefined = e._targetInst;
116 |
117 | let activeDisplayName = null;
118 | const componentTreeNames: string[] = [];
119 |
120 | while (
121 | currentInst &&
122 | // maxComponentTreeSize will always be defined as we have a defaultProps. But ts needs a check so this is here.
123 | this.props.maxComponentTreeSize &&
124 | componentTreeNames.length < this.props.maxComponentTreeSize
125 | ) {
126 | if (currentInst.elementType) {
127 | if (
128 | // If the loop gets to the boundary itself, break.
129 | currentInst.elementType.displayName ===
130 | TouchEventBoundary.displayName
131 | ) {
132 | break;
133 | }
134 |
135 | if (
136 | typeof currentInst.elementType.displayName === "string" &&
137 | !this._isNameIgnored(currentInst.elementType.displayName)
138 | ) {
139 | const { displayName } = currentInst.elementType;
140 | if (activeDisplayName === null) {
141 | activeDisplayName = displayName;
142 | }
143 | componentTreeNames.push(displayName);
144 | } else if (
145 | typeof currentInst.elementType.name === "string" &&
146 | !this._isNameIgnored(currentInst.elementType.name)
147 | ) {
148 | componentTreeNames.push(currentInst.elementType.name);
149 | }
150 | }
151 |
152 | currentInst = currentInst.return;
153 | }
154 |
155 | if (componentTreeNames.length > 0 || activeDisplayName) {
156 | this._logTouchEvent(componentTreeNames, activeDisplayName);
157 | }
158 | }
159 | };
160 | }
161 |
162 | /**
163 | * Convenience Higher-Order-Component for TouchEventBoundary
164 | * @param WrappedComponent any React Component
165 | * @param boundaryProps TouchEventBoundaryProps
166 | */
167 | const withTouchEventBoundary = (
168 | InnerComponent: React.ComponentType,
169 | boundaryProps: TouchEventBoundaryProps
170 | ): React.FunctionComponent => {
171 | const WrappedComponent: React.FunctionComponent = (props) => (
172 |
173 |
174 |
175 | );
176 |
177 | WrappedComponent.displayName = "WithTouchEventBoundary";
178 |
179 | return WrappedComponent;
180 | };
181 |
182 | export { TouchEventBoundary, withTouchEventBoundary };
183 |
--------------------------------------------------------------------------------
/sample/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=`expr $i + 1`
158 | done
159 | case $i in
160 | 0) set -- ;;
161 | 1) set -- "$args0" ;;
162 | 2) set -- "$args0" "$args1" ;;
163 | 3) set -- "$args0" "$args1" "$args2" ;;
164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=`save "$@"`
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | exec "$JAVACMD" "$@"
184 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/gradle,android,intellij,androidstudio
3 | # Edit at https://www.gitignore.io/?templates=gradle,android,intellij,androidstudio
4 |
5 | ### Android ###
6 | # Built application files
7 | *.apk
8 | *.ap_
9 | *.aab
10 |
11 | # Files for the ART/Dalvik VM
12 | *.dex
13 |
14 | # Java class files
15 | *.class
16 |
17 | # Generated files
18 | bin/
19 | gen/
20 | out/
21 | release/
22 |
23 | # Gradle files
24 | .gradle/
25 | build/
26 |
27 | # Local configuration file (sdk path, etc)
28 | local.properties
29 |
30 | # Proguard folder generated by Eclipse
31 | proguard/
32 |
33 | # Log Files
34 | *.log
35 |
36 | # Android Studio Navigation editor temp files
37 | .navigation/
38 |
39 | # Android Studio captures folder
40 | captures/
41 |
42 | # IntelliJ
43 | *.iml
44 | .idea/workspace.xml
45 | .idea/tasks.xml
46 | .idea/gradle.xml
47 | .idea/assetWizardSettings.xml
48 | .idea/dictionaries
49 | .idea/libraries
50 | # Android Studio 3 in .gitignore file.
51 | .idea/caches
52 | .idea/modules.xml
53 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
54 | .idea/navEditor.xml
55 |
56 | # Keystore files
57 | # Uncomment the following lines if you do not want to check your keystore files in.
58 | #*.jks
59 | #*.keystore
60 |
61 | # External native build folder generated in Android Studio 2.2 and later
62 | .externalNativeBuild
63 |
64 | # Google Services (e.g. APIs or Firebase)
65 | # google-services.json
66 |
67 | # Freeline
68 | freeline.py
69 | freeline/
70 | freeline_project_description.json
71 |
72 | # fastlane
73 | fastlane/report.xml
74 | fastlane/Preview.html
75 | fastlane/screenshots
76 | fastlane/test_output
77 | fastlane/readme.md
78 |
79 | # Version control
80 | vcs.xml
81 |
82 | # lint
83 | lint/intermediates/
84 | lint/generated/
85 | lint/outputs/
86 | lint/tmp/
87 | # lint/reports/
88 |
89 | ### Android Patch ###
90 | gen-external-apklibs
91 | output.json
92 |
93 | # Replacement of .externalNativeBuild directories introduced
94 | # with Android Studio 3.5.
95 | .cxx/
96 |
97 | ### Intellij ###
98 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
99 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
100 |
101 | # User-specific stuff
102 | .idea/**/workspace.xml
103 | .idea/**/tasks.xml
104 | .idea/**/usage.statistics.xml
105 | .idea/**/dictionaries
106 | .idea/**/shelf
107 |
108 | # Generated files
109 | .idea/**/contentModel.xml
110 |
111 | # Sensitive or high-churn files
112 | .idea/**/dataSources/
113 | .idea/**/dataSources.ids
114 | .idea/**/dataSources.local.xml
115 | .idea/**/sqlDataSources.xml
116 | .idea/**/dynamic.xml
117 | .idea/**/uiDesigner.xml
118 | .idea/**/dbnavigator.xml
119 |
120 | # Gradle
121 | .idea/**/gradle.xml
122 | .idea/**/libraries
123 |
124 | # Gradle and Maven with auto-import
125 | # When using Gradle or Maven with auto-import, you should exclude module files,
126 | # since they will be recreated, and may cause churn. Uncomment if using
127 | # auto-import.
128 | # .idea/modules.xml
129 | # .idea/*.iml
130 | # .idea/modules
131 | # *.iml
132 | # *.ipr
133 |
134 | # CMake
135 | cmake-build-*/
136 |
137 | # Mongo Explorer plugin
138 | .idea/**/mongoSettings.xml
139 |
140 | # File-based project format
141 | *.iws
142 |
143 | # IntelliJ
144 |
145 | # mpeltonen/sbt-idea plugin
146 | .idea_modules/
147 |
148 | # JIRA plugin
149 | atlassian-ide-plugin.xml
150 |
151 | # Cursive Clojure plugin
152 | .idea/replstate.xml
153 |
154 | # Crashlytics plugin (for Android Studio and IntelliJ)
155 | com_crashlytics_export_strings.xml
156 | crashlytics.properties
157 | crashlytics-build.properties
158 | fabric.properties
159 |
160 | # Editor-based Rest Client
161 | .idea/httpRequests
162 |
163 | # Android studio 3.1+ serialized cache file
164 | .idea/caches/build_file_checksums.ser
165 |
166 | ### Intellij Patch ###
167 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
168 |
169 | # *.iml
170 | # modules.xml
171 | # .idea/misc.xml
172 | # *.ipr
173 |
174 | # Sonarlint plugin
175 | .idea/**/sonarlint/
176 |
177 | # SonarQube Plugin
178 | .idea/**/sonarIssues.xml
179 |
180 | # Markdown Navigator plugin
181 | .idea/**/markdown-navigator.xml
182 | .idea/**/markdown-navigator/
183 |
184 | ### Gradle ###
185 | .gradle
186 |
187 | # Ignore Gradle GUI config
188 | gradle-app.setting
189 |
190 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
191 | !gradle-wrapper.jar
192 |
193 | # Cache of project
194 | .gradletasknamecache
195 |
196 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
197 | # gradle/wrapper/gradle-wrapper.properties
198 |
199 | ### Gradle Patch ###
200 | **/build/
201 |
202 | ### AndroidStudio ###
203 | # Covers files to be ignored for android development using Android Studio.
204 |
205 | # Built application files
206 |
207 | # Files for the ART/Dalvik VM
208 |
209 | # Java class files
210 |
211 | # Generated files
212 |
213 | # Gradle files
214 |
215 | # Signing files
216 | .signing/
217 |
218 | # Local configuration file (sdk path, etc)
219 |
220 | # Proguard folder generated by Eclipse
221 |
222 | # Log Files
223 |
224 | # Android Studio
225 | /*/build/
226 | /*/local.properties
227 | /*/out
228 | /*/*/build
229 | /*/*/production
230 | *.ipr
231 | *~
232 | *.swp
233 |
234 | # Android Patch
235 |
236 | # External native build folder generated in Android Studio 2.2 and later
237 |
238 | # NDK
239 | obj/
240 |
241 | # IntelliJ IDEA
242 | /out/
243 |
244 | # User-specific configurations
245 | .idea/caches/
246 | .idea/libraries/
247 | .idea/shelf/
248 | .idea/.name
249 | .idea/compiler.xml
250 | .idea/copyright/profiles_settings.xml
251 | .idea/encodings.xml
252 | .idea/misc.xml
253 | .idea/scopes/scope_settings.xml
254 | .idea/vcs.xml
255 | .idea/jsLibraryMappings.xml
256 | .idea/datasources.xml
257 | .idea/dataSources.ids
258 | .idea/sqlDataSources.xml
259 | .idea/dynamic.xml
260 | .idea/uiDesigner.xml
261 |
262 | # OS-specific files
263 | .DS_Store
264 | .DS_Store?
265 | ._*
266 | .Spotlight-V100
267 | .Trashes
268 | ehthumbs.db
269 | Thumbs.db
270 |
271 | # Legacy Eclipse project files
272 | .classpath
273 | .project
274 | .cproject
275 | .settings/
276 |
277 | # Mobile Tools for Java (J2ME)
278 | .mtj.tmp/
279 |
280 | # Package Files #
281 | *.war
282 | *.ear
283 |
284 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
285 | hs_err_pid*
286 |
287 | ## Plugin-specific files:
288 |
289 | # mpeltonen/sbt-idea plugin
290 |
291 | # JIRA plugin
292 |
293 | # Mongo Explorer plugin
294 | .idea/mongoSettings.xml
295 |
296 | # Crashlytics plugin (for Android Studio and IntelliJ)
297 |
298 | ### AndroidStudio Patch ###
299 |
300 | !/gradle/wrapper/gradle-wrapper.jar
301 |
302 | # End of https://www.gitignore.io/api/gradle,android,intellij,androidstudio
303 |
--------------------------------------------------------------------------------
/src/js/tracing/reactnativetracing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultRequestInstrumentationOptions,
3 | registerRequestInstrumentation,
4 | RequestInstrumentationOptions,
5 | startIdleTransaction,
6 | } from "@sentry/tracing";
7 | import {
8 | EventProcessor,
9 | Hub,
10 | Integration,
11 | Transaction as TransactionType,
12 | TransactionContext,
13 | } from "@sentry/types";
14 | import { logger } from "@sentry/utils";
15 |
16 | import { RoutingInstrumentationInstance } from "../tracing/routingInstrumentation";
17 | import { adjustTransactionDuration } from "./utils";
18 |
19 | export type BeforeNavigate = (
20 | context: TransactionContext
21 | ) => TransactionContext;
22 |
23 | export interface ReactNativeTracingOptions
24 | extends RequestInstrumentationOptions {
25 | /**
26 | * The time to wait in ms until the transaction will be finished. The transaction will use the end timestamp of
27 | * the last finished span as the endtime for the transaction.
28 | * Time is in ms.
29 | *
30 | * Default: 1000
31 | */
32 | idleTimeout: number;
33 |
34 | /**
35 | * The maximum duration of a transaction before it will be marked as "deadline_exceeded".
36 | * If you never want to mark a transaction set it to 0.
37 | * Time is in seconds.
38 | *
39 | * Default: 600
40 | */
41 | maxTransactionDuration: number;
42 |
43 | /**
44 | * The routing instrumentation to be used with the tracing integration.
45 | * There is no routing instrumentation if nothing is passed.
46 | */
47 | routingInstrumentation?: RoutingInstrumentationInstance;
48 |
49 | /**
50 | * Does not sample transactions that are from routes that have been seen any more and don't have any spans.
51 | * This removes a lot of the clutter as most back navigation transactions are now ignored.
52 | *
53 | * Default: true
54 | */
55 | ignoreEmptyBackNavigationTransactions: boolean;
56 |
57 | /**
58 | * beforeNavigate is called before a navigation transaction is created and allows users to modify transaction
59 | * context data, or drop the transaction entirely (by setting `sampled = false` in the context).
60 | *
61 | * @param context: The context data which will be passed to `startTransaction` by default
62 | *
63 | * @returns A (potentially) modified context object, with `sampled = false` if the transaction should be dropped.
64 | */
65 | beforeNavigate: BeforeNavigate;
66 | }
67 |
68 | const defaultReactNativeTracingOptions: ReactNativeTracingOptions = {
69 | ...defaultRequestInstrumentationOptions,
70 | idleTimeout: 1000,
71 | maxTransactionDuration: 600,
72 | ignoreEmptyBackNavigationTransactions: true,
73 | beforeNavigate: (context) => context,
74 | };
75 |
76 | /**
77 | * Tracing integration for React Native.
78 | */
79 | export class ReactNativeTracing implements Integration {
80 | /**
81 | * @inheritDoc
82 | */
83 | public static id: string = "ReactNativeTracing";
84 | /**
85 | * @inheritDoc
86 | */
87 | public name: string = ReactNativeTracing.id;
88 |
89 | /** ReactNativeTracing options */
90 | public options: ReactNativeTracingOptions;
91 |
92 | private _getCurrentHub?: () => Hub;
93 |
94 | constructor(options: Partial = {}) {
95 | this.options = {
96 | ...defaultReactNativeTracingOptions,
97 | ...options,
98 | };
99 | }
100 |
101 | /**
102 | * Registers routing and request instrumentation.
103 | */
104 | public setupOnce(
105 | // @ts-ignore TODO
106 | addGlobalEventProcessor: (callback: EventProcessor) => void,
107 | getCurrentHub: () => Hub
108 | ): void {
109 | // eslint-disable-next-line @typescript-eslint/unbound-method
110 | const {
111 | traceFetch,
112 | traceXHR,
113 | tracingOrigins,
114 | // @ts-ignore TODO
115 | shouldCreateSpanForRequest,
116 | routingInstrumentation,
117 | } = this.options;
118 |
119 | this._getCurrentHub = getCurrentHub;
120 |
121 | routingInstrumentation?.registerRoutingInstrumentation(
122 | this._onRouteWillChange.bind(this),
123 | this.options.beforeNavigate
124 | );
125 |
126 | if (!routingInstrumentation) {
127 | logger.log(
128 | `[ReactNativeTracing] Not instrumenting route changes as routingInstrumentation has not been set.`
129 | );
130 | }
131 |
132 | registerRequestInstrumentation({
133 | traceFetch,
134 | traceXHR,
135 | tracingOrigins,
136 | shouldCreateSpanForRequest,
137 | });
138 | }
139 |
140 | /** To be called when the route changes, but BEFORE the components of the new route mount. */
141 | private _onRouteWillChange(
142 | context: TransactionContext
143 | ): TransactionType | undefined {
144 | // TODO: Consider more features on route change, one example is setting a tag of what route the user is on
145 | return this._createRouteTransaction(context);
146 | }
147 |
148 | /** Create routing idle transaction. */
149 | private _createRouteTransaction(
150 | context: TransactionContext
151 | ): TransactionType | undefined {
152 | if (!this._getCurrentHub) {
153 | logger.warn(
154 | `[ReactNativeTracing] Did not create ${context.op} transaction because _getCurrentHub is invalid.`
155 | );
156 | return undefined;
157 | }
158 |
159 | // eslint-disable-next-line @typescript-eslint/unbound-method
160 | const { idleTimeout, maxTransactionDuration } = this.options;
161 |
162 | const expandedContext = {
163 | ...context,
164 | trimEnd: true,
165 | };
166 |
167 | const hub = this._getCurrentHub();
168 | const idleTransaction = startIdleTransaction(
169 | hub as any,
170 | expandedContext,
171 | idleTimeout,
172 | true
173 | );
174 | logger.log(
175 | `[ReactNativeTracing] Starting ${context.op} transaction "${context.name}" on scope`
176 | );
177 | idleTransaction.registerBeforeFinishCallback(
178 | (transaction, endTimestamp) => {
179 | adjustTransactionDuration(
180 | maxTransactionDuration,
181 | transaction,
182 | endTimestamp
183 | );
184 | }
185 | );
186 |
187 | if (this.options.ignoreEmptyBackNavigationTransactions) {
188 | idleTransaction.registerBeforeFinishCallback((transaction) => {
189 | if (
190 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
191 | transaction.data?.route?.hasBeenSeen &&
192 | (!transaction.spanRecorder ||
193 | transaction.spanRecorder.spans.filter(
194 | (span) => span.spanId !== transaction.spanId
195 | ).length === 0)
196 | ) {
197 | logger.log(
198 | `[ReactNativeTracing] Not sampling transaction as route has been seen before. Pass ignoreEmptyBackNavigationTransactions = false to disable this feature.`
199 | );
200 | // Route has been seen before and has no child spans.
201 | transaction.sampled = false;
202 | }
203 | });
204 | }
205 |
206 | return idleTransaction as TransactionType;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/js/integrations/debugsymbolicator.ts:
--------------------------------------------------------------------------------
1 | import { addGlobalEventProcessor, getCurrentHub } from "@sentry/core";
2 | import { Event, EventHint, Integration, StackFrame } from "@sentry/types";
3 | import { addContextToFrame, logger } from "@sentry/utils";
4 |
5 | const INTERNAL_CALLSITES_REGEX = new RegExp(
6 | ["ReactNativeRenderer-dev\\.js$", "MessageQueue\\.js$"].join("|")
7 | );
8 |
9 | interface GetDevServer {
10 | (): { url: string };
11 | }
12 |
13 | /**
14 | * React Native Stack Frame
15 | */
16 | interface ReactNativeFrame {
17 | // arguments: []
18 | column: number;
19 | file: string;
20 | lineNumber: number;
21 | methodName: string;
22 | }
23 |
24 | /**
25 | * React Native Error
26 | */
27 | type ReactNativeError = Error & {
28 | framesToPop?: number;
29 | jsEngine?: string;
30 | preventSymbolication?: boolean;
31 | componentStack?: string;
32 | };
33 |
34 | /** Tries to symbolicate the JS stack trace on the device. */
35 | export class DebugSymbolicator implements Integration {
36 | /**
37 | * @inheritDoc
38 | */
39 | public static id: string = "DebugSymbolicator";
40 | /**
41 | * @inheritDoc
42 | */
43 | public name: string = DebugSymbolicator.id;
44 |
45 | /**
46 | * @inheritDoc
47 | */
48 | public setupOnce(): void {
49 | addGlobalEventProcessor(async (event: Event, hint?: EventHint) => {
50 | const self = getCurrentHub().getIntegration(DebugSymbolicator);
51 |
52 | if (!self || hint === undefined || hint.originalException === undefined) {
53 | return event;
54 | }
55 |
56 | const reactError = hint.originalException as ReactNativeError;
57 |
58 | // eslint-disable-next-line @typescript-eslint/no-var-requires
59 | const parseErrorStack = require("react-native/Libraries/Core/Devtools/parseErrorStack");
60 |
61 | let stack;
62 | try {
63 | stack = parseErrorStack(reactError);
64 | } catch (e) {
65 | // In RN 0.64 `parseErrorStack` now only takes a string
66 | stack = parseErrorStack(reactError.stack);
67 | }
68 |
69 | // Ideally this should go into contexts but android sdk doesn't support it
70 | event.extra = {
71 | ...event.extra,
72 | componentStack: reactError.componentStack,
73 | jsEngine: reactError.jsEngine,
74 | };
75 |
76 | await self._symbolicate(event, stack);
77 |
78 | event.platform = "node"; // Setting platform node makes sure we do not show source maps errors
79 |
80 | return event;
81 | });
82 | }
83 |
84 | /**
85 | * Symbolicates the stack on the device talking to local dev server.
86 | * Mutates the passed event.
87 | */
88 | private async _symbolicate(
89 | event: Event,
90 | stack: string | undefined
91 | ): Promise {
92 | try {
93 | // eslint-disable-next-line @typescript-eslint/no-var-requires
94 | const symbolicateStackTrace = require("react-native/Libraries/Core/Devtools/symbolicateStackTrace");
95 | const prettyStack = await symbolicateStackTrace(stack);
96 |
97 | if (prettyStack) {
98 | let newStack = prettyStack;
99 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
100 | if (prettyStack.stack) {
101 | // This has been changed in an react-native version so stack is contained in here
102 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
103 | newStack = prettyStack.stack;
104 | }
105 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
106 | const stackWithoutInternalCallsites = newStack.filter(
107 | (frame: { file?: string }) =>
108 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
109 | frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null
110 | );
111 |
112 | const symbolicatedFrames = await this._convertReactNativeFramesToSentryFrames(
113 | stackWithoutInternalCallsites
114 | );
115 | this._replaceFramesInEvent(event, symbolicatedFrames);
116 | } else {
117 | logger.error("The stack is null");
118 | }
119 | } catch (error) {
120 | if (error instanceof Error) {
121 | logger.warn(`Unable to symbolicate stack trace: ${error.message}`);
122 | }
123 | }
124 | }
125 |
126 | /**
127 | * Converts ReactNativeFrames to frames in the Sentry format
128 | * @param frames ReactNativeFrame[]
129 | */
130 | private async _convertReactNativeFramesToSentryFrames(
131 | frames: ReactNativeFrame[]
132 | ): Promise {
133 | let getDevServer: GetDevServer;
134 | try {
135 | getDevServer = require("react-native/Libraries/Core/Devtools/getDevServer");
136 | } catch (_oO) {
137 | // We can't load devserver URL
138 | }
139 | // Below you will find lines marked with :HACK to prevent showing errors in the sentry ui
140 | // But since this is a debug only feature: This is Fine (TM)
141 | return Promise.all(
142 | frames.map(
143 | async (frame: ReactNativeFrame): Promise => {
144 | let inApp = !!frame.column && !!frame.lineNumber;
145 | inApp =
146 | inApp &&
147 | frame.file !== undefined &&
148 | !frame.file.includes("node_modules") &&
149 | !frame.file.includes("native code");
150 |
151 | const newFrame: StackFrame = {
152 | colno: frame.column,
153 | filename: frame.file,
154 | function: frame.methodName,
155 | in_app: inApp,
156 | lineno: inApp ? frame.lineNumber : undefined, // :HACK
157 | platform: inApp ? "javascript" : "node", // :HACK
158 | };
159 |
160 | // The upstream `react-native@0.61` delegates parsing of stacks to `stacktrace-parser`, which is buggy and
161 | // leaves a trailing `(address at` in the function name.
162 | // `react-native@0.62` seems to have custom logic to parse hermes frames specially.
163 | // Anyway, all we do here is throw away the bogus suffix.
164 | if (newFrame.function) {
165 | const addressAtPos = newFrame.function.indexOf("(address at");
166 | if (addressAtPos >= 0) {
167 | newFrame.function = newFrame.function
168 | .substr(0, addressAtPos)
169 | .trim();
170 | }
171 | }
172 |
173 | if (inApp) {
174 | await this._addSourceContext(newFrame, getDevServer);
175 | }
176 |
177 | return newFrame;
178 | }
179 | )
180 | );
181 | }
182 |
183 | /**
184 | * Replaces the frames in the exception of a error.
185 | * @param event Event
186 | * @param frames StackFrame[]
187 | */
188 | private _replaceFramesInEvent(event: Event, frames: StackFrame[]): void {
189 | if (
190 | event.exception &&
191 | event.exception.values &&
192 | event.exception.values[0] &&
193 | event.exception.values[0].stacktrace
194 | ) {
195 | event.exception.values[0].stacktrace.frames = frames.reverse();
196 | }
197 | }
198 |
199 | /**
200 | * This tries to add source context for in_app Frames
201 | *
202 | * @param frame StackFrame
203 | * @param getDevServer function from RN to get DevServer URL
204 | */
205 | private async _addSourceContext(
206 | frame: StackFrame,
207 | getDevServer?: GetDevServer
208 | ): Promise {
209 | let response;
210 |
211 | const segments = frame.filename?.split("/") ?? [];
212 |
213 | if (getDevServer) {
214 | for (const idx in segments) {
215 | if (Object.prototype.hasOwnProperty.call(segments, idx)) {
216 | response = await fetch(
217 | `${getDevServer().url}${segments.slice(-idx).join("/")}`,
218 | {
219 | method: "GET",
220 | }
221 | );
222 |
223 | if (response.ok) {
224 | break;
225 | }
226 | }
227 | }
228 | }
229 |
230 | if (response && response.ok) {
231 | const content = await response.text();
232 | const lines = content.split("\n");
233 |
234 | addContextToFrame(lines, frame);
235 | }
236 | }
237 | }
238 |
--------------------------------------------------------------------------------