Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d0c0cf7
implement fallback system
lucas-zimerman Aug 22, 2024
1a52620
clear format
lucas-zimerman Aug 23, 2024
84301f7
backup
lucas-zimerman Aug 30, 2024
3423a61
wip
lucas-zimerman Sep 2, 2024
84c12ff
Merge branch 'main' into test/deadline
lucas-zimerman Sep 3, 2024
1e59d5b
wip iOS
lucas-zimerman Sep 11, 2024
8851215
ioscode
lucas-zimerman Sep 11, 2024
feb08cd
Merge branch 'main' into test/deadline
lucas-zimerman Sep 11, 2024
299fb34
fix typo
lucas-zimerman Sep 11, 2024
734354d
fix-time
lucas-zimerman Sep 11, 2024
dabf98b
removed wrapper placeholder, moved java implementation to separated f…
lucas-zimerman Sep 11, 2024
41879c1
refactored android to use turbo module, refactored fallback emitter
lucas-zimerman Sep 12, 2024
a181bcc
add tests and minor fixes
lucas-zimerman Sep 12, 2024
298a007
missing test file
lucas-zimerman Sep 12, 2024
a93b249
yarn fix
lucas-zimerman Sep 12, 2024
da76e21
fix ios module name
lucas-zimerman Sep 12, 2024
53e67ba
Update samples/react-native/src/Screens/PerformanceScreen.tsx
lucas-zimerman Sep 12, 2024
0344adb
Update samples/react-native/src/Screens/PerformanceScreen.tsx
lucas-zimerman Sep 12, 2024
b115fdd
Update samples/react-native/src/Screens/PerformanceScreen.tsx
lucas-zimerman Sep 12, 2024
99e74cf
name missing
lucas-zimerman Sep 12, 2024
1bb5c4d
Merge branch 'main' into test/deadline
lucas-zimerman Sep 18, 2024
6507648
fix java module name, add isAvailable to ignore macos
lucas-zimerman Sep 19, 2024
2f838b8
yarn fix
lucas-zimerman Sep 19, 2024
a8f5126
use impl method
lucas-zimerman Sep 19, 2024
fa03cc1
nit impl name
lucas-zimerman Sep 19, 2024
b6093d5
refactor how to use time to display fallback
lucas-zimerman Sep 20, 2024
d03004c
yarn fix
lucas-zimerman Sep 20, 2024
7e5f764
fix ios reference
lucas-zimerman Sep 20, 2024
49272a1
Merge branch 'main' into test/deadline
lucas-zimerman Sep 23, 2024
a22824b
changelog
lucas-zimerman Sep 23, 2024
c99200d
Apply suggestions from code review
lucas-zimerman Sep 23, 2024
64fd760
Apply suggestions from code review
lucas-zimerman Sep 24, 2024
dc9ca95
fix build
lucas-zimerman Sep 24, 2024
4710843
Removes unused java imports (#4119)
antonis Sep 25, 2024
1cd3b8e
Merge branch 'main' into test/deadline
lucas-zimerman Oct 2, 2024
54189db
Merge branch 'main' into test/deadline
lucas-zimerman Oct 10, 2024
45fca09
add missing App Metrics import
lucas-zimerman Oct 10, 2024
5c0681d
Merge branch 'main' into test/deadline
lucas-zimerman Oct 14, 2024
966ce1e
refactor tests / remove closeAll
lucas-zimerman Oct 14, 2024
4bb6434
commit java suggestion
lucas-zimerman Oct 14, 2024
add7f95
build fix
lucas-zimerman Oct 14, 2024
55e6589
small refactor and add clearTimeout
lucas-zimerman Oct 14, 2024
9885a15
use sentry timestampInSeconds
lucas-zimerman Oct 14, 2024
d87b40e
removed fallback from navigation, comment on timeout difference
lucas-zimerman Oct 14, 2024
962bb35
requested changes java
lucas-zimerman Oct 14, 2024
4b820fe
yarn fix / remove clearTimeout
lucas-zimerman Oct 14, 2024
a99c3c1
add note and fix time tests
lucas-zimerman Oct 14, 2024
4203401
Kw fallback emitter suggestion (#4171)
lucas-zimerman Oct 14, 2024
3dbcc41
Merge branch 'main' into test/deadline
lucas-zimerman Oct 14, 2024
4557f7a
fix build
lucas-zimerman Oct 14, 2024
9a5ce6d
fix test
lucas-zimerman Oct 14, 2024
342961d
forgot git add for mocked file
lucas-zimerman Oct 14, 2024
427e1c6
add try/catch block on java code
lucas-zimerman Oct 14, 2024
fcb9935
fix lint
lucas-zimerman Oct 14, 2024
2a7c8fa
always use main thread on ttid
lucas-zimerman Oct 15, 2024
d43ce2b
Merge branch 'v5' into test/deadline
lucas-zimerman Oct 15, 2024
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package io.sentry.react;

import android.view.Choreographer;
import static java.util.concurrent.TimeUnit.SECONDS;
import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot;
import static io.sentry.vendor.Base64.NO_PADDING;
Expand Down
7 changes: 3 additions & 4 deletions android/src/main/java/io/sentry/react/RNSentryPackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public class RNSentryPackage extends TurboReactPackage {
@Nullable
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(RNSentryModuleImpl.NAME)) {
return new RNSentryModule(reactContext);
} else {
if (name.equals(RNSentryModuleImpl.NAME)) {
return new RNSentryModule(reactContext);
} else {
return null;
}
}
Expand Down Expand Up @@ -55,5 +55,4 @@ public List<ViewManager> createViewManagers(
new RNSentryOnDrawReporterManager(reactContext)
);
}

}
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;
};
}
}
7 changes: 7 additions & 0 deletions ios/RNSentryTimeToDisplay.h
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
38 changes: 38 additions & 0 deletions ios/RNSentryTimeToDisplay.m
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
{
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
4 changes: 2 additions & 2 deletions react-native.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module.exports = {
platforms: {
ios: {},
android: {
packageInstance: 'new RNSentryPackage()',
packageImportPath: 'import io.sentry.react.RNSentryPackage;'
packageInstance: 'new RNSentryPackage(),\n new RNSentryTimeToDisplayPackage()',
packageImportPath: 'import io.sentry.react.RNSentryPackage;\nimport io.sentry.react.RNSentryTimeToDisplayPackage;'
}
}
}
Expand Down
12 changes: 11 additions & 1 deletion samples/react-native/src/Screens/PerformanceScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
StatusBar,
ScrollView,
Expand Down Expand Up @@ -32,6 +32,16 @@ const PerformanceScreen = (props: Props) => {
}),
);
};
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Simulate heavy computation for 5 seconds
const start = Date.now();
while (Date.now() - start < 5000) {
// Perform some meaningless computation to occupy the CPU
Math.sqrt(Math.random() * Math.random());
}
setIsLoading(false);
}, []);

return (
<>
Expand Down
5 changes: 5 additions & 0 deletions src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface Spec extends TurboModule {
hardCrashed: boolean;
},
): Promise<boolean>;

captureScreenshot(): Promise<NativeScreenshot[] | undefined | null>;
clearBreadcrumbs(): void;
crash(): void;
Expand Down Expand Up @@ -153,5 +154,9 @@ export type NativeScreenshot = {
filename: string;
};

export interface RNSentryTimeToDisplayModuleSpec {
requestAnimationFrame(): Promise<number>;
}

// The export must be here to pass codegen even if not used
export default TurboModuleRegistry.getEnforcing<Spec>('RNSentry');
11 changes: 11 additions & 0 deletions src/js/NativeRNSentryTimeToDisplay.ts
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');
10 changes: 7 additions & 3 deletions src/js/tracing/reactnavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { logger, timestampInSeconds } from '@sentry/utils';

import type { NewFrameEvent } from '../utils/sentryeventemitter';
import { type SentryEventEmitter, createSentryEventEmitter, NewFrameEventName } from '../utils/sentryeventemitter';
import { type SentryEventEmitterFallback, createSentryFallbackEventEmitter } from '../utils/sentryeventemitterfallback';
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
import { NATIVE } from '../wrapper';
import type { OnConfirmRoute, TransactionCreator } from './routingInstrumentation';
Expand Down Expand Up @@ -78,7 +79,7 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati

private _navigationContainer: NavigationContainer | null = null;
private _newScreenFrameEventEmitter: SentryEventEmitter | null = null;

private _newFallbackEventEmitter: SentryEventEmitterFallback | null = null;
private readonly _maxRecentRouteLen: number = 200;

private _latestRoute?: NavigationRoute;
Expand All @@ -101,7 +102,9 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati

if (this._options.enableTimeToInitialDisplay) {
this._newScreenFrameEventEmitter = createSentryEventEmitter();
this._newFallbackEventEmitter = createSentryFallbackEventEmitter();
this._newScreenFrameEventEmitter.initAsync(NewFrameEventName);
this._newFallbackEventEmitter.initAsync();
NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason: unknown) => {
logger.error(`[ReactNavigationInstrumentation] Failed to initialize native new frame tracking: ${reason}`);
});
Expand Down Expand Up @@ -247,8 +250,8 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati
isAutoInstrumented: true,
});

!routeHasBeenSeen &&
latestTtidSpan &&
if (!routeHasBeenSeen && latestTtidSpan) {
this._newFallbackEventEmitter?.startListenerAsync();
this._newScreenFrameEventEmitter?.once(
NewFrameEventName,
({ newFrameTimestampInSeconds }: NewFrameEvent) => {
Expand All @@ -265,6 +268,7 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati
setSpanDurationAsMeasurementOnTransaction(latestTransaction, 'time_to_initial_display', latestTtidSpan);
},
);
}

this._navigationProcessingSpan?.updateName(`Processing navigation to ${route.name}`);
this._navigationProcessingSpan?.setStatus('ok');
Expand Down
114 changes: 114 additions & 0 deletions src/js/utils/sentryeventemitterfallback.ts
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(() => {
if (NativeEmitterCalled) {
NativeEmitterCalled = false;
isListening = false;
return;
}
const timestampInSeconds = timeNowNanosecond();
waitForNativeResponseOrFallback(timestampInSeconds, 'JavaScript');
});
}

function waitForNativeResponseOrFallback(fallbackSeconds: number, origin: string): void {
const maxRetries = 3;
let retries = 0;

const retryCheck = (): void => {
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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The closeAll doesn't seem to be used anywhere.

Copy link
Collaborator Author

@lucas-zimerman lucas-zimerman Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also remove the closeAll on sentryeventemitter? It seems to not be used other than tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can clean that up, after the fallback. I agree.

subscription?.remove();
},
};
}
Loading