Skip to content

Commit

Permalink
feat(react-native): support for starting native spans on ios
Browse files Browse the repository at this point in the history
  • Loading branch information
yousif-bugsnag committed Dec 2, 2024
1 parent ab5a293 commit 7373858
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#import <Foundation/Foundation.h>
#import "BugsnagPerformanceConfiguration.h"
#import "BugsnagPerformanceSpan.h"
#import "BugsnagPerformanceSpanOptions.h"

NS_ASSUME_NONNULL_BEGIN

Expand All @@ -18,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN

- (BugsnagPerformanceConfiguration * _Nullable)getConfiguration;

- (BugsnagPerformanceSpan * _Nullable)startSpan:(NSString *)name options:(BugsnagPerformanceSpanOptions *)options;

#pragma mark Shared Instance

+ (instancetype _Nullable) sharedInstance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ + (void)initialize {
}
}

if ((err = [cls mapAPINamed:@"startSpanV1"
toSelector:@selector(startSpan:options:)]) != nil) {
NSLog(@"Failed to map Bugsnag Performance API startSpanV1: %@", err);
NSString *mapped = err.userInfo[BSGUserInfoKeyMapped];
if (![mapped isEqualToString:BSGUserInfoValueMappedYes]) {
// Must abort because this method is not mapped, so we'd crash if we tried to call it.
return;
}
}

// Our "sharedInstance" will actually be the cross-talk API whose class we loaded.
bugsnagPerformanceCrossTalkAPI = [cls sharedInstance];
}
Expand Down
28 changes: 28 additions & 0 deletions packages/platforms/react-native/ios/BugsnagPerformanceSpan.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#import "BugsnagPerformanceSpanContext.h"

NS_ASSUME_NONNULL_BEGIN

@interface BugsnagPerformanceSpan : BugsnagPerformanceSpanContext

@property(nonatomic,readonly) BOOL isValid;

@property (nonatomic,readonly) NSString *name;
@property (nonatomic,readonly) NSDate *_Nullable startTime;
@property (nonatomic,readonly) NSDate *_Nullable endTime;

@property (nonatomic,readwrite) SpanId parentId;
@property (nonatomic,readonly) NSMutableDictionary *attributes;

- (void)abortIfOpen;

- (void)abortUnconditionally;

- (void)end;

- (void)endWithEndTime:(NSDate *)endTime NS_SWIFT_NAME(end(endTime:));

- (void)setAttribute:(NSString *)attributeName withValue:(_Nullable id)value;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef union {
__uint128_t value;
struct {
uint64_t lo;
uint64_t hi;
};
} TraceId;

typedef uint64_t SpanId;

@interface BugsnagPerformanceSpanContext : NSObject

@property(nonatomic) TraceId traceId;
@property(nonatomic) SpanId spanId;

- (instancetype) initWithTraceIdHi:(uint64_t)traceIdHi traceIdLo:(uint64_t)traceIdLo spanId:(SpanId)spanId;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#import "BugsnagPerformanceSpanContext.h"

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(uint8_t, BSGFirstClass) {
BSGFirstClassNo = 0,
BSGFirstClassYes = 1,
BSGFirstClassUnset = 2,
};

// Affects whether or not a span should include rendering metrics
typedef NS_ENUM(uint8_t, BSGInstrumentRendering) {
BSGInstrumentRenderingNo = 0, // Never include rendering metrics
BSGInstrumentRenderingYes = 1, // Always include rendering metrics, as long as the autoInstrumentRendering configuration option is on
BSGInstrumentRenderingUnset = 2, // Include rendering metrics only if the span is first class, start and end times were not set when creating/closing the span and the autoInstrumentRendering configuration option is on
};

@interface BugsnagPerformanceSpanOptions : NSObject

@property(nonatomic) NSDate * _Nullable startTime;
@property(nonatomic) BugsnagPerformanceSpanContext * _Nullable parentContext;
@property(nonatomic) BOOL makeCurrentContext;
@property(nonatomic) BSGFirstClass firstClass;
@property(nonatomic) BSGInstrumentRendering instrumentRendering;

@end
NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ @implementation BugsnagReactNativePerformance
#endif
}

static uint64_t hexStringToUInt64(NSString *hexString) {
uint64_t result = 0;
NSScanner *scanner = [NSScanner scannerWithString:hexString];
[scanner setScanLocation:0];
[scanner scanHexLongLong:&result];

return result;
}

static NSString *getRandomBytes() noexcept {
const int POOL_SIZE = 1024;
UInt8 bytes[POOL_SIZE];
Expand Down Expand Up @@ -129,6 +138,46 @@ @implementation BugsnagReactNativePerformance
return config;
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(startNativeSpan:(NSString *)name
options:(NSDictionary *)options) {

BugsnagPerformanceSpanOptions *spanOptions = [BugsnagPerformanceSpanOptions new];
spanOptions.makeCurrentContext = NO;
spanOptions.firstClass = BSGFirstClassYes;
spanOptions.parentContext = nil;

// Javascript start times are Unix nanosecond timestamps
NSNumber *startTime = options[@"startTime"];
spanOptions.startTime = [NSDate dateWithTimeIntervalSince1970:([startTime doubleValue] / NSEC_PER_SEC)];

NSDictionary *parentContext = options[@"parentContext"];
if (parentContext != nil) {
NSString *parentSpanId = parentContext[@"id"];
NSString *parentTraceId = parentContext[@"traceId"];

uint64_t spanId = hexStringToUInt64(parentSpanId);
uint64_t traceIdHi = hexStringToUInt64([parentTraceId substringToIndex:16]);
uint64_t traceIdLo = hexStringToUInt64([parentTraceId substringFromIndex:16]);

spanOptions.parentContext = [[BugsnagPerformanceSpanContext alloc] initWithTraceIdHi:traceIdHi
traceIdLo:traceIdLo spanId:spanId];
}

BugsnagPerformanceSpan *nativeSpan = [BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.sharedInstance startSpan:name options:spanOptions];
[nativeSpan.attributes removeAllObjects];

NSMutableDictionary *span = [NSMutableDictionary new];
span[@"name"] = nativeSpan.name;
span[@"id"] = [NSString stringWithFormat:@"%llx", nativeSpan.spanId];
span[@"traceId"] = [NSString stringWithFormat:@"%llx%llx", nativeSpan.traceId.hi, nativeSpan.traceId.lo];
span[@"startTime"] = [NSNumber numberWithDouble: [nativeSpan.startTime timeIntervalSince1970] * NSEC_PER_SEC];
if (nativeSpan.parentId > 0) {
span[@"parentSpanId"] = [NSString stringWithFormat:@"%llx", nativeSpan.parentId];
}

return span;
}

#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/* Begin PBXBuildFile section */
DA396E142CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m in Sources */ = {isa = PBXBuildFile; fileRef = DA396E132CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m */; };
DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.m in Sources */ = {isa = PBXBuildFile; fileRef = DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.m */; };
DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.mm in Sources */ = {isa = PBXBuildFile; fileRef = DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.mm */; };
DAE18DD72C58E02C00D52529 /* BugsnagReactNativePerformance.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = DAE18DD22C58DF2500D52529 /* BugsnagReactNativePerformance.h */; };
/* End PBXBuildFile section */

Expand All @@ -30,8 +30,11 @@
DA396E122CCFD242009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h; sourceTree = "<group>"; };
DA396E132CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m; sourceTree = "<group>"; };
DA396E152CCFD72E009B37C2 /* BugsnagPerformanceConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceConfiguration.h; sourceTree = "<group>"; };
DAC1DC592CF87E770009C7F9 /* BugsnagPerformanceSpanContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceSpanContext.h; sourceTree = "<group>"; };
DAC1DC5A2CF87EBA0009C7F9 /* BugsnagPerformanceSpanOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceSpanOptions.h; sourceTree = "<group>"; };
DAC1DC5B2CF8859A0009C7F9 /* BugsnagPerformanceSpan.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceSpan.h; sourceTree = "<group>"; };
DAE18DD22C58DF2500D52529 /* BugsnagReactNativePerformance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagReactNativePerformance.h; sourceTree = "<group>"; };
DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagReactNativePerformance.m; sourceTree = "<group>"; };
DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagReactNativePerformance.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -56,11 +59,14 @@
58B511D21A9E6C8500147676 = {
isa = PBXGroup;
children = (
DAC1DC5B2CF8859A0009C7F9 /* BugsnagPerformanceSpan.h */,
DAC1DC5A2CF87EBA0009C7F9 /* BugsnagPerformanceSpanOptions.h */,
DAC1DC592CF87E770009C7F9 /* BugsnagPerformanceSpanContext.h */,
DA396E152CCFD72E009B37C2 /* BugsnagPerformanceConfiguration.h */,
DA396E132CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m */,
DA396E122CCFD242009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h */,
DAE18DD22C58DF2500D52529 /* BugsnagReactNativePerformance.h */,
DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.m */,
DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.mm */,
134814211AA4EA7D00B7C361 /* Products */,
);
sourceTree = "<group>";
Expand Down Expand Up @@ -123,7 +129,7 @@
buildActionMask = 2147483647;
files = (
DA396E142CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m in Sources */,
DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.m in Sources */,
DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
10 changes: 3 additions & 7 deletions packages/platforms/react-native/lib/NativeBugsnagPerformance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'
import type { UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'
import { TurboModuleRegistry } from 'react-native'

export type DeviceInfo = {
Expand Down Expand Up @@ -28,17 +29,12 @@ export type ParentContext = {
traceId: string
}

export type NativeSpanOptions = {
startTime: number | undefined
parentContext: ParentContext | null
}

export type NativeSpan = {
name: string
id: string
traceId: string
startTime: number
parentSpanId: string
parentSpanId: string | undefined
}

export interface Spec extends TurboModule {
Expand All @@ -47,7 +43,7 @@ export interface Spec extends TurboModule {
requestEntropyAsync: () => Promise<string>
isNativePerformanceAvailable: () => boolean
getNativeConfiguration: () => NativeConfiguration | null
startNativeSpan: (name: string, options: NativeSpanOptions) => NativeSpan
startNativeSpan: (name: string, options: UnsafeObject) => NativeSpan
}

export default TurboModuleRegistry.get<Spec>(
Expand Down
15 changes: 10 additions & 5 deletions packages/platforms/react-native/lib/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@ interface Performance {
now: () => number
}

const createClock = (performance: Performance): Clock => {
interface ReactNativeClock extends Clock {
toUnixNanoseconds: (time: number) => number
}

const createClock = (performance: Performance): ReactNativeClock => {
// Measurable "monotonic" time
// In React Native, `performance.now` often returns some very high values, but does not expose the `timeOrigin` it uses to calculate what "now" is.
// by storing the value of `performance.now` when the app starts, we can remove that value from any further `.now` calculations, and add it to the current "wall time" to get a useful timestamp.
const startPerfTime = performance.now()
const startWallTime = Date.now()

const toUnixNanoseconds = (time: number) => millisecondsToNanoseconds(time - startPerfTime + startWallTime)

return {
now: () => performance.now(),
date: () => new Date(performance.now() - startPerfTime + startWallTime),
convert: (date: Date) => date.getTime() - startWallTime + startPerfTime,
// convert milliseconds since timeOrigin to unix time in nanoseconds
toUnixNanoseconds,
// convert milliseconds since timeOrigin to full timestamp
toUnixTimestampNanoseconds: (time: number) =>
millisecondsToNanoseconds(
time - startPerfTime + startWallTime
).toString()
toUnixTimestampNanoseconds: (time: number) => toUnixNanoseconds(time).toString()
}
}

Expand Down

0 comments on commit 7373858

Please sign in to comment.