Skip to content

Commit 7b02433

Browse files
authored
Experimental: Android UI profiling (#5518)
* Experimental: Android UI profiling * Fixes to imports * Lint fixes * Fixes * Changelog entry
1 parent 462b16e commit 7b02433

File tree

6 files changed

+295
-7
lines changed

6 files changed

+295
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- Experimental support of UI profiling on Android ([#5518](https://github.com/getsentry/sentry-react-native/pull/5518))
14+
1115
### Fixes
1216

1317
- Fix duplicate error reporting on iOS with New Architecture ([#5532](https://github.com/getsentry/sentry-react-native/pull/5532))

packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import io.sentry.ISentryExecutorService;
3737
import io.sentry.ISerializer;
3838
import io.sentry.Integration;
39+
import io.sentry.ProfileLifecycle;
3940
import io.sentry.ScopesAdapter;
4041
import io.sentry.ScreenshotStrategyType;
4142
import io.sentry.Sentry;
@@ -324,7 +325,8 @@ protected void getSentryAndroidOptions(
324325

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

338+
// Configure Android UI Profiling
339+
configureAndroidProfiling(options, rnOptions);
340+
336341
// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
337342
String dsn = getURLFromDSN(rnOptions.getString("dsn"));
338343
String devServerUrl = rnOptions.getString("devServerUrl");
@@ -482,17 +487,70 @@ private SentryReplayQuality parseReplayQuality(@Nullable String qualityString) {
482487
}
483488
}
484489

490+
private void configureAndroidProfiling(
491+
@NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions) {
492+
if (!rnOptions.hasKey("_experiments")) {
493+
return;
494+
}
495+
496+
@Nullable final ReadableMap experiments = rnOptions.getMap("_experiments");
497+
if (experiments == null || !experiments.hasKey("androidProfilingOptions")) {
498+
return;
499+
}
500+
501+
@Nullable
502+
final ReadableMap androidProfilingOptions = experiments.getMap("androidProfilingOptions");
503+
if (androidProfilingOptions == null) {
504+
return;
505+
}
506+
507+
// Set profile session sample rate
508+
if (androidProfilingOptions.hasKey("profileSessionSampleRate")) {
509+
final double profileSessionSampleRate =
510+
androidProfilingOptions.getDouble("profileSessionSampleRate");
511+
options.setProfileSessionSampleRate(profileSessionSampleRate);
512+
logger.log(
513+
SentryLevel.INFO,
514+
String.format(
515+
"Android UI Profiling profileSessionSampleRate set to: %.2f",
516+
profileSessionSampleRate));
517+
}
518+
519+
// Set profiling lifecycle mode
520+
if (androidProfilingOptions.hasKey("lifecycle")) {
521+
final String lifecycle = androidProfilingOptions.getString("lifecycle");
522+
if ("manual".equalsIgnoreCase(lifecycle)) {
523+
options.setProfileLifecycle(ProfileLifecycle.MANUAL);
524+
logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to MANUAL");
525+
} else if ("trace".equalsIgnoreCase(lifecycle)) {
526+
options.setProfileLifecycle(ProfileLifecycle.TRACE);
527+
logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to TRACE");
528+
}
529+
}
530+
531+
// Set start on app start
532+
if (androidProfilingOptions.hasKey("startOnAppStart")) {
533+
final boolean startOnAppStart = androidProfilingOptions.getBoolean("startOnAppStart");
534+
options.setStartProfilerOnAppStart(startOnAppStart);
535+
logger.log(
536+
SentryLevel.INFO,
537+
String.format("Android UI Profiling startOnAppStart set to %b", startOnAppStart));
538+
}
539+
}
540+
485541
public void crash() {
486542
throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)");
487543
}
488544

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

494551
public void removeListeners(double id) {
495-
// Is must be defined otherwise the generated interface from TS won't be fulfilled
552+
// Is must be defined otherwise the generated interface from TS won't be
553+
// fulfilled
496554
logger.log(
497555
SentryLevel.ERROR, "removeListeners of NativeEventEmitter can't be used on Android!");
498556
}
@@ -557,7 +615,8 @@ protected void fetchNativeAppStart(
557615
// When activity is destroyed but the application process is kept alive
558616
// the next activity creation is considered warm start.
559617
// The app start metrics will be updated by the the Android SDK.
560-
// To let the RN JS layer know these are new start data we compare the start timestamps.
618+
// To let the RN JS layer know these are new start data we compare the start
619+
// timestamps.
561620
lastStartTimestampMs = currentStartTimestampMs;
562621

563622
// Clears start metrics, making them ready for recording warm app start
@@ -1292,7 +1351,8 @@ protected void trySetIgnoreErrors(SentryAndroidOptions options, ReadableMap rnOp
12921351
}
12931352
}
12941353
if (strErrors != null) {
1295-
// Use the same behaviour of JavaScript instead of Android when dealing with strings.
1354+
// Use the same behaviour of JavaScript instead of Android when dealing with
1355+
// strings.
12961356
for (int i = 0; i < strErrors.size(); i++) {
12971357
String pattern = ".*" + Pattern.quote(strErrors.getString(i)) + ".*";
12981358
list.add(pattern);

packages/core/src/js/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export class ReactNativeClient extends Client<ReactNativeClientOptions> {
224224
'options' in this._integrations[MOBILE_REPLAY_INTEGRATION_NAME]
225225
? (this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] as ReturnType<typeof mobileReplayIntegration>).options
226226
: undefined,
227+
androidProfilingOptions: this._options._experiments?.androidProfilingOptions,
227228
})
228229
.then(
229230
(result: boolean) => {

packages/core/src/js/options.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,14 +285,25 @@ export interface BaseReactNativeOptions {
285285
/**
286286
* Experiment: A more reliable way to report unhandled C++ exceptions in iOS.
287287
*
288-
* 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.
288+
* 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.
289289
*
290290
* - Note: The mechanism of hooking into `__cxa_throw` could cause issues with symbolication on iOS due to caching of symbol references.
291291
*
292292
* @default false
293293
* @platform ios
294294
*/
295295
enableUnhandledCPPExceptionsV2?: boolean;
296+
297+
/**
298+
* Configuration options for Android UI profiling.
299+
* UI profiling supports two modes: `manual` and `trace`.
300+
* - In `trace` mode, the profiler runs based on active sampled spans.
301+
* - In `manual` mode, profiling is controlled via start/stop API calls.
302+
*
303+
* @experimental
304+
* @platform android
305+
*/
306+
androidProfilingOptions?: AndroidProfilingOptions;
296307
};
297308

298309
/**
@@ -330,6 +341,48 @@ export interface BaseReactNativeOptions {
330341

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

344+
/**
345+
* Android UI profiling lifecycle modes.
346+
* - `trace`: Profiler runs based on active sampled spans
347+
* - `manual`: Profiler is controlled manually via start/stop API calls
348+
*/
349+
export type AndroidProfilingLifecycle = 'trace' | 'manual';
350+
351+
/**
352+
* Configuration options for Android UI profiling.
353+
*
354+
* @experimental
355+
* @platform android
356+
*/
357+
export interface AndroidProfilingOptions {
358+
/**
359+
* Sample rate for profiling sessions.
360+
* This is evaluated once per session and determines if profiling should be enabled for that session.
361+
* 1.0 will enable profiling for all sessions, 0.0 will disable profiling.
362+
*
363+
* @default undefined (profiling disabled)
364+
*/
365+
profileSessionSampleRate?: number;
366+
367+
/**
368+
* Profiling lifecycle mode.
369+
* - `trace`: Profiler runs while there is at least one active sampled span
370+
* - `manual`: Profiler is controlled manually via Sentry.profiler.startProfiler/stopProfiler
371+
*
372+
* @default 'trace'
373+
*/
374+
lifecycle?: AndroidProfilingLifecycle;
375+
376+
/**
377+
* Enable profiling on app start.
378+
* - In `trace` mode: The app start profile stops automatically when the app start root span finishes
379+
* - In `manual` mode: The app start profile must be stopped through Sentry.profiler.stopProfiler()
380+
*
381+
* @default false
382+
*/
383+
startOnAppStart?: boolean;
384+
}
385+
333386
export interface ReactNativeTransportOptions extends BrowserTransportOptions {
334387
/**
335388
* @deprecated use `maxQueueSize` in the root of the SDK options.

packages/core/src/js/wrapper.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type {
2222
NativeStackFrames,
2323
Spec,
2424
} from './NativeRNSentry';
25-
import type { ReactNativeClientOptions } from './options';
25+
import type { AndroidProfilingOptions, ReactNativeClientOptions } from './options';
2626
import type * as Hermes from './profiling/hermes';
2727
import type { NativeAndroidProfileEvent, NativeProfileEvent } from './profiling/nativeTypes';
2828
import type { MobileReplayOptions } from './replay/mobilereplay';
@@ -57,6 +57,7 @@ export type NativeSdkOptions = Partial<ReactNativeClientOptions> & {
5757
ignoreErrorsRegex?: string[] | undefined;
5858
} & {
5959
mobileReplayOptions: MobileReplayOptions | undefined;
60+
androidProfilingOptions?: AndroidProfilingOptions | undefined;
6061
};
6162

6263
interface SentryNativeWrapper {
@@ -286,9 +287,19 @@ export const NATIVE: SentryNativeWrapper = {
286287
integrations,
287288
ignoreErrors,
288289
logsOrigin,
290+
androidProfilingOptions,
289291
...filteredOptions
290292
} = options;
291293
/* eslint-enable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */
294+
295+
// Move androidProfilingOptions into _experiments
296+
if (androidProfilingOptions) {
297+
filteredOptions._experiments = {
298+
...filteredOptions._experiments,
299+
androidProfilingOptions,
300+
};
301+
}
302+
292303
const nativeIsReady = await RNSentry.initNativeSdk(filteredOptions);
293304

294305
this.nativeIsReady = nativeIsReady;

0 commit comments

Comments
 (0)