Skip to content

Commit 48bbb87

Browse files
committed
fix: use BigInt for OTLP nanosecond timestamps to avoid precision loss
1 parent 9925c72 commit 48bbb87

File tree

2 files changed

+78
-2
lines changed

2 files changed

+78
-2
lines changed

apps/supervisor/src/services/otlpTraceService.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,71 @@ describe("buildPayload", () => {
8383
expect(span.endTimeUnixNano).toBe("1250000000");
8484
});
8585

86+
it("converts real epoch timestamps without precision loss", () => {
87+
// Date.now() values exceed Number.MAX_SAFE_INTEGER when multiplied by 1e6
88+
const startMs = 1711929600000; // 2024-04-01T00:00:00Z
89+
const endMs = 1711929600250;
90+
91+
const payload = buildPayload({
92+
traceId: "abcd1234abcd1234abcd1234abcd1234",
93+
spanName: "test",
94+
startTimeMs: startMs,
95+
endTimeMs: endMs,
96+
resourceAttributes: {},
97+
spanAttributes: {},
98+
});
99+
100+
const span = payload.resourceSpans[0]!.scopeSpans[0]!.spans[0]!;
101+
expect(span.startTimeUnixNano).toBe("1711929600000000000");
102+
expect(span.endTimeUnixNano).toBe("1711929600250000000");
103+
});
104+
105+
it("preserves sub-millisecond precision from performance.now() arithmetic", () => {
106+
// provisionStartEpochMs = Date.now() - (performance.now() - startMs) produces fractional ms.
107+
// Use small epoch + fraction to avoid IEEE 754 noise in the fractional part.
108+
const startMs = 1000.322;
109+
const endMs = 1045.789;
110+
111+
const payload = buildPayload({
112+
traceId: "abcd1234abcd1234abcd1234abcd1234",
113+
spanName: "test",
114+
startTimeMs: startMs,
115+
endTimeMs: endMs,
116+
resourceAttributes: {},
117+
spanAttributes: {},
118+
});
119+
120+
const span = payload.resourceSpans[0]!.scopeSpans[0]!.spans[0]!;
121+
expect(span.startTimeUnixNano).toBe("1000322000");
122+
expect(span.endTimeUnixNano).toBe("1045789000");
123+
});
124+
125+
it("sub-ms precision affects ordering for real epoch values", () => {
126+
// Two spans within the same millisecond should have different nanosecond timestamps
127+
const spanA = buildPayload({
128+
traceId: "abcd1234abcd1234abcd1234abcd1234",
129+
spanName: "a",
130+
startTimeMs: 1711929600000.3,
131+
endTimeMs: 1711929600001,
132+
resourceAttributes: {},
133+
spanAttributes: {},
134+
});
135+
136+
const spanB = buildPayload({
137+
traceId: "abcd1234abcd1234abcd1234abcd1234",
138+
spanName: "b",
139+
startTimeMs: 1711929600000.7,
140+
endTimeMs: 1711929600001,
141+
resourceAttributes: {},
142+
spanAttributes: {},
143+
});
144+
145+
const startA = BigInt(spanA.resourceSpans[0]!.scopeSpans[0]!.spans[0]!.startTimeUnixNano);
146+
const startB = BigInt(spanB.resourceSpans[0]!.scopeSpans[0]!.spans[0]!.startTimeUnixNano);
147+
// A should sort before B (both in the same ms but different sub-ms positions)
148+
expect(startA).toBeLessThan(startB);
149+
});
150+
86151
it("omits parentSpanId when not provided", () => {
87152
const payload = buildPayload({
88153
traceId: "abcd1234abcd1234abcd1234abcd1234",

apps/supervisor/src/services/otlpTraceService.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ export function buildPayload(span: OtlpTraceSpan) {
6363
parentSpanId: span.parentSpanId,
6464
name: span.spanName,
6565
kind: 3, // SPAN_KIND_CLIENT
66-
startTimeUnixNano: String(span.startTimeMs * 1_000_000),
67-
endTimeUnixNano: String(span.endTimeMs * 1_000_000),
66+
startTimeUnixNano: msToNano(span.startTimeMs),
67+
endTimeUnixNano: msToNano(span.endTimeMs),
6868
attributes: toOtlpAttributes(span.spanAttributes),
6969
status: { code: 1 }, // STATUS_CODE_OK
7070
},
@@ -91,3 +91,14 @@ function toOtlpValue(value: string | number | boolean): Record<string, unknown>
9191
if (Number.isInteger(value)) return { intValue: value };
9292
return { doubleValue: value };
9393
}
94+
95+
/**
96+
* Convert epoch milliseconds to nanosecond string, preserving sub-ms precision.
97+
* Fractional ms from performance.now() arithmetic carry meaningful microsecond
98+
* data that affects span sort ordering when events happen within the same ms.
99+
*/
100+
function msToNano(ms: number): string {
101+
const wholeMs = Math.trunc(ms);
102+
const fracNs = Math.round((ms - wholeMs) * 1_000_000);
103+
return String(BigInt(wholeMs) * 1_000_000n + BigInt(fracNs));
104+
}

0 commit comments

Comments
 (0)