Skip to content

Commit fda9ff8

Browse files
Merge branch 'main' into 5.0.0 (screenshots)
2 parents 0a6403e + ca810b9 commit fda9ff8

File tree

14 files changed

+239
-24
lines changed

14 files changed

+239
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
- Latest changes from 4.10.1
66

7+
### Features
8+
9+
- Screenshots ([#2610](https://github.com/getsentry/sentry-react-native/pull/2610))
10+
711
### Dependencies
812

913
- Bump CLI from v1.74.4 to v2.10.0 ([#2669](https://github.com/getsentry/sentry-react-native/pull/2669))

android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.sentry.react;
22

3+
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;
4+
35
import android.app.Activity;
46
import android.content.Context;
57
import android.content.pm.PackageInfo;
@@ -16,7 +18,11 @@
1618
import com.facebook.react.bridge.ReadableArray;
1719
import com.facebook.react.bridge.ReadableMap;
1820
import com.facebook.react.bridge.ReadableMapKeySetIterator;
21+
import com.facebook.react.bridge.WritableArray;
1922
import com.facebook.react.bridge.WritableMap;
23+
import com.facebook.react.bridge.WritableNativeArray;
24+
import com.facebook.react.bridge.WritableNativeMap;
25+
import com.facebook.react.module.annotations.ReactModule;
2026

2127
import java.io.BufferedInputStream;
2228
import java.io.File;
@@ -29,20 +35,23 @@
2935
import java.util.List;
3036
import java.util.Map;
3137
import java.util.UUID;
32-
import java.util.logging.Level;
33-
import java.util.logging.Logger;
3438

3539
import io.sentry.Breadcrumb;
3640
import io.sentry.HubAdapter;
41+
import io.sentry.ILogger;
3742
import io.sentry.Integration;
3843
import io.sentry.Sentry;
3944
import io.sentry.SentryEvent;
4045
import io.sentry.SentryLevel;
4146
import io.sentry.UncaughtExceptionHandlerIntegration;
4247
import io.sentry.android.core.AnrIntegration;
4348
import io.sentry.android.core.AppStartState;
49+
import io.sentry.android.core.BuildInfoProvider;
50+
import io.sentry.android.core.CurrentActivityHolder;
4451
import io.sentry.android.core.NdkIntegration;
52+
import io.sentry.android.core.ScreenshotEventProcessor;
4553
import io.sentry.android.core.SentryAndroid;
54+
import io.sentry.android.core.AndroidLogger;
4655
import io.sentry.protocol.SdkVersion;
4756
import io.sentry.protocol.SentryException;
4857
import io.sentry.protocol.SentryPackage;
@@ -52,14 +61,16 @@ public class RNSentryModuleImpl {
5261

5362
public static final String NAME = "RNSentry";
5463

55-
private static final Logger logger = Logger.getLogger("react-native-sentry");
64+
private static final ILogger logger = new AndroidLogger(NAME);
65+
private static final BuildInfoProvider buildInfo = new BuildInfoProvider(logger);
5666
private static final String modulesPath = "modules.json";
5767
private static final Charset UTF_8 = Charset.forName("UTF-8");
5868

5969
private final ReactApplicationContext reactApplicationContext;
6070
private final PackageInfo packageInfo;
6171
private FrameMetricsAggregator frameMetricsAggregator = null;
6272
private boolean androidXAvailable;
73+
private ScreenshotEventProcessor screenshotEventProcessor;
6374

6475
private static boolean didFetchAppStart;
6576

@@ -86,11 +97,10 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
8697
SentryAndroid.init(this.getReactApplicationContext(), options -> {
8798
if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) {
8899
options.setDebug(true);
89-
logger.setLevel(Level.INFO);
90100
}
91101
if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) {
92102
String dsn = rnOptions.getString("dsn");
93-
logger.info(String.format("Starting with DSN: '%s'", dsn));
103+
logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn));
94104
options.setDsn(dsn);
95105
} else {
96106
// SentryAndroid needs an empty string fallback for the dsn.
@@ -134,6 +144,9 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
134144
// by default we hide.
135145
options.setAttachThreads(rnOptions.getBoolean("attachThreads"));
136146
}
147+
if (rnOptions.hasKey("attachScreenshot")) {
148+
options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot"));
149+
}
137150
if (rnOptions.hasKey("sendDefaultPii")) {
138151
options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii"));
139152
}
@@ -169,8 +182,13 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
169182
}
170183
}
171184
}
185+
logger.log(SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations()));
172186

173-
logger.info(String.format("Native Integrations '%s'", options.getIntegrations()));
187+
final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance();
188+
final Activity currentActivity = getCurrentActivity();
189+
if (currentActivity != null) {
190+
currentActivityHolder.setActivity(currentActivity);
191+
}
174192
});
175193

176194
promise.resolve(true);
@@ -193,7 +211,7 @@ public void fetchModules(Promise promise) {
193211
} catch (FileNotFoundException e) {
194212
promise.resolve(null);
195213
} catch (Throwable e) {
196-
logger.warning("Fetching JS Modules failed.");
214+
logger.log(SentryLevel.WARNING, "Fetching JS Modules failed.");
197215
promise.resolve(null);
198216
}
199217
}
@@ -212,10 +230,10 @@ public void fetchNativeAppStart(Promise promise) {
212230
final Boolean isColdStart = appStartInstance.isColdStart();
213231

214232
if (appStartTime == null) {
215-
logger.warning("App start won't be sent due to missing appStartTime.");
233+
logger.log(SentryLevel.WARNING, "App start won't be sent due to missing appStartTime.");
216234
promise.resolve(null);
217235
} else if (isColdStart == null) {
218-
logger.warning("App start won't be sent due to missing isColdStart.");
236+
logger.log(SentryLevel.WARNING, "App start won't be sent due to missing isColdStart.");
219237
promise.resolve(null);
220238
} else {
221239
final double appStartTimestamp = (double) appStartTime.getTime();
@@ -280,7 +298,7 @@ public void fetchNativeFrames(Promise promise) {
280298

281299
promise.resolve(map);
282300
} catch (Throwable ignored) {
283-
logger.warning("Error fetching native frames.");
301+
logger.log(SentryLevel.WARNING, "Error fetching native frames.");
284302
promise.resolve(null);
285303
}
286304
}
@@ -296,7 +314,7 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise
296314
final String outboxPath = HubAdapter.getInstance().getOptions().getOutboxPath();
297315

298316
if (outboxPath == null) {
299-
logger.severe(
317+
logger.log(SentryLevel.ERROR,
300318
"Error retrieving outboxPath. Envelope will not be sent. Is the Android SDK initialized?");
301319
} else {
302320
File installation = new File(outboxPath, UUID.randomUUID().toString());
@@ -305,16 +323,46 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise
305323
}
306324
}
307325
} catch (Throwable ignored) {
308-
logger.severe("Error while writing envelope to outbox.");
326+
logger.log(SentryLevel.ERROR, "Error while writing envelope to outbox.");
309327
}
310328
promise.resolve(true);
311329
}
312330

331+
public void captureScreenshot(Promise promise) {
332+
333+
final Activity activity = getCurrentActivity();
334+
if (activity == null) {
335+
logger.log(SentryLevel.WARNING, "CurrentActivity is null, can't capture screenshot.");
336+
promise.resolve(null);
337+
return;
338+
}
339+
340+
final byte[] raw = takeScreenshot(activity, logger, buildInfo);
341+
if (raw == null) {
342+
logger.log(SentryLevel.WARNING, "Screenshot is null, screen was not captured.");
343+
promise.resolve(null);
344+
return;
345+
}
346+
347+
final WritableNativeArray data = new WritableNativeArray();
348+
for (final byte b : raw) {
349+
data.pushInt(b);
350+
}
351+
final WritableMap screenshot = new WritableNativeMap();
352+
screenshot.putString("contentType", "image/png");
353+
screenshot.putArray("data", data);
354+
screenshot.putString("filename", "screenshot.png");
355+
356+
final WritableArray screenshotsArray = new WritableNativeArray();
357+
screenshotsArray.pushMap(screenshot);
358+
promise.resolve(screenshotsArray);
359+
}
360+
313361
private static PackageInfo getPackageInfo(Context ctx) {
314362
try {
315363
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
316364
} catch (PackageManager.NameNotFoundException e) {
317-
logger.warning("Error getting package info.");
365+
logger.log(SentryLevel.WARNING, "Error getting package info.");
318366
return null;
319367
}
320368
}
@@ -469,17 +517,17 @@ public void enableNativeFramesTracking() {
469517
try {
470518
frameMetricsAggregator.add(currentActivity);
471519

472-
logger.info("FrameMetricsAggregator installed.");
520+
logger.log(SentryLevel.INFO, "FrameMetricsAggregator installed.");
473521
} catch (Throwable ignored) {
474522
// throws ConcurrentModification when calling addOnFrameMetricsAvailableListener
475523
// this is a best effort since we can't reproduce it
476-
logger.severe("Error adding Activity to frameMetricsAggregator.");
524+
logger.log(SentryLevel.ERROR, "Error adding Activity to frameMetricsAggregator.");
477525
}
478526
} else {
479-
logger.info("currentActivity isn't available.");
527+
logger.log(SentryLevel.INFO, "currentActivity isn't available.");
480528
}
481529
} else {
482-
logger.warning("androidx.core' isn't available as a dependency.");
530+
logger.log(SentryLevel.WARNING, "androidx.core' isn't available as a dependency.");
483531
}
484532
}
485533

android/src/newarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise
5757
this.impl.captureEnvelope(rawBytes, options, promise);
5858
}
5959

60+
@Override
61+
public void captureScreenshot(Promise promise) {
62+
this.impl.captureScreenshot(promise);
63+
}
64+
6065
@Override
6166
public void setUser(final ReadableMap user, final ReadableMap otherUserKeys) {
6267
this.impl.setUser(user, otherUserKeys);

android/src/oldarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise
5757
this.impl.captureEnvelope(rawBytes, options, promise);
5858
}
5959

60+
@ReactMethod
61+
public void captureScreenshot(Promise promise) {
62+
this.impl.captureScreenshot(promise);
63+
}
64+
6065
@ReactMethod
6166
public void setUser(final ReadableMap user, final ReadableMap otherUserKeys) {
6267
this.impl.setUser(user, otherUserKeys);

ios/RNSentry.mm

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,35 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
306306
resolve(@YES);
307307
}
308308

309+
RCT_EXPORT_METHOD(captureScreenshot: (RCTPromiseResolveBlock)resolve
310+
rejecter: (RCTPromiseRejectBlock)reject)
311+
{
312+
NSArray<NSData *>* rawScreenshots = [PrivateSentrySDKOnly captureScreenshots];
313+
NSMutableArray *screenshotsArray = [NSMutableArray arrayWithCapacity:[rawScreenshots count]];
314+
315+
int counter = 1;
316+
for (NSData* raw in rawScreenshots) {
317+
NSMutableArray *screenshot = [NSMutableArray arrayWithCapacity:raw.length];
318+
const char *bytes = (char*) [raw bytes];
319+
for (int i = 0; i < [raw length]; i++) {
320+
[screenshot addObject:[[NSNumber alloc] initWithChar:bytes[i]]];
321+
}
322+
323+
NSString* filename = @"screenshot.png";
324+
if (counter > 1) {
325+
filename = [NSString stringWithFormat:@"screenshot-%d.png", counter];
326+
}
327+
[screenshotsArray addObject:@{
328+
@"data": screenshot,
329+
@"contentType": @"image/png",
330+
@"filename": filename,
331+
}];
332+
counter++;
333+
}
334+
335+
resolve(screenshotsArray);
336+
}
337+
309338
RCT_EXPORT_METHOD(setUser:(NSDictionary *)userKeys
310339
otherUserKeys:(NSDictionary *)userDataKeys
311340
)

sample/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ Sentry.init({
6868
// release: 'myapp@1.2.3+1',
6969
// dist: `1`,
7070
attachStacktrace: true,
71+
// Attach screenshots to events.
72+
attachScreenshot: true,
7173
});
7274

7375
const Stack = createStackNavigator();

src/js/NativeRNSentry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface Spec extends TurboModule {
1212
store: boolean,
1313
},
1414
): Promise<boolean>;
15+
captureScreenshot(): Promise<NativeScreenshot[]>;
1516
clearBreadcrumbs(): void;
1617
crash(): void;
1718
closeNativeSdk(): Promise<void>;
@@ -55,5 +56,11 @@ export type NativeDeviceContextsResponse = {
5556
[key: string]: Record<string, unknown>;
5657
};
5758

59+
export type NativeScreenshot = {
60+
data: number[];
61+
contentType: string;
62+
filename: string;
63+
}
64+
5865
// The export must be here to pass codegen even if not used
5966
export default TurboModuleRegistry.getEnforcing<Spec>('RNSentry');

src/js/client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils';
1616
// @ts-ignore LogBox introduced in RN 0.63
1717
import { Alert, LogBox, YellowBox } from 'react-native';
1818

19+
import { Screenshot } from './integrations/screenshot';
1920
import { defaultSdkInfo } from './integrations/sdkinfo';
2021
import { ReactNativeClientOptions, ReactNativeTransportOptions } from './options';
2122
import { makeReactNativeTransport } from './transports/native';
@@ -81,8 +82,9 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
8182
/**
8283
* @inheritDoc
8384
*/
84-
public eventFromException(_exception: unknown, _hint?: EventHint): PromiseLike<Event> {
85-
return this._browserClient.eventFromException(_exception, _hint);
85+
public eventFromException(exception: unknown, hint: EventHint = {}): PromiseLike<Event> {
86+
return Screenshot.attachScreenshotToEventHint(hint, this._options)
87+
.then(enrichedHint => this._browserClient.eventFromException(exception, enrichedHint));
8688
}
8789

8890
/**

src/js/integrations/screenshot.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { EventHint, Integration } from '@sentry/types';
2+
import { resolvedSyncPromise } from '@sentry/utils';
3+
4+
import { NATIVE } from '../wrapper';
5+
6+
/** Adds screenshots to error events */
7+
export class Screenshot implements Integration {
8+
/**
9+
* @inheritDoc
10+
*/
11+
public static id: string = 'Screenshot';
12+
13+
/**
14+
* @inheritDoc
15+
*/
16+
public name: string = Screenshot.id;
17+
18+
/**
19+
* If enabled attaches a screenshot to the event hint.
20+
*/
21+
public static attachScreenshotToEventHint(
22+
hint: EventHint,
23+
{ attachScreenshot }: { attachScreenshot?: boolean },
24+
): PromiseLike<EventHint> {
25+
if (!attachScreenshot) {
26+
return resolvedSyncPromise(hint);
27+
}
28+
29+
return NATIVE.captureScreenshot()
30+
.then((screenshots) => {
31+
if (screenshots !== null && screenshots.length > 0) {
32+
hint.attachments = [
33+
...screenshots,
34+
...(hint?.attachments || []),
35+
];
36+
}
37+
return hint;
38+
});
39+
}
40+
41+
/**
42+
* @inheritDoc
43+
*/
44+
// eslint-disable-next-line @typescript-eslint/no-empty-function
45+
public setupOnce(): void {}
46+
}

src/js/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ export interface BaseReactNativeOptions {
126126
* The max queue size for capping the number of envelopes waiting to be sent by Transport.
127127
*/
128128
maxQueueSize?: number;
129+
130+
/**
131+
* When enabled and a user experiences an error, Sentry provides the ability to take a screenshot and include it as an attachment.
132+
*
133+
* @default false
134+
*/
135+
attachScreenshot?: boolean;
129136
}
130137

131138
export interface ReactNativeTransportOptions extends BrowserTransportOptions {

0 commit comments

Comments
 (0)