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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: @alwx Since the feature is experimental and we don't have documentation published yet wdyt of adding a sample like the following and a link to the doc?

Suggested change
- Experimental support of UI profiling on Android ([#5518](https://github.com/getsentry/sentry-react-native/pull/5518))
- Experimental support of UI profiling on Android ([#5518](https://github.com/getsentry/sentry-react-native/pull/5518))
```typescript
Sentry.init({
// other options...
_experiments: {
androidProfilingOptions: {
profileSessionSampleRate: 1.0,
lifecycle: 'trace',
startOnAppStart: true,
},
}
});
```
To learn more visit [the documentation](https://docs.sentry.io/platforms/android/profiling/)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antonis the sample is coming, yes!


### 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 @@ -36,6 +36,7 @@
import io.sentry.ISentryExecutorService;
import io.sentry.ISerializer;
import io.sentry.Integration;
import io.sentry.ProfileLifecycle;
import io.sentry.ScopesAdapter;
import io.sentry.ScreenshotStrategyType;
import io.sentry.Sentry;
Expand Down Expand Up @@ -324,7 +325,8 @@ protected void getSentryAndroidOptions(

SentryReplayOptions replayOptions = getReplayOptions(rnOptions);
options.setSessionReplay(replayOptions);
// Check if the replay integration is available on the classpath. It's already kept from R8
// Check if the replay integration is available on the classpath. It's already
// kept from R8
// shrinking by sentry-android-core
final boolean isReplayAvailable =
loadClass.isClassAvailable("io.sentry.android.replay.ReplayIntegration", logger);
Expand All @@ -333,6 +335,9 @@ protected void getSentryAndroidOptions(
initFragmentReplayTracking();
}

// 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 @@ -482,17 +487,70 @@ private SentryReplayQuality parseReplayQuality(@Nullable String qualityString) {
}
}

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,
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)) {
Comment on lines +521 to +522

This comment was marked as outdated.

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));
}
}

public void crash() {
throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)");
}

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 @@ -557,7 +615,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 @@ -1292,7 +1351,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
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,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: androidProfilingOptions are read from _experiments before they are moved there, causing them to be undefined when passed to the native SDK.
Severity: HIGH

Suggested Fix

In client.ts, modify the code to read androidProfilingOptions directly from the top-level options object. Change the access from this._options._experiments?.androidProfilingOptions to this._options.androidProfilingOptions.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/core/src/js/client.ts#L227

Potential issue: In `client.ts`, the `androidProfilingOptions` are incorrectly read from
`this._options._experiments?.androidProfilingOptions`. At this stage of the
initialization process, these options are still located at the top level of
`this._options` and have not yet been moved into the `_experiments` object. This move
happens later in `wrapper.ts`. As a result, `undefined` is passed to the native SDK,
which completely disables the Android UI profiling feature even when it is correctly
configured by the user.

Did we get this right? 👍 / 👎 to inform future reviews.

})
.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