Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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);

// 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,57 @@ static void getSentryAndroidOptions(
SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations()));
}

private void configureAndroidProfiling(
@NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions) {
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")) {
final double profileSessionSampleRate =
androidProfilingOptions.getDouble("profileSessionSampleRate");
options.setProfileSessionSampleRate(profileSessionSampleRate);
logger.log(
SentryLevel.INFO,
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated
String.format(
"Android UI Profiling profileSessionSampleRate set to: %.2f",
profileSessionSampleRate));
}

// Set profiling lifecycle mode
if (androidProfilingOptions.hasKey("lifecycle")) {
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");
}
}

// Set start on app start
if (androidProfilingOptions.hasKey("startOnAppStart")) {
final boolean startOnAppStart = androidProfilingOptions.getBoolean("startOnAppStart");
options.setStartProfilerOnAppStart(startOnAppStart);
logger.log(
SentryLevel.INFO,
String.format("Android UI Profiling startOnAppStart set to %b", startOnAppStart));
}
}

/**
* 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