Skip to content
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

feat: add new packages for RN #2

Open
wants to merge 52 commits into
base: main
Choose a base branch
from
Open
Changes from 4 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
4630be4
feat: add new packages for RN
ilia-kurganskii Dec 20, 2024
e2f8fe2
fix: remove deps
ilia-kurganskii Dec 20, 2024
efc8ebb
fix: ignore browser sdk if flag enabled
ilia-kurganskii Dec 20, 2024
90358b9
fix: fix related bundles
ilia-kurganskii Dec 21, 2024
6745ef6
feat: add initial `instrumentation-websocket` as experimental
lucasbento Dec 21, 2024
b366bc4
Merge branch 'feat/add-react-native-support' of https://github.com/bi…
lucasbento Dec 21, 2024
b008d43
feat: init react navigation
marcocaldera Dec 21, 2024
d26abdf
feat: add WS message duration tracking
lucasbento Dec 23, 2024
1615683
fix: instrumentation navigation
marcocaldera Dec 23, 2024
744f3a1
Merge remote-tracking branch 'origin/feat/add-react-native-support' i…
marcocaldera Dec 23, 2024
4dfeaf2
feat: improve code and add span
marcocaldera Dec 24, 2024
02005ea
feat: improve code and add span
marcocaldera Dec 24, 2024
406c930
fix: undefined `ErrorEvent` in case of a WS error
lucasbento Dec 25, 2024
4ae14e2
feat: add react-saga and axios packages
ilia-kurganskii Dec 25, 2024
911a288
feat: add app startup instrumentation
lucasbento Dec 25, 2024
aadcff5
Merge branch 'feat/add-react-native-support' of https://github.com/bi…
lucasbento Dec 25, 2024
6b9b359
feat: add tracing for takeLatest
ilia-kurganskii Dec 26, 2024
bcfa527
refactor: add `messageTransform`
lucasbento Dec 26, 2024
c4097c8
Merge branch 'feat/add-react-native-support' of https://github.com/bi…
lucasbento Dec 26, 2024
3862f83
feat: add propagation context to call
ilia-kurganskii Dec 26, 2024
61e99f2
feat: update logs for instumentations
ilia-kurganskii Dec 30, 2024
5a98193
feat: add axios handler for span
ilia-kurganskii Dec 30, 2024
de38a9f
feat: add initial route change
marcocaldera Dec 31, 2024
3f27496
Merge remote-tracking branch 'origin/feat/add-react-native-support' i…
marcocaldera Dec 31, 2024
364acf1
feat: add `sendTransform` option to `instrumentation-websocket`
lucasbento Jan 2, 2025
d5f4bc6
fix: missing class methods
lucasbento Jan 2, 2025
448c807
Merge branch 'feat/add-react-native-support' of https://github.com/bi…
lucasbento Jan 2, 2025
fdab13c
fix: fix build issue
ilia-kurganskii Jan 2, 2025
ec6c4d2
fix: fix build issue
ilia-kurganskii Jan 2, 2025
cd0ac84
fix: fix build issue
ilia-kurganskii Jan 2, 2025
0f15391
fix: fix build issue
ilia-kurganskii Jan 2, 2025
ff9ec96
fix: fix build issue
ilia-kurganskii Jan 2, 2025
8f5b153
Revert "fix: fix build issue"
ilia-kurganskii Jan 3, 2025
56b8416
refactor: change from milliseconds to seconds
lucasbento Jan 4, 2025
0f6e372
refactor: change from milliseconds to seconds
lucasbento Jan 5, 2025
f5060fc
fix: use correct values for `browser` meta
lucasbento Jan 6, 2025
24d3895
refactor: remove unneeded param
lucasbento Jan 6, 2025
c66f2cf
fix: logger
marcocaldera Jan 7, 2025
23599dd
Merge branch 'feat/add-react-native-support' of github.com:bitvavo/fa…
marcocaldera Jan 7, 2025
8d9b5e8
refactor: switch `console.error` to `console.warn`
lucasbento Jan 7, 2025
d74230a
refactor: move import from `faro-web-sdk`
lucasbento Jan 7, 2025
fd9a135
fix: writable map
marcocaldera Jan 13, 2025
db2eead
refactor: add a better way to measure startup time
lucasbento Jan 30, 2025
761df19
refactor: remove logs
lucasbento Jan 30, 2025
8438b12
refactor: add missing `do-catch`
lucasbento Jan 30, 2025
3face70
Merge pull request #4 from bitvavo/refactor/better-statup-time-measure
lucasbento Jan 31, 2025
a72bdc0
refactor: add missing name variable for logs
lucasbento Jan 31, 2025
5e7262a
feat: add `whiteListedURLs` to web-socket instrumentation config
lucasbento Feb 6, 2025
2aa2688
refactor: initialize `whiteListedURLs` with empty array
lucasbento Feb 7, 2025
1a2882e
feat: add support for persistent sessions (#5)
ilia-kurganskii Feb 7, 2025
185d655
refactor: change startup duration to submit as seconds
lucasbento Feb 10, 2025
5615ba3
Merge branch 'feat/add-react-native-support' of https://github.com/bi…
lucasbento Feb 10, 2025
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
@@ -11,93 +11,73 @@
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import android.os.Process;
import android.os.SystemClock;
import java.math.BigInteger;
import android.util.Log;

@ReactModule(name = NativeInstrumentationModule.NAME)
public class NativeInstrumentationModule extends ReactContextBaseJavaModule implements RCTEventEmitter {
public static final String NAME = "NativeInstrumentation";
private static Long startTime = null;

private static Double cachedStartStartupTime = null;
private static Double cachedEndStartupTime = null;
private static Double cachedStartupDuration = null;
private static boolean hasAppRestarted = false;
private static int bundleLoadCounter = 0;

static {
ReactMarker.addListener((name, tag, instanceKey) -> {
long currentTime = System.currentTimeMillis();

if (name == ReactMarkerConstants.PRE_RUN_JS_BUNDLE_START) {
android.util.Log.d(NAME, String.format("JS bundle load started at: %d", currentTime));
initializeNativeInstrumentation();
if (!hasAppRestarted) {
if (bundleLoadCounter > 0) {
hasAppRestarted = true;
}
bundleLoadCounter++;
}
}
});
}

public NativeInstrumentationModule(ReactApplicationContext reactContext) {
super(reactContext);
android.util.Log.d(NAME, "Module constructor called");
}

@Override
public String getName() {
return NAME;
}

public static void initializeNativeInstrumentation() {
android.util.Log.d(NAME, "Initializing native instrumentation...");
cachedStartStartupTime = null;
cachedEndStartupTime = null;
cachedStartupDuration = null;
startTime = System.currentTimeMillis();
android.util.Log.d(NAME, String.format("Initialized with start time: %d (previous metrics cleared)", startTime));
}
@ReactMethod(isBlockingSynchronousMethod = true)
public double getStartupTimeSync() throws Exception {
try {
long currentTime = System.currentTimeMillis();
long processStartTime = Process.getStartUptimeMillis();
long currentUptime = SystemClock.uptimeMillis();

/**
* Creates a fresh WritableMap with startup metrics.
* Note: Each WritableMap can only be consumed once when passed through the React Native bridge.
* This method ensures we always create a new instance for each request.
*
* Each map can be consumed once by the JS side (i.e., going through the bridge).
*
* @return A new WritableMap instance containing the startup metrics
*/
private WritableMap createStartupMetricsMap(double startStartupTime, double endStartupTime, double startupDuration) {
WritableMap params = Arguments.createMap();
params.putDouble("startStartupTime", startStartupTime);
params.putDouble("endStartupTime", endStartupTime);
params.putDouble("startupDuration", startupDuration);
return params;
}
long startupTime = currentTime - currentUptime + processStartTime;

@ReactMethod
public void getStartupTime(Promise promise) {
android.util.Log.d(NAME, "Getting startup time...");
return BigInteger.valueOf(startupTime).doubleValue();
} catch (Exception e) {
Log.e(NAME, "Error calculating startup time", e);

if (startTime == null) {
android.util.Log.e(NAME, "Error: Start time was not initialized");
promise.reject("NO_START_TIME", "[NativeInstrumentation] Start time was not initialized");
return;
throw e;
}
}

if (cachedStartupDuration != null) {
android.util.Log.d(NAME, "Returning cached metrics");
promise.resolve(createStartupMetricsMap(cachedStartStartupTime, cachedEndStartupTime, cachedStartupDuration));
return;
}
@ReactMethod
public void getStartupTime(Promise promise) {
try {
WritableMap response = Arguments.createMap();

long endTime = System.currentTimeMillis();
double duration = (endTime - startTime) / 1000.0;
double startupTime = getStartupTimeSync();

android.util.Log.d(NAME, String.format(
"Calculating metrics - Start: %d, End: %d, Duration: %f seconds",
startTime, endTime, duration
));
response.putDouble("startupTime", startupTime);

cachedStartStartupTime = (double) startTime;
cachedEndStartupTime = (double) endTime;
cachedStartupDuration = duration;
promise.resolve(response);
} catch (Exception e) {
promise.reject("STARTUP_TIME_ERROR", "Failed to get startup time: " + e.getMessage(), e);
}
}

android.util.Log.d(NAME, "Metrics cached and being returned");
promise.resolve(createStartupMetricsMap(cachedStartStartupTime, cachedEndStartupTime, cachedStartupDuration));
@ReactMethod
public void getHasAppRestarted(Promise promise) {
promise.resolve(hasAppRestarted);
}

@ReactMethod
Original file line number Diff line number Diff line change
@@ -12,16 +12,13 @@
public class NativeInstrumentationPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
android.util.Log.d("NativeInstrumentation", "Creating view managers (none needed)");
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
android.util.Log.d("NativeInstrumentation", "Creating native modules");
List<NativeModule> modules = new ArrayList<>();
modules.add(new NativeInstrumentationModule(reactContext));
android.util.Log.d("NativeInstrumentation", "Native instrumentation module added to modules list");
return modules;
}
}
3 changes: 2 additions & 1 deletion packages/react-native-tracing/ios/NativeInstrumentation.h
Original file line number Diff line number Diff line change
@@ -7,7 +7,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)getStartupTime:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject;

+ (void)initializeNativeInstrumentation;
- (void)getHasAppRestarted:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject;

@end

5 changes: 4 additions & 1 deletion packages/react-native-tracing/ios/NativeInstrumentation.m
Original file line number Diff line number Diff line change
@@ -4,5 +4,8 @@ @interface RCT_EXTERN_REMAP_MODULE(NativeInstrumentation, NativeInstrumentation,

RCT_EXTERN_METHOD(getStartupTime:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(getHasAppRestarted:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end
@end
90 changes: 58 additions & 32 deletions packages/react-native-tracing/ios/NativeInstrumentation.swift
Original file line number Diff line number Diff line change
@@ -3,18 +3,7 @@ import React

@objc(NativeInstrumentation)
public class NativeInstrumentation: NSObject, RCTBridgeModule {
private static var startTime: TimeInterval?
private static var cachedMetrics: [String: Double]?

@objc
public static func initializeNativeInstrumentation() {
NativeInstrumentation.cachedMetrics = nil
NativeInstrumentation.startTime = Date().timeIntervalSince1970
}

override init() {
super.init()
}
private static var hasAppRestarted: Bool = false

@objc
public static func requiresMainQueueSetup() -> Bool {
@@ -26,28 +15,65 @@ public class NativeInstrumentation: NSObject, RCTBridgeModule {
return "NativeInstrumentation"
}

@objc
public func getStartupTime(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
guard let startTime = NativeInstrumentation.startTime else {
reject("NO_START_TIME", "[NativeInstrumentation] Start time was not initialized", nil)
override init() {
super.init()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleBundleLoadStart(_:)),
name: NSNotification.Name("RCTJavaScriptWillStartLoadingNotification"),
object: nil
)
}

@objc private func handleBundleLoadStart(_ notification: Notification) {
if NativeInstrumentation.hasAppRestarted {
return
}

if let metrics = NativeInstrumentation.cachedMetrics {
resolve(metrics)
return

NativeInstrumentation.hasAppRestarted = true
}

@objc
public func getStartupTime(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
do {
var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var size = MemoryLayout<kinfo_proc>.size
var kp = kinfo_proc()

let result = mib.withUnsafeMutableBytes { mibBytes in
withUnsafeMutablePointer(to: &size) { sizeBytes in
withUnsafeMutablePointer(to: &kp) { kpBytes in
sysctl(mibBytes.baseAddress?.assumingMemoryBound(to: Int32.self), 4,
kpBytes,
sizeBytes,
nil, 0)
}
}
}

let startTimeMs: Int64
if result == 0 {
let startTime = kp.kp_proc.p_un.__p_starttime
startTimeMs = Int64(startTime.tv_sec) * 1000 + Int64(startTime.tv_usec) / 1000
} else {
throw NSError(domain: "NativeInstrumentation",
code: Int(result),
userInfo: [NSLocalizedDescriptionKey: "Failed to get process info"])
}

let response = ["startupTime": startTimeMs]
resolve(response)
} catch {
reject("STARTUP_TIME_ERROR", "Failed to get startup time: \(error.localizedDescription)", error)
}

let endTime = Date().timeIntervalSince1970
let duration = endTime - startTime

let metrics: [String: Double] = [
"startStartupTime": startTime,
"endStartupTime": endTime,
"startupDuration": duration
]

NativeInstrumentation.cachedMetrics = metrics
resolve(metrics)
}

@objc
public func getHasAppRestarted(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
resolve(NativeInstrumentation.hasAppRestarted)
}

deinit {
NotificationCenter.default.removeObserver(self)
}
}
26 changes: 18 additions & 8 deletions packages/react-native-tracing/src/wrapHOC.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import React, { useEffect } from 'react';
// @ts-ignore
// eslint-disable-next-line import/namespace
import { Alert, NativeModules } from 'react-native';

import { api } from './dependencies';

interface StartupMetrics {
startStartupTime: number;
endStartupTime: number;
startupDuration: number;
startupTime: number;
}

// TODO(@lucasbento): figure out where to best place this function
const measureStartupTime = async (): Promise<void> => {
try {
const metrics: StartupMetrics =
await require('react-native')['NativeModules']['NativeInstrumentation'].getStartupTime();
const hasAppRestarted = await NativeModules['NativeInstrumentation'].getHasAppRestarted();

if (hasAppRestarted) {
return;
}

const metrics: StartupMetrics = await NativeModules['NativeInstrumentation'].getStartupTime();

const currentTime = Date.now();
const startupDuration = currentTime - metrics.startupTime;

api.pushMeasurement({
type: 'app_startup_time',
values: {
startup_duration: metrics.startupDuration,
startup_duration: startupDuration,
},
timestamp: new Date().toISOString(),
});
} catch (error) {
} catch (error: unknown) {
console.warn('[NativeInstrumentation] Failed to measure startup time:', error);
}
};
@@ -31,6 +40,7 @@ export function wrap<P extends object>(WrappedComponent: React.ComponentType<P>)
measureStartupTime();
}, []);

// @ts-ignore
return <WrappedComponent {...props} />;
};
}