@sentry/react-native 6.7.0-alpha.0 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RNSentry.podspec +1 -1
- package/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +280 -2
- package/android/src/main/java/io/sentry/react/RNSentryVersion.java +1 -1
- package/dist/js/sdk.d.ts.map +1 -1
- package/dist/js/sdk.js +10 -21
- package/dist/js/sdk.js.map +1 -1
- package/dist/js/tools/metroconfig.d.ts +7 -11
- package/dist/js/tools/metroconfig.d.ts.map +1 -1
- package/dist/js/tools/metroconfig.js +10 -12
- package/dist/js/tools/metroconfig.js.map +1 -1
- package/dist/js/tools/sentryBabelTransformer.d.ts +1 -2
- package/dist/js/tools/sentryBabelTransformer.d.ts.map +1 -1
- package/dist/js/tools/sentryBabelTransformer.js +1 -23
- package/dist/js/tools/sentryBabelTransformer.js.map +1 -1
- package/dist/js/tools/sentryBabelTransformerUtils.d.ts +18 -0
- package/dist/js/tools/sentryBabelTransformerUtils.d.ts.map +1 -1
- package/dist/js/tools/sentryBabelTransformerUtils.js +71 -1
- package/dist/js/tools/sentryBabelTransformerUtils.js.map +1 -1
- package/dist/js/tools/sentryMetroSerializer.d.ts.map +1 -1
- package/dist/js/tools/sentryMetroSerializer.js +0 -1
- package/dist/js/tools/sentryMetroSerializer.js.map +1 -1
- package/dist/js/tools/utils.d.ts +1 -2
- package/dist/js/tools/utils.d.ts.map +1 -1
- package/dist/js/tools/utils.js.map +1 -1
- package/dist/js/utils/worldwide.d.ts +0 -2
- package/dist/js/utils/worldwide.d.ts.map +1 -1
- package/dist/js/utils/worldwide.js.map +1 -1
- package/dist/js/version.d.ts +1 -1
- package/dist/js/version.d.ts.map +1 -1
- package/dist/js/version.js +1 -1
- package/dist/js/version.js.map +1 -1
- package/ios/RNSentry.h +5 -3
- package/ios/RNSentry.mm +169 -2
- package/ios/RNSentryVersion.m +1 -1
- package/package.json +6 -6
- package/scripts/sentry-xcode.sh +0 -19
- package/sentry.gradle +1 -52
- package/ts3.8/dist/js/utils/worldwide.d.ts +0 -2
- package/ts3.8/dist/js/version.d.ts +1 -1
- package/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java +0 -25
- package/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java +0 -76
- package/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java +0 -41
- package/android/src/main/java/io/sentry/react/RNSentrySDK.java +0 -68
- package/android/src/main/java/io/sentry/react/RNSentryStart.java +0 -365
- package/dist/js/tools/sentryOptionsSerializer.d.ts +0 -6
- package/dist/js/tools/sentryOptionsSerializer.d.ts.map +0 -1
- package/dist/js/tools/sentryOptionsSerializer.js +0 -91
- package/dist/js/tools/sentryOptionsSerializer.js.map +0 -1
- package/ios/RNSentrySDK.h +0 -31
- package/ios/RNSentrySDK.m +0 -71
- package/ios/RNSentryStart.h +0 -26
- package/ios/RNSentryStart.m +0 -222
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
package io.sentry.react;
|
|
2
|
-
|
|
3
|
-
import android.app.Activity;
|
|
4
|
-
import android.content.Context;
|
|
5
|
-
import com.facebook.react.bridge.ReadableMap;
|
|
6
|
-
import com.facebook.react.bridge.ReadableType;
|
|
7
|
-
import com.facebook.react.common.JavascriptException;
|
|
8
|
-
import io.sentry.ILogger;
|
|
9
|
-
import io.sentry.Integration;
|
|
10
|
-
import io.sentry.Sentry;
|
|
11
|
-
import io.sentry.SentryEvent;
|
|
12
|
-
import io.sentry.SentryLevel;
|
|
13
|
-
import io.sentry.SentryOptions.BeforeSendCallback;
|
|
14
|
-
import io.sentry.SentryReplayOptions;
|
|
15
|
-
import io.sentry.UncaughtExceptionHandlerIntegration;
|
|
16
|
-
import io.sentry.android.core.AnrIntegration;
|
|
17
|
-
import io.sentry.android.core.BuildConfig;
|
|
18
|
-
import io.sentry.android.core.CurrentActivityHolder;
|
|
19
|
-
import io.sentry.android.core.NdkIntegration;
|
|
20
|
-
import io.sentry.android.core.SentryAndroid;
|
|
21
|
-
import io.sentry.android.core.SentryAndroidOptions;
|
|
22
|
-
import io.sentry.protocol.SdkVersion;
|
|
23
|
-
import io.sentry.protocol.SentryPackage;
|
|
24
|
-
import io.sentry.react.replay.RNSentryReplayMask;
|
|
25
|
-
import io.sentry.react.replay.RNSentryReplayUnmask;
|
|
26
|
-
import java.net.URI;
|
|
27
|
-
import java.net.URISyntaxException;
|
|
28
|
-
import java.util.List;
|
|
29
|
-
import org.jetbrains.annotations.NotNull;
|
|
30
|
-
import org.jetbrains.annotations.Nullable;
|
|
31
|
-
|
|
32
|
-
final class RNSentryStart {
|
|
33
|
-
|
|
34
|
-
private RNSentryStart() {
|
|
35
|
-
throw new AssertionError("Utility class should not be instantiated");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
static void startWithConfiguration(
|
|
39
|
-
@NotNull final Context context,
|
|
40
|
-
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {
|
|
41
|
-
Sentry.OptionsConfiguration<SentryAndroidOptions> defaults =
|
|
42
|
-
options -> updateWithReactDefaults(options, null);
|
|
43
|
-
RNSentryCompositeOptionsConfiguration compositeConfiguration =
|
|
44
|
-
new RNSentryCompositeOptionsConfiguration(
|
|
45
|
-
defaults, configuration, RNSentryStart::updateWithReactFinals);
|
|
46
|
-
SentryAndroid.init(context, compositeConfiguration);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
static void startWithOptions(
|
|
50
|
-
@NotNull final Context context,
|
|
51
|
-
@NotNull final ReadableMap rnOptions,
|
|
52
|
-
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration,
|
|
53
|
-
@NotNull ILogger logger) {
|
|
54
|
-
Sentry.OptionsConfiguration<SentryAndroidOptions> defaults =
|
|
55
|
-
options -> updateWithReactDefaults(options, null);
|
|
56
|
-
Sentry.OptionsConfiguration<SentryAndroidOptions> rnConfigurationOptions =
|
|
57
|
-
options -> getSentryAndroidOptions(options, rnOptions, logger);
|
|
58
|
-
RNSentryCompositeOptionsConfiguration compositeConfiguration =
|
|
59
|
-
new RNSentryCompositeOptionsConfiguration(
|
|
60
|
-
rnConfigurationOptions, defaults, configuration, RNSentryStart::updateWithReactFinals);
|
|
61
|
-
SentryAndroid.init(context, compositeConfiguration);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
static void startWithOptions(
|
|
65
|
-
@NotNull final Context context,
|
|
66
|
-
@NotNull final ReadableMap rnOptions,
|
|
67
|
-
@Nullable Activity currentActivity,
|
|
68
|
-
@NotNull ILogger logger) {
|
|
69
|
-
Sentry.OptionsConfiguration<SentryAndroidOptions> defaults =
|
|
70
|
-
options -> updateWithReactDefaults(options, currentActivity);
|
|
71
|
-
Sentry.OptionsConfiguration<SentryAndroidOptions> rnConfigurationOptions =
|
|
72
|
-
options -> getSentryAndroidOptions(options, rnOptions, logger);
|
|
73
|
-
RNSentryCompositeOptionsConfiguration compositeConfiguration =
|
|
74
|
-
new RNSentryCompositeOptionsConfiguration(
|
|
75
|
-
rnConfigurationOptions, defaults, RNSentryStart::updateWithReactFinals);
|
|
76
|
-
SentryAndroid.init(context, compositeConfiguration);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
static void getSentryAndroidOptions(
|
|
80
|
-
@NotNull SentryAndroidOptions options,
|
|
81
|
-
@NotNull ReadableMap rnOptions,
|
|
82
|
-
@NotNull ILogger logger) {
|
|
83
|
-
if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) {
|
|
84
|
-
options.setDebug(true);
|
|
85
|
-
}
|
|
86
|
-
if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) {
|
|
87
|
-
String dsn = rnOptions.getString("dsn");
|
|
88
|
-
logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn));
|
|
89
|
-
options.setDsn(dsn);
|
|
90
|
-
} else {
|
|
91
|
-
// SentryAndroid needs an empty string fallback for the dsn.
|
|
92
|
-
options.setDsn("");
|
|
93
|
-
}
|
|
94
|
-
if (rnOptions.hasKey("sampleRate")) {
|
|
95
|
-
options.setSampleRate(rnOptions.getDouble("sampleRate"));
|
|
96
|
-
}
|
|
97
|
-
if (rnOptions.hasKey("sendClientReports")) {
|
|
98
|
-
options.setSendClientReports(rnOptions.getBoolean("sendClientReports"));
|
|
99
|
-
}
|
|
100
|
-
if (rnOptions.hasKey("maxBreadcrumbs")) {
|
|
101
|
-
options.setMaxBreadcrumbs(rnOptions.getInt("maxBreadcrumbs"));
|
|
102
|
-
}
|
|
103
|
-
if (rnOptions.hasKey("maxCacheItems")) {
|
|
104
|
-
options.setMaxCacheItems(rnOptions.getInt("maxCacheItems"));
|
|
105
|
-
}
|
|
106
|
-
if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) {
|
|
107
|
-
options.setEnvironment(rnOptions.getString("environment"));
|
|
108
|
-
}
|
|
109
|
-
if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) {
|
|
110
|
-
options.setRelease(rnOptions.getString("release"));
|
|
111
|
-
}
|
|
112
|
-
if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) {
|
|
113
|
-
options.setDist(rnOptions.getString("dist"));
|
|
114
|
-
}
|
|
115
|
-
if (rnOptions.hasKey("enableAutoSessionTracking")) {
|
|
116
|
-
options.setEnableAutoSessionTracking(rnOptions.getBoolean("enableAutoSessionTracking"));
|
|
117
|
-
}
|
|
118
|
-
if (rnOptions.hasKey("sessionTrackingIntervalMillis")) {
|
|
119
|
-
options.setSessionTrackingIntervalMillis(rnOptions.getInt("sessionTrackingIntervalMillis"));
|
|
120
|
-
}
|
|
121
|
-
if (rnOptions.hasKey("shutdownTimeout")) {
|
|
122
|
-
options.setShutdownTimeoutMillis(rnOptions.getInt("shutdownTimeout"));
|
|
123
|
-
}
|
|
124
|
-
if (rnOptions.hasKey("enableNdkScopeSync")) {
|
|
125
|
-
options.setEnableScopeSync(rnOptions.getBoolean("enableNdkScopeSync"));
|
|
126
|
-
}
|
|
127
|
-
if (rnOptions.hasKey("attachStacktrace")) {
|
|
128
|
-
options.setAttachStacktrace(rnOptions.getBoolean("attachStacktrace"));
|
|
129
|
-
}
|
|
130
|
-
if (rnOptions.hasKey("attachThreads")) {
|
|
131
|
-
// JS use top level stacktrace and android attaches Threads which hides them so
|
|
132
|
-
// by default we hide.
|
|
133
|
-
options.setAttachThreads(rnOptions.getBoolean("attachThreads"));
|
|
134
|
-
}
|
|
135
|
-
if (rnOptions.hasKey("attachScreenshot")) {
|
|
136
|
-
options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot"));
|
|
137
|
-
}
|
|
138
|
-
if (rnOptions.hasKey("attachViewHierarchy")) {
|
|
139
|
-
options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy"));
|
|
140
|
-
}
|
|
141
|
-
if (rnOptions.hasKey("sendDefaultPii")) {
|
|
142
|
-
options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii"));
|
|
143
|
-
}
|
|
144
|
-
if (rnOptions.hasKey("maxQueueSize")) {
|
|
145
|
-
options.setMaxQueueSize(rnOptions.getInt("maxQueueSize"));
|
|
146
|
-
}
|
|
147
|
-
if (rnOptions.hasKey("enableNdk")) {
|
|
148
|
-
options.setEnableNdk(rnOptions.getBoolean("enableNdk"));
|
|
149
|
-
}
|
|
150
|
-
if (rnOptions.hasKey("spotlight")) {
|
|
151
|
-
if (rnOptions.getType("spotlight") == ReadableType.Boolean) {
|
|
152
|
-
options.setEnableSpotlight(rnOptions.getBoolean("spotlight"));
|
|
153
|
-
options.setSpotlightConnectionUrl(rnOptions.getString("defaultSidecarUrl"));
|
|
154
|
-
} else if (rnOptions.getType("spotlight") == ReadableType.String) {
|
|
155
|
-
options.setEnableSpotlight(true);
|
|
156
|
-
options.setSpotlightConnectionUrl(rnOptions.getString("spotlight"));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
SentryReplayOptions replayOptions = getReplayOptions(rnOptions);
|
|
161
|
-
options.setSessionReplay(replayOptions);
|
|
162
|
-
if (isReplayEnabled(replayOptions)) {
|
|
163
|
-
options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter());
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
|
|
167
|
-
String dsn = getURLFromDSN(rnOptions.getString("dsn"));
|
|
168
|
-
String devServerUrl = rnOptions.getString("devServerUrl");
|
|
169
|
-
options.setBeforeBreadcrumb(
|
|
170
|
-
(breadcrumb, hint) -> {
|
|
171
|
-
Object urlObject = breadcrumb.getData("url");
|
|
172
|
-
String url = urlObject instanceof String ? (String) urlObject : "";
|
|
173
|
-
if ("http".equals(breadcrumb.getType())
|
|
174
|
-
&& ((dsn != null && url.startsWith(dsn))
|
|
175
|
-
|| (devServerUrl != null && url.startsWith(devServerUrl)))) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
return breadcrumb;
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
if (rnOptions.hasKey("enableNativeCrashHandling")
|
|
182
|
-
&& !rnOptions.getBoolean("enableNativeCrashHandling")) {
|
|
183
|
-
final List<Integration> integrations = options.getIntegrations();
|
|
184
|
-
for (final Integration integration : integrations) {
|
|
185
|
-
if (integration instanceof UncaughtExceptionHandlerIntegration
|
|
186
|
-
|| integration instanceof AnrIntegration
|
|
187
|
-
|| integration instanceof NdkIntegration) {
|
|
188
|
-
integrations.remove(integration);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
logger.log(
|
|
193
|
-
SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations()));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* This function updates the options with RNSentry defaults. These default can be overwritten by
|
|
198
|
-
* users during manual native initialization.
|
|
199
|
-
*/
|
|
200
|
-
static void updateWithReactDefaults(
|
|
201
|
-
@NotNull SentryAndroidOptions options, @Nullable Activity currentActivity) {
|
|
202
|
-
@Nullable SdkVersion sdkVersion = options.getSdkVersion();
|
|
203
|
-
if (sdkVersion == null) {
|
|
204
|
-
sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME);
|
|
205
|
-
} else {
|
|
206
|
-
sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME);
|
|
207
|
-
}
|
|
208
|
-
sdkVersion.addPackage(
|
|
209
|
-
RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME,
|
|
210
|
-
RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION);
|
|
211
|
-
|
|
212
|
-
options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion());
|
|
213
|
-
options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME);
|
|
214
|
-
options.setSdkVersion(sdkVersion);
|
|
215
|
-
|
|
216
|
-
// Tracing is only enabled in JS to avoid duplicate navigation spans
|
|
217
|
-
options.setTracesSampleRate(null);
|
|
218
|
-
options.setTracesSampler(null);
|
|
219
|
-
options.setEnableTracing(false);
|
|
220
|
-
|
|
221
|
-
// React native internally throws a JavascriptException.
|
|
222
|
-
// we want to ignore it on the native side to avoid sending it twice.
|
|
223
|
-
options.addIgnoredExceptionForType(JavascriptException.class);
|
|
224
|
-
|
|
225
|
-
setCurrentActivity(currentActivity);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* This function updates options with changes RNSentry users should not change and so this is
|
|
230
|
-
* applied after the configureOptions callback during manual native initialization.
|
|
231
|
-
*/
|
|
232
|
-
static void updateWithReactFinals(@NotNull SentryAndroidOptions options) {
|
|
233
|
-
BeforeSendCallback userBeforeSend = options.getBeforeSend();
|
|
234
|
-
options.setBeforeSend(
|
|
235
|
-
(event, hint) -> {
|
|
236
|
-
setEventOriginTag(event);
|
|
237
|
-
addPackages(event, options.getSdkVersion());
|
|
238
|
-
if (userBeforeSend != null) {
|
|
239
|
-
return userBeforeSend.execute(event, hint);
|
|
240
|
-
}
|
|
241
|
-
return event;
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private static void setCurrentActivity(Activity currentActivity) {
|
|
246
|
-
final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance();
|
|
247
|
-
if (currentActivity != null) {
|
|
248
|
-
currentActivityHolder.setActivity(currentActivity);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private static boolean isReplayEnabled(SentryReplayOptions replayOptions) {
|
|
253
|
-
return replayOptions.getSessionSampleRate() != null
|
|
254
|
-
|| replayOptions.getOnErrorSampleRate() != null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private static SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) {
|
|
258
|
-
final SdkVersion replaySdkVersion =
|
|
259
|
-
new SdkVersion(
|
|
260
|
-
RNSentryVersion.REACT_NATIVE_SDK_NAME,
|
|
261
|
-
RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION);
|
|
262
|
-
@NotNull
|
|
263
|
-
final SentryReplayOptions androidReplayOptions =
|
|
264
|
-
new SentryReplayOptions(false, replaySdkVersion);
|
|
265
|
-
|
|
266
|
-
if (!(rnOptions.hasKey("replaysSessionSampleRate")
|
|
267
|
-
|| rnOptions.hasKey("replaysOnErrorSampleRate"))) {
|
|
268
|
-
return androidReplayOptions;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
androidReplayOptions.setSessionSampleRate(
|
|
272
|
-
rnOptions.hasKey("replaysSessionSampleRate")
|
|
273
|
-
? rnOptions.getDouble("replaysSessionSampleRate")
|
|
274
|
-
: null);
|
|
275
|
-
androidReplayOptions.setOnErrorSampleRate(
|
|
276
|
-
rnOptions.hasKey("replaysOnErrorSampleRate")
|
|
277
|
-
? rnOptions.getDouble("replaysOnErrorSampleRate")
|
|
278
|
-
: null);
|
|
279
|
-
|
|
280
|
-
if (!rnOptions.hasKey("mobileReplayOptions")) {
|
|
281
|
-
return androidReplayOptions;
|
|
282
|
-
}
|
|
283
|
-
@Nullable final ReadableMap rnMobileReplayOptions = rnOptions.getMap("mobileReplayOptions");
|
|
284
|
-
if (rnMobileReplayOptions == null) {
|
|
285
|
-
return androidReplayOptions;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
androidReplayOptions.setMaskAllText(
|
|
289
|
-
!rnMobileReplayOptions.hasKey("maskAllText")
|
|
290
|
-
|| rnMobileReplayOptions.getBoolean("maskAllText"));
|
|
291
|
-
androidReplayOptions.setMaskAllImages(
|
|
292
|
-
!rnMobileReplayOptions.hasKey("maskAllImages")
|
|
293
|
-
|| rnMobileReplayOptions.getBoolean("maskAllImages"));
|
|
294
|
-
|
|
295
|
-
final boolean redactVectors =
|
|
296
|
-
!rnMobileReplayOptions.hasKey("maskAllVectors")
|
|
297
|
-
|| rnMobileReplayOptions.getBoolean("maskAllVectors");
|
|
298
|
-
if (redactVectors) {
|
|
299
|
-
androidReplayOptions.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName());
|
|
303
|
-
androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName());
|
|
304
|
-
|
|
305
|
-
return androidReplayOptions;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
private static void setEventOriginTag(SentryEvent event) {
|
|
309
|
-
// We hardcode native-java as only java events are processed by the Android SDK.
|
|
310
|
-
SdkVersion sdk = event.getSdk();
|
|
311
|
-
if (sdk != null) {
|
|
312
|
-
switch (sdk.getName()) {
|
|
313
|
-
case RNSentryVersion.NATIVE_SDK_NAME:
|
|
314
|
-
setEventEnvironmentTag(event, "native");
|
|
315
|
-
break;
|
|
316
|
-
case RNSentryVersion.ANDROID_SDK_NAME:
|
|
317
|
-
setEventEnvironmentTag(event, "java");
|
|
318
|
-
break;
|
|
319
|
-
default:
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private static void setEventEnvironmentTag(SentryEvent event, String environment) {
|
|
326
|
-
event.setTag("event.origin", "android");
|
|
327
|
-
event.setTag("event.environment", environment);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private static void addPackages(SentryEvent event, SdkVersion sdk) {
|
|
331
|
-
SdkVersion eventSdk = event.getSdk();
|
|
332
|
-
if (eventSdk != null
|
|
333
|
-
&& "sentry.javascript.react-native".equals(eventSdk.getName())
|
|
334
|
-
&& sdk != null) {
|
|
335
|
-
List<SentryPackage> sentryPackages = sdk.getPackages();
|
|
336
|
-
if (sentryPackages != null) {
|
|
337
|
-
for (SentryPackage sentryPackage : sentryPackages) {
|
|
338
|
-
eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion());
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
List<String> integrations = sdk.getIntegrations();
|
|
343
|
-
if (integrations != null) {
|
|
344
|
-
for (String integration : integrations) {
|
|
345
|
-
eventSdk.addIntegration(integration);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
event.setSdk(eventSdk);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private static @Nullable String getURLFromDSN(@Nullable String dsn) {
|
|
354
|
-
if (dsn == null) {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
URI uri = null;
|
|
358
|
-
try {
|
|
359
|
-
uri = new URI(dsn);
|
|
360
|
-
} catch (URISyntaxException e) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
return uri.getScheme() + "://" + uri.getHost();
|
|
364
|
-
}
|
|
365
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sentryOptionsSerializer.d.ts","sourceRoot":"","sources":["../../../src/js/tools/sentryOptionsSerializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,OAAO,CAAC;AAUjD;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CA6CzG"}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2
|
-
exports.withSentryOptionsFromFile = void 0;
|
|
3
|
-
const core_1 = require("@sentry/core");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
6
|
-
const countLines = require("metro/src/lib/countLines");
|
|
7
|
-
const path = require("path");
|
|
8
|
-
const utils_1 = require("./utils");
|
|
9
|
-
const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json';
|
|
10
|
-
/**
|
|
11
|
-
* Loads Sentry options from a file in
|
|
12
|
-
*/
|
|
13
|
-
function withSentryOptionsFromFile(config, optionsFile) {
|
|
14
|
-
var _a;
|
|
15
|
-
if (optionsFile === false) {
|
|
16
|
-
return config;
|
|
17
|
-
}
|
|
18
|
-
const { projectRoot } = config;
|
|
19
|
-
if (!projectRoot) {
|
|
20
|
-
// eslint-disable-next-line no-console
|
|
21
|
-
console.error('[@sentry/react-native/metro] Project root is required to load Sentry options from a file');
|
|
22
|
-
return config;
|
|
23
|
-
}
|
|
24
|
-
let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME);
|
|
25
|
-
if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) {
|
|
26
|
-
optionsPath = optionsFile;
|
|
27
|
-
}
|
|
28
|
-
else if (typeof optionsFile === 'string') {
|
|
29
|
-
optionsPath = path.join(projectRoot, optionsFile);
|
|
30
|
-
}
|
|
31
|
-
const originalSerializer = (_a = config.serializer) === null || _a === void 0 ? void 0 : _a.customSerializer;
|
|
32
|
-
if (!originalSerializer) {
|
|
33
|
-
// It's okay to bail here because we don't expose this for direct usage, but as part of `withSentryConfig`
|
|
34
|
-
// If used directly in RN, the user is responsible for providing a custom serializer first, Expo provides serializer in default config
|
|
35
|
-
// eslint-disable-next-line no-console
|
|
36
|
-
console.error('[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file');
|
|
37
|
-
return config;
|
|
38
|
-
}
|
|
39
|
-
const sentryOptionsSerializer = (entryPoint, preModules, graph, options) => {
|
|
40
|
-
const sentryOptionsModule = createSentryOptionsModule(optionsPath);
|
|
41
|
-
if (sentryOptionsModule) {
|
|
42
|
-
preModules.push(sentryOptionsModule);
|
|
43
|
-
}
|
|
44
|
-
return originalSerializer(entryPoint, preModules, graph, options);
|
|
45
|
-
};
|
|
46
|
-
return Object.assign(Object.assign({}, config), { serializer: Object.assign(Object.assign({}, config.serializer), { customSerializer: sentryOptionsSerializer }) });
|
|
47
|
-
}
|
|
48
|
-
exports.withSentryOptionsFromFile = withSentryOptionsFromFile;
|
|
49
|
-
function createSentryOptionsModule(filePath) {
|
|
50
|
-
let content;
|
|
51
|
-
try {
|
|
52
|
-
content = fs.readFileSync(filePath, 'utf8');
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
if (error.code === 'ENOENT') {
|
|
56
|
-
core_1.logger.debug(`[@sentry/react-native/metro] Sentry options file does not exist at ${filePath}`);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
core_1.logger.error(`[@sentry/react-native/metro] Failed to read Sentry options file at ${filePath}`);
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
let parsedContent;
|
|
64
|
-
try {
|
|
65
|
-
parsedContent = JSON.parse(content);
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
core_1.logger.error(`[@sentry/react-native/metro] Failed to parse Sentry options file at ${filePath}`);
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
const minifiedContent = JSON.stringify(parsedContent);
|
|
72
|
-
const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`;
|
|
73
|
-
core_1.logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`);
|
|
74
|
-
return {
|
|
75
|
-
dependencies: new Map(),
|
|
76
|
-
getSource: () => Buffer.from(optionsCode),
|
|
77
|
-
inverseDependencies: (0, utils_1.createSet)(),
|
|
78
|
-
path: '__sentry-options__',
|
|
79
|
-
output: [
|
|
80
|
-
{
|
|
81
|
-
type: 'js/script/virtual',
|
|
82
|
-
data: {
|
|
83
|
-
code: optionsCode,
|
|
84
|
-
lineCount: countLines(optionsCode),
|
|
85
|
-
map: [],
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
//# sourceMappingURL=sentryOptionsSerializer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sentryOptionsSerializer.js","sourceRoot":"","sources":["../../../src/js/tools/sentryOptionsSerializer.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger } from '@sentry/core';\nimport * as fs from 'fs';\nimport type { MetroConfig, Module } from 'metro';\n// eslint-disable-next-line import/no-extraneous-dependencies\nimport * as countLines from 'metro/src/lib/countLines';\nimport * as path from 'path';\n\nimport type { MetroCustomSerializer, VirtualJSOutput } from './utils';\nimport { createSet } from './utils';\n\nconst DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json';\n\n/**\n * Loads Sentry options from a file in\n */\nexport function withSentryOptionsFromFile(config: MetroConfig, optionsFile: string | boolean): MetroConfig {\n if (optionsFile === false) {\n return config;\n }\n\n const { projectRoot } = config;\n if (!projectRoot) {\n // eslint-disable-next-line no-console\n console.error('[@sentry/react-native/metro] Project root is required to load Sentry options from a file');\n return config;\n }\n\n let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME);\n if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) {\n optionsPath = optionsFile;\n } else if (typeof optionsFile === 'string') {\n optionsPath = path.join(projectRoot, optionsFile);\n }\n\n const originalSerializer = config.serializer?.customSerializer;\n if (!originalSerializer) {\n // It's okay to bail here because we don't expose this for direct usage, but as part of `withSentryConfig`\n // If used directly in RN, the user is responsible for providing a custom serializer first, Expo provides serializer in default config\n // eslint-disable-next-line no-console\n console.error(\n '[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file',\n );\n return config;\n }\n\n const sentryOptionsSerializer: MetroCustomSerializer = (entryPoint, preModules, graph, options) => {\n const sentryOptionsModule = createSentryOptionsModule(optionsPath);\n if (sentryOptionsModule) {\n (preModules as Module[]).push(sentryOptionsModule);\n }\n return originalSerializer(entryPoint, preModules, graph, options);\n };\n\n return {\n ...config,\n serializer: {\n ...config.serializer,\n customSerializer: sentryOptionsSerializer,\n },\n };\n}\n\nfunction createSentryOptionsModule(filePath: string): Module<VirtualJSOutput> | null {\n let content: string;\n try {\n content = fs.readFileSync(filePath, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n logger.debug(`[@sentry/react-native/metro] Sentry options file does not exist at ${filePath}`);\n } else {\n logger.error(`[@sentry/react-native/metro] Failed to read Sentry options file at ${filePath}`);\n }\n return null;\n }\n\n let parsedContent: Record<string, unknown>;\n try {\n parsedContent = JSON.parse(content);\n } catch (error) {\n logger.error(`[@sentry/react-native/metro] Failed to parse Sentry options file at ${filePath}`);\n return null;\n }\n\n const minifiedContent = JSON.stringify(parsedContent);\n const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`;\n\n logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`);\n return {\n dependencies: new Map(),\n getSource: () => Buffer.from(optionsCode),\n inverseDependencies: createSet(),\n path: '__sentry-options__',\n output: [\n {\n type: 'js/script/virtual',\n data: {\n code: optionsCode,\n lineCount: countLines(optionsCode),\n map: [],\n },\n },\n ],\n };\n}\n"]}
|
package/ios/RNSentrySDK.h
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#import <Sentry/Sentry.h>
|
|
2
|
-
|
|
3
|
-
@interface RNSentrySDK : NSObject
|
|
4
|
-
SENTRY_NO_INIT
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @experimental
|
|
8
|
-
* Inits and configures Sentry for React Native applications using `sentry.options.json`
|
|
9
|
-
* configuration file.
|
|
10
|
-
*
|
|
11
|
-
* @discussion Call this method on the main thread. When calling it from a background thread, the
|
|
12
|
-
* SDK starts on the main thread async.
|
|
13
|
-
*/
|
|
14
|
-
+ (void)start;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @experimental
|
|
18
|
-
* Inits and configures Sentry for React Native applicationsusing `sentry.options.json`
|
|
19
|
-
* configuration file and `configureOptions` callback.
|
|
20
|
-
*
|
|
21
|
-
* The `configureOptions` callback can overwrite the config file options
|
|
22
|
-
* and add non-serializable items to the options object.
|
|
23
|
-
*
|
|
24
|
-
* @discussion Call this method on the main thread. When calling it from a background thread, the
|
|
25
|
-
* SDK starts on the main thread async.
|
|
26
|
-
*/
|
|
27
|
-
+ (void)startWithConfigureOptions:
|
|
28
|
-
(void (^_Nullable)(SentryOptions *_Nonnull options))configureOptions
|
|
29
|
-
NS_SWIFT_NAME(start(configureOptions:));
|
|
30
|
-
|
|
31
|
-
@end
|
package/ios/RNSentrySDK.m
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
#import "RNSentrySDK.h"
|
|
2
|
-
#import "RNSentryStart.h"
|
|
3
|
-
|
|
4
|
-
static NSString *SENTRY_OPTIONS_RESOURCE_NAME = @"sentry.options";
|
|
5
|
-
static NSString *SENTRY_OPTIONS_RESOURCE_TYPE = @"json";
|
|
6
|
-
|
|
7
|
-
@implementation RNSentrySDK
|
|
8
|
-
|
|
9
|
-
+ (void)start
|
|
10
|
-
{
|
|
11
|
-
[self startWithConfigureOptions:nil];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
+ (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions
|
|
15
|
-
{
|
|
16
|
-
NSString *path = [[NSBundle mainBundle] pathForResource:SENTRY_OPTIONS_RESOURCE_NAME
|
|
17
|
-
ofType:SENTRY_OPTIONS_RESOURCE_TYPE];
|
|
18
|
-
|
|
19
|
-
[self start:path configureOptions:configureOptions];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
+ (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options))configureOptions
|
|
23
|
-
{
|
|
24
|
-
NSError *readError = nil;
|
|
25
|
-
NSError *parseError = nil;
|
|
26
|
-
NSError *optionsError = nil;
|
|
27
|
-
|
|
28
|
-
NSData *_Nullable content = nil;
|
|
29
|
-
if (path != nil) {
|
|
30
|
-
content = [NSData dataWithContentsOfFile:path options:0 error:&readError];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
NSDictionary *dict = nil;
|
|
34
|
-
if (content != nil) {
|
|
35
|
-
dict = [NSJSONSerialization JSONObjectWithData:content options:0 error:&parseError];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (readError != nil) {
|
|
39
|
-
NSLog(@"[RNSentry] Failed to load options from %@, with error: %@", path,
|
|
40
|
-
readError.localizedDescription);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (parseError != nil) {
|
|
44
|
-
NSLog(@"[RNSentry] Failed to parse JSON from %@, with error: %@", path,
|
|
45
|
-
parseError.localizedDescription);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
SentryOptions *options = nil;
|
|
49
|
-
if (dict != nil) {
|
|
50
|
-
options = [RNSentryStart createOptionsWithDictionary:dict error:&optionsError];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (optionsError != nil) {
|
|
54
|
-
NSLog(@"[RNSentry] Failed to parse options from %@, with error: %@", path,
|
|
55
|
-
optionsError.localizedDescription);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (options == nil) {
|
|
59
|
-
// Fallback in case that options file could not be parsed.
|
|
60
|
-
options = [[SentryOptions alloc] init];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
[RNSentryStart updateWithReactDefaults:options];
|
|
64
|
-
if (configureOptions != nil) {
|
|
65
|
-
configureOptions(options);
|
|
66
|
-
}
|
|
67
|
-
[RNSentryStart updateWithReactFinals:options];
|
|
68
|
-
[RNSentryStart startWithOptions:options];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
@end
|
package/ios/RNSentryStart.h
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#import <Sentry/SentryDefines.h>
|
|
2
|
-
#import <Sentry/SentryOptions.h>
|
|
3
|
-
|
|
4
|
-
@interface RNSentryStart : NSObject
|
|
5
|
-
SENTRY_NO_INIT
|
|
6
|
-
|
|
7
|
-
+ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions
|
|
8
|
-
error:(NSError *_Nullable *_Nullable)errorPointer;
|
|
9
|
-
|
|
10
|
-
+ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options
|
|
11
|
-
error:(NSError *_Nonnull *_Nonnull)errorPointer;
|
|
12
|
-
|
|
13
|
-
+ (void)updateWithReactDefaults:(SentryOptions *)options;
|
|
14
|
-
+ (void)updateWithReactFinals:(SentryOptions *)options;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @experimental
|
|
18
|
-
* Inits and configures Sentry for React Native applications. Make sure to
|
|
19
|
-
* set a valid DSN.
|
|
20
|
-
*
|
|
21
|
-
* @discussion Call this method on the main thread. When calling it from a background thread, the
|
|
22
|
-
* SDK starts on the main thread async.
|
|
23
|
-
*/
|
|
24
|
-
+ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:));
|
|
25
|
-
|
|
26
|
-
@end
|