Skip to content

Commit 8cd3a45

Browse files
committed
fix(react-native): added Clock.fromUnixNanosecondsTimestamp so that all JS/ReactNative timestamps can be sent as strings
1 parent 50f52dd commit 8cd3a45

File tree

12 files changed

+186
-122
lines changed

12 files changed

+186
-122
lines changed

packages/core/lib/clock.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,9 @@ export interface Clock {
2121
// a function to convert a Date object into the format returned by 'now'
2222
convert: (date: Date) => number
2323

24-
// convert the format returned by 'now' to a unix time in nanoseconds
25-
toUnixNanoseconds: (time: number) => number
26-
27-
// convert a unix time in nanoseconds to the format returned by 'now'
28-
fromUnixNanoseconds: (time: number) => number
29-
3024
// convert the format returned by 'now' into a unix timestamp in nanoseconds
3125
toUnixTimestampNanoseconds: (time: number) => string
26+
27+
// convert a unix timestamp in nanoseconds into the format returned by 'now'
28+
fromUnixNanosecondsTimestamp: (timestamp: string) => number
3229
}

packages/platforms/browser/lib/clock.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,16 @@ function createClock (performance: PerformanceWithOptionalTimeOrigin, background
4545
})
4646

4747
const toUnixNanoseconds = (time: number) => millisecondsToNanoseconds(calculatedTimeOrigin + time)
48+
const fromUnixNanoseconds = (time: number) => nanosecondsToMilliseconds(time) - calculatedTimeOrigin
4849

4950
return {
5051
now: () => performance.now(),
5152
date: () => new Date(calculatedTimeOrigin + performance.now()),
5253
convert: (date) => date.getTime() - calculatedTimeOrigin,
53-
// convert milliseconds since timeOrigin to unix time in nanoseconds
54-
toUnixNanoseconds,
5554
// convert milliseconds since timeOrigin to full timestamp
5655
toUnixTimestampNanoseconds: (time: number) => toUnixNanoseconds(time).toString(),
57-
// convert unix time in nanoseconds back to milliseconds since timeOrigin
58-
fromUnixNanoseconds: (time: number) => nanosecondsToMilliseconds(time) - calculatedTimeOrigin
56+
// convert a unix timestamp in nanoseconds back to milliseconds since timeOrigin
57+
fromUnixNanosecondsTimestamp: (timestamp: string) => fromUnixNanoseconds(parseInt(timestamp))
5958
}
6059
}
6160

packages/platforms/browser/tests/clock.test.ts

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -164,53 +164,4 @@ describe('Browser Clock', () => {
164164
expect(clock.convert(new Date())).toEqual(0)
165165
})
166166
})
167-
168-
describe('clock.toUnixNanoseconds()', () => {
169-
it('converts performance time to unix nanoseconds', () => {
170-
const timeOrigin = new Date('2023-01-02T00:00:00.000Z')
171-
jest.setSystemTime(timeOrigin)
172-
173-
const clock = createClock(new PerformanceFake(), new ControllableBackgroundingListener())
174-
175-
jest.advanceTimersByTime(250)
176-
177-
const performanceTime = clock.now()
178-
const unixNanoseconds = clock.toUnixNanoseconds(performanceTime)
179-
180-
// Expected: timeOrigin + 250ms in nanoseconds
181-
const expectedNanoseconds = (timeOrigin.getTime() + 250) * 1000000
182-
expect(unixNanoseconds).toBe(expectedNanoseconds)
183-
})
184-
})
185-
186-
describe('clock.fromUnixNanoseconds()', () => {
187-
it('converts unix nanoseconds back to performance time', () => {
188-
const timeOrigin = new Date('2023-01-02T00:00:00.000Z')
189-
jest.setSystemTime(timeOrigin)
190-
191-
const clock = createClock(new PerformanceFake(), new ControllableBackgroundingListener())
192-
193-
// Test with a specific unix nanoseconds value 123ms after timeOrigin
194-
const unixNanoseconds = 1672617600123000000 // 2023-01-02T00:00:00.123Z in nanoseconds
195-
const performanceTime = clock.fromUnixNanoseconds(unixNanoseconds)
196-
197-
expect(typeof performanceTime).toBe('number')
198-
expect(performanceTime).toEqual(123) // 123ms after timeOrigin
199-
})
200-
201-
it('is the inverse of toUnixNanoseconds', () => {
202-
const timeOrigin = new Date('2023-01-02T00:00:00.000Z')
203-
jest.setSystemTime(timeOrigin)
204-
205-
const clock = createClock(new PerformanceFake(), new ControllableBackgroundingListener())
206-
207-
jest.advanceTimersByTime(123)
208-
209-
const originalTime = clock.now()
210-
const unixNanoseconds = clock.toUnixNanoseconds(originalTime)
211-
const roundTripTime = clock.fromUnixNanoseconds(unixNanoseconds)
212-
213-
expect(roundTripTime).toBe(originalTime)
214-
})
215-
})
216167
})

packages/platforms/react-native/lib/clock.ts

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { Clock } from '@bugsnag/core-performance'
2-
import { millisecondsToNanoseconds, nanosecondsToMilliseconds } from '@bugsnag/core-performance'
32

43
interface Performance {
54
now: () => number
@@ -27,22 +26,106 @@ const addStrings = (num1: string, num2: string): string => {
2726
return result
2827
}
2928

29+
// Helper function to subtract two numbers represented as strings (num1 - num2)
30+
const subtractStrings = (num1: string, num2: string): string => {
31+
// Ensure num1 >= num2 for simplicity
32+
if (num1.length < num2.length || (num1.length === num2.length && num1 < num2)) {
33+
throw new Error('Cannot subtract larger number from smaller number')
34+
}
35+
36+
let result = ''
37+
let borrow = 0
38+
let i = num1.length - 1
39+
let j = num2.length - 1
40+
41+
while (i >= 0) {
42+
const digit1 = parseInt(num1[i], 10) - borrow
43+
const digit2 = j >= 0 ? parseInt(num2[j], 10) : 0
44+
45+
if (digit1 >= digit2) {
46+
result = (digit1 - digit2) + result
47+
borrow = 0
48+
} else {
49+
result = (digit1 + 10 - digit2) + result
50+
borrow = 1
51+
}
52+
53+
i--
54+
j--
55+
}
56+
57+
// Remove leading zeros
58+
return result.replace(/^0+/, '') || '0'
59+
}
60+
61+
// Helper function to handle decimal arithmetic with strings
62+
const subtractDecimalStrings = (num1Str: string, num2Str: string): string => {
63+
// Parse decimal numbers
64+
const [int1, frac1 = ''] = num1Str.split('.')
65+
const [int2, frac2 = ''] = num2Str.split('.')
66+
67+
// Pad fractional parts to same length
68+
const maxFracLen = Math.max(frac1.length, frac2.length)
69+
const padded1 = frac1.padEnd(maxFracLen, '0')
70+
const padded2 = frac2.padEnd(maxFracLen, '0')
71+
72+
// Combine integer and fractional parts
73+
const combined1 = int1 + padded1
74+
const combined2 = int2 + padded2
75+
76+
// Subtract
77+
const resultCombined = subtractStrings(combined1, combined2)
78+
79+
// Split back into integer and fractional parts
80+
if (maxFracLen === 0) {
81+
return resultCombined
82+
}
83+
84+
const resultInt = resultCombined.slice(0, -maxFracLen) || '0'
85+
const resultFrac = resultCombined.slice(-maxFracLen).replace(/0+$/, '')
86+
87+
return resultFrac ? `${resultInt}.${resultFrac}` : resultInt
88+
}
89+
90+
const addDecimalStrings = (num1Str: string, num2Str: string): string => {
91+
// Parse decimal numbers
92+
const [int1, frac1 = ''] = num1Str.split('.')
93+
const [int2, frac2 = ''] = num2Str.split('.')
94+
95+
// Pad fractional parts to same length
96+
const maxFracLen = Math.max(frac1.length, frac2.length)
97+
const padded1 = frac1.padEnd(maxFracLen, '0')
98+
const padded2 = frac2.padEnd(maxFracLen, '0')
99+
100+
// Combine integer and fractional parts
101+
const combined1 = int1 + padded1
102+
const combined2 = int2 + padded2
103+
104+
// Add
105+
const resultCombined = addStrings(combined1, combined2)
106+
107+
// Split back into integer and fractional parts
108+
if (maxFracLen === 0) {
109+
return resultCombined
110+
}
111+
112+
const resultInt = resultCombined.slice(0, -maxFracLen) || '0'
113+
const resultFrac = resultCombined.slice(-maxFracLen).replace(/0+$/, '')
114+
115+
return resultFrac ? `${resultInt}.${resultFrac}` : resultInt
116+
}
117+
30118
const createClock = (performance: Performance): Clock => {
31119
// Measurable "monotonic" time
32120
// In React Native, `performance.now` often returns some very high values, but does not expose the `timeOrigin` it uses to calculate what "now" is.
33121
// 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.
34122
const startPerfTime = performance.now()
35123
const startWallTime = Date.now()
36124

37-
const toUnixNanoseconds = (time: number) => millisecondsToNanoseconds(time - startPerfTime + startWallTime)
38-
39125
return {
40126
now: () => performance.now(),
41127
date: () => new Date(performance.now() - startPerfTime + startWallTime),
42128
convert: (date: Date) => date.getTime() - startWallTime + startPerfTime,
43-
toUnixNanoseconds,
44-
// convert unix time in nanoseconds back to milliseconds since timeOrigin
45-
fromUnixNanoseconds: (time: number) => nanosecondsToMilliseconds(time) - startWallTime + startPerfTime,
46129
// convert milliseconds since timeOrigin to full timestamp
47130
toUnixTimestampNanoseconds: (time: number) => {
48131
// Calculate the unix timestamp in milliseconds with high precision
@@ -85,6 +168,44 @@ const createClock = (performance: Performance): Clock => {
85168
}
86169

87170
return result
171+
},
172+
// convert unix timestamp in nanoseconds (string) back to milliseconds since timeOrigin
173+
fromUnixNanosecondsTimestamp: (nanosStr: string) => {
174+
// Convert nanoseconds string to milliseconds string with full precision
175+
let millisecondsStr: string
176+
177+
if (nanosStr.length <= 6) {
178+
// Less than 1 millisecond - create fractional milliseconds
179+
const paddedNanos = nanosStr.padStart(6, '0')
180+
millisecondsStr = '0.' + paddedNanos
181+
} else {
182+
// Split nanoseconds into milliseconds and fractional nanoseconds
183+
const msLength = nanosStr.length - 6
184+
const integerMs = nanosStr.substring(0, msLength)
185+
const fractionalNs = nanosStr.substring(msLength)
186+
187+
if (fractionalNs === '000000') {
188+
millisecondsStr = integerMs
189+
} else {
190+
// Remove trailing zeros from fractional part
191+
const trimmedFractional = fractionalNs.replace(/0+$/, '')
192+
millisecondsStr = integerMs + '.' + trimmedFractional
193+
}
194+
}
195+
196+
// Perform calculation using string arithmetic to maintain precision
197+
// unixMilliseconds - startWallTime + startPerfTime
198+
const startWallTimeStr = startWallTime.toString()
199+
const startPerfTimeStr = startPerfTime.toString()
200+
201+
// Step 1: unixMilliseconds - startWallTime
202+
const afterSubtraction = subtractDecimalStrings(millisecondsStr, startWallTimeStr)
203+
204+
// Step 2: result + startPerfTime
205+
const finalResult = addDecimalStrings(afterSubtraction, startPerfTimeStr)
206+
207+
// Convert to number only at the very end
208+
return parseFloat(finalResult)
88209
}
89210
}
90211
}

packages/platforms/react-native/tests/clock.test.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -113,52 +113,42 @@ describe('React Native Clock', () => {
113113
})
114114
})
115115

116-
describe('clock.toUnixNanoseconds()', () => {
117-
it('converts performance time to unix nanoseconds', () => {
118-
const timeOrigin = new Date('2023-01-02T00:00:00.000Z')
116+
describe('clock.fromUnixNanosecondsTimestamp()', () => {
117+
it('converts a timestamp to a valid time', () => {
118+
const timeOrigin = new Date('1970-01-01T00:00:00.000Z')
119119
jest.setSystemTime(timeOrigin)
120120

121121
const clock = createClock(performance)
122122

123-
jest.advanceTimersByTime(250)
124-
125-
const performanceTime = clock.now()
126-
const unixNanoseconds = clock.toUnixNanoseconds(performanceTime)
127-
128-
// Expected: timeOrigin + 250ms in nanoseconds
129-
const expectedNanoseconds = (timeOrigin.getTime() + 250) * 1000000
130-
expect(unixNanoseconds).toBe(expectedNanoseconds)
131-
})
132-
})
133-
134-
describe('clock.fromUnixNanoseconds()', () => {
135-
it('converts unix nanoseconds back to performance time', () => {
136-
const timeOrigin = new Date('2023-01-02T00:00:00.000Z')
137-
jest.setSystemTime(timeOrigin)
138-
139-
const clock = createClock(performance)
123+
// Set up initial time
124+
jest.advanceTimersByTime(69)
140125

141-
// Test with a specific unix nanoseconds value 123ms after timeOrigin
142-
const unixNanoseconds = 1672617600123000000 // 2023-01-02T00:00:00.123Z in nanoseconds
143-
const performanceTime = clock.fromUnixNanoseconds(unixNanoseconds)
126+
// Convert to nanosecond timestamp string and back
127+
const startTime = clock.now()
128+
const unixTimeStamp = clock.toUnixTimestampNanoseconds(startTime)
129+
const convertedTime = clock.fromUnixNanosecondsTimestamp(unixTimeStamp)
144130

145-
expect(typeof performanceTime).toBe('number')
146-
expect(performanceTime).toEqual(123) // 123ms after timeOrigin
131+
// The converted time should match the original time
132+
expect(convertedTime).toBe(startTime)
147133
})
148134

149-
it('is the inverse of toUnixNanoseconds', () => {
150-
const timeOrigin = new Date('2023-01-02T00:00:00.000Z')
135+
it('converts extreme timestamps into valid time values', () => {
136+
// Use a large timestamp value
137+
const timeOrigin = new Date(18446744073709)
151138
jest.setSystemTime(timeOrigin)
152139

153140
const clock = createClock(performance)
154141

155-
jest.advanceTimersByTime(123)
142+
// Add a fractional time value
143+
jest.advanceTimersByTime(0.55)
156144

157-
const originalTime = clock.now()
158-
const unixNanoseconds = clock.toUnixNanoseconds(originalTime)
159-
const roundTripTime = clock.fromUnixNanoseconds(unixNanoseconds)
145+
// Convert to nanosecond timestamp string and back
146+
const startTime = clock.now()
147+
const unixTimeStamp = clock.toUnixTimestampNanoseconds(startTime)
148+
const convertedTime = clock.fromUnixNanosecondsTimestamp(unixTimeStamp)
160149

161-
expect(roundTripTime).toBe(originalTime)
150+
// The converted time should match the original time
151+
expect(convertedTime).toBe(startTime)
162152
})
163153
})
164154
})

packages/plugin-named-spans/tests/named-span-access.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('BugsnagNamedSpansPlugin', () => {
1313
plugin = new BugsnagNamedSpansPlugin()
1414
context = new PluginContext<Configuration>(
1515
createConfiguration<Configuration>(),
16-
{ now: jest.fn(() => 1000), toUnixNanoseconds: jest.fn(() => 1000000000) } as unknown as IncrementingClock
16+
{ now: jest.fn(() => 1000), toUnixTimestampNanoseconds: jest.fn(() => '1000000000') } as unknown as IncrementingClock
1717
)
1818

1919
// Install the plugin to the context

packages/plugin-react-native-span-access/android/src/main/java/com/bugsnag/reactnative/performance/nativespans/JavascriptSpanTransactionImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public void commit(OnRemoteSpanUpdatedCallback callback) {
129129
updateTransaction.putArray(ATTRIBUTES, attributesArray);
130130

131131
if (isEnded) {
132-
updateTransaction.putDouble(END_TIME, endTime);
132+
updateTransaction.putString(END_TIME, Long.toString(endTime));
133133
updateTransaction.putBoolean(IS_ENDED, true);
134134
}
135135

0 commit comments

Comments
 (0)