-
-
Notifications
You must be signed in to change notification settings - Fork 355
Fix: Implement fallback system to screens that aren't reporting on the native layer the time to display. #4042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
d0c0cf7
1a52620
84301f7
3423a61
84c12ff
1e59d5b
8851215
feb08cd
299fb34
734354d
dabf98b
41879c1
a181bcc
298a007
a93b249
da76e21
53e67ba
0344adb
b115fdd
99e74cf
1bb5c4d
6507648
2f838b8
a8f5126
fa03cc1
b6093d5
d03004c
7e5f764
49272a1
a22824b
c99200d
64fd760
dc9ca95
4710843
1cd3b8e
54189db
45fca09
5c0681d
966ce1e
4bb6434
add7f95
55e6589
9885a15
d87b40e
962bb35
4b820fe
a99c3c1
4203401
3dbcc41
4557f7a
9a5ce6d
342961d
427e1c6
fcb9935
2a7c8fa
d43ce2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package io.sentry.react; | ||
|
|
||
| import com.facebook.react.bridge.ReactContextBaseJavaModule; | ||
| import com.facebook.react.bridge.ReactApplicationContext; | ||
| import com.facebook.react.bridge.ReactMethod; | ||
| import com.facebook.react.bridge.Promise; | ||
| import com.facebook.react.turbomodule.core.interfaces.TurboModule; | ||
|
|
||
| import android.view.Choreographer; | ||
|
|
||
| import androidx.annotation.NonNull; | ||
|
|
||
| import org.jetbrains.annotations.NotNull; | ||
| import io.sentry.SentryDate; | ||
| import io.sentry.SentryDateProvider; | ||
| import io.sentry.android.core.SentryAndroidDateProvider; | ||
|
|
||
|
|
||
| public class RNSentryTimeToDisplayModule extends NativeRNSentryTimeToDisplaySpec { | ||
|
|
||
|
|
||
| public RNSentryTimeToDisplayModule(ReactApplicationContext reactContext) { | ||
| super(reactContext); | ||
| } | ||
|
|
||
| @ReactMethod | ||
| public void requestAnimationFrame(Promise promise) { | ||
| Choreographer choreographer = Choreographer.getInstance(); | ||
|
|
||
| // Invoke the callback after the frame is rendered | ||
| choreographer.postFrameCallback(new Choreographer.FrameCallback() { | ||
| @Override | ||
| public void doFrame(long frameTimeNanos) { | ||
| final @NotNull SentryDateProvider dateProvider = new SentryAndroidDateProvider(); | ||
|
|
||
| final SentryDate endDate = dateProvider.now(); | ||
|
|
||
| promise.resolve(endDate.nanoTimestamp() / 1e9); | ||
| } | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package io.sentry.react; | ||
|
|
||
| import androidx.annotation.Nullable; | ||
|
|
||
| import com.facebook.react.TurboReactPackage; | ||
| import com.facebook.react.bridge.NativeModule; | ||
| import com.facebook.react.bridge.ReactApplicationContext; | ||
| import com.facebook.react.module.model.ReactModuleInfo; | ||
| import com.facebook.react.module.model.ReactModuleInfoProvider; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class RNSentryTimeToDisplayPackage extends TurboReactPackage { | ||
|
|
||
| @Nullable | ||
| @Override | ||
| public NativeModule getModule(String name, ReactApplicationContext reactContext) { | ||
| if (name.equals(RNSentryTimeToDisplayModule.NAME)) { | ||
| return new RNSentryTimeToDisplayModule(reactContext); | ||
| } else { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public ReactModuleInfoProvider getReactModuleInfoProvider() { | ||
| return () -> { | ||
| final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>(); | ||
| boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; | ||
| moduleInfos.put( | ||
| RNSentryTimeToDisplayModule.NAME, | ||
| new ReactModuleInfo( | ||
| RNSentryTimeToDisplayModule.NAME, | ||
| RNSentryTimeToDisplayModule.NAME, | ||
| false, // canOverrideExistingModule | ||
| false, // needsEagerInit | ||
| true, // hasConstants | ||
| false, // isCxxModule | ||
| isTurboModule // isTurboModule | ||
| )); | ||
| return moduleInfos; | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| #import <React/RCTBridgeModule.h> | ||
|
|
||
| @interface RNSentryTimeToDisplay : NSObject <RCTBridgeModule> | ||
|
|
||
| - (void)requestAnimationFrame:(RCTPromiseResolveBlock)resolve | ||
| rejecter:(RCTPromiseRejectBlock)reject; | ||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| #import "RNSentryTimeToDisplay.h" | ||
| #import <QuartzCore/QuartzCore.h> | ||
| #import <React/RCTLog.h> | ||
|
|
||
| @implementation RNSentryTimeToDisplay | ||
antonis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| CADisplayLink *displayLink; | ||
| RCTPromiseResolveBlock resolveBlock; | ||
| } | ||
|
|
||
| RCT_EXPORT_MODULE(); | ||
|
|
||
| RCT_EXPORT_METHOD(requestAnimationFrame:(RCTPromiseResolveBlock)resolve | ||
| rejecter:(RCTPromiseRejectBlock)reject) | ||
| { | ||
| // Store the resolve block to use in the callback | ||
| resolveBlock = resolve; | ||
|
|
||
| // Create and add a display link to get the callback after the screen is rendered | ||
| displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; | ||
| [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; | ||
| } | ||
|
|
||
| - (void)handleDisplayLink:(CADisplayLink *)link | ||
| { | ||
| NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; | ||
|
|
||
| if (resolveBlock) { | ||
| resolveBlock(@(currentTime)); | ||
| resolveBlock = nil; | ||
| } | ||
|
|
||
| // Invalidate the display link | ||
| [displayLink invalidate]; | ||
| displayLink = nil; | ||
| } | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import type { TurboModule } from 'react-native'; | ||
| import { TurboModuleRegistry } from 'react-native'; | ||
|
|
||
| // There has to be only one interface and it has to be named `Spec` | ||
| // Only extra allowed definitions are types (probably codegen bug) | ||
| export interface Spec extends TurboModule { | ||
| requestAnimationFrame(): Promise<number>; | ||
| } | ||
|
|
||
| // The export must be here to pass codegen even if not used | ||
| export default TurboModuleRegistry.getEnforcing<Spec>('RNSentryTimeToDisplay'); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import { logger } from '@sentry/utils'; | ||
| import type { EmitterSubscription } from 'react-native'; | ||
| import { DeviceEventEmitter, NativeModules } from 'react-native'; | ||
|
|
||
| import type { Spec } from '../NativeRNSentryTimeToDisplay'; | ||
| import { NATIVE } from '../wrapper'; | ||
| import { isTurboModuleEnabled } from './environment'; | ||
| import { ReactNativeLibraries } from './rnlibraries'; | ||
| import { NewFrameEventName } from './sentryeventemitter'; | ||
|
|
||
| function getTimeToDisplayModule(): Spec | undefined { | ||
| return isTurboModuleEnabled() | ||
| ? ReactNativeLibraries.TurboModuleRegistry && | ||
| ReactNativeLibraries.TurboModuleRegistry.get<Spec>('RNSentryTimeToDisplay') | ||
| : NativeModules.RNSentryTimeToDisplay; | ||
| } | ||
|
|
||
| const RNSentryTimeToDisplay: Spec | undefined = getTimeToDisplayModule(); | ||
|
|
||
| export type FallBackNewFrameEvent = { newFrameTimestampInSeconds: number; isFallback?: boolean }; | ||
| export interface SentryEventEmitterFallback { | ||
| /** | ||
| * Initializes the fallback event emitter | ||
| * This method is synchronous in JS but the event emitter starts asynchronously. | ||
| */ | ||
| initAsync: () => void; | ||
| closeAll: () => void; | ||
| startListenerAsync: () => void; | ||
| } | ||
|
|
||
| function timeNowNanosecond(): number { | ||
| return Date.now() / 1000; // Convert to nanoseconds | ||
| } | ||
|
|
||
| /** | ||
| * Creates emitter that allows to listen to UI Frame events when ready. | ||
| */ | ||
| export function createSentryFallbackEventEmitter(): SentryEventEmitterFallback { | ||
| let NativeEmitterCalled: boolean = false; | ||
| let subscription: EmitterSubscription | undefined = undefined; | ||
| let isListening = false; | ||
|
|
||
| function defaultFallbackEventEmitter(): void { | ||
| // Schedule the callback to be executed when all UI Frames have flushed. | ||
| requestAnimationFrame(() => { | ||
lucas-zimerman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (NativeEmitterCalled) { | ||
| NativeEmitterCalled = false; | ||
| isListening = false; | ||
| return; | ||
| } | ||
| const timestampInSeconds = timeNowNanosecond(); | ||
lucas-zimerman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| waitForNativeResponseOrFallback(timestampInSeconds, 'JavaScript'); | ||
| }); | ||
| } | ||
|
|
||
| function waitForNativeResponseOrFallback(fallbackSeconds: number, origin: string): void { | ||
| const maxRetries = 3; | ||
| let retries = 0; | ||
|
|
||
| const retryCheck = (): void => { | ||
krystofwoldrich marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (NativeEmitterCalled) { | ||
| NativeEmitterCalled = false; | ||
| isListening = false; | ||
| return; // Native Replied the bridge with a timestamp. | ||
| } | ||
|
|
||
| retries++; | ||
| if (retries < maxRetries) { | ||
| setTimeout(retryCheck, 1_000); | ||
| } else { | ||
| logger.log(`[Sentry] Native event emitter did not reply in time. Using ${origin} fallback emitter.`); | ||
| isListening = false; | ||
| DeviceEventEmitter.emit(NewFrameEventName, { | ||
| newFrameTimestampInSeconds: fallbackSeconds, | ||
| isFallback: true, | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| // Start the retry process | ||
| retryCheck(); | ||
| } | ||
|
|
||
| return { | ||
| initAsync() { | ||
| subscription = DeviceEventEmitter.addListener(NewFrameEventName, () => { | ||
| // Avoid noise from pages that we do not want to track. | ||
| if (isListening) { | ||
| NativeEmitterCalled = true; | ||
| } | ||
| }); | ||
| }, | ||
|
|
||
| startListenerAsync() { | ||
| isListening = true; | ||
| if (NATIVE.isNativeAvailable() && RNSentryTimeToDisplay !== undefined) { | ||
| RNSentryTimeToDisplay.requestAnimationFrame() | ||
| .then((time: number) => { | ||
| waitForNativeResponseOrFallback(time, 'Native'); | ||
| }) | ||
| .catch((reason: Error) => { | ||
| logger.error('Failed to recceive Native fallback timestamp.', reason); | ||
| defaultFallbackEventEmitter(); | ||
| }); | ||
| } else { | ||
| defaultFallbackEventEmitter(); | ||
| } | ||
| }, | ||
|
|
||
| closeAll() { | ||
|
||
| subscription?.remove(); | ||
| }, | ||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.