Skip to content

Commit ae0635d

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

File tree

12 files changed

+186
-121
lines changed

12 files changed

+186
-121
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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,106 @@ const addStrings = (num1: string, num2: string): string => {
2727
return result
2828
}
2929

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

37-
const toUnixNanoseconds = (time: number) => millisecondsToNanoseconds(time - startPerfTime + startWallTime)
38-
39126
return {
40127
now: () => performance.now(),
41128
date: () => new Date(performance.now() - startPerfTime + startWallTime),
42129
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,
46130
// convert milliseconds since timeOrigin to full timestamp
47131
toUnixTimestampNanoseconds: (time: number) => {
48132
// Calculate the unix timestamp in milliseconds with high precision
@@ -85,6 +169,44 @@ const createClock = (performance: Performance): Clock => {
85169
}
86170

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

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)