diff --git a/packages/core/lib/clock.ts b/packages/core/lib/clock.ts index 64c94cf40..704e7e0bb 100644 --- a/packages/core/lib/clock.ts +++ b/packages/core/lib/clock.ts @@ -21,12 +21,9 @@ export interface Clock { // a function to convert a Date object into the format returned by 'now' convert: (date: Date) => number - // convert the format returned by 'now' to a unix time in nanoseconds - toUnixNanoseconds: (time: number) => number - - // convert a unix time in nanoseconds to the format returned by 'now' - fromUnixNanoseconds: (time: number) => number - // convert the format returned by 'now' into a unix timestamp in nanoseconds toUnixTimestampNanoseconds: (time: number) => string + + // convert a unix timestamp in nanoseconds into the format returned by 'now' + fromUnixNanosecondsTimestamp: (timestamp: string) => number } diff --git a/packages/platforms/browser/lib/clock.ts b/packages/platforms/browser/lib/clock.ts index 6c8803d0e..33a71aab2 100644 --- a/packages/platforms/browser/lib/clock.ts +++ b/packages/platforms/browser/lib/clock.ts @@ -45,17 +45,16 @@ function createClock (performance: PerformanceWithOptionalTimeOrigin, background }) const toUnixNanoseconds = (time: number) => millisecondsToNanoseconds(calculatedTimeOrigin + time) + const fromUnixNanoseconds = (time: number) => nanosecondsToMilliseconds(time) - calculatedTimeOrigin return { now: () => performance.now(), date: () => new Date(calculatedTimeOrigin + performance.now()), convert: (date) => date.getTime() - calculatedTimeOrigin, - // convert milliseconds since timeOrigin to unix time in nanoseconds - toUnixNanoseconds, // convert milliseconds since timeOrigin to full timestamp toUnixTimestampNanoseconds: (time: number) => toUnixNanoseconds(time).toString(), - // convert unix time in nanoseconds back to milliseconds since timeOrigin - fromUnixNanoseconds: (time: number) => nanosecondsToMilliseconds(time) - calculatedTimeOrigin + // convert a unix timestamp in nanoseconds back to milliseconds since timeOrigin + fromUnixNanosecondsTimestamp: (timestamp: string) => fromUnixNanoseconds(parseInt(timestamp)) } } diff --git a/packages/platforms/browser/tests/clock.test.ts b/packages/platforms/browser/tests/clock.test.ts index 438433e59..85ceaf2a2 100644 --- a/packages/platforms/browser/tests/clock.test.ts +++ b/packages/platforms/browser/tests/clock.test.ts @@ -164,53 +164,4 @@ describe('Browser Clock', () => { expect(clock.convert(new Date())).toEqual(0) }) }) - - describe('clock.toUnixNanoseconds()', () => { - it('converts performance time to unix nanoseconds', () => { - const timeOrigin = new Date('2023-01-02T00:00:00.000Z') - jest.setSystemTime(timeOrigin) - - const clock = createClock(new PerformanceFake(), new ControllableBackgroundingListener()) - - jest.advanceTimersByTime(250) - - const performanceTime = clock.now() - const unixNanoseconds = clock.toUnixNanoseconds(performanceTime) - - // Expected: timeOrigin + 250ms in nanoseconds - const expectedNanoseconds = (timeOrigin.getTime() + 250) * 1000000 - expect(unixNanoseconds).toBe(expectedNanoseconds) - }) - }) - - describe('clock.fromUnixNanoseconds()', () => { - it('converts unix nanoseconds back to performance time', () => { - const timeOrigin = new Date('2023-01-02T00:00:00.000Z') - jest.setSystemTime(timeOrigin) - - const clock = createClock(new PerformanceFake(), new ControllableBackgroundingListener()) - - // Test with a specific unix nanoseconds value 123ms after timeOrigin - const unixNanoseconds = 1672617600123000000 // 2023-01-02T00:00:00.123Z in nanoseconds - const performanceTime = clock.fromUnixNanoseconds(unixNanoseconds) - - expect(typeof performanceTime).toBe('number') - expect(performanceTime).toEqual(123) // 123ms after timeOrigin - }) - - it('is the inverse of toUnixNanoseconds', () => { - const timeOrigin = new Date('2023-01-02T00:00:00.000Z') - jest.setSystemTime(timeOrigin) - - const clock = createClock(new PerformanceFake(), new ControllableBackgroundingListener()) - - jest.advanceTimersByTime(123) - - const originalTime = clock.now() - const unixNanoseconds = clock.toUnixNanoseconds(originalTime) - const roundTripTime = clock.fromUnixNanoseconds(unixNanoseconds) - - expect(roundTripTime).toBe(originalTime) - }) - }) }) diff --git a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java index f0cf92f7a..4cbe5b283 100644 --- a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java +++ b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java @@ -76,8 +76,7 @@ public NativeBugsnagPerformanceImpl(ReactApplicationContext reactContext) { try { BugsnagPerformanceImpl.INSTANCE.getInstrumentedAppState().getConfig$internal(); isNativePerformanceAvailable = true; - } - catch (LinkageError e) { + } catch (LinkageError e) { // do nothing, Android Performance SDK is not installed or is incompatible } } @@ -117,12 +116,12 @@ String requestEntropy() { random.nextBytes(bytes); StringBuilder hex = new StringBuilder(bytes.length * 2); - for(byte b : bytes) { - int byteValue = ((int)b & 0xff); - if(byteValue < 16) { - hex.append('0'); - } - hex.append(Integer.toHexString(byteValue)); + for (byte b : bytes) { + int byteValue = ((int) b & 0xff); + if (byteValue < 16) { + hex.append('0'); + } + hex.append(Integer.toHexString(byteValue)); } return hex.toString(); } @@ -204,15 +203,15 @@ WritableMap startNativeSpan(String name, ReadableMap options) { return nativeSpanToJsSpan(nativeSpan); } - void markNativeSpanEndTime(String spanId, String traceId, double endTime) { + void markNativeSpanEndTime(String spanId, String traceId, String endTime) { SpanImpl nativeSpan = openSpans.get(spanId + traceId); if (nativeSpan != null) { - long nativeEndTime = BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime((long)endTime); + long nativeEndTime = BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime(Long.parseLong(endTime)); nativeSpan.markEndTime$internal(nativeEndTime); } } - void endNativeSpan(String spanId, String traceId, double endTime, ReadableMap jsAttributes, Promise promise) { + void endNativeSpan(String spanId, String traceId, String endTime, ReadableMap jsAttributes, Promise promise) { SpanImpl nativeSpan = openSpans.remove(spanId + traceId); if (nativeSpan == null) { promise.resolve(null); @@ -220,7 +219,7 @@ void endNativeSpan(String spanId, String traceId, double endTime, ReadableMap js } ReactNativeSpanAttributes.setAttributesFromReadableMap(nativeSpan.getAttributes(), jsAttributes); - long nativeEndTime = BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime((long)endTime); + long nativeEndTime = BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime(Long.parseLong(endTime)); if (nativeEndTime > nativeSpan.getEndTime$internal()) { nativeSpan.markEndTime$internal(nativeEndTime); } @@ -245,7 +244,7 @@ private WritableMap nativeSpanToJsSpan(SpanImpl nativeSpan) { span.putString("traceId", EncodingUtils.toHexString(nativeSpan.getTraceId())); long unixNanoStartTime = BugsnagClock.INSTANCE.elapsedNanosToUnixTime(nativeSpan.getStartTime$internal()); - span.putDouble("startTime", (double)unixNanoStartTime); + span.putString("startTime", Long.toString(unixNanoStartTime)); long parentSpanId = nativeSpan.getParentSpanId(); if (parentSpanId != 0L) { @@ -262,8 +261,8 @@ private SpanOptions readableMapToSpanOptions(ReadableMap jsOptions) { .within(null); if (jsOptions.hasKey("startTime")) { - double startTime = jsOptions.getDouble("startTime"); - long nativeStartTime = BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime((long)startTime); + String startTime = jsOptions.getString("startTime"); + long nativeStartTime = BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime(Long.parseLong(startTime)); spanOptions = spanOptions.startTime(nativeStartTime); } @@ -305,7 +304,7 @@ void exists(String path, Promise promise) { try { boolean result = new File(path).exists(); promise.resolve(result); - } catch(Exception e) { + } catch (Exception e) { promise.reject(e); } } @@ -314,7 +313,7 @@ void isDir(String path, Promise promise) { try { boolean result = new File(path).isDirectory(); promise.resolve(result); - } catch(Exception e) { + } catch (Exception e) { promise.reject(e); } } @@ -328,7 +327,7 @@ void ls(String path, Promise promise) { } promise.resolve(resultArray); - } catch(Exception e) { + } catch (Exception e) { promise.reject(e); } } @@ -347,7 +346,7 @@ void mkdir(String path, Promise promise) { } else { promise.reject("EPERM", new Exception("Failed to create directory")); } - } catch(Exception e) { + } catch (Exception e) { promise.reject(e); } } @@ -355,7 +354,7 @@ void mkdir(String path, Promise promise) { void readFile(String path, String encoding, Promise promise) { File file = new File(path); StringBuilder fileContent = new StringBuilder((int) file.length()); - try( + try ( FileInputStream fin = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(fin, encoding); ) { @@ -378,13 +377,13 @@ void unlink(String path, Promise promise) { } else { promise.reject(new Exception("Failed to delete file/directory")); } - } catch(Exception e) { + } catch (Exception e) { promise.reject(e); } } void writeFile(String path, String data, String encoding, Promise promise) { - try( + try ( FileOutputStream fout = new FileOutputStream(path); Writer w = new OutputStreamWriter(fout, encoding); ) { diff --git a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java index 94f7fd05a..1bc9403ef 100644 --- a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java +++ b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java @@ -86,18 +86,18 @@ public WritableMap attachToNativeSDK() { return impl.attachToNativeSDK(); } - @Override + @Override public WritableMap startNativeSpan(String name, ReadableMap options) { return impl.startNativeSpan(name, options); } @Override - public void endNativeSpan(String spanId, String traceId, double endTime, ReadableMap attributes, Promise promise) { + public void endNativeSpan(String spanId, String traceId, String endTime, ReadableMap attributes, Promise promise) { impl.endNativeSpan(spanId, traceId, endTime, attributes, promise); } - @Override - public void markNativeSpanEndTime(String spanId, String traceId, double endTime) { + @Override + public void markNativeSpanEndTime(String spanId, String traceId, String endTime) { impl.markNativeSpanEndTime(spanId, traceId, endTime); } diff --git a/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java b/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java index 7c2f4ac0d..396af7006 100644 --- a/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java +++ b/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java @@ -10,7 +10,7 @@ import java.util.Map; public class BugsnagReactNativePerformance extends ReactContextBaseJavaModule { - + private final NativeBugsnagPerformanceImpl impl; public BugsnagReactNativePerformance(ReactApplicationContext reactContext) { @@ -94,12 +94,12 @@ public WritableMap startNativeSpan(String name, ReadableMap options) { } @ReactMethod - public void endNativeSpan(String spanId, String traceId, double endTime, ReadableMap attributes, Promise promise) { - impl.endNativeSpan(spanId, traceId, endTime, attributes, promise); + public void endNativeSpan(String spanId, String traceId, String endTime, ReadableMap attributes, Promise promise) { + impl.endNativeSpan(spanId, traceId, endTime, attributes, promise); } @ReactMethod(isBlockingSynchronousMethod = true) - public void markNativeSpanEndTime(String spanId, String traceId, double endTime) { + public void markNativeSpanEndTime(String spanId, String traceId, String endTime) { impl.markNativeSpanEndTime(spanId, traceId, endTime); } diff --git a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm index a9a695412..a2056ef1d 100644 --- a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm +++ b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm @@ -16,7 +16,7 @@ @implementation BugsnagReactNativePerformance /** * A dictionary of open native spans, keyed by the span ID and trace ID, * so that they can be retrieved and closed/discarded from JS. -* +* * Since native spans are only ever started and ended from the JS thread, * no thread synchronization is required when accessing. */ @@ -91,7 +91,7 @@ static uint64_t hexStringToUInt64(NSString *hexString) { [hexStr appendFormat:@"%02x", bytes[i]]; } } - + return hexStr; } @@ -298,11 +298,11 @@ static uint64_t hexStringToUInt64(NSString *hexString) { spanOptions.makeCurrentContext = NO; spanOptions.instrumentRendering = BSGInstrumentRenderingYes; spanOptions.parentContext = nil; - + // Start times are passsed from JS as 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"]; @@ -314,10 +314,10 @@ static uint64_t hexStringToUInt64(NSString *hexString) { spanOptions.parentContext = [BugsnagReactNativePerformanceCrossTalkAPIClient.sharedInstance newSpanContext:traceIdHi traceIdLo:traceIdLo spanId:spanId]; } - + BugsnagPerformanceSpan *nativeSpan = [BugsnagReactNativePerformanceCrossTalkAPIClient.sharedInstance startSpan:name options:spanOptions]; [nativeSpan.attributes removeAllObjects]; - + NSString *spanId = [NSString stringWithFormat:@"%llx", nativeSpan.spanId]; NSString *traceId = [NSString stringWithFormat:@"%llx%llx", nativeSpan.traceIdHi, nativeSpan.traceIdLo]; @@ -329,17 +329,17 @@ static uint64_t hexStringToUInt64(NSString *hexString) { span[@"name"] = nativeSpan.name; span[@"id"] = spanId; span[@"traceId"] = traceId; - span[@"startTime"] = [NSNumber numberWithDouble: [nativeSpan.startTime timeIntervalSince1970] * NSEC_PER_SEC]; + span[@"startTime"] = [NSString stringWithFormat:@"%.0f", [nativeSpan.startTime timeIntervalSince1970] * NSEC_PER_SEC]; if (nativeSpan.parentId > 0) { span[@"parentSpanId"] = [NSString stringWithFormat:@"%llx", nativeSpan.parentId]; } - + return span; } RCT_EXPORT_METHOD(endNativeSpan:(NSString *)spanId traceId:(NSString *)traceId - endTime:(double)endTime + endTime:(NSString *)endTime attributes:(NSDictionary *)attributes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { @@ -359,28 +359,35 @@ static uint64_t hexStringToUInt64(NSString *hexString) { // We need to reinstate the bugsnag.sampling.p attribute here as it might not be re-populated on span end nativeSpan.attributes[@"bugsnag.sampling.p"] = @(nativeSpan.samplingProbability); - - // If the end time is later than the current end time, update it - NSDate *nativeEndTime = [NSDate dateWithTimeIntervalSince1970: endTime / NSEC_PER_SEC]; + + NSDecimalNumber *endTimeDecimal = [NSDecimalNumber decimalNumberWithString:endTime]; + NSDecimalNumber *nsecPerSecDecimal = [NSDecimalNumber decimalNumberWithMantissa:1 exponent:9 isNegative:NO]; + NSDecimalNumber *endTimeSecondsDecimal = [endTimeDecimal decimalNumberByDividingBy:nsecPerSecDecimal]; + NSTimeInterval endTimeInterval = [endTimeSecondsDecimal doubleValue]; + NSDate *nativeEndTime = [NSDate dateWithTimeIntervalSince1970:endTimeInterval]; if ([nativeEndTime timeIntervalSinceDate:nativeSpan.endTime] > 0) { [nativeSpan markEndTime:nativeEndTime]; } - + [nativeSpan sendForProcessing]; } resolve(nil); } -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(markNativeSpanEndTime:(NSString *)spanId traceId:(NSString *)traceId endTime:(double)endTime) { +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(markNativeSpanEndTime:(NSString *)spanId traceId:(NSString *)traceId endTime:(NSString *)endTime) { @synchronized (openSpans) { BugsnagPerformanceSpan *nativeSpan = openSpans[[spanId stringByAppendingString:traceId]]; if (nativeSpan != nil) { - NSDate *nativeEndTime = [NSDate dateWithTimeIntervalSince1970: endTime / NSEC_PER_SEC]; + NSDecimalNumber *endTimeDecimal = [NSDecimalNumber decimalNumberWithString:endTime]; + NSDecimalNumber *nsecPerSecDecimal = [NSDecimalNumber decimalNumberWithMantissa:1 exponent:9 isNegative:NO]; + NSDecimalNumber *endTimeSecondsDecimal = [endTimeDecimal decimalNumberByDividingBy:nsecPerSecDecimal]; + NSTimeInterval endTimeInterval = [endTimeSecondsDecimal doubleValue]; + NSDate *nativeEndTime = [NSDate dateWithTimeIntervalSince1970:endTimeInterval]; [nativeSpan markEndTime:nativeEndTime]; } } - + return nil; } @@ -390,7 +397,7 @@ static uint64_t hexStringToUInt64(NSString *hexString) { reject:(RCTPromiseRejectBlock)reject) { NSString *spanKey = [spanId stringByAppendingString:traceId]; @synchronized (openSpans) { - BugsnagPerformanceSpan *nativeSpan = openSpans[spanKey]; + BugsnagPerformanceSpan *nativeSpan = openSpans[spanKey]; if (nativeSpan != nil) { [openSpans removeObjectForKey:spanKey]; [nativeSpan abortUnconditionally]; @@ -403,7 +410,7 @@ static uint64_t hexStringToUInt64(NSString *hexString) { - (void)discardLongRunningSpans { NSDate *oneHourAgo = [NSDate dateWithTimeIntervalSinceNow:-hourInSeconds]; NSMutableArray *keysToRemove = [NSMutableArray new]; - + @synchronized (openSpans) { for (NSString *key in openSpans) { BugsnagPerformanceSpan *span = openSpans[key]; @@ -411,7 +418,7 @@ - (void)discardLongRunningSpans { [keysToRemove addObject:key]; } } - + [openSpans removeObjectsForKeys:keysToRemove]; } } diff --git a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts index a11d9d345..2acaebbcc 100644 --- a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts +++ b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts @@ -57,8 +57,8 @@ export interface Spec extends TurboModule { isNativePerformanceAvailable: () => boolean attachToNativeSDK: () => NativeConfiguration | null startNativeSpan: (name: string, options: UnsafeObject) => NativeSpan - endNativeSpan: (spanId: string, traceId: string, endTime: number, attributes: UnsafeObject) => Promise - markNativeSpanEndTime: (spanId: string, traceId: string, endTime: number) => void + endNativeSpan: (spanId: string, traceId: string, endTime: string, attributes: UnsafeObject) => Promise + markNativeSpanEndTime: (spanId: string, traceId: string, endTime: string) => void discardNativeSpan: (spanId: string, traceId: string) => Promise } diff --git a/packages/platforms/react-native/lib/clock.ts b/packages/platforms/react-native/lib/clock.ts index 67e6c3395..495b892eb 100644 --- a/packages/platforms/react-native/lib/clock.ts +++ b/packages/platforms/react-native/lib/clock.ts @@ -1,10 +1,120 @@ import type { Clock } from '@bugsnag/core-performance' -import { millisecondsToNanoseconds, nanosecondsToMilliseconds } from '@bugsnag/core-performance' interface Performance { now: () => number } +// Helper function to add two numbers represented as strings +const addStrings = (num1: string, num2: string): string => { + let result = '' + let carry = 0 + let i = num1.length - 1 + let j = num2.length - 1 + + while (i >= 0 || j >= 0 || carry > 0) { + const digit1 = i >= 0 ? parseInt(num1[i], 10) : 0 + const digit2 = j >= 0 ? parseInt(num2[j], 10) : 0 + const sum = digit1 + digit2 + carry + + result = (sum % 10) + result + carry = Math.floor(sum / 10) + + i-- + j-- + } + + return result +} + +// Helper function to subtract two numbers represented as strings (num1 - num2) +const subtractStrings = (num1: string, num2: string): string => { + // Ensure num1 >= num2 for simplicity + if (num1.length < num2.length || (num1.length === num2.length && num1 < num2)) { + throw new Error('Cannot subtract larger number from smaller number') + } + + let result = '' + let borrow = 0 + let i = num1.length - 1 + let j = num2.length - 1 + + while (i >= 0) { + const digit1 = parseInt(num1[i], 10) - borrow + const digit2 = j >= 0 ? parseInt(num2[j], 10) : 0 + + if (digit1 >= digit2) { + result = (digit1 - digit2) + result + borrow = 0 + } else { + result = (digit1 + 10 - digit2) + result + borrow = 1 + } + + i-- + j-- + } + + // Remove leading zeros + return result.replace(/^0+/, '') || '0' +} + +// Helper function to handle decimal arithmetic with strings +const subtractDecimalStrings = (num1Str: string, num2Str: string): string => { + // Parse decimal numbers + const [int1, frac1 = ''] = num1Str.split('.') + const [int2, frac2 = ''] = num2Str.split('.') + + // Pad fractional parts to same length + const maxFracLen = Math.max(frac1.length, frac2.length) + const padded1 = frac1.padEnd(maxFracLen, '0') + const padded2 = frac2.padEnd(maxFracLen, '0') + + // Combine integer and fractional parts + const combined1 = int1 + padded1 + const combined2 = int2 + padded2 + + // Subtract + const resultCombined = subtractStrings(combined1, combined2) + + // Split back into integer and fractional parts + if (maxFracLen === 0) { + return resultCombined + } + + const resultInt = resultCombined.slice(0, -maxFracLen) || '0' + const resultFrac = resultCombined.slice(-maxFracLen).replace(/0+$/, '') + + return resultFrac ? `${resultInt}.${resultFrac}` : resultInt +} + +const addDecimalStrings = (num1Str: string, num2Str: string): string => { + // Parse decimal numbers + const [int1, frac1 = ''] = num1Str.split('.') + const [int2, frac2 = ''] = num2Str.split('.') + + // Pad fractional parts to same length + const maxFracLen = Math.max(frac1.length, frac2.length) + const padded1 = frac1.padEnd(maxFracLen, '0') + const padded2 = frac2.padEnd(maxFracLen, '0') + + // Combine integer and fractional parts + const combined1 = int1 + padded1 + const combined2 = int2 + padded2 + + // Add + const resultCombined = addStrings(combined1, combined2) + + // Split back into integer and fractional parts + if (maxFracLen === 0) { + return resultCombined + } + + const resultInt = resultCombined.slice(0, -maxFracLen) || '0' + const resultFrac = resultCombined.slice(-maxFracLen).replace(/0+$/, '') + + return resultFrac ? `${resultInt}.${resultFrac}` : resultInt +} + const createClock = (performance: Performance): Clock => { // 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. @@ -12,18 +122,91 @@ const createClock = (performance: Performance): Clock => { 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 unix time in nanoseconds back to milliseconds since timeOrigin - fromUnixNanoseconds: (time: number) => nanosecondsToMilliseconds(time) - startWallTime + startPerfTime, // convert milliseconds since timeOrigin to full timestamp - toUnixTimestampNanoseconds: (time: number) => toUnixNanoseconds(time).toString() + toUnixTimestampNanoseconds: (time: number) => { + // Calculate the unix timestamp in milliseconds with high precision + const unixMilliseconds = time - startPerfTime + startWallTime + + // Convert to string to preserve all precision from the input + const unixMillisecondsStr = unixMilliseconds.toString() + + // Find the decimal point to split integer and fractional parts + const decimalIndex = unixMillisecondsStr.indexOf('.') + + let integerMsStr: string + let fractionalMsStr: string + + if (decimalIndex === -1) { + // No decimal point, all integer + integerMsStr = unixMillisecondsStr + fractionalMsStr = '0' + } else { + // Split at decimal point + integerMsStr = unixMillisecondsStr.substring(0, decimalIndex) + fractionalMsStr = unixMillisecondsStr.substring(decimalIndex + 1) + } + + // Convert integer milliseconds to nanoseconds (append 6 zeros) + let result = integerMsStr === '0' ? '0' : integerMsStr + '000000' + + // Convert fractional milliseconds to nanoseconds + if (fractionalMsStr !== '0') { + // Pad or truncate fractional part to 6 digits (microsecond precision) + // since 1 ms = 1,000,000 ns, the fractional part needs to be scaled + if (fractionalMsStr.length > 6) { + fractionalMsStr = fractionalMsStr.substring(0, 6) + } else { + fractionalMsStr = fractionalMsStr.padEnd(6, '0') + } + + // Add the fractional nanoseconds + result = addStrings(result, fractionalMsStr) + } + + return result + }, + // convert unix timestamp in nanoseconds (string) back to milliseconds since timeOrigin + fromUnixNanosecondsTimestamp: (nanosStr: string) => { + // Convert nanoseconds string to milliseconds string with full precision + let millisecondsStr: string + + if (nanosStr.length <= 6) { + // Less than 1 millisecond - create fractional milliseconds + const paddedNanos = nanosStr.padStart(6, '0') + millisecondsStr = '0.' + paddedNanos + } else { + // Split nanoseconds into milliseconds and fractional nanoseconds + const msLength = nanosStr.length - 6 + const integerMs = nanosStr.substring(0, msLength) + const fractionalNs = nanosStr.substring(msLength) + + if (fractionalNs === '000000') { + millisecondsStr = integerMs + } else { + // Remove trailing zeros from fractional part + const trimmedFractional = fractionalNs.replace(/0+$/, '') + millisecondsStr = integerMs + '.' + trimmedFractional + } + } + + // Perform calculation using string arithmetic to maintain precision + // unixMilliseconds - startWallTime + startPerfTime + const startWallTimeStr = startWallTime.toString() + const startPerfTimeStr = startPerfTime.toString() + + // Step 1: unixMilliseconds - startWallTime + const afterSubtraction = subtractDecimalStrings(millisecondsStr, startWallTimeStr) + + // Step 2: result + startPerfTime + const finalResult = addDecimalStrings(afterSubtraction, startPerfTimeStr) + + // Convert to number only at the very end + return parseFloat(finalResult) + } } } diff --git a/packages/platforms/react-native/lib/native.ts b/packages/platforms/react-native/lib/native.ts index 7ce7f623a..a7711244e 100644 --- a/packages/platforms/react-native/lib/native.ts +++ b/packages/platforms/react-native/lib/native.ts @@ -26,9 +26,9 @@ const NativeBugsnagPerformance = NativeBsgModule || { writeFile: async (path: string, data: string, encoding: string) => { }, isNativePerformanceAvailable: () => false, attachToNativeSDK: () => null, - startNativeSpan: (name: string, options: object) => ({ name, id: '', traceId: '', startTime: 0, parentSpanId: '' }), - endNativeSpan: async (spanId: string, traceId: string, endTime: number, attributes: object) => { }, - markNativeSpanEndTime: (spanId: string, traceId: string, endTime: number) => { }, + startNativeSpan: (name: string, options: object) => ({ name, id: '', traceId: '', startTime: '0', parentSpanId: '' }), + endNativeSpan: async (spanId: string, traceId: string, endTime: string, attributes: object) => { }, + markNativeSpanEndTime: (spanId: string, traceId: string, endTime: string) => { }, discardNativeSpan: async (spanId: string, traceId: string) => { } } diff --git a/packages/platforms/react-native/lib/span-factory.ts b/packages/platforms/react-native/lib/span-factory.ts index 3f70da8e2..71a6fcc4a 100644 --- a/packages/platforms/react-native/lib/span-factory.ts +++ b/packages/platforms/react-native/lib/span-factory.ts @@ -28,7 +28,7 @@ export class ReactNativeSpanFactory extends SpanFactory { expect(unixTimeStamp).toBe('69000000') }) - }) - describe('clock.toUnixNanoseconds()', () => { - it('converts performance time to unix nanoseconds', () => { - const timeOrigin = new Date('2023-01-02T00:00:00.000Z') + it('converts extreme time values into a valid timestamp', () => { + // 18446744073709ms * 1000000(nanos) is near uInt64.MAX_VALUE and well over Number.MAX_SAFE_INTEGER + const timeOrigin = new Date(18446744073709) jest.setSystemTime(timeOrigin) const clock = createClock(performance) - jest.advanceTimersByTime(250) + jest.advanceTimersByTime(0.55) - const performanceTime = clock.now() - const unixNanoseconds = clock.toUnixNanoseconds(performanceTime) + const startTime = clock.now() + const unixTimeStamp = clock.toUnixTimestampNanoseconds(startTime) - // Expected: timeOrigin + 250ms in nanoseconds - const expectedNanoseconds = (timeOrigin.getTime() + 250) * 1000000 - expect(unixNanoseconds).toBe(expectedNanoseconds) + expect(unixTimeStamp).toBe('18446744073709550000') }) }) - describe('clock.fromUnixNanoseconds()', () => { - it('converts unix nanoseconds back to performance time', () => { - const timeOrigin = new Date('2023-01-02T00:00:00.000Z') + describe('clock.fromUnixNanosecondsTimestamp()', () => { + it('converts a timestamp to a valid time', () => { + const timeOrigin = new Date('1970-01-01T00:00:00.000Z') jest.setSystemTime(timeOrigin) const clock = createClock(performance) - // Test with a specific unix nanoseconds value 123ms after timeOrigin - const unixNanoseconds = 1672617600123000000 // 2023-01-02T00:00:00.123Z in nanoseconds - const performanceTime = clock.fromUnixNanoseconds(unixNanoseconds) + // Set up initial time + jest.advanceTimersByTime(69) - expect(typeof performanceTime).toBe('number') - expect(performanceTime).toEqual(123) // 123ms after timeOrigin + // Convert to nanosecond timestamp string and back + const startTime = clock.now() + const unixTimeStamp = clock.toUnixTimestampNanoseconds(startTime) + const convertedTime = clock.fromUnixNanosecondsTimestamp(unixTimeStamp) + + // The converted time should match the original time + expect(convertedTime).toBe(startTime) }) - it('is the inverse of toUnixNanoseconds', () => { - const timeOrigin = new Date('2023-01-02T00:00:00.000Z') + it('converts extreme timestamps into valid time values', () => { + // Use a large timestamp value + const timeOrigin = new Date(18446744073709) jest.setSystemTime(timeOrigin) const clock = createClock(performance) - jest.advanceTimersByTime(123) + // Add a fractional time value + jest.advanceTimersByTime(0.55) - const originalTime = clock.now() - const unixNanoseconds = clock.toUnixNanoseconds(originalTime) - const roundTripTime = clock.fromUnixNanoseconds(unixNanoseconds) + // Convert to nanosecond timestamp string and back + const startTime = clock.now() + const unixTimeStamp = clock.toUnixTimestampNanoseconds(startTime) + const convertedTime = clock.fromUnixNanosecondsTimestamp(unixTimeStamp) - expect(roundTripTime).toBe(originalTime) + // The converted time should match the original time + expect(convertedTime).toBe(startTime) }) }) }) diff --git a/packages/platforms/react-native/tests/native.test.ts b/packages/platforms/react-native/tests/native.test.ts index 384ee8676..408c91277 100644 --- a/packages/platforms/react-native/tests/native.test.ts +++ b/packages/platforms/react-native/tests/native.test.ts @@ -30,9 +30,9 @@ describe('NativeBugsnagPerformance', () => { await expect(NativeBugsnagPerformance.writeFile('', '', '')).resolves.toBeUndefined() expect(NativeBugsnagPerformance.isNativePerformanceAvailable()).toBe(false) expect(NativeBugsnagPerformance.attachToNativeSDK()).toBeNull() - expect(NativeBugsnagPerformance.startNativeSpan('', {})).toStrictEqual({ name: '', id: '', traceId: '', startTime: 0, parentSpanId: '' }) - await expect(NativeBugsnagPerformance.endNativeSpan('', '', 0, {})).resolves.toBeUndefined() + expect(NativeBugsnagPerformance.startNativeSpan('', {})).toStrictEqual({ name: '', id: '', traceId: '', startTime: '0', parentSpanId: '' }) + await expect(NativeBugsnagPerformance.endNativeSpan('', '', '0', {})).resolves.toBeUndefined() await expect(NativeBugsnagPerformance.discardNativeSpan('', '')).resolves.toBeUndefined() - expect(() => { NativeBugsnagPerformance.markNativeSpanEndTime('', '', 0) }).not.toThrow() + expect(() => { NativeBugsnagPerformance.markNativeSpanEndTime('', '', '0') }).not.toThrow() }) }) diff --git a/packages/platforms/react-native/tests/span-factory.test.ts b/packages/platforms/react-native/tests/span-factory.test.ts index 27f90630d..01b0223d0 100644 --- a/packages/platforms/react-native/tests/span-factory.test.ts +++ b/packages/platforms/react-native/tests/span-factory.test.ts @@ -47,7 +47,7 @@ describe('ReactNativeSpanFactory', () => { const startTime = clock.now() const nativeSpan = spanFactory.startSpan('native span', { startTime, isFirstClass: true }) - expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixNanoseconds(startTime) })) + expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixTimestampNanoseconds(startTime) })) expect(contextStorage.current).toBe(nativeSpan) }) @@ -107,7 +107,7 @@ describe('ReactNativeSpanFactory', () => { const startTime = clock.now() const nativeSpan = spanFactory.startSpan('native span', { startTime, isFirstClass: true }) - expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixNanoseconds(startTime) })) + expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixTimestampNanoseconds(startTime) })) expect(contextStorage.current).toBe(nativeSpan) const endTime = clock.now() @@ -119,7 +119,7 @@ describe('ReactNativeSpanFactory', () => { expect(NativeBugsnagPerformance.endNativeSpan).toHaveBeenCalledWith( nativeSpan.id, nativeSpan.traceId, - clock.toUnixNanoseconds(endTime), + clock.toUnixTimestampNanoseconds(endTime), { 'bugsnag.span.first_class': true, 'additional.attribute': 'test' }) }) @@ -149,11 +149,11 @@ describe('ReactNativeSpanFactory', () => { spanFactory.configure({ logger: jestLogger, onSpanEnd: [onSpanEndCallback] } as unknown as InternalConfiguration) const startTime = clock.now() const validSpan = spanFactory.startSpan('should send', { startTime, isFirstClass: true }) - expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('should send', expect.objectContaining({ startTime: clock.toUnixNanoseconds(startTime) })) + expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('should send', expect.objectContaining({ startTime: clock.toUnixTimestampNanoseconds(startTime) })) expect(contextStorage.current).toBe(validSpan) const invalidSpan = spanFactory.startSpan('should discard', { startTime, isFirstClass: true }) - expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('should discard', expect.objectContaining({ startTime: clock.toUnixNanoseconds(startTime) })) + expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('should discard', expect.objectContaining({ startTime: clock.toUnixTimestampNanoseconds(startTime) })) expect(contextStorage.current).toBe(invalidSpan) const endTime = clock.now() @@ -184,7 +184,7 @@ describe('ReactNativeSpanFactory', () => { spanFactory.configure({ logger: jestLogger, onSpanStart: [onSpanStartCallback] } as unknown as InternalConfiguration) const startTime = clock.now() const span = spanFactory.startSpan('native span', { startTime, isFirstClass: true }) - expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixNanoseconds(startTime) })) + expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixTimestampNanoseconds(startTime) })) // @ts-expect-error 'attributes' is private but very awkward to test otherwise expect(span.attributes.attributes.get('start_callback')).toBe(true) }) @@ -196,7 +196,7 @@ describe('ReactNativeSpanFactory', () => { const startTime = clock.now() const nativeSpan = spanFactory.startSpan('native span', { startTime, isFirstClass: true }) - expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixNanoseconds(startTime) })) + expect(NativeBugsnagPerformance.startNativeSpan).toHaveBeenCalledWith('native span', expect.objectContaining({ startTime: clock.toUnixTimestampNanoseconds(startTime) })) spanFactory.endSpan(nativeSpan, DISCARD_END_TIME) expect(NativeBugsnagPerformance.endNativeSpan).not.toHaveBeenCalled() diff --git a/packages/plugin-named-spans/tests/named-span-access.test.ts b/packages/plugin-named-spans/tests/named-span-access.test.ts index 1a852f870..c44ccf82c 100644 --- a/packages/plugin-named-spans/tests/named-span-access.test.ts +++ b/packages/plugin-named-spans/tests/named-span-access.test.ts @@ -13,7 +13,7 @@ describe('BugsnagNamedSpansPlugin', () => { plugin = new BugsnagNamedSpansPlugin() context = new PluginContext( createConfiguration(), - { now: jest.fn(() => 1000), toUnixNanoseconds: jest.fn(() => 1000000000) } as unknown as IncrementingClock + { now: jest.fn(() => 1000), toUnixTimestampNanoseconds: jest.fn(() => '1000000000') } as unknown as IncrementingClock ) // Install the plugin to the context diff --git a/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/BugsnagNativeSpans.java b/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/BugsnagNativeSpans.java index cecd470d9..ffff012a9 100644 --- a/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/BugsnagNativeSpans.java +++ b/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/BugsnagNativeSpans.java @@ -178,8 +178,8 @@ OnSpanContextRetrievedCallback takeSpanContextCallback(int callbackId) { private static void endSpan(ReadableMap updates, Span span) { if (updates.hasKey(END_TIME)) { - double endTime = updates.getDouble(END_TIME); - span.end(BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime((long) endTime)); + String endTimeTimestamp = updates.getString(END_TIME); + span.end(BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime(Long.parseLong(endTimeTimestamp))); } else { span.end(); } diff --git a/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/JavascriptSpanTransactionImpl.java b/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/JavascriptSpanTransactionImpl.java index 77097b0af..cfbb9a67a 100644 --- a/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/JavascriptSpanTransactionImpl.java +++ b/packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/JavascriptSpanTransactionImpl.java @@ -129,7 +129,7 @@ public void commit(OnRemoteSpanUpdatedCallback callback) { updateTransaction.putArray(ATTRIBUTES, attributesArray); if (isEnded) { - updateTransaction.putDouble(END_TIME, endTime); + updateTransaction.putString(END_TIME, Long.toString(endTime)); updateTransaction.putBoolean(IS_ENDED, true); } diff --git a/packages/plugin-react-native-span-access/ios/BugsnagJavascriptSpanControl.mm b/packages/plugin-react-native-span-access/ios/BugsnagJavascriptSpanControl.mm index 92262de6a..36c2ac71c 100644 --- a/packages/plugin-react-native-span-access/ios/BugsnagJavascriptSpanControl.mm +++ b/packages/plugin-react-native-span-access/ios/BugsnagJavascriptSpanControl.mm @@ -47,7 +47,12 @@ - (void)endWithEndTime:(NSDate *)endTime { } NSTimeInterval currentTime = [endTime timeIntervalSince1970]; - NSNumber *unixNanos = @((double)(currentTime * NSEC_PER_SEC)); + // Calculate seconds and nanoseconds separately to maintain precision + uint64_t seconds = (uint64_t)currentTime; + uint64_t nanos = (uint64_t)round((currentTime - seconds) * NSEC_PER_SEC); + uint64_t nanoseconds = seconds * NSEC_PER_SEC + nanos; + // Convert to string for JS + NSString *unixNanos = [NSString stringWithFormat:@"%llu", nanoseconds]; updateEvent[endTimeTransactionKey] = unixNanos; updateEvent[isEndedTransactionKey] = @YES; } @@ -82,7 +87,7 @@ - (void)commit:(OnSpanUpdatedCallback)callback { callback(NO); return; } - + int eventId = [plugin registerSpanUpdateCallback:callback]; updateEvent[idTransactionKey] = @(eventId); [plugin sendSpanUpdateEvent:updateEvent]; @@ -92,21 +97,21 @@ - (BOOL)isValidAttribute:(_Nullable id)value { if (value == nil || [value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]]) { return YES; } - + if ([value isKindOfClass:[NSArray class]]) { NSUInteger idx = [value indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) { return NO; } - + // stop the iteration if we find an invalid object *stop = YES; return YES; }]; - + return idx == NSNotFound; // means all objects are valid } - + return NO; } @@ -136,12 +141,12 @@ - (void)retrieveSpanContext:(RemoteSpanContextCallback)callback { } int eventId = [plugin registerSpanContextCallback:callback]; - + NSDictionary *contextEvent = @{ idTransactionKey: @(eventId), nameTransactionKey: spanName, }; - + [plugin sendSpanContextEvent:contextEvent]; } diff --git a/packages/plugin-react-native-span-access/ios/BugsnagNativeSpans.mm b/packages/plugin-react-native-span-access/ios/BugsnagNativeSpans.mm index f74063833..c66e27f01 100644 --- a/packages/plugin-react-native-span-access/ios/BugsnagNativeSpans.mm +++ b/packages/plugin-react-native-span-access/ios/BugsnagNativeSpans.mm @@ -124,10 +124,13 @@ - (void)stopObserving } - (void)endSpan:(NSDictionary *)updates span:(BugsnagPerformanceSpan *)span { - NSNumber *timestampString = updates[@"endTime"]; + NSString *timestampString = updates[@"endTime"]; if (timestampString) { - double endTimestampValue = [timestampString doubleValue]; - NSDate *endTimestamp = [NSDate dateWithTimeIntervalSince1970:(endTimestampValue / NSEC_PER_SEC)]; + NSDecimalNumber *endTimeDecimal = [NSDecimalNumber decimalNumberWithString:timestampString]; + NSDecimalNumber *nsecPerSecDecimal = [NSDecimalNumber decimalNumberWithMantissa:1 exponent:9 isNegative:NO]; + NSDecimalNumber *endTimeSecondsDecimal = [endTimeDecimal decimalNumberByDividingBy:nsecPerSecDecimal]; + NSTimeInterval endTimeInterval = [endTimeSecondsDecimal doubleValue]; + NSDate *endTimestamp = [NSDate dateWithTimeIntervalSince1970:endTimeInterval]; [span endWithEndTime:endTimestamp]; } else { [span end]; diff --git a/packages/plugin-react-native-span-access/lib/javascript-spans-plugin.ts b/packages/plugin-react-native-span-access/lib/javascript-spans-plugin.ts index 7aaff46cd..9237820d1 100644 --- a/packages/plugin-react-native-span-access/lib/javascript-spans-plugin.ts +++ b/packages/plugin-react-native-span-access/lib/javascript-spans-plugin.ts @@ -11,7 +11,7 @@ interface SpanUpdateEvent { name: string attributes: Array<{ name: string, value: SpanAttribute }> isEnded: boolean - endTime?: number + endTime?: string } interface SpanContextEvent { @@ -73,7 +73,7 @@ export class BugsnagJavascriptSpansPlugin implements Plugin('BugsnagNativeSpan interface SpanTransaction { attributes: Array<{ name: string, value?: SpanAttribute | null }> isEnded: boolean - endTime?: number + endTime?: string } interface SpanId { @@ -48,7 +48,7 @@ class NativeSpanControlImpl implements NativeSpanControl { update({ end: (endTime?: Time) => { const safeEndTime = timeToNumber(this.clock, endTime) - transaction.endTime = this.clock.toUnixNanoseconds(safeEndTime) + transaction.endTime = this.clock.toUnixTimestampNanoseconds(safeEndTime) transaction.isEnded = true }, setAttribute: (name: string, value?: SpanAttribute | null) => { diff --git a/packages/plugin-react-native-span-access/tests/native-spans-plugin.test.ts b/packages/plugin-react-native-span-access/tests/native-spans-plugin.test.ts index 0d367db63..5f501a8a0 100644 --- a/packages/plugin-react-native-span-access/tests/native-spans-plugin.test.ts +++ b/packages/plugin-react-native-span-access/tests/native-spans-plugin.test.ts @@ -177,7 +177,7 @@ describe('BugsnagNativeSpansPlugin', () => { it('should handle end time correctly', async () => { mockNativeModule.updateSpan.mockResolvedValue(true) clock.now = jest.fn(() => 1234567) - clock.toUnixNanoseconds = jest.fn(() => 1234567000000000) + clock.toUnixTimestampNanoseconds = jest.fn(() => '1234567000000000') const result = await spanControl.updateSpan((mutator: any) => { mutator.end() @@ -189,7 +189,7 @@ describe('BugsnagNativeSpansPlugin', () => { { attributes: [], isEnded: true, - endTime: 1234567000000000 + endTime: '1234567000000000' } ) }) @@ -197,7 +197,7 @@ describe('BugsnagNativeSpansPlugin', () => { it('should handle end time with custom time', async () => { mockNativeModule.updateSpan.mockResolvedValue(true) clock.now = jest.fn(() => 1234567) - clock.toUnixNanoseconds = jest.fn(() => 9876543000000000) + clock.toUnixTimestampNanoseconds = jest.fn(() => '9876543000000000') const customTime = 9876543 const result = await spanControl.updateSpan((mutator: any) => { @@ -210,7 +210,7 @@ describe('BugsnagNativeSpansPlugin', () => { { attributes: [], isEnded: true, - endTime: 9876543000000000 + endTime: '9876543000000000' } ) }) @@ -243,7 +243,7 @@ describe('BugsnagNativeSpansPlugin', () => { it('should handle both attributes and end time', async () => { mockNativeModule.updateSpan.mockResolvedValue(true) clock.now = jest.fn(() => 1234567) - clock.toUnixNanoseconds = jest.fn(() => 1234567000000000) + clock.toUnixTimestampNanoseconds = jest.fn(() => '1234567000000000') const result = await spanControl.updateSpan((mutator: any) => { mutator.setAttribute('finalAttribute', 'finalValue') @@ -256,7 +256,7 @@ describe('BugsnagNativeSpansPlugin', () => { { attributes: [{ name: 'finalAttribute', value: 'finalValue' }], isEnded: true, - endTime: 1234567000000000 + endTime: '1234567000000000' } ) }) diff --git a/packages/test-utilities/lib/incrementing-clock.ts b/packages/test-utilities/lib/incrementing-clock.ts index da1463cdf..9771c0bbc 100644 --- a/packages/test-utilities/lib/incrementing-clock.ts +++ b/packages/test-utilities/lib/incrementing-clock.ts @@ -32,16 +32,17 @@ class IncrementingClock implements Clock { return date.getTime() - this.timeOrigin } - toUnixNanoseconds (time: number) { + private _toUnixNanoseconds (time: number) { return ((this.timeOrigin + time) * NANOSECONDS_IN_MILLISECONDS) } - fromUnixNanoseconds (time: number) { - return (time / NANOSECONDS_IN_MILLISECONDS) - this.timeOrigin + toUnixTimestampNanoseconds (time: number) { + return this._toUnixNanoseconds(time).toString() } - toUnixTimestampNanoseconds (time: number) { - return this.toUnixNanoseconds(time).toString() + fromUnixNanosecondsTimestamp (timestamp: string) { + const nanos = parseInt(timestamp, 10) + return (nanos / NANOSECONDS_IN_MILLISECONDS) - this.timeOrigin } }