Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

## Unreleased

### Features

- Experimental support of UI profiling on Android ([#5518](https://github.com/getsentry/sentry-react-native/pull/5518))

### Fixes

- Fix duplicate error reporting on iOS with New Architecture ([#5532](https://github.com/getsentry/sentry-react-native/pull/5532))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,14 @@ public void crash() {
}

public void addListener(String eventType) {
// Is must be defined otherwise the generated interface from TS won't be fulfilled
// Is must be defined otherwise the generated interface from TS won't be
// fulfilled
logger.log(SentryLevel.ERROR, "addListener of NativeEventEmitter can't be used on Android!");
}

public void removeListeners(double id) {
// Is must be defined otherwise the generated interface from TS won't be fulfilled
// Is must be defined otherwise the generated interface from TS won't be
// fulfilled
logger.log(
SentryLevel.ERROR, "removeListeners of NativeEventEmitter can't be used on Android!");
}
Expand Down Expand Up @@ -262,7 +264,8 @@ protected void fetchNativeAppStart(
// When activity is destroyed but the application process is kept alive
// the next activity creation is considered warm start.
// The app start metrics will be updated by the the Android SDK.
// To let the RN JS layer know these are new start data we compare the start timestamps.
// To let the RN JS layer know these are new start data we compare the start
// timestamps.
lastStartTimestampMs = currentStartTimestampMs;

// Clears start metrics, making them ready for recording warm app start
Expand Down Expand Up @@ -952,7 +955,8 @@ protected void trySetIgnoreErrors(SentryAndroidOptions options, ReadableMap rnOp
}
}
if (strErrors != null) {
// Use the same behaviour of JavaScript instead of Android when dealing with strings.
// Use the same behaviour of JavaScript instead of Android when dealing with
// strings.
for (int i = 0; i < strErrors.size(); i++) {
String pattern = ".*" + Pattern.quote(strErrors.getString(i)) + ".*";
list.add(pattern);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.facebook.react.common.JavascriptException;
import io.sentry.ILogger;
import io.sentry.Integration;
import io.sentry.ProfileLifecycle;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
Expand Down Expand Up @@ -162,6 +163,9 @@ static void getSentryAndroidOptions(
options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter());
}

// Configure Android UI Profiling
configureAndroidProfiling(options, rnOptions, logger);

// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
String dsn = getURLFromDSN(rnOptions.getString("dsn"));
String devServerUrl = rnOptions.getString("devServerUrl");
Expand Down Expand Up @@ -192,6 +196,78 @@ static void getSentryAndroidOptions(
SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations()));
}

private static void configureAndroidProfiling(
@NotNull SentryAndroidOptions options,
@NotNull ReadableMap rnOptions,
@NotNull ILogger logger) {
if (!rnOptions.hasKey("_experiments")) {
return;
}

@Nullable final ReadableMap experiments = rnOptions.getMap("_experiments");
if (experiments == null || !experiments.hasKey("androidProfilingOptions")) {
return;
}

@Nullable
final ReadableMap androidProfilingOptions = experiments.getMap("androidProfilingOptions");
if (androidProfilingOptions == null) {
return;
}

// Set profile session sample rate
if (androidProfilingOptions.hasKey("profileSessionSampleRate")) {
if (androidProfilingOptions.getType("profileSessionSampleRate") == ReadableType.Number) {
final double profileSessionSampleRate =
androidProfilingOptions.getDouble("profileSessionSampleRate");
options.setProfileSessionSampleRate(profileSessionSampleRate);
logger.log(
SentryLevel.INFO,
String.format(
"Android UI Profiling profileSessionSampleRate set to: %.2f",
profileSessionSampleRate));
} else {
logger.log(
SentryLevel.WARNING,
"Android UI Profiling profileSessionSampleRate must be a number, ignoring invalid"
+ " value");
}
}

// Set profiling lifecycle mode
if (androidProfilingOptions.hasKey("lifecycle")) {
if (androidProfilingOptions.getType("lifecycle") == ReadableType.String) {
final String lifecycle = androidProfilingOptions.getString("lifecycle");
if ("manual".equalsIgnoreCase(lifecycle)) {
options.setProfileLifecycle(ProfileLifecycle.MANUAL);
logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to MANUAL");
} else if ("trace".equalsIgnoreCase(lifecycle)) {
options.setProfileLifecycle(ProfileLifecycle.TRACE);
logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to TRACE");
}
} else {
logger.log(
SentryLevel.WARNING,
"Android UI Profiling lifecycle must be a string, ignoring invalid value");
}
}

// Set start on app start
if (androidProfilingOptions.hasKey("startOnAppStart")) {
if (androidProfilingOptions.getType("startOnAppStart") == ReadableType.Boolean) {
final boolean startOnAppStart = androidProfilingOptions.getBoolean("startOnAppStart");
options.setStartProfilerOnAppStart(startOnAppStart);
logger.log(
SentryLevel.INFO,
String.format("Android UI Profiling startOnAppStart set to %b", startOnAppStart));
} else {
logger.log(
SentryLevel.WARNING,
"Android UI Profiling startOnAppStart must be a boolean, ignoring invalid value");
}
}
}

/**
* This function updates the options with RNSentry defaults. These default can be overwritten by
* users during manual native initialization.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export class ReactNativeClient extends Client<ReactNativeClientOptions> {
'options' in this._integrations[MOBILE_REPLAY_INTEGRATION_NAME]
? (this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] as ReturnType<typeof mobileReplayIntegration>).options
: undefined,
androidProfilingOptions: this._options._experiments?.androidProfilingOptions,
})
.then(
(result: boolean) => {
Expand Down
55 changes: 54 additions & 1 deletion packages/core/src/js/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,25 @@ export interface BaseReactNativeOptions {
/**
* Experiment: A more reliable way to report unhandled C++ exceptions in iOS.
*
* This approach hooks into all instances of the `__cxa_throw` function, which provides a more comprehensive and consistent exception handling across an apps runtime, regardless of the number of C++ modules or how theyre linked. It helps in obtaining accurate stack traces.
* This approach hooks into all instances of the `__cxa_throw` function, which provides a more comprehensive and consistent exception handling across an app's runtime, regardless of the number of C++ modules or how they're linked. It helps in obtaining accurate stack traces.
*
* - Note: The mechanism of hooking into `__cxa_throw` could cause issues with symbolication on iOS due to caching of symbol references.
*
* @default false
* @platform ios
*/
enableUnhandledCPPExceptionsV2?: boolean;

/**
* Configuration options for Android UI profiling.
* UI profiling supports two modes: `manual` and `trace`.
* - In `trace` mode, the profiler runs based on active sampled spans.
* - In `manual` mode, profiling is controlled via start/stop API calls.
*
* @experimental
* @platform android
*/
androidProfilingOptions?: AndroidProfilingOptions;
};

/**
Expand Down Expand Up @@ -330,6 +341,48 @@ export interface BaseReactNativeOptions {

export type SentryReplayQuality = 'low' | 'medium' | 'high';

/**
* Android UI profiling lifecycle modes.
* - `trace`: Profiler runs based on active sampled spans
* - `manual`: Profiler is controlled manually via start/stop API calls
*/
export type AndroidProfilingLifecycle = 'trace' | 'manual';

/**
* Configuration options for Android UI profiling.
*
* @experimental
* @platform android
*/
export interface AndroidProfilingOptions {
/**
* Sample rate for profiling sessions.
* This is evaluated once per session and determines if profiling should be enabled for that session.
* 1.0 will enable profiling for all sessions, 0.0 will disable profiling.
*
* @default undefined (profiling disabled)
*/
profileSessionSampleRate?: number;

/**
* Profiling lifecycle mode.
* - `trace`: Profiler runs while there is at least one active sampled span
* - `manual`: Profiler is controlled manually via Sentry.profiler.startProfiler/stopProfiler
*
* @default 'trace'
*/
lifecycle?: AndroidProfilingLifecycle;

/**
* Enable profiling on app start.
* - In `trace` mode: The app start profile stops automatically when the app start root span finishes
* - In `manual` mode: The app start profile must be stopped through Sentry.profiler.stopProfiler()
*
* @default false
*/
startOnAppStart?: boolean;
}

export interface ReactNativeTransportOptions extends BrowserTransportOptions {
/**
* @deprecated use `maxQueueSize` in the root of the SDK options.
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type {
NativeStackFrames,
Spec,
} from './NativeRNSentry';
import type { ReactNativeClientOptions } from './options';
import type { AndroidProfilingOptions, ReactNativeClientOptions } from './options';
import type * as Hermes from './profiling/hermes';
import type { NativeAndroidProfileEvent, NativeProfileEvent } from './profiling/nativeTypes';
import type { MobileReplayOptions } from './replay/mobilereplay';
Expand Down Expand Up @@ -57,6 +57,7 @@ export type NativeSdkOptions = Partial<ReactNativeClientOptions> & {
ignoreErrorsRegex?: string[] | undefined;
} & {
mobileReplayOptions: MobileReplayOptions | undefined;
androidProfilingOptions?: AndroidProfilingOptions | undefined;
};

interface SentryNativeWrapper {
Expand Down Expand Up @@ -286,9 +287,19 @@ export const NATIVE: SentryNativeWrapper = {
integrations,
ignoreErrors,
logsOrigin,
androidProfilingOptions,
...filteredOptions
} = options;
/* eslint-enable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */

// Move androidProfilingOptions into _experiments
if (androidProfilingOptions) {
filteredOptions._experiments = {
...filteredOptions._experiments,
androidProfilingOptions,
};
}

const nativeIsReady = await RNSentry.initNativeSdk(filteredOptions);

this.nativeIsReady = nativeIsReady;
Expand Down
Loading
Loading