Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
### Fixes

- Find `ios/.xcode.env` in the Expo plugin "Upload Debug Symbols to Sentry" phase when the iOS project uses Swift Package Manager instead of CocoaPods ([#6911](https://github.com/getsentry/sentry-react-native/pull/6911))
- Start and stop the native UI profiler for `_experiments.profilingOptions` in both the `trace` and `manual` lifecycles, and export `Sentry.profiler` ([#6916](https://github.com/getsentry/sentry-react-native/pull/6916))
- Before, the profiler ran only for the app start profile, because native tracing is off in React Native and no API controlled the profiler from JS.

## 8.31.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify

Expand All @@ -38,7 +37,7 @@ class RNSentryProfilingOptionsTest {
}

@Test
fun `profilingOptions wires sessionSampleRate, trace lifecycle, and startOnAppStart`() {
fun `profilingOptions wires sessionSampleRate, startOnAppStart, and a MANUAL native lifecycle`() {
val rnOptions =
JavaOnlyMap.of(
"_experiments",
Expand All @@ -59,42 +58,8 @@ class RNSentryProfilingOptionsTest {
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

assertEquals(1.0, options.profileSessionSampleRate!!, 0.0)
assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle)
assertTrue(options.isStartProfilerOnAppStart)
}

@Test
fun `profilingOptions lifecycle manual maps to ProfileLifecycle MANUAL`() {
val rnOptions =
JavaOnlyMap.of(
"_experiments",
JavaOnlyMap.of(
"profilingOptions",
JavaOnlyMap.of("lifecycle", "manual"),
),
)
val options = SentryAndroidOptions()

RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
}

@Test
fun `profilingOptions lifecycle is case-insensitive`() {
val rnOptions =
JavaOnlyMap.of(
"_experiments",
JavaOnlyMap.of(
"profilingOptions",
JavaOnlyMap.of("lifecycle", "TRACE"),
),
)
val options = SentryAndroidOptions()

RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle)
assertTrue(options.isStartProfilerOnAppStart)
}

@Test
Expand Down Expand Up @@ -172,24 +137,6 @@ class RNSentryProfilingOptionsTest {
verify(logger, times(1)).log(eq(SentryLevel.WARNING), anyString())
}

@Test
fun `non-string lifecycle logs a warning and is ignored`() {
val rnOptions =
JavaOnlyMap.of(
"_experiments",
JavaOnlyMap.of(
"profilingOptions",
JavaOnlyMap.of("lifecycle", 1),
),
)
val options = SentryAndroidOptions()

RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
verify(logger, times(1)).log(eq(SentryLevel.WARNING), anyString())
}

@Test
fun `non-boolean startOnAppStart logs a warning and is ignored`() {
val rnOptions =
Expand All @@ -207,24 +154,4 @@ class RNSentryProfilingOptionsTest {
assertFalse(options.isStartProfilerOnAppStart)
verify(logger, times(1)).log(eq(SentryLevel.WARNING), anyString())
}

@Test
fun `unknown lifecycle string is silently ignored without warning`() {
val rnOptions =
JavaOnlyMap.of(
"_experiments",
JavaOnlyMap.of(
"profilingOptions",
JavaOnlyMap.of("lifecycle", "not-a-real-mode"),
),
)
val options = SentryAndroidOptions()

RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

// Implementation only logs a warning for type mismatches, not unknown string values.
// Lifecycle stays at the default.
assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle)
verify(logger, never()).log(eq(SentryLevel.WARNING), anyString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ - (void)testStartWithDictionaryInstallsConfigureProfilingFromExperimentsProfilin
SentryProfileOptions *probe = [[SentryProfileOptions alloc] init];
actualOptions.configureProfiling(probe);
XCTAssertEqual(probe.sessionSampleRate, 1.0f);
XCTAssertEqual(probe.lifecycle, SentryProfileLifecycleTrace);
XCTAssertEqual(probe.lifecycle, SentryProfileLifecycleManual);
XCTAssertTrue(probe.profileAppStarts);
}

Expand Down Expand Up @@ -678,7 +678,7 @@ - (void)testStartCreateOptionsWithDictionaryProfilingOptionsInstallsConfigurePro
SentryProfileOptions *probe = [[SentryProfileOptions alloc] init];
actualOptions.configureProfiling(probe);
XCTAssertEqual(probe.sessionSampleRate, 1.0f);
XCTAssertEqual(probe.lifecycle, SentryProfileLifecycleTrace);
XCTAssertEqual(probe.lifecycle, SentryProfileLifecycleManual);
XCTAssertTrue(probe.profileAppStarts);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.Process;
import android.util.SparseIntArray;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.FrameMetricsAggregator;
Expand Down Expand Up @@ -556,6 +557,32 @@ public void flushReplay(Promise promise) {
}
}

public void startProfiler(Promise promise) {
try {
Sentry.startProfiler();
final SentryId profilerId =
Sentry.getCurrentScopes().getOptions().getContinuousProfiler().getProfilerId();
promise.resolve(SentryId.EMPTY_ID.equals(profilerId) ? null : profilerId.toString());
} catch (Throwable e) { // NOPMD - degrade at the bridge boundary, never crash the host app
logger.log(SentryLevel.ERROR, "Failed to start the profiler", e);
promise.reject("SentryReactNative", e.getMessage(), e);
}
}

public String getCurrentThreadId() {
return String.valueOf(Process.myTid());
}

public void stopProfiler(Promise promise) {
try {
Sentry.stopProfiler();
promise.resolve(null);
} catch (Throwable e) { // NOPMD - degrade at the bridge boundary, never crash the host app
logger.log(SentryLevel.ERROR, "Failed to stop the profiler", e);
promise.reject("SentryReactNative", e.getMessage(), e);
}
}

public void registerReplayTraceId(String traceId) {
// Forward a trace id seen on a sent event to Session Replay so it lands in
// the current segment's `trace_ids`. The controller owns dedup, the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,22 +318,8 @@ private static void configureAndroidProfiling(
}
}

// Set profiling lifecycle mode
if (profilingOptions.hasKey("lifecycle")) {
if (profilingOptions.getType("lifecycle") == ReadableType.String) {
final String lifecycle = profilingOptions.getString("lifecycle");
if ("manual".equalsIgnoreCase(lifecycle)) {
options.setProfileLifecycle(ProfileLifecycle.MANUAL);
logger.log(SentryLevel.INFO, "UI Profile Lifecycle set to MANUAL");
} else if ("trace".equalsIgnoreCase(lifecycle)) {
options.setProfileLifecycle(ProfileLifecycle.TRACE);
logger.log(SentryLevel.INFO, "UI Profile Lifecycle set to TRACE");
}
} else {
logger.log(
SentryLevel.WARNING, "UI Profiling lifecycle must be a string, ignoring invalid value");
}
}
// Native tracing is off, so JS drives both lifecycles through startProfiler/stopProfiler.
options.setProfileLifecycle(ProfileLifecycle.MANUAL);

// Set start on app start
if (profilingOptions.hasKey("startOnAppStart")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,21 @@ public void flushReplay(Promise promise) {
this.impl.flushReplay(promise);
}

@Override
public void startProfiler(Promise promise) {
this.impl.startProfiler(promise);
}

@Override
public String getCurrentThreadId() {
return this.impl.getCurrentThreadId();
}

@Override
public void stopProfiler(Promise promise) {
this.impl.stopProfiler(promise);
}

@Override
public void registerReplayTraceId(String traceId) {
this.impl.registerReplayTraceId(traceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,21 @@ public void flushReplay(Promise promise) {
this.impl.flushReplay(promise);
}

@ReactMethod
public void startProfiler(Promise promise) {
this.impl.startProfiler(promise);
}

@ReactMethod(isBlockingSynchronousMethod = true)
public String getCurrentThreadId() {
return this.impl.getCurrentThreadId();
}

@ReactMethod
public void stopProfiler(Promise promise) {
this.impl.stopProfiler(promise);
}

@ReactMethod
public void registerReplayTraceId(String traceId) {
this.impl.registerReplayTraceId(traceId);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/etc/sentry-react-native.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { OpenAiClient } from '@sentry/core';
import { OpenAiOptions } from '@sentry/core';
import type { Options } from '@sentry/core';
import { Profiler } from '@sentry/react';
import { profiler } from '@sentry/core';
import * as React_2 from 'react';
import { replayIntegration } from '@sentry/react';
import type { ReplayRecordingMode } from '@sentry/core';
Expand Down Expand Up @@ -826,6 +827,8 @@ export const primitiveTagIntegration: () => Integration;

export { Profiler }

export { profiler }

// @public
export type ProfilingLifecycle = 'trace' | 'manual';

Expand Down
32 changes: 32 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "RNSentry.h"
#import "RNSentryTimeToDisplay.h"
#import <dlfcn.h>
#import <pthread.h>

#if __has_include(<React/RCTConvert.h>)
# import <React/RCTConvert.h>
Expand Down Expand Up @@ -1216,6 +1217,37 @@ + (BOOL)isPathUnderAllowedRootsForTesting:(NSString *)path
#endif
}

// sentry-cocoa has no public API for the current profiler id, so this resolves nil.
RCT_EXPORT_METHOD(
startProfiler : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
@try {
[SentrySDKWrapper startProfiler];
resolve(nil);
} @catch (NSException *exception) {
NSLog(@"[RNSentry] Failed to call startProfiler: %@", exception);
reject(@"SentryReactNative", exception.reason, nil);
}
}

// sentry-cocoa profiles identify threads by their Mach thread port.
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentThreadId)
{
return [NSString stringWithFormat:@"%u", pthread_mach_thread_np(pthread_self())];
}

RCT_EXPORT_METHOD(
stopProfiler : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
@try {
[SentrySDKWrapper stopProfiler];
resolve(nil);
} @catch (NSException *exception) {
NSLog(@"[RNSentry] Failed to call stopProfiler: %@", exception);
reject(@"SentryReactNative", exception.reason, nil);
}
}

RCT_EXPORT_METHOD(
crashedLastRun : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject)
{
Expand Down
13 changes: 2 additions & 11 deletions packages/core/ios/RNSentryExperimentalOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,8 @@ + (void)configureProfilingWithOptions:(NSDictionary *)profilingOptions
profiling.sessionSampleRate);
}

// Set lifecycle mode
NSString *lifecycle = profilingOptions[@"lifecycle"];
if ([lifecycle isKindOfClass:[NSString class]]) {
if ([lifecycle caseInsensitiveCompare:@"manual"] == NSOrderedSame) {
profiling.lifecycle = SentryProfileLifecycleManual;
NSLog(@"Sentry: UI Profiling Lifecycle set to MANUAL");
} else if ([lifecycle caseInsensitiveCompare:@"trace"] == NSOrderedSame) {
profiling.lifecycle = SentryProfileLifecycleTrace;
NSLog(@"Sentry: UI Profiling Lifecycle set to TRACE");
}
}
// Native tracing is off, so JS drives both lifecycles through startProfiler/stopProfiler.
profiling.lifecycle = SentryProfileLifecycleManual;

// Set profile app starts
id startOnAppStart = profilingOptions[@"startOnAppStart"];
Expand Down
4 changes: 4 additions & 0 deletions packages/core/ios/SentrySDKWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

+ (void)resumeAppHangTracking;

+ (void)startProfiler;

+ (void)stopProfiler;

+ (BOOL)debug;

+ (NSString *)releaseName;
Expand Down
15 changes: 15 additions & 0 deletions packages/core/ios/SentrySDKWrapper.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "SentrySDKWrapper.h"
#import "RNSentrySwiftBridge.h"
#import <Sentry/SentryProfilingConditionals.h>
@import Sentry;

@implementation SentrySDKWrapper
Expand Down Expand Up @@ -29,6 +30,20 @@ + (void)resumeAppHangTracking
[SentrySDK resumeAppHangTracking];
}

+ (void)startProfiler
{
#if SENTRY_TARGET_PROFILING_SUPPORTED
[SentrySDK startProfiler];
#endif
}

+ (void)stopProfiler
{
#if SENTRY_TARGET_PROFILING_SUPPORTED
[SentrySDK stopProfiler];
#endif
}

+ (void)configureScope:(void (^)(SentryScope *scope))callback
{
[SentrySDK configureScope:callback];
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export interface Spec extends TurboModule {
androidProfile?: UnsafeObject;
error?: string;
};
startProfiler(): Promise<string | undefined | null>;
stopProfiler(): Promise<void>;
getCurrentThreadId(): string | undefined | null;
fetchNativePackageName(): string | undefined | null;
fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | undefined | null;
initNativeReactNavigationNewFrameTracking(): Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export {
instrumentStateGraph,
instrumentStateGraphCompile,
addConsoleInstrumentationFilter,
profiler,
} from '@sentry/core';

/** @deprecated Use `instrumentStateGraph` instead. `instrumentLangGraph` will be removed in a future major version. */
Expand Down
Loading
Loading