Skip to content

Commit

Permalink
Add View Hierarchy (#2708)
Browse files Browse the repository at this point in the history
Co-authored-by: GitHub <noreply@github.com>
  • Loading branch information
krystofwoldrich and web-flow authored Feb 7, 2023
1 parent 7406fdc commit a71ab71
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Add View Hierarchy to the crashed/errored events ([#2708](https://github.com/getsentry/sentry-react-native/pull/2708))
- 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))

### Dependencies
Expand Down
39 changes: 36 additions & 3 deletions android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
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;
import java.io.File;
Expand All @@ -42,6 +41,7 @@
import io.sentry.DateUtils;
import io.sentry.HubAdapter;
import io.sentry.ILogger;
import io.sentry.ISerializer;
import io.sentry.Integration;
import io.sentry.Sentry;
import io.sentry.SentryDate;
Expand All @@ -54,12 +54,14 @@
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.ViewHierarchyEventProcessor;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryException;
import io.sentry.protocol.SentryPackage;
import io.sentry.protocol.User;
import io.sentry.protocol.ViewHierarchy;
import io.sentry.util.JsonSerializationUtils;

public class RNSentryModuleImpl {

Expand All @@ -74,7 +76,6 @@ public class RNSentryModuleImpl {
private final PackageInfo packageInfo;
private FrameMetricsAggregator frameMetricsAggregator = null;
private boolean androidXAvailable;
private ScreenshotEventProcessor screenshotEventProcessor;

private static boolean didFetchAppStart;

Expand Down Expand Up @@ -153,6 +154,9 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
if (rnOptions.hasKey("attachScreenshot")) {
options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot"));
}
if (rnOptions.hasKey("attachViewHierarchy")) {
options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy"));
}
if (rnOptions.hasKey("sendDefaultPii")) {
options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii"));
}
Expand Down Expand Up @@ -389,6 +393,35 @@ private static byte[] takeScreenshotOnUiThread(Activity activity) {
return bytesWrapper[0];
}

public void fetchViewHierarchy(Promise promise) {
final @Nullable Activity activity = getCurrentActivity();
final @Nullable ViewHierarchy viewHierarchy = ViewHierarchyEventProcessor.snapshotViewHierarchy(activity, logger);
if (viewHierarchy == null) {
logger.log(SentryLevel.ERROR, "Could not get ViewHierarchy.");
promise.resolve(null);
return;
}

ISerializer serializer = HubAdapter.getInstance().getOptions().getSerializer();
final @Nullable byte[] bytes = JsonSerializationUtils.bytesFrom(serializer, logger, viewHierarchy);
if (bytes == null) {
logger.log(SentryLevel.ERROR, "Could not serialize ViewHierarchy.");
promise.resolve(null);
return;
}
if (bytes.length < 1) {
logger.log(SentryLevel.ERROR, "Got empty bytes array after serializing ViewHierarchy.");
promise.resolve(null);
return;
}

final WritableNativeArray data = new WritableNativeArray();
for (final byte b : bytes) {
data.pushInt(b);
}
promise.resolve(data);
}

private static PackageInfo getPackageInfo(Context ctx) {
try {
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
Expand Down
5 changes: 5 additions & 0 deletions android/src/newarch/java/io/sentry/react/RNSentryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public void captureScreenshot(Promise promise) {
this.impl.captureScreenshot(promise);
}

@Override
public void fetchViewHierarchy(Promise promise){
this.impl.fetchViewHierarchy(promise);
}

@Override
public void setUser(final ReadableMap user, final ReadableMap otherUserKeys) {
this.impl.setUser(user, otherUserKeys);
Expand Down
5 changes: 5 additions & 0 deletions android/src/oldarch/java/io/sentry/react/RNSentryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public void captureScreenshot(Promise promise) {
this.impl.captureScreenshot(promise);
}

@ReactMethod
public void fetchViewHierarchy(Promise promise){
this.impl.fetchViewHierarchy(promise);
}

@ReactMethod
public void setUser(final ReadableMap user, final ReadableMap otherUserKeys) {
this.impl.setUser(user, otherUserKeys);
Expand Down
14 changes: 14 additions & 0 deletions ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,20 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
resolve(screenshotsArray);
}

RCT_EXPORT_METHOD(fetchViewHierarchy: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject)
{
NSData * rawViewHierarchy = [PrivateSentrySDKOnly captureViewHierarchy];

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

resolve(viewHierarchy);
}

RCT_EXPORT_METHOD(setUser:(NSDictionary *)userKeys
otherUserKeys:(NSDictionary *)userDataKeys
)
Expand Down
1 change: 1 addition & 0 deletions sample-new-architecture/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Sentry.init({
// otherwise they will not work.
// release: 'myapp@1.2.3+1',
// dist: `1`,
attachViewHierarchy: true,
});

const Stack = createStackNavigator();
Expand Down
2 changes: 2 additions & 0 deletions sample/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ Sentry.init({
attachStacktrace: true,
// Attach screenshots to events.
attachScreenshot: true,
// Attach view hierarchy to events.
attachViewHierarchy: true,
});

const Stack = createStackNavigator();
Expand Down
1 change: 1 addition & 0 deletions src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface Spec extends TurboModule {
setTag(key: string, value: string): void;
enableNativeFramesTracking(): void;
fetchModules(): Promise<string | undefined | null>;
fetchViewHierarchy(): Promise<number[] | undefined | null>;
}

export type NativeAppStartResponse = {
Expand Down
54 changes: 54 additions & 0 deletions src/js/integrations/viewhierarchy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Event, EventHint, EventProcessor, Integration } from '@sentry/types';
import { logger } from '@sentry/utils';

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

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

private static _fileName: string = 'view-hierarchy.json';
private static _contentType: string = 'application/json';
private static _attachmentType: string = 'event.view_hierarchy';

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

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (e: EventProcessor) => void): void {
addGlobalEventProcessor(async (event: Event, hint: EventHint) => {
const hasException = event.exception && event.exception.values && event.exception.values.length > 0;
if (!hasException) {
return event;
}

let viewHierarchy: Uint8Array | null = null;
try {
viewHierarchy = await NATIVE.fetchViewHierarchy()
} catch (e) {
logger.error('Failed to get view hierarchy from native.', e);
}

if (viewHierarchy) {
hint.attachments = [
{
filename: ViewHierarchy._fileName,
contentType: ViewHierarchy._contentType,
attachmentType: ViewHierarchy._attachmentType,
data: viewHierarchy,
},
...(hint?.attachments || []),
];
}

return event;
});
}
}
7 changes: 7 additions & 0 deletions src/js/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export interface BaseReactNativeOptions {
* @default false
*/
attachScreenshot?: boolean;

/**
* When enabled Sentry includes the current view hierarchy in the error attachments.
*
* @default false
*/
attachViewHierarchy?: boolean;
}

export interface ReactNativeTransportOptions extends BrowserTransportOptions {
Expand Down
12 changes: 8 additions & 4 deletions src/js/sdk.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Scope} from '@sentry/core';
import type { Scope } from '@sentry/core';
import { getIntegrationsToSetup, Hub, initAndBind, makeMain, setExtra } from '@sentry/core';
import { RewriteFrames } from '@sentry/integrations';
import {
Expand All @@ -23,6 +23,7 @@ import {
SdkInfo,
} from './integrations';
import { Screenshot } from './integrations/screenshot';
import { ViewHierarchy } from './integrations/viewhierarchy';
import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
import { ReactNativeScope } from './scope';
import { TouchEventBoundary } from './touchevents';
Expand Down Expand Up @@ -140,6 +141,9 @@ export function init(passedOptions: ReactNativeOptions): void {
if (options.attachScreenshot) {
defaultIntegrations.push(new Screenshot());
}
if (options.attachViewHierarchy) {
defaultIntegrations.push(new ViewHierarchy());
}
}

options.integrations = getIntegrationsToSetup({
Expand Down Expand Up @@ -247,9 +251,9 @@ export async function close(): Promise<void> {
/**
* Captures user feedback and sends it to Sentry.
*/
export function captureUserFeedback(feedback: UserFeedback): void {
export function captureUserFeedback(feedback: UserFeedback): void {
getCurrentHub().getClient<ReactNativeClient>()?.captureUserFeedback(feedback);
}
}

/**
* Creates a new scope with and executes the given operation within.
Expand Down Expand Up @@ -279,7 +283,7 @@ export function withScope(callback: (scope: Scope) => void): ReturnType<Hub['wit
* Callback to set context information onto the scope.
* @param callback Callback function that receives Scope.
*/
export function configureScope(callback: (scope: Scope) => void): ReturnType<Hub['configureScope']> {
export function configureScope(callback: (scope: Scope) => void): ReturnType<Hub['configureScope']> {
const safeCallback = (scope: Scope): void => {
try {
callback(scope);
Expand Down
13 changes: 13 additions & 0 deletions src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ interface SentryNativeWrapper {
nativeCrash(): void;

fetchModules(): Promise<Record<string, string> | null>;
fetchViewHierarchy(): PromiseLike<Uint8Array | null>;
}

/**
Expand Down Expand Up @@ -491,6 +492,18 @@ export const NATIVE: SentryNativeWrapper = {
}
},

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

const raw = await RNSentry.fetchViewHierarchy();
return raw ? new Uint8Array(raw) : 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
Loading

0 comments on commit a71ab71

Please sign in to comment.