Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add `maxQueueSize` option ([#2578](https://github.com/getsentry/sentry-react-native/pull/2578))
- Screenshots ([#2373](https://github.com/getsentry/sentry-react-native/pull/2373))

### Dependencies

Expand Down
61 changes: 61 additions & 0 deletions android/src/main/java/io/sentry/react/RNSentryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.SparseIntArray;
import android.view.View;

import androidx.core.app.FrameMetricsAggregator;

Expand All @@ -17,8 +20,11 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
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.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Date;
Expand Down Expand Up @@ -127,6 +133,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"));
}*/
if (rnOptions.hasKey("sendDefaultPii")) {
options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii"));
}
Expand Down Expand Up @@ -290,6 +299,58 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise
promise.resolve(true);
}

@ReactMethod
public void captureScreenshot(Promise promise) {
final Activity activity = this.getReactApplicationContext().getCurrentActivity();
if (activity == null
|| activity.isFinishing()
|| activity.getWindow() == null
|| activity.getWindow().getDecorView() == null
|| activity.getWindow().getDecorView().getRootView() == null) {
promise.reject("Invalid Activity Error", "Activity isn't valid, not taking screenshot.");
return;
}

final View view = activity.getWindow().getDecorView().getRootView();

if (view.getWidth() <= 0 || view.getHeight() <= 0) {
promise.reject("Zero Size View Error", "View's width and height is zeroed, not taking screenshot.");
return;
}

try {
// ARGB_8888 -> This configuration is very flexible and offers the best quality
final Bitmap bitmap =
Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);

final Canvas canvas = new Canvas(bitmap);
view.draw(canvas);

final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

// 0 meaning compress for small size, 100 meaning compress for max quality.
// Some formats, like PNG which is lossless, will ignore the quality setting.
bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);

if (byteArrayOutputStream.size() <= 0) {
throw new Exception("Screenshot is 0 bytes, not attaching the image.");
}

// screenshot png is around ~100-150 kb
final WritableNativeArray screenshot = new WritableNativeArray();
for (final byte b:byteArrayOutputStream.toByteArray()) {
screenshot.pushInt(b);
}
final WritableMap result = new WritableNativeMap();
result.putString("contentType", "image/png");
result.putArray("data", screenshot);
result.putString("filename", "screenshot.png");
promise.resolve(result);
} catch (Throwable e) {
promise.reject("Screenshot Failed Error", e);
}
}

private static PackageInfo getPackageInfo(Context ctx) {
try {
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
Expand Down
27 changes: 27 additions & 0 deletions ios/RNSentry.m
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,33 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
resolve(@YES);
}

RCT_EXPORT_METHOD(captureScreenshot: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject)
{
NSData *data;
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIGraphicsBeginImageContext(window.frame.size);

if ([window drawViewHierarchyInRect:window.bounds afterScreenUpdates:false]) {
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
data = UIImagePNGRepresentation(img);
}

UIGraphicsEndImageContext();

NSMutableArray *screenshot = [NSMutableArray arrayWithCapacity:data.length];
const char *bytes = [data bytes];
for (int i = 0; i < [data length]; i++) {
[screenshot addObject:[[NSNumber alloc] initWithChar:bytes[i]]];
}

resolve(@{
@"data": screenshot,
@"contentType": @"image/png",
@"filename": @"screenshot.png",
});
}

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
23 changes: 21 additions & 2 deletions src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
/**
* @inheritDoc
*/
public eventFromException(_exception: unknown, _hint?: EventHint): PromiseLike<Event> {
return this._browserClient.eventFromException(_exception, _hint);
public async eventFromException(_exception: unknown, _hint: EventHint = {}): Promise<Event> {
const hint = await this._attachScreenshotToEventHint(_hint);
return this._browserClient.eventFromException(_exception, hint);
}

/**
Expand Down Expand Up @@ -204,4 +205,22 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
envelope[items].push(clientReportItem);
}
}

/**
* If enabled attaches a screenshot to the event hint.
*/
private async _attachScreenshotToEventHint(hint: EventHint): Promise<EventHint> {
if (!this._options.attachScreenshot) {
return hint;
}

const screenshot = await NATIVE.captureScreenshot();
if (screenshot) {
hint.attachments = [
screenshot,
...(hint?.attachments || []),
];
}
return hint;
}
}
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
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
27 changes: 27 additions & 0 deletions src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ import { utf8ToBytes } from './vendor';

const RNSentry = NativeModules.RNSentry as SentryNativeBridgeModule | undefined;

export interface Screenshot {
data: Uint8Array;
contentType: string;
filename: string;
}

interface SentryNativeWrapper {
enableNative: boolean;
nativeIsReady: boolean;
Expand All @@ -49,6 +55,7 @@ interface SentryNativeWrapper {
closeNativeSdk(): PromiseLike<void>;

sendEnvelope(envelope: Envelope): Promise<void>;
captureScreenshot(): Promise<Screenshot | null>;

fetchNativeRelease(): PromiseLike<NativeReleaseResponse>;
fetchNativeDeviceContexts(): PromiseLike<NativeDeviceContextsResponse>;
Expand Down Expand Up @@ -438,6 +445,26 @@ export const NATIVE: SentryNativeWrapper = {
return this.enableNative && this._isModuleLoaded(RNSentry);
},

async captureScreenshot(): Promise<Screenshot | null> {
if (!this.enableNative) {
throw this._DisabledNativeError;
}
if (!this._isModuleLoaded(RNSentry)) {
throw this._NativeClientError;
}

try {
const raw = await RNSentry.captureScreenshot();
return {
...raw,
data: new Uint8Array(raw.data),
}
} catch (e) {
console.error(e);
return null;
}
},

/**
* Gets the event from envelopeItem and applies the level filter to the selected event.
* @param data An envelope item containing the event.
Expand Down