Skip to content

Commit a71ab71

Browse files
Add View Hierarchy (#2708)
Co-authored-by: GitHub <[email protected]>
1 parent 7406fdc commit a71ab71

File tree

14 files changed

+285
-7
lines changed

14 files changed

+285
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Add View Hierarchy to the crashed/errored events ([#2708](https://github.com/getsentry/sentry-react-native/pull/2708))
78
- Collect modules script for XCode builds supports NODE_BINARY to set path to node executable ([#2805](https://github.com/getsentry/sentry-react-native/pull/2805))
89

910
### Dependencies

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import com.facebook.react.bridge.WritableMap;
2424
import com.facebook.react.bridge.WritableNativeArray;
2525
import com.facebook.react.bridge.WritableNativeMap;
26-
import com.facebook.react.module.annotations.ReactModule;
2726

2827
import java.io.BufferedInputStream;
2928
import java.io.File;
@@ -42,6 +41,7 @@
4241
import io.sentry.DateUtils;
4342
import io.sentry.HubAdapter;
4443
import io.sentry.ILogger;
44+
import io.sentry.ISerializer;
4545
import io.sentry.Integration;
4646
import io.sentry.Sentry;
4747
import io.sentry.SentryDate;
@@ -54,12 +54,14 @@
5454
import io.sentry.android.core.BuildInfoProvider;
5555
import io.sentry.android.core.CurrentActivityHolder;
5656
import io.sentry.android.core.NdkIntegration;
57-
import io.sentry.android.core.ScreenshotEventProcessor;
5857
import io.sentry.android.core.SentryAndroid;
58+
import io.sentry.android.core.ViewHierarchyEventProcessor;
5959
import io.sentry.protocol.SdkVersion;
6060
import io.sentry.protocol.SentryException;
6161
import io.sentry.protocol.SentryPackage;
6262
import io.sentry.protocol.User;
63+
import io.sentry.protocol.ViewHierarchy;
64+
import io.sentry.util.JsonSerializationUtils;
6365

6466
public class RNSentryModuleImpl {
6567

@@ -74,7 +76,6 @@ public class RNSentryModuleImpl {
7476
private final PackageInfo packageInfo;
7577
private FrameMetricsAggregator frameMetricsAggregator = null;
7678
private boolean androidXAvailable;
77-
private ScreenshotEventProcessor screenshotEventProcessor;
7879

7980
private static boolean didFetchAppStart;
8081

@@ -153,6 +154,9 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
153154
if (rnOptions.hasKey("attachScreenshot")) {
154155
options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot"));
155156
}
157+
if (rnOptions.hasKey("attachViewHierarchy")) {
158+
options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy"));
159+
}
156160
if (rnOptions.hasKey("sendDefaultPii")) {
157161
options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii"));
158162
}
@@ -389,6 +393,35 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) {
389393
return bytesWrapper[0];
390394
}
391395

396+
public void fetchViewHierarchy(Promise promise) {
397+
final @Nullable Activity activity = getCurrentActivity();
398+
final @Nullable ViewHierarchy viewHierarchy = ViewHierarchyEventProcessor.snapshotViewHierarchy(activity, logger);
399+
if (viewHierarchy == null) {
400+
logger.log(SentryLevel.ERROR, "Could not get ViewHierarchy.");
401+
promise.resolve(null);
402+
return;
403+
}
404+
405+
ISerializer serializer = HubAdapter.getInstance().getOptions().getSerializer();
406+
final @Nullable byte[] bytes = JsonSerializationUtils.bytesFrom(serializer, logger, viewHierarchy);
407+
if (bytes == null) {
408+
logger.log(SentryLevel.ERROR, "Could not serialize ViewHierarchy.");
409+
promise.resolve(null);
410+
return;
411+
}
412+
if (bytes.length < 1) {
413+
logger.log(SentryLevel.ERROR, "Got empty bytes array after serializing ViewHierarchy.");
414+
promise.resolve(null);
415+
return;
416+
}
417+
418+
final WritableNativeArray data = new WritableNativeArray();
419+
for (final byte b : bytes) {
420+
data.pushInt(b);
421+
}
422+
promise.resolve(data);
423+
}
424+
392425
private static PackageInfo getPackageInfo(Context ctx) {
393426
try {
394427
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public void captureScreenshot(Promise promise) {
6262
this.impl.captureScreenshot(promise);
6363
}
6464

65+
@Override
66+
public void fetchViewHierarchy(Promise promise){
67+
this.impl.fetchViewHierarchy(promise);
68+
}
69+
6570
@Override
6671
public void setUser(final ReadableMap user, final ReadableMap otherUserKeys) {
6772
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
@@ -62,6 +62,11 @@ public void captureScreenshot(Promise promise) {
6262
this.impl.captureScreenshot(promise);
6363
}
6464

65+
@ReactMethod
66+
public void fetchViewHierarchy(Promise promise){
67+
this.impl.fetchViewHierarchy(promise);
68+
}
69+
6570
@ReactMethod
6671
public void setUser(final ReadableMap user, final ReadableMap otherUserKeys) {
6772
this.impl.setUser(user, otherUserKeys);

ios/RNSentry.mm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,20 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
337337
resolve(screenshotsArray);
338338
}
339339

340+
RCT_EXPORT_METHOD(fetchViewHierarchy: (RCTPromiseResolveBlock)resolve
341+
rejecter: (RCTPromiseRejectBlock)reject)
342+
{
343+
NSData * rawViewHierarchy = [PrivateSentrySDKOnly captureViewHierarchy];
344+
345+
NSMutableArray *viewHierarchy = [NSMutableArray arrayWithCapacity:rawViewHierarchy.length];
346+
const char *bytes = (char*) [rawViewHierarchy bytes];
347+
for (int i = 0; i < [rawViewHierarchy length]; i++) {
348+
[viewHierarchy addObject:[[NSNumber alloc] initWithChar:bytes[i]]];
349+
}
350+
351+
resolve(viewHierarchy);
352+
}
353+
340354
RCT_EXPORT_METHOD(setUser:(NSDictionary *)userKeys
341355
otherUserKeys:(NSDictionary *)userDataKeys
342356
)

sample-new-architecture/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Sentry.init({
6363
// otherwise they will not work.
6464
// release: '[email protected]+1',
6565
// dist: `1`,
66+
attachViewHierarchy: true,
6667
});
6768

6869
const Stack = createStackNavigator();

sample/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ Sentry.init({
7979
attachStacktrace: true,
8080
// Attach screenshots to events.
8181
attachScreenshot: true,
82+
// Attach view hierarchy to events.
83+
attachViewHierarchy: true,
8284
});
8385

8486
const Stack = createStackNavigator();

src/js/NativeRNSentry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface Spec extends TurboModule {
3333
setTag(key: string, value: string): void;
3434
enableNativeFramesTracking(): void;
3535
fetchModules(): Promise<string | undefined | null>;
36+
fetchViewHierarchy(): Promise<number[] | undefined | null>;
3637
}
3738

3839
export type NativeAppStartResponse = {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Event, EventHint, EventProcessor, Integration } from '@sentry/types';
2+
import { logger } from '@sentry/utils';
3+
4+
import { NATIVE } from '../wrapper';
5+
6+
/** Adds ViewHierarchy to error events */
7+
export class ViewHierarchy implements Integration {
8+
/**
9+
* @inheritDoc
10+
*/
11+
public static id: string = 'ViewHierarchy';
12+
13+
private static _fileName: string = 'view-hierarchy.json';
14+
private static _contentType: string = 'application/json';
15+
private static _attachmentType: string = 'event.view_hierarchy';
16+
17+
/**
18+
* @inheritDoc
19+
*/
20+
public name: string = ViewHierarchy.id;
21+
22+
/**
23+
* @inheritDoc
24+
*/
25+
public setupOnce(addGlobalEventProcessor: (e: EventProcessor) => void): void {
26+
addGlobalEventProcessor(async (event: Event, hint: EventHint) => {
27+
const hasException = event.exception && event.exception.values && event.exception.values.length > 0;
28+
if (!hasException) {
29+
return event;
30+
}
31+
32+
let viewHierarchy: Uint8Array | null = null;
33+
try {
34+
viewHierarchy = await NATIVE.fetchViewHierarchy()
35+
} catch (e) {
36+
logger.error('Failed to get view hierarchy from native.', e);
37+
}
38+
39+
if (viewHierarchy) {
40+
hint.attachments = [
41+
{
42+
filename: ViewHierarchy._fileName,
43+
contentType: ViewHierarchy._contentType,
44+
attachmentType: ViewHierarchy._attachmentType,
45+
data: viewHierarchy,
46+
},
47+
...(hint?.attachments || []),
48+
];
49+
}
50+
51+
return event;
52+
});
53+
}
54+
}

src/js/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ export interface BaseReactNativeOptions {
134134
* @default false
135135
*/
136136
attachScreenshot?: boolean;
137+
138+
/**
139+
* When enabled Sentry includes the current view hierarchy in the error attachments.
140+
*
141+
* @default false
142+
*/
143+
attachViewHierarchy?: boolean;
137144
}
138145

139146
export interface ReactNativeTransportOptions extends BrowserTransportOptions {

0 commit comments

Comments
 (0)