Skip to content

Commit fde71c6

Browse files
committed
chore: Merge Android UI profiling on the capture startup crashes branch
1 parent 093a889 commit fde71c6

File tree

7 files changed

+293
-6
lines changed

7 files changed

+293
-6
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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,14 @@ public void crash() {
192192
}
193193

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

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

268271
// Clears start metrics, making them ready for recording warm app start
@@ -952,7 +955,8 @@ protected void trySetIgnoreErrors(SentryAndroidOptions options, ReadableMap rnOp
952955
}
953956
}
954957
if (strErrors != null) {
955-
// Use the same behaviour of JavaScript instead of Android when dealing with strings.
958+
// Use the same behaviour of JavaScript instead of Android when dealing with
959+
// strings.
956960
for (int i = 0; i < strErrors.size(); i++) {
957961
String pattern = ".*" + Pattern.quote(strErrors.getString(i)) + ".*";
958962
list.add(pattern);

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.facebook.react.common.JavascriptException;
88
import io.sentry.ILogger;
99
import io.sentry.Integration;
10+
import io.sentry.ProfileLifecycle;
1011
import io.sentry.Sentry;
1112
import io.sentry.SentryEvent;
1213
import io.sentry.SentryLevel;
@@ -162,6 +163,9 @@ static void getSentryAndroidOptions(
162163
options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter());
163164
}
164165

166+
// Configure Android UI Profiling
167+
configureAndroidProfiling(options, rnOptions);
168+
165169
// Exclude Dev Server and Sentry Dsn request from Breadcrumbs
166170
String dsn = getURLFromDSN(rnOptions.getString("dsn"));
167171
String devServerUrl = rnOptions.getString("devServerUrl");
@@ -192,6 +196,57 @@ static void getSentryAndroidOptions(
192196
SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations()));
193197
}
194198

199+
private void configureAndroidProfiling(
200+
@NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions) {
201+
if (!rnOptions.hasKey("_experiments")) {
202+
return;
203+
}
204+
205+
@Nullable final ReadableMap experiments = rnOptions.getMap("_experiments");
206+
if (experiments == null || !experiments.hasKey("androidProfilingOptions")) {
207+
return;
208+
}
209+
210+
@Nullable
211+
final ReadableMap androidProfilingOptions = experiments.getMap("androidProfilingOptions");
212+
if (androidProfilingOptions == null) {
213+
return;
214+
}
215+
216+
// Set profile session sample rate
217+
if (androidProfilingOptions.hasKey("profileSessionSampleRate")) {
218+
final double profileSessionSampleRate =
219+
androidProfilingOptions.getDouble("profileSessionSampleRate");
220+
options.setProfileSessionSampleRate(profileSessionSampleRate);
221+
logger.log(
222+
SentryLevel.INFO,
223+
String.format(
224+
"Android UI Profiling profileSessionSampleRate set to: %.2f",
225+
profileSessionSampleRate));
226+
}
227+
228+
// Set profiling lifecycle mode
229+
if (androidProfilingOptions.hasKey("lifecycle")) {
230+
final String lifecycle = androidProfilingOptions.getString("lifecycle");
231+
if ("manual".equalsIgnoreCase(lifecycle)) {
232+
options.setProfileLifecycle(ProfileLifecycle.MANUAL);
233+
logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to MANUAL");
234+
} else if ("trace".equalsIgnoreCase(lifecycle)) {
235+
options.setProfileLifecycle(ProfileLifecycle.TRACE);
236+
logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to TRACE");
237+
}
238+
}
239+
240+
// Set start on app start
241+
if (androidProfilingOptions.hasKey("startOnAppStart")) {
242+
final boolean startOnAppStart = androidProfilingOptions.getBoolean("startOnAppStart");
243+
options.setStartProfilerOnAppStart(startOnAppStart);
244+
logger.log(
245+
SentryLevel.INFO,
246+
String.format("Android UI Profiling startOnAppStart set to %b", startOnAppStart));
247+
}
248+
}
249+
195250
/**
196251
* This function updates the options with RNSentry defaults. These default can be overwritten by
197252
* users during manual native initialization.

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)