Skip to content

Commit c639edf

Browse files
Fix(V6): Implement fallback system to screens that aren't reporting on the native layer the time to display. #4042 (#4189)
* merge main * fix tests / yarn fix * changelog * undoo changes to sample * fix android * NIT * NIT extra line * fix-android-lint * fix java format * pub private constructor * make it final * test * add missing dependencies changelog
1 parent 4c3c7db commit c639edf

17 files changed

+510
-57
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+
### Fixes
12+
13+
- Enhanced accuracy of time-to-display spans. ([#4189](https://github.com/getsentry/sentry-react-native/pull/4189))
14+
1115
### Dependencies
1216

1317
- Bump JavaScript SDK from v8.34.0 to v8.35.0 ([#4196](https://github.com/getsentry/sentry-react-native/pull/4196))

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,13 @@ public class RNSentryModuleImpl {
126126
/** Max trace file size in bytes. */
127127
private long maxTraceFileSize = 5 * 1024 * 1024;
128128

129+
private final @NotNull SentryDateProvider dateProvider;
130+
129131
public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) {
130132
packageInfo = getPackageInfo(reactApplicationContext);
131133
this.reactApplicationContext = reactApplicationContext;
132134
this.emitNewFrameEvent = createEmitNewFrameEvent();
135+
this.dateProvider = new SentryAndroidDateProvider();
133136
}
134137

135138
private ReactApplicationContext getReactApplicationContext() {
@@ -141,8 +144,6 @@ private ReactApplicationContext getReactApplicationContext() {
141144
}
142145

143146
private @NotNull Runnable createEmitNewFrameEvent() {
144-
final @NotNull SentryDateProvider dateProvider = new SentryAndroidDateProvider();
145-
146147
return () -> {
147148
final SentryDate endDate = dateProvider.now();
148149
WritableMap event = Arguments.createMap();
@@ -745,6 +746,10 @@ public void disableNativeFramesTracking() {
745746
}
746747
}
747748

749+
public void getNewScreenTimeToDisplay(Promise promise) {
750+
RNSentryTimeToDisplay.getTimeToDisplay(promise, dateProvider);
751+
}
752+
748753
private String getProfilingTracesDirPath() {
749754
if (cacheDirPath == null) {
750755
cacheDirPath =
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.sentry.react;
2+
3+
import android.os.Handler;
4+
import android.os.Looper;
5+
import android.view.Choreographer;
6+
import com.facebook.react.bridge.Promise;
7+
import io.sentry.SentryDate;
8+
import io.sentry.SentryDateProvider;
9+
10+
public final class RNSentryTimeToDisplay {
11+
12+
private RNSentryTimeToDisplay() {}
13+
14+
public static void getTimeToDisplay(Promise promise, SentryDateProvider dateProvider) {
15+
Looper mainLooper = Looper.getMainLooper();
16+
17+
if (mainLooper == null) {
18+
promise.reject(
19+
"GetTimeToDisplay is not able to measure the time to display: Main looper not"
20+
+ " available.");
21+
return;
22+
}
23+
24+
// Ensure the code runs on the main thread
25+
new Handler(mainLooper)
26+
.post(
27+
() -> {
28+
try {
29+
Choreographer choreographer = Choreographer.getInstance();
30+
31+
// Invoke the callback after the frame is rendered
32+
choreographer.postFrameCallback(
33+
frameTimeNanos -> {
34+
final SentryDate endDate = dateProvider.now();
35+
promise.resolve(endDate.nanoTimestamp() / 1e9);
36+
});
37+
} catch (Exception exception) {
38+
promise.reject("Failed to receive the instance of Choreographer", exception);
39+
}
40+
});
41+
}
42+
}

packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,9 @@ public String getCurrentReplayId() {
172172
public void crashedLastRun(Promise promise) {
173173
this.impl.crashedLastRun(promise);
174174
}
175+
176+
@Override
177+
public void getNewScreenTimeToDisplay(Promise promise) {
178+
this.impl.getNewScreenTimeToDisplay(promise);
179+
}
175180
}

packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,9 @@ public String getCurrentReplayId() {
172172
public void crashedLastRun(Promise promise) {
173173
this.impl.crashedLastRun(promise);
174174
}
175+
176+
@ReactMethod()
177+
public void getNewScreenTimeToDisplay(Promise promise) {
178+
this.impl.getNewScreenTimeToDisplay(promise);
179+
}
175180
}

packages/core/ios/RNSentry.mm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import <dlfcn.h>
22
#import "RNSentry.h"
3+
#import "RNSentryTimeToDisplay.h"
34

45
#if __has_include(<React/RCTConvert.h>)
56
#import <React/RCTConvert.h>
@@ -62,6 +63,7 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope;
6263
@implementation RNSentry {
6364
bool sentHybridSdkDidBecomeActive;
6465
bool hasListeners;
66+
RNSentryTimeToDisplay *_timeToDisplay;
6567
}
6668

6769
- (dispatch_queue_t)methodQueue
@@ -139,6 +141,8 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)
139141
[mutableOptions removeObjectForKey:@"tracesSampler"];
140142
[mutableOptions removeObjectForKey:@"enableTracing"];
141143

144+
_timeToDisplay = [[RNSentryTimeToDisplay alloc] init];
145+
142146
#if SENTRY_TARGET_REPLAY_SUPPORTED
143147
[RNSentryReplay updateOptions:mutableOptions];
144148
#endif
@@ -786,4 +790,9 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
786790
}
787791
#endif
788792

793+
RCT_EXPORT_METHOD(getNewScreenTimeToDisplay:(RCTPromiseResolveBlock)resolve
794+
rejecter:(RCTPromiseRejectBlock)reject) {
795+
[_timeToDisplay getTimeToDisplay:resolve];
796+
}
797+
789798
@end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <React/RCTBridgeModule.h>
2+
3+
@interface RNSentryTimeToDisplay : NSObject
4+
5+
- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback;
6+
7+
@end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#import "RNSentryTimeToDisplay.h"
2+
#import <QuartzCore/QuartzCore.h>
3+
#import <React/RCTLog.h>
4+
5+
@implementation RNSentryTimeToDisplay
6+
{
7+
CADisplayLink *displayLink;
8+
RCTResponseSenderBlock resolveBlock;
9+
}
10+
11+
// Rename requestAnimationFrame to getTimeToDisplay
12+
- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback
13+
{
14+
// Store the resolve block to use in the callback.
15+
resolveBlock = callback;
16+
17+
#if TARGET_OS_IOS
18+
// Create and add a display link to get the callback after the screen is rendered.
19+
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
20+
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
21+
#else
22+
resolveBlock(@[]); // Return nothing if not iOS.
23+
#endif
24+
}
25+
26+
#if TARGET_OS_IOS
27+
- (void)handleDisplayLink:(CADisplayLink *)link {
28+
// Get the current time
29+
NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000.0; // Convert to milliseconds
30+
31+
// Ensure the callback is valid and pass the current time back
32+
if (resolveBlock) {
33+
resolveBlock(@[@(currentTime)]); // Call the callback with the current time
34+
resolveBlock = nil; // Clear the block after it's called
35+
}
36+
37+
// Invalidate the display link to stop future callbacks
38+
[displayLink invalidate];
39+
displayLink = nil;
40+
}
41+
#endif
42+
43+
@end

packages/core/src/js/NativeRNSentry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { UnsafeObject } from './utils/rnlibrariesinterface';
99
export interface Spec extends TurboModule {
1010
addListener: (eventType: string) => void;
1111
removeListeners: (id: number) => void;
12+
getNewScreenTimeToDisplay(): Promise<number | undefined | null>;
1213
addBreadcrumb(breadcrumb: UnsafeObject): void;
1314
captureEnvelope(
1415
bytes: string,

packages/core/src/js/tracing/reactnavigation.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212
import type { Client, Integration, Span } from '@sentry/types';
1313
import { isPlainObject, logger, timestampInSeconds } from '@sentry/utils';
1414

15-
import type { NewFrameEvent, SentryEventEmitter } from '../utils/sentryeventemitter';
16-
import { createSentryEventEmitter, NewFrameEventName } from '../utils/sentryeventemitter';
15+
import type { NewFrameEvent } from '../utils/sentryeventemitter';
16+
import type { SentryEventEmitterFallback } from '../utils/sentryeventemitterfallback';
17+
import { createSentryFallbackEventEmitter } from '../utils/sentryeventemitterfallback';
1718
import { isSentrySpan } from '../utils/span';
1819
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
1920
import { NATIVE } from '../wrapper';
@@ -30,7 +31,6 @@ import {
3031
} from './span';
3132
import { manualInitialDisplaySpans, startTimeToInitialDisplaySpan } from './timetodisplay';
3233
import { setSpanDurationAsMeasurementOnSpan } from './utils';
33-
3434
export const INTEGRATION_NAME = 'ReactNavigation';
3535

3636
const NAVIGATION_HISTORY_MAX_SIZE = 200;
@@ -81,7 +81,7 @@ export const reactNavigationIntegration = ({
8181
registerNavigationContainer: (navigationContainerRef: unknown) => void;
8282
} => {
8383
let navigationContainer: NavigationContainer | undefined;
84-
let newScreenFrameEventEmitter: SentryEventEmitter | undefined;
84+
let newScreenFrameEventEmitter: SentryEventEmitterFallback | undefined;
8585

8686
let tracing: ReactNativeTracingIntegration | undefined;
8787
let idleSpanOptions: Parameters<typeof startGenericIdleNavigationSpan>[1] = defaultIdleOptions;
@@ -95,8 +95,8 @@ export const reactNavigationIntegration = ({
9595
let recentRouteKeys: string[] = [];
9696

9797
if (enableTimeToInitialDisplay) {
98-
newScreenFrameEventEmitter = createSentryEventEmitter();
99-
newScreenFrameEventEmitter.initAsync(NewFrameEventName);
98+
newScreenFrameEventEmitter = createSentryFallbackEventEmitter();
99+
newScreenFrameEventEmitter.initAsync();
100100
NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason: unknown) => {
101101
logger.error(`${INTEGRATION_NAME} Failed to initialize native new frame tracking: ${reason}`);
102102
});
@@ -258,9 +258,8 @@ export const reactNavigationIntegration = ({
258258
});
259259

260260
const navigationSpanWithTtid = latestNavigationSpan;
261-
!routeHasBeenSeen &&
262-
latestTtidSpan &&
263-
newScreenFrameEventEmitter?.once(NewFrameEventName, ({ newFrameTimestampInSeconds }: NewFrameEvent) => {
261+
if (!routeHasBeenSeen && latestTtidSpan) {
262+
newScreenFrameEventEmitter?.onceNewFrame(({ newFrameTimestampInSeconds }: NewFrameEvent) => {
264263
const activeSpan = getActiveSpan();
265264
if (activeSpan && manualInitialDisplaySpans.has(activeSpan)) {
266265
logger.warn('[ReactNavigationInstrumentation] Detected manual instrumentation for the current active span.');
@@ -271,6 +270,7 @@ export const reactNavigationIntegration = ({
271270
latestTtidSpan.end(newFrameTimestampInSeconds);
272271
setSpanDurationAsMeasurementOnSpan('time_to_initial_display', latestTtidSpan, navigationSpanWithTtid);
273272
});
273+
}
274274

275275
navigationProcessingSpan?.updateName(`Processing navigation to ${route.name}`);
276276
navigationProcessingSpan?.setStatus({ code: SPAN_STATUS_OK });

0 commit comments

Comments
 (0)