Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
Copy Markdown
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
Copy Markdown
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 for missing `replay_id` from metrics ([#5483](https://github.com/getsentry/sentry-react-native/pull/5483))
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);
Comment thread
sentry[bot] marked this conversation as resolved.
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
Copy Markdown

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