Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Screenshots #2610

Merged
merged 30 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
799f47c
Add attachScreenshot option
PaitoAnderson Jul 15, 2022
c4ef535
Update CHANGELOG.md
PaitoAnderson Jul 15, 2022
2150a89
Update CHANGELOG.md
marandaneto Jul 20, 2022
5c14aae
Merge branch 'main' into feat-screenshots
krystofwoldrich Nov 9, 2022
b1f24f4
Add android screenshots
krystofwoldrich Nov 13, 2022
c8f8fa3
Add ios screenshots
krystofwoldrich Nov 14, 2022
236af14
Update changelog
krystofwoldrich Nov 14, 2022
56ee9f7
Run view draw on ui thread
krystofwoldrich Nov 14, 2022
94036ac
Return attachScreenshot to android options
krystofwoldrich Nov 14, 2022
c16aba5
Return png to android
krystofwoldrich Nov 14, 2022
9b5f50d
Fix envelope item content_type
krystofwoldrich Nov 14, 2022
b606e56
Use sentry-cocoa implementation of screenshots
krystofwoldrich Nov 15, 2022
be91755
Use screenshots implementation from android sdk and set correct activ…
krystofwoldrich Nov 15, 2022
3f24ae8
Change android implementation to use activity holder and static scree…
krystofwoldrich Nov 16, 2022
6c8c85c
Merge remote-tracking branch 'origin/main' into feat-screenshots
krystofwoldrich Nov 16, 2022
6aaae4d
Add multiple screenshots support
krystofwoldrich Nov 16, 2022
a6350df
Fix lint
krystofwoldrich Nov 16, 2022
60bb606
Use promise like and sync promise
krystofwoldrich Nov 16, 2022
1f5c846
Merge branch 'main' into feat-screenshots
krystofwoldrich Nov 17, 2022
6bb2430
Add log if take screenshot fails
krystofwoldrich Nov 17, 2022
be6041c
Add screenshot integration
krystofwoldrich Nov 21, 2022
1e368e9
Merge branch 'main' into feat-screenshots
krystofwoldrich Nov 21, 2022
990f9e0
chore: RNSentry call getCurrentActivity directly
krystofwoldrich Nov 22, 2022
e9a389d
Merge remote-tracking branch 'origin/main' into feat-screenshots
krystofwoldrich Nov 22, 2022
ec6927f
Merge remote-tracking branch 'origin/main' into feat-screenshots
krystofwoldrich Nov 29, 2022
4a46f54
Fix changelog move screenshots to unreleased section
krystofwoldrich Nov 29, 2022
efe56ce
Fix fetchModules, use sentry logger
krystofwoldrich Nov 29, 2022
5b1dc60
Merge branch 'main' into feat-screenshots
marandaneto Nov 30, 2022
9757ab5
Merge branch 'main' into feat-screenshots
krystofwoldrich Dec 1, 2022
69b7454
Fix changelog
krystofwoldrich Dec 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

- Screenshots ([#2610](https://github.com/getsentry/sentry-react-native/pull/2610))

## 4.10.1

### Fixes
Expand Down
82 changes: 65 additions & 17 deletions android/src/main/java/io/sentry/react/RNSentryModule.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.sentry.react;

import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;

import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
Expand All @@ -17,7 +19,10 @@
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.module.annotations.ReactModule;

import java.io.BufferedInputStream;
Expand All @@ -31,20 +36,23 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import io.sentry.Breadcrumb;
import io.sentry.HubAdapter;
import io.sentry.ILogger;
import io.sentry.Integration;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.UncaughtExceptionHandlerIntegration;
import io.sentry.android.core.AnrIntegration;
import io.sentry.android.core.AppStartState;
import io.sentry.android.core.BuildInfoProvider;
import io.sentry.android.core.CurrentActivityHolder;
import io.sentry.android.core.NdkIntegration;
import io.sentry.android.core.ScreenshotEventProcessor;
import io.sentry.android.core.SentryAndroid;
import io.sentry.android.core.AndroidLogger;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryException;
import io.sentry.protocol.SentryPackage;
Expand All @@ -55,13 +63,15 @@ public class RNSentryModule extends ReactContextBaseJavaModule {

public static final String NAME = "RNSentry";

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

private final PackageInfo packageInfo;
private FrameMetricsAggregator frameMetricsAggregator = null;
private boolean androidXAvailable;
private ScreenshotEventProcessor screenshotEventProcessor;

private static boolean didFetchAppStart;

Expand All @@ -86,11 +96,10 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
SentryAndroid.init(this.getReactApplicationContext(), options -> {
if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) {
options.setDebug(true);
logger.setLevel(Level.INFO);
}
if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) {
String dsn = rnOptions.getString("dsn");
logger.info(String.format("Starting with DSN: '%s'", dsn));
logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn));
options.setDsn(dsn);
} else {
// SentryAndroid needs an empty string fallback for the dsn.
Expand Down Expand Up @@ -134,6 +143,9 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
// by default we hide.
options.setAttachThreads(rnOptions.getBoolean("attachThreads"));
}
if (rnOptions.hasKey("attachScreenshot")) {
options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot"));
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
if (rnOptions.hasKey("sendDefaultPii")) {
options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii"));
}
Expand Down Expand Up @@ -169,8 +181,13 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
}
}
}
logger.log(SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations()));

logger.info(String.format("Native Integrations '%s'", options.getIntegrations()));
final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance();
final Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
currentActivityHolder.setActivity(currentActivity);
}
});

promise.resolve(true);
Expand All @@ -195,7 +212,7 @@ public void fetchModules(Promise promise) {
} catch (FileNotFoundException e) {
promise.resolve(null);
} catch (Throwable e) {
logger.warning("Fetching JS Modules failed.");
logger.log(SentryLevel.WARNING, "Fetching JS Modules failed.");
promise.resolve(null);
}
}
Expand All @@ -216,10 +233,10 @@ public void fetchNativeAppStart(Promise promise) {
final Boolean isColdStart = appStartInstance.isColdStart();

if (appStartTime == null) {
logger.warning("App start won't be sent due to missing appStartTime.");
logger.log(SentryLevel.WARNING, "App start won't be sent due to missing appStartTime.");
promise.resolve(null);
} else if (isColdStart == null) {
logger.warning("App start won't be sent due to missing isColdStart.");
logger.log(SentryLevel.WARNING, "App start won't be sent due to missing isColdStart.");
promise.resolve(null);
} else {
final double appStartTimestamp = (double) appStartTime.getTime();
Expand Down Expand Up @@ -285,7 +302,7 @@ public void fetchNativeFrames(Promise promise) {

promise.resolve(map);
} catch (Throwable ignored) {
logger.warning("Error fetching native frames.");
logger.log(SentryLevel.WARNING, "Error fetching native frames.");
promise.resolve(null);
}
}
Expand All @@ -302,7 +319,7 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise
final String outboxPath = HubAdapter.getInstance().getOptions().getOutboxPath();

if (outboxPath == null) {
logger.severe(
logger.log(SentryLevel.ERROR,
"Error retrieving outboxPath. Envelope will not be sent. Is the Android SDK initialized?");
} else {
File installation = new File(outboxPath, UUID.randomUUID().toString());
Expand All @@ -311,16 +328,47 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise
}
}
} catch (Throwable ignored) {
logger.severe("Error while writing envelope to outbox.");
logger.log(SentryLevel.ERROR, "Error while writing envelope to outbox.");
}
promise.resolve(true);
}

@ReactMethod
public void captureScreenshot(Promise promise) {

final Activity activity = getCurrentActivity();
if (activity == null) {
logger.log(SentryLevel.WARNING, "CurrentActivity is null, can't capture screenshot.");
promise.resolve(null);
return;
}

final byte[] raw = takeScreenshot(activity, logger, buildInfo);
if (raw == null) {
logger.log(SentryLevel.WARNING, "Screenshot is null, screen was not captured.");
promise.resolve(null);
return;
}

final WritableNativeArray data = new WritableNativeArray();
for (final byte b : raw) {
data.pushInt(b);
}
final WritableMap screenshot = new WritableNativeMap();
screenshot.putString("contentType", "image/png");
screenshot.putArray("data", data);
screenshot.putString("filename", "screenshot.png");

final WritableArray screenshotsArray = new WritableNativeArray();
screenshotsArray.pushMap(screenshot);
promise.resolve(screenshotsArray);
}

private static PackageInfo getPackageInfo(Context ctx) {
try {
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
logger.warning("Error getting package info.");
logger.log(SentryLevel.WARNING, "Error getting package info.");
return null;
}
}
Expand Down Expand Up @@ -483,17 +531,17 @@ public void enableNativeFramesTracking() {
try {
frameMetricsAggregator.add(currentActivity);

logger.info("FrameMetricsAggregator installed.");
logger.log(SentryLevel.INFO, "FrameMetricsAggregator installed.");
} catch (Throwable ignored) {
// throws ConcurrentModification when calling addOnFrameMetricsAvailableListener
// this is a best effort since we can't reproduce it
logger.severe("Error adding Activity to frameMetricsAggregator.");
logger.log(SentryLevel.ERROR, "Error adding Activity to frameMetricsAggregator.");
}
} else {
logger.info("currentActivity isn't available.");
logger.log(SentryLevel.INFO, "currentActivity isn't available.");
}
} else {
logger.warning("androidx.core' isn't available as a dependency.");
logger.log(SentryLevel.WARNING, "androidx.core' isn't available as a dependency.");
}
}

Expand Down
29 changes: 29 additions & 0 deletions ios/RNSentry.m
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,35 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
resolve(@YES);
}

RCT_EXPORT_METHOD(captureScreenshot: (RCTPromiseResolveBlock)resolve
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
rejecter: (RCTPromiseRejectBlock)reject)
{
NSArray<NSData *>* rawScreenshots = [PrivateSentrySDKOnly captureScreenshots];
NSMutableArray *screenshotsArray = [NSMutableArray arrayWithCapacity:[rawScreenshots count]];

int counter = 1;
for (NSData* raw in rawScreenshots) {
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
NSMutableArray *screenshot = [NSMutableArray arrayWithCapacity:raw.length];
const char *bytes = [raw bytes];
for (int i = 0; i < [raw length]; i++) {
[screenshot addObject:[[NSNumber alloc] initWithChar:bytes[i]]];
}

NSString* filename = @"screenshot.png";
if (counter > 1) {
filename = [NSString stringWithFormat:@"screenshot-%d.png", counter];
}
[screenshotsArray addObject:@{
@"data": screenshot,
@"contentType": @"image/png",
@"filename": filename,
}];
counter++;
}

resolve(screenshotsArray);
}

RCT_EXPORT_METHOD(setUser:(NSDictionary *)userKeys
otherUserKeys:(NSDictionary *)userDataKeys
)
Expand Down
2 changes: 2 additions & 0 deletions sample/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Sentry.init({
// release: '[email protected]+1',
// dist: `1`,
attachStacktrace: true,
// Attach screenshots to events.
attachScreenshot: true,
});

const Stack = createStackNavigator();
Expand Down
6 changes: 4 additions & 2 deletions src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils';
// @ts-ignore LogBox introduced in RN 0.63
import { Alert, LogBox, YellowBox } from 'react-native';

import { Screenshot } from './integrations/screenshot';
import { defaultSdkInfo } from './integrations/sdkinfo';
import { ReactNativeClientOptions, ReactNativeTransportOptions } from './options';
import { makeReactNativeTransport } from './transports/native';
Expand Down Expand Up @@ -81,8 +82,9 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
/**
* @inheritDoc
*/
public eventFromException(_exception: unknown, _hint?: EventHint): PromiseLike<Event> {
return this._browserClient.eventFromException(_exception, _hint);
public eventFromException(exception: unknown, hint: EventHint = {}): PromiseLike<Event> {
return Screenshot.attachScreenshotToEventHint(hint, this._options)
.then(enrichedHint => this._browserClient.eventFromException(exception, enrichedHint));
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/js/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export type NativeDeviceContextsResponse = {
[key: string]: Record<string, unknown>;
};

export interface NativeScreenshot {
data: number[];
contentType: string;
filename: string;
}

interface SerializedObject {
[key: string]: string;
}
Expand All @@ -37,6 +43,7 @@ export interface SentryNativeBridgeModule {
store: boolean,
},
): PromiseLike<boolean>;
captureScreenshot(): PromiseLike<NativeScreenshot[]>;
clearBreadcrumbs(): void;
crash(): void;
closeNativeSdk(): PromiseLike<void>;
Expand Down
46 changes: 46 additions & 0 deletions src/js/integrations/screenshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { EventHint, Integration } from '@sentry/types';
import { resolvedSyncPromise } from '@sentry/utils';

import { NATIVE } from '../wrapper';

/** Adds screenshots to error events */
export class Screenshot implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'Screenshot';

/**
* @inheritDoc
*/
public name: string = Screenshot.id;

/**
* If enabled attaches a screenshot to the event hint.
*/
public static attachScreenshotToEventHint(
hint: EventHint,
{ attachScreenshot }: { attachScreenshot?: boolean },
): PromiseLike<EventHint> {
if (!attachScreenshot) {
return resolvedSyncPromise(hint);
}

return NATIVE.captureScreenshot()
.then((screenshots) => {
if (screenshots !== null && screenshots.length > 0) {
hint.attachments = [
...screenshots,
...(hint?.attachments || []),
];
}
return hint;
});
}

/**
* @inheritDoc
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
public setupOnce(): void {}
}
7 changes: 7 additions & 0 deletions src/js/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export interface BaseReactNativeOptions {
* The max queue size for capping the number of envelopes waiting to be sent by Transport.
*/
maxQueueSize?: number;

/**
* When enabled and a user experiences an error, Sentry provides the ability to take a screenshot and include it as an attachment.
*
* @default false
*/
attachScreenshot?: boolean;
}

export interface ReactNativeTransportOptions extends BrowserTransportOptions {
Expand Down
4 changes: 4 additions & 0 deletions src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Release,
SdkInfo,
} from './integrations';
import { Screenshot } from './integrations/screenshot';
import { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
import { ReactNativeScope } from './scope';
import { TouchEventBoundary } from './touchevents';
Expand Down Expand Up @@ -132,6 +133,9 @@ export function init(passedOptions: ReactNativeOptions): void {
defaultIntegrations.push(new ReactNativeTracing());
}
}
if (options.attachScreenshot) {
defaultIntegrations.push(new Screenshot());
}
}

options.integrations = getIntegrationsToSetup({
Expand Down
Loading