From 799f47c981a5c6103f855a43e390d068ed4628f6 Mon Sep 17 00:00:00 2001 From: Paito Anderson Date: Fri, 15 Jul 2022 14:53:48 -0400 Subject: [PATCH 01/22] Add attachScreenshot option --- .../main/java/io/sentry/react/RNSentryModule.java | 3 +++ sample/src/App.tsx | 2 ++ src/js/options.ts | 15 +++++++++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index 8820ea33fb..88a1299a4c 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -133,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")); } diff --git a/sample/src/App.tsx b/sample/src/App.tsx index 39526d01a4..d9d1cd5762 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -62,6 +62,8 @@ Sentry.init({ // release: 'myapp@1.2.3+1', // dist: `1`, attachStacktrace: true, + // Attach screenshots to events. + attachScreenshot: true, }); const Stack = createStackNavigator(); diff --git a/src/js/options.ts b/src/js/options.ts index bb9f5b934c..2d2eea6d60 100644 --- a/src/js/options.ts +++ b/src/js/options.ts @@ -93,11 +93,18 @@ export interface BaseReactNativeOptions { patchGlobalPromise?: boolean; /** - * The max cache items for capping the number of envelopes. - * - * @default 30 - */ + * The max cache items for capping the number of envelopes. + * + * @default 30 + */ maxCacheItems?: 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; } /** From c4ef53587f9bf4b86f36dbe7e9b0bf00a904b2ca Mon Sep 17 00:00:00 2001 From: Paito Anderson Date: Fri, 15 Jul 2022 15:07:04 -0400 Subject: [PATCH 02/22] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c079c5ee6..e4a500c291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- feat: Adds `attachScreenshot` option. + ## 4.1.3 fix: Solve reference to private cocoa SDK class #2369 From 2150a89f7df38b3dffa264390884bf92a3da8364 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Wed, 20 Jul 2022 09:50:47 +0200 Subject: [PATCH 03/22] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a500c291..ece4de09d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased -- feat: Adds `attachScreenshot` option. +### Features + +- feat: Adds `attachScreenshot` option ([#2373](https://github.com/getsentry/sentry-react-native/pull/2373)) ## 4.1.3 From b1f24f477f1798c051440187aa7189f4a478eaa5 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Sun, 13 Nov 2022 17:46:53 +0100 Subject: [PATCH 04/22] Add android screenshots --- .../java/io/sentry/react/RNSentryModule.java | 62 ++++++++++++++++++- src/js/client.ts | 13 +++- src/js/definitions.ts | 7 +++ src/js/wrapper.ts | 27 ++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index 06f696f167..9fe223bbcf 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -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; @@ -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; @@ -127,9 +133,9 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { // by default we hide. options.setAttachThreads(rnOptions.getBoolean("attachThreads")); } - if (rnOptions.hasKey("attachScreenshot")) { +/* if (rnOptions.hasKey("attachScreenshot")) { options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); - } + }*/ if (rnOptions.hasKey("sendDefaultPii")) { options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); } @@ -293,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); diff --git a/src/js/client.ts b/src/js/client.ts index 6c9813567b..29930887e8 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -21,7 +21,7 @@ import { ReactNativeClientOptions, ReactNativeTransportOptions } from './options import { makeReactNativeTransport } from './transports/native'; import { createUserFeedbackEnvelope, items } from './utils/envelope'; import { mergeOutcomes } from './utils/outcome'; -import { NATIVE } from './wrapper'; +import { NATIVE, Screenshot } from './wrapper'; /** * The Sentry React Native SDK Client. @@ -81,8 +81,15 @@ export class ReactNativeClient extends BaseClient { /** * @inheritDoc */ - public eventFromException(_exception: unknown, _hint?: EventHint): PromiseLike { - return this._browserClient.eventFromException(_exception, _hint); + public eventFromException(_exception: unknown, _hint: EventHint = {}): PromiseLike { + const capturingScreenshot = NATIVE.captureScreenshot(); + return capturingScreenshot.then((screenshot: Screenshot | null) => { + _hint.attachments = [ + ...(screenshot ? [screenshot] : []), + ...(_hint?.attachments || []), + ]; + return this._browserClient.eventFromException(_exception, _hint); + }); } /** diff --git a/src/js/definitions.ts b/src/js/definitions.ts index df74af1942..99379a5a0d 100644 --- a/src/js/definitions.ts +++ b/src/js/definitions.ts @@ -24,6 +24,12 @@ export type NativeDeviceContextsResponse = { [key: string]: Record; }; +export interface NativeScreenshot { + data: number[]; + contentType: string; + filename: string; +} + interface SerializedObject { [key: string]: string; } @@ -37,6 +43,7 @@ export interface SentryNativeBridgeModule { store: boolean, }, ): PromiseLike; + captureScreenshot(): PromiseLike; clearBreadcrumbs(): void; crash(): void; closeNativeSdk(): PromiseLike; diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 5f9ffe0472..e30393a9b2 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -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; @@ -49,6 +55,7 @@ interface SentryNativeWrapper { closeNativeSdk(): PromiseLike; sendEnvelope(envelope: Envelope): Promise; + captureScreenshot(): Promise; fetchNativeRelease(): PromiseLike; fetchNativeDeviceContexts(): PromiseLike; @@ -438,6 +445,26 @@ export const NATIVE: SentryNativeWrapper = { return this.enableNative && this._isModuleLoaded(RNSentry); }, + async captureScreenshot(): Promise { + 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. From c8f8fa3fa21464b41396930a2b6b3207932b997a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 14 Nov 2022 10:58:23 +0100 Subject: [PATCH 05/22] Add ios screenshots --- ios/RNSentry.m | 27 +++++++++++++++++++++++++++ src/js/client.ts | 32 ++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/ios/RNSentry.m b/ios/RNSentry.m index f67c3698f5..b8f5e051b7 100644 --- a/ios/RNSentry.m +++ b/ios/RNSentry.m @@ -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 ) diff --git a/src/js/client.ts b/src/js/client.ts index 29930887e8..1e488d8abd 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -21,7 +21,7 @@ import { ReactNativeClientOptions, ReactNativeTransportOptions } from './options import { makeReactNativeTransport } from './transports/native'; import { createUserFeedbackEnvelope, items } from './utils/envelope'; import { mergeOutcomes } from './utils/outcome'; -import { NATIVE, Screenshot } from './wrapper'; +import { NATIVE } from './wrapper'; /** * The Sentry React Native SDK Client. @@ -81,15 +81,9 @@ export class ReactNativeClient extends BaseClient { /** * @inheritDoc */ - public eventFromException(_exception: unknown, _hint: EventHint = {}): PromiseLike { - const capturingScreenshot = NATIVE.captureScreenshot(); - return capturingScreenshot.then((screenshot: Screenshot | null) => { - _hint.attachments = [ - ...(screenshot ? [screenshot] : []), - ...(_hint?.attachments || []), - ]; - return this._browserClient.eventFromException(_exception, _hint); - }); + public async eventFromException(_exception: unknown, _hint: EventHint = {}): Promise { + const hint = await this._attachScreenshotToEventHint(_hint); + return this._browserClient.eventFromException(_exception, hint); } /** @@ -211,4 +205,22 @@ export class ReactNativeClient extends BaseClient { envelope[items].push(clientReportItem); } } + + /** + * If enabled attaches a screenshot to the event hint. + */ + private async _attachScreenshotToEventHint(hint: EventHint): Promise { + if (!this._options.attachScreenshot) { + return hint; + } + + const screenshot = await NATIVE.captureScreenshot(); + if (screenshot) { + hint.attachments = [ + screenshot, + ...(hint?.attachments || []), + ]; + } + return hint; + } } From 236af1408caace01b4f1ad49e8dc6e83fef8c61f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 14 Nov 2022 11:24:45 +0100 Subject: [PATCH 06/22] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f871d92f96..2043dbafcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +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)) +- Screenshots ([#2610](https://github.com/getsentry/sentry-react-native/pull/2610)) ### Dependencies From 56ee9f76004640f18f2975dcf698dd451ddeeb49 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 14 Nov 2022 12:17:11 +0100 Subject: [PATCH 07/22] Run view draw on ui thread --- android/src/main/java/io/sentry/react/RNSentryModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index 9fe223bbcf..34f62133ad 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -324,7 +324,7 @@ public void captureScreenshot(Promise promise) { Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); - view.draw(canvas); + UiThreadUtil.runOnUiThread(() -> view.draw(canvas)); final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); From 94036ac1b6be06eb09473ae759fdd996ca655686 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 14 Nov 2022 12:27:54 +0100 Subject: [PATCH 08/22] Return attachScreenshot to android options --- .../src/main/java/io/sentry/react/RNSentryModule.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index 34f62133ad..46e817962a 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -19,6 +19,7 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; @@ -133,9 +134,9 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { // by default we hide. options.setAttachThreads(rnOptions.getBoolean("attachThreads")); } -/* if (rnOptions.hasKey("attachScreenshot")) { + if (rnOptions.hasKey("attachScreenshot")) { options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); - }*/ + } if (rnOptions.hasKey("sendDefaultPii")) { options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); } @@ -330,7 +331,7 @@ public void captureScreenshot(Promise promise) { // 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); + bitmap.compress(Bitmap.CompressFormat.JPEG, 0, byteArrayOutputStream); if (byteArrayOutputStream.size() <= 0) { throw new Exception("Screenshot is 0 bytes, not attaching the image."); @@ -342,9 +343,9 @@ public void captureScreenshot(Promise promise) { screenshot.pushInt(b); } final WritableMap result = new WritableNativeMap(); - result.putString("contentType", "image/png"); + result.putString("contentType", "image/jpeg"); result.putArray("data", screenshot); - result.putString("filename", "screenshot.png"); + result.putString("filename", "screenshot.jpg"); promise.resolve(result); } catch (Throwable e) { promise.reject("Screenshot Failed Error", e); From c16aba54aac7d252750ac054a4793fd8effa5c55 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 14 Nov 2022 12:32:29 +0100 Subject: [PATCH 09/22] Return png to android --- android/src/main/java/io/sentry/react/RNSentryModule.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index 46e817962a..b61547917c 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -331,7 +331,7 @@ public void captureScreenshot(Promise promise) { // 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.JPEG, 0, byteArrayOutputStream); + bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream); if (byteArrayOutputStream.size() <= 0) { throw new Exception("Screenshot is 0 bytes, not attaching the image."); @@ -343,9 +343,9 @@ public void captureScreenshot(Promise promise) { screenshot.pushInt(b); } final WritableMap result = new WritableNativeMap(); - result.putString("contentType", "image/jpeg"); + result.putString("contentType", "image/png"); result.putArray("data", screenshot); - result.putString("filename", "screenshot.jpg"); + result.putString("filename", "screenshot.png"); promise.resolve(result); } catch (Throwable e) { promise.reject("Screenshot Failed Error", e); From 9b5f50d1db0191b8ccf41f7d78f884a254e76d2b Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 14 Nov 2022 14:54:23 +0100 Subject: [PATCH 10/22] Fix envelope item content_type --- src/js/wrapper.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index e30393a9b2..d6cf7ce8ce 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -106,12 +106,18 @@ export const NATIVE: SentryNativeWrapper = { for (const rawItem of envelopeItems) { const [itemHeader, itemPayload] = this._processItem(rawItem); + let bytesContentType: string; let bytesPayload: number[] = []; if (typeof itemPayload === 'string') { + bytesContentType = 'text/plain'; bytesPayload = utf8ToBytes(itemPayload); } else if (itemPayload instanceof Uint8Array) { + bytesContentType = typeof itemHeader.content_type === 'string' + ? itemHeader.content_type + : 'application/octet-stream'; bytesPayload = [...itemPayload]; } else { + bytesContentType = 'application/json'; bytesPayload = utf8ToBytes(JSON.stringify(itemPayload)); if (!hardCrashed) { hardCrashed = isHardCrash(itemPayload); @@ -119,7 +125,7 @@ export const NATIVE: SentryNativeWrapper = { } // Content type is not inside BaseEnvelopeItemHeaders. - (itemHeader as BaseEnvelopeItemHeaders).content_type = 'application/json'; + (itemHeader as BaseEnvelopeItemHeaders).content_type = bytesContentType; (itemHeader as BaseEnvelopeItemHeaders).length = bytesPayload.length; const serializedItemHeader = JSON.stringify(itemHeader); From b606e564f995298e6d3f34ff063baf050414d1d6 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 15 Nov 2022 10:45:17 +0100 Subject: [PATCH 11/22] Use sentry-cocoa implementation of screenshots --- ios/RNSentry.m | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/ios/RNSentry.m b/ios/RNSentry.m index b8f5e051b7..a834a144f5 100644 --- a/ios/RNSentry.m +++ b/ios/RNSentry.m @@ -7,6 +7,7 @@ #endif #import +#import #import @interface SentryTraceContext : NSObject @@ -294,20 +295,12 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event RCT_EXPORT_METHOD(captureScreenshot: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject) { - NSData *data; - UIWindow *window = [[UIApplication sharedApplication] keyWindow]; - UIGraphicsBeginImageContext(window.frame.size); + NSArray* screenshots = [PrivateSentrySDKOnly captureScreenshots]; - 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++) { + //TODO: Return array of screenshots + NSMutableArray *screenshot = [NSMutableArray arrayWithCapacity:screenshots[0].length]; + const char *bytes = [screenshots[0] bytes]; + for (int i = 0; i < [screenshots[0] length]; i++) { [screenshot addObject:[[NSNumber alloc] initWithChar:bytes[i]]]; } From be9175550d083590259f212926e1d80e888b3a1d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 15 Nov 2022 16:02:52 +0100 Subject: [PATCH 12/22] Use screenshots implementation from android sdk and set correct activity to screenshot processor --- .../java/io/sentry/react/RNSentryModule.java | 72 +++++++++---------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index b61547917c..e1311dbfb3 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -37,6 +37,7 @@ import java.util.logging.Logger; import io.sentry.Breadcrumb; +import io.sentry.EventProcessor; import io.sentry.HubAdapter; import io.sentry.Integration; import io.sentry.Sentry; @@ -46,6 +47,7 @@ import io.sentry.android.core.AnrIntegration; import io.sentry.android.core.AppStartState; import io.sentry.android.core.NdkIntegration; +import io.sentry.android.core.ScreenshotEventProcessor; import io.sentry.android.core.SentryAndroid; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryException; @@ -62,6 +64,7 @@ public class RNSentryModule extends ReactContextBaseJavaModule { private final PackageInfo packageInfo; private FrameMetricsAggregator frameMetricsAggregator = null; private boolean androidXAvailable; + private ScreenshotEventProcessor screenshotEventProcessor; private static boolean didFetchAppStart; @@ -144,6 +147,13 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); } + screenshotEventProcessor = ScreenshotEventProcessor.getInstance(); + final Activity currentActivity = getCurrentActivity(); + if (screenshotEventProcessor != null + && currentActivity != null) { + screenshotEventProcessor.setCurrentActivity(currentActivity); + } + options.setBeforeSend((event, hint) -> { // React native internally throws a JavascriptException // Since we catch it before that, we don't want to send this one @@ -302,54 +312,36 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise @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."); + if (screenshotEventProcessor == null) { + logger.warning("ScreenshotEventProcessor is null, can't capture screenshot."); + promise.resolve(null); 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."); + final Activity activity = this.getReactApplicationContext().getCurrentActivity(); + if (activity == null) { + logger.warning("CurrentActivity is null, can't capture screenshot."); + promise.resolve(null); 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); - UiThreadUtil.runOnUiThread(() -> 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."); - } + screenshotEventProcessor.setCurrentActivity(activity); + final byte[] data = screenshotEventProcessor.takeScreenshot(); + if (data == null) { + logger.warning("Screenshot is null, screen was not captured."); + promise.resolve(null); + return; + } - // 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); + final WritableNativeArray screenshot = new WritableNativeArray(); + for (final byte b:data) { + 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); } private static PackageInfo getPackageInfo(Context ctx) { From 3f24ae851e4c90eda08e2a6e3613fb5546117a85 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 16 Nov 2022 15:34:54 +0100 Subject: [PATCH 13/22] Change android implementation to use activity holder and static screenshot method --- .../java/io/sentry/react/RNSentryModule.java | 66 ++++++++----------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index e1311dbfb3..fcb030c539 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -1,13 +1,12 @@ 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; 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; @@ -19,13 +18,11 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; -import com.facebook.react.bridge.UiThreadUtil; 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; @@ -33,12 +30,10 @@ 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.EventProcessor; import io.sentry.HubAdapter; +import io.sentry.ILogger; import io.sentry.Integration; import io.sentry.Sentry; import io.sentry.SentryEvent; @@ -46,9 +41,12 @@ 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; @@ -59,7 +57,8 @@ 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 final PackageInfo packageInfo; private FrameMetricsAggregator frameMetricsAggregator = null; @@ -89,11 +88,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. @@ -147,13 +145,6 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); } - screenshotEventProcessor = ScreenshotEventProcessor.getInstance(); - final Activity currentActivity = getCurrentActivity(); - if (screenshotEventProcessor != null - && currentActivity != null) { - screenshotEventProcessor.setCurrentActivity(currentActivity); - } - options.setBeforeSend((event, hint) -> { // React native internally throws a JavascriptException // Since we catch it before that, we don't want to send this one @@ -182,8 +173,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); @@ -210,10 +206,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(); @@ -279,7 +275,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); } } @@ -296,7 +292,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()); @@ -305,30 +301,24 @@ 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) { - if (screenshotEventProcessor == null) { - logger.warning("ScreenshotEventProcessor is null, can't capture screenshot."); - promise.resolve(null); - return; - } final Activity activity = this.getReactApplicationContext().getCurrentActivity(); if (activity == null) { - logger.warning("CurrentActivity is null, can't capture screenshot."); + logger.log(SentryLevel.WARNING, "CurrentActivity is null, can't capture screenshot."); promise.resolve(null); return; } - screenshotEventProcessor.setCurrentActivity(activity); - final byte[] data = screenshotEventProcessor.takeScreenshot(); + final byte[] data = takeScreenshot(activity, logger, buildInfo); if (data == null) { - logger.warning("Screenshot is null, screen was not captured."); + logger.log(SentryLevel.WARNING, "Screenshot is null, screen was not captured."); promise.resolve(null); return; } @@ -348,7 +338,7 @@ 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; } } @@ -511,17 +501,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."); } } From 6aaae4dfe0702f509272f355f7fbbd4a0137090c Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 16 Nov 2022 17:09:54 +0100 Subject: [PATCH 14/22] Add multiple screenshots support --- .../java/io/sentry/react/RNSentryModule.java | 24 ++++++++------ ios/RNSentry.m | 32 ++++++++++++------- src/js/client.ts | 6 ++-- src/js/definitions.ts | 2 +- src/js/wrapper.ts | 17 +++++----- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index fcb030c539..52dbc23382 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -18,6 +18,7 @@ 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; @@ -316,22 +317,25 @@ public void captureScreenshot(Promise promise) { return; } - final byte[] data = takeScreenshot(activity, logger, buildInfo); - if (data == null) { + 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 screenshot = new WritableNativeArray(); - for (final byte b:data) { - screenshot.pushInt(b); + final WritableNativeArray data = new WritableNativeArray(); + for (final byte b : raw) { + data.pushInt(b); } - final WritableMap result = new WritableNativeMap(); - result.putString("contentType", "image/png"); - result.putArray("data", screenshot); - result.putString("filename", "screenshot.png"); - promise.resolve(result); + 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) { diff --git a/ios/RNSentry.m b/ios/RNSentry.m index a834a144f5..ec0d4304f6 100644 --- a/ios/RNSentry.m +++ b/ios/RNSentry.m @@ -295,20 +295,30 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event RCT_EXPORT_METHOD(captureScreenshot: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject) { - NSArray* screenshots = [PrivateSentrySDKOnly captureScreenshots]; + NSArray* rawScreenshots = [PrivateSentrySDKOnly captureScreenshots]; + NSMutableArray *screenshotsArray = [NSMutableArray arrayWithCapacity:[rawScreenshots count]]; + + int counter = 1; + for (NSData* raw in rawScreenshots) { + 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]]]; + } - //TODO: Return array of screenshots - NSMutableArray *screenshot = [NSMutableArray arrayWithCapacity:screenshots[0].length]; - const char *bytes = [screenshots[0] bytes]; - for (int i = 0; i < [screenshots[0] 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(@{ - @"data": screenshot, - @"contentType": @"image/png", - @"filename": @"screenshot.png", - }); + resolve(screenshotsArray); } RCT_EXPORT_METHOD(setUser:(NSDictionary *)userKeys diff --git a/src/js/client.ts b/src/js/client.ts index 1e488d8abd..887d78f111 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -214,10 +214,10 @@ export class ReactNativeClient extends BaseClient { return hint; } - const screenshot = await NATIVE.captureScreenshot(); - if (screenshot) { + const screenshots = await NATIVE.captureScreenshot(); + if (screenshots !== null && screenshots.length > 0) { hint.attachments = [ - screenshot, + ...screenshots, ...(hint?.attachments || []), ]; } diff --git a/src/js/definitions.ts b/src/js/definitions.ts index 99379a5a0d..a4c73da548 100644 --- a/src/js/definitions.ts +++ b/src/js/definitions.ts @@ -43,7 +43,7 @@ export interface SentryNativeBridgeModule { store: boolean, }, ): PromiseLike; - captureScreenshot(): PromiseLike; + captureScreenshot(): PromiseLike; clearBreadcrumbs(): void; crash(): void; closeNativeSdk(): PromiseLike; diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index d6cf7ce8ce..56144648e6 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -17,6 +17,7 @@ import { NativeDeviceContextsResponse, NativeFramesResponse, NativeReleaseResponse, + NativeScreenshot, SentryNativeBridgeModule, } from './definitions'; import { isHardCrash } from './misc'; @@ -55,7 +56,7 @@ interface SentryNativeWrapper { closeNativeSdk(): PromiseLike; sendEnvelope(envelope: Envelope): Promise; - captureScreenshot(): Promise; + captureScreenshot(): Promise; fetchNativeRelease(): PromiseLike; fetchNativeDeviceContexts(): PromiseLike; @@ -99,7 +100,7 @@ export const NATIVE: SentryNativeWrapper = { const [envelopeHeader, envelopeItems] = envelope; const headerString = JSON.stringify(envelopeHeader); - const envelopeBytes: number[] = utf8ToBytes(headerString); + let envelopeBytes: number[] = utf8ToBytes(headerString); envelopeBytes.push(EOL); let hardCrashed: boolean = false; @@ -131,7 +132,7 @@ export const NATIVE: SentryNativeWrapper = { envelopeBytes.push(...utf8ToBytes(serializedItemHeader)); envelopeBytes.push(EOL); - bytesPayload.forEach(byte => envelopeBytes.push(byte)); + envelopeBytes = envelopeBytes.concat(bytesPayload); envelopeBytes.push(EOL); } @@ -451,7 +452,7 @@ export const NATIVE: SentryNativeWrapper = { return this.enableNative && this._isModuleLoaded(RNSentry); }, - async captureScreenshot(): Promise { + async captureScreenshot(): Promise { if (!this.enableNative) { throw this._DisabledNativeError; } @@ -461,10 +462,10 @@ export const NATIVE: SentryNativeWrapper = { try { const raw = await RNSentry.captureScreenshot(); - return { - ...raw, - data: new Uint8Array(raw.data), - } + return raw.map((item: NativeScreenshot) => ({ + ...item, + data: new Uint8Array(item.data), + })); } catch (e) { console.error(e); return null; From a6350dfa33d5593f2223a44cb8ce4ff407497674 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 16 Nov 2022 17:18:50 +0100 Subject: [PATCH 15/22] Fix lint --- src/js/wrapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 56144648e6..8d3f5e9494 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -467,7 +467,6 @@ export const NATIVE: SentryNativeWrapper = { data: new Uint8Array(item.data), })); } catch (e) { - console.error(e); return null; } }, From 60bb606f99d778161f8a12217f288e56075af49e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 16 Nov 2022 23:09:19 +0100 Subject: [PATCH 16/22] Use promise like and sync promise --- src/js/client.ts | 30 ++++++++++++++++-------------- test/client.test.ts | 6 ++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/js/client.ts b/src/js/client.ts index 887d78f111..ab459c9b56 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -12,7 +12,7 @@ import { Transport, UserFeedback, } from '@sentry/types'; -import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils'; +import { dateTimestampInSeconds, logger, resolvedSyncPromise, SentryError } from '@sentry/utils'; // @ts-ignore LogBox introduced in RN 0.63 import { Alert, LogBox, YellowBox } from 'react-native'; @@ -81,9 +81,9 @@ export class ReactNativeClient extends BaseClient { /** * @inheritDoc */ - public async eventFromException(_exception: unknown, _hint: EventHint = {}): Promise { - const hint = await this._attachScreenshotToEventHint(_hint); - return this._browserClient.eventFromException(_exception, hint); + public eventFromException(_exception: unknown, _hint: EventHint = {}): PromiseLike { + return this._attachScreenshotToEventHint(_hint) + .then(hint => this._browserClient.eventFromException(_exception, hint)); } /** @@ -209,18 +209,20 @@ export class ReactNativeClient extends BaseClient { /** * If enabled attaches a screenshot to the event hint. */ - private async _attachScreenshotToEventHint(hint: EventHint): Promise { + private _attachScreenshotToEventHint(hint: EventHint): PromiseLike { if (!this._options.attachScreenshot) { - return hint; + return resolvedSyncPromise(hint); } - const screenshots = await NATIVE.captureScreenshot(); - if (screenshots !== null && screenshots.length > 0) { - hint.attachments = [ - ...screenshots, - ...(hint?.attachments || []), - ]; - } - return hint; + return NATIVE.captureScreenshot() + .then((screenshots) => { + if (screenshots !== null && screenshots.length > 0) { + hint.attachments = [ + ...screenshots, + ...(hint?.attachments || []), + ]; + } + return hint; + }); } } diff --git a/test/client.test.ts b/test/client.test.ts index 10db54a942..7c2df4fb47 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -27,6 +27,7 @@ interface MockedReactNative { initNativeSdk: jest.Mock; crash: jest.Mock; captureEnvelope: jest.Mock; + captureScreenshot: jest.Mock; }; }; Platform: { @@ -48,6 +49,7 @@ jest.mock( initNativeSdk: jest.fn(() => Promise.resolve(true)), crash: jest.fn(), captureEnvelope: jest.fn(), + captureScreenshot: jest.fn().mockResolvedValue(null), }, }, Platform: { @@ -284,12 +286,12 @@ describe('Tests ReactNativeClient', () => { describe('event data enhancement', () => { test('event contains sdk default information', async () => { - const mockedSend = jest.fn, [Envelope]>(); + const mockedSend = jest.fn, [Envelope]>().mockResolvedValue(undefined); const mockedTransport = (): Transport => ({ send: mockedSend, flush: jest.fn().mockResolvedValue(true), }); - const client = new ReactNativeClient( { + const client = new ReactNativeClient({ ...DEFAULT_OPTIONS, dsn: EXAMPLE_DSN, transport: mockedTransport, From 6bb24307a4aa0cdba60f729373d1b9d2b2b13b58 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 17 Nov 2022 13:49:15 +0100 Subject: [PATCH 17/22] Add log if take screenshot fails --- src/js/wrapper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 8d3f5e9494..351b5e3b46 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -467,6 +467,7 @@ export const NATIVE: SentryNativeWrapper = { data: new Uint8Array(item.data), })); } catch (e) { + logger.warn('Failed to capture screenshot', e); return null; } }, From be6041c3644e07487e8454c0d479a5b4e46a67bf Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 21 Nov 2022 12:53:22 +0100 Subject: [PATCH 18/22] Add screenshot integration --- src/js/client.ts | 29 ++++--------------- src/js/integrations/screenshot.ts | 46 +++++++++++++++++++++++++++++++ src/js/sdk.tsx | 4 +++ test/sdk.test.ts | 20 ++++++++++++++ 4 files changed, 75 insertions(+), 24 deletions(-) create mode 100644 src/js/integrations/screenshot.ts diff --git a/src/js/client.ts b/src/js/client.ts index ab459c9b56..ebf79ed0ed 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -12,10 +12,11 @@ import { Transport, UserFeedback, } from '@sentry/types'; -import { dateTimestampInSeconds, logger, resolvedSyncPromise, SentryError } from '@sentry/utils'; +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'; @@ -81,9 +82,9 @@ export class ReactNativeClient extends BaseClient { /** * @inheritDoc */ - public eventFromException(_exception: unknown, _hint: EventHint = {}): PromiseLike { - return this._attachScreenshotToEventHint(_hint) - .then(hint => this._browserClient.eventFromException(_exception, hint)); + public eventFromException(exception: unknown, hint: EventHint = {}): PromiseLike { + return Screenshot.attachScreenshotToEventHint(hint, this._options) + .then(enrichedHint => this._browserClient.eventFromException(exception, enrichedHint)); } /** @@ -205,24 +206,4 @@ export class ReactNativeClient extends BaseClient { envelope[items].push(clientReportItem); } } - - /** - * If enabled attaches a screenshot to the event hint. - */ - private _attachScreenshotToEventHint(hint: EventHint): PromiseLike { - if (!this._options.attachScreenshot) { - return resolvedSyncPromise(hint); - } - - return NATIVE.captureScreenshot() - .then((screenshots) => { - if (screenshots !== null && screenshots.length > 0) { - hint.attachments = [ - ...screenshots, - ...(hint?.attachments || []), - ]; - } - return hint; - }); - } } diff --git a/src/js/integrations/screenshot.ts b/src/js/integrations/screenshot.ts new file mode 100644 index 0000000000..f7db742abe --- /dev/null +++ b/src/js/integrations/screenshot.ts @@ -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 { + 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 {} +} diff --git a/src/js/sdk.tsx b/src/js/sdk.tsx index ae38a2c9ce..cca34b30b3 100644 --- a/src/js/sdk.tsx +++ b/src/js/sdk.tsx @@ -18,6 +18,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'; @@ -130,6 +131,9 @@ export function init(passedOptions: ReactNativeOptions): void { defaultIntegrations.push(new ReactNativeTracing()); } } + if (options.attachScreenshot) { + defaultIntegrations.push(new Screenshot()); + } } options.integrations = getIntegrationsToSetup({ diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 06fe51b53b..c154aff4e7 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -293,6 +293,26 @@ describe('Tests the SDK functionality', () => { expect(actualIntegrations).toEqual([mockDefaultIntegration]); }); + it('no screenshot integration by default', () => { + init({}); + + const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; + const actualIntegrations = actualOptions.integrations; + + expect(actualIntegrations).toEqual(expect.not.arrayContaining([expect.objectContaining({ name: 'Screenshot' })])); + }); + + it('adds screenshot integration', () => { + init({ + attachScreenshot: true, + }); + + const actualOptions = mockedInitAndBind.mock.calls[0][secondArg] as ReactNativeClientOptions; + const actualIntegrations = actualOptions.integrations; + + expect(actualIntegrations).toEqual(expect.arrayContaining([expect.objectContaining({ name: 'Screenshot' })])); + }); + it('no default integrations', () => { init({ defaultIntegrations: false, From 990f9e0c97ad74ece16e92f79990b53455bced68 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 22 Nov 2022 13:47:34 +0100 Subject: [PATCH 19/22] chore: RNSentry call getCurrentActivity directly --- android/src/main/java/io/sentry/react/RNSentryModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index 52dbc23382..4737691f2c 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -310,7 +310,7 @@ public void captureEnvelope(ReadableArray rawBytes, ReadableMap options, Promise @ReactMethod public void captureScreenshot(Promise promise) { - final Activity activity = this.getReactApplicationContext().getCurrentActivity(); + final Activity activity = getCurrentActivity(); if (activity == null) { logger.log(SentryLevel.WARNING, "CurrentActivity is null, can't capture screenshot."); promise.resolve(null); From 4a46f5498167fb51b9f6b01c015f509c8cd1a710 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 29 Nov 2022 10:36:32 +0100 Subject: [PATCH 20/22] Fix changelog move screenshots to unreleased section --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d20d211f4..fb1762cf0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Screenshots ([#2610](https://github.com/getsentry/sentry-react-native/pull/2610)) + ### Dependencies - Bump Android SDK from v6.8.0 to v6.9.0 ([#2643](https://github.com/getsentry/sentry-react-native/pull/2643)) @@ -26,7 +30,6 @@ ### Features - Add `maxQueueSize` option ([#2578](https://github.com/getsentry/sentry-react-native/pull/2578)) -- Screenshots ([#2610](https://github.com/getsentry/sentry-react-native/pull/2610)) ### Fixes From efe56ceec09b2fca8ea0feb7be93562b2d1b022b Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 29 Nov 2022 10:40:20 +0100 Subject: [PATCH 21/22] Fix fetchModules, use sentry logger --- android/src/main/java/io/sentry/react/RNSentryModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModule.java b/android/src/main/java/io/sentry/react/RNSentryModule.java index 529498ad5b..b241d89efc 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryModule.java @@ -212,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); } } From 69b7454723cc2cce026362ad4729ffb3709ec95d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 1 Dec 2022 15:02:24 +0100 Subject: [PATCH 22/22] Fix changelog --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f392dfd266..62f77e5bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,18 @@ # Changelog -## 4.10.1 +## Unreleased ### Features - Screenshots ([#2610](https://github.com/getsentry/sentry-react-native/pull/2610)) +## 4.10.1 + ### Fixes +- Bump Wizard from v1.2.17 to v1.4.0 ([#2645](https://github.com/getsentry/sentry-react-native/pull/2645)) + - [changelog](https://github.com/getsentry/sentry-wizard/blob/master/CHANGELOG.md#140) + - [diff](https://github.com/getsentry/sentry-wizard/compare/v1.2.17...v1.4.0) - Android builds without ext config, auto create assets dir for modules ([#2652](https://github.com/getsentry/sentry-react-native/pull/2652)) - Exit gracefully if source map file for collecting modules doesn't exist ([#2655](https://github.com/getsentry/sentry-react-native/pull/2655)) - Create only one clean-up tasks for modules collection ([#2657](https://github.com/getsentry/sentry-react-native/pull/2657)) @@ -17,9 +22,6 @@ - Bump Android SDK from v6.8.0 to v6.9.1 ([#2653](https://github.com/getsentry/sentry-react-native/pull/2653)) - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#691) - [diff](https://github.com/getsentry/sentry-java/compare/6.8.0...6.9.1) -- Bump Wizard from v1.2.17 to v1.4.0 ([#2645](https://github.com/getsentry/sentry-react-native/pull/2645)) - - [changelog](https://github.com/getsentry/sentry-wizard/blob/master/CHANGELOG.md#140) - - [diff](https://github.com/getsentry/sentry-wizard/compare/v1.2.17...v1.4.0) ## 4.10.0