From 9d2f1389e4b7ce6196ee0bc6af0db716482fdbda Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Sun, 26 Jul 2026 04:01:19 +0100 Subject: [PATCH] fix(webapp): prevent OTLP nanosecond timestamp overflow (#3292) Move BigInt() before multiplication to avoid IEEE 754 precision loss when converting epoch milliseconds to nanoseconds. Fixes 4 overflow sites: - getNowInNanoseconds(): BigInt(Date.now()) * 1_000_000n - calculateDurationFromStart(): BigInt(getTime()) * 1_000_000n - recordEvent() startTime: BigInt(getTime()) * 1_000_000n - runEngineHandlers OOM retry: BigInt(getTime()) * 1_000_000n Also adds epochMsToNanoseconds() helper to durations.ts for safe epoch-level conversions, and fixes seed script consistency. --- apps/webapp/app/v3/eventRepository/common.server.ts | 4 ++-- apps/webapp/app/v3/eventRepository/index.server.ts | 2 +- apps/webapp/app/v3/runEngineHandlers.server.ts | 2 +- apps/webapp/seed-ai-spans.mts | 2 +- packages/core/src/v3/utils/durations.ts | 5 +++++ 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/v3/eventRepository/common.server.ts b/apps/webapp/app/v3/eventRepository/common.server.ts index eb8bf7f23ec..b2da1b0cecc 100644 --- a/apps/webapp/app/v3/eventRepository/common.server.ts +++ b/apps/webapp/app/v3/eventRepository/common.server.ts @@ -25,7 +25,7 @@ export function extractContextFromCarrier(carrier: Record) { } export function getNowInNanoseconds(): bigint { - return BigInt(new Date().getTime() * 1_000_000); + return BigInt(Date.now()) * 1_000_000n; } export function getDateFromNanoseconds(nanoseconds: bigint): Date { @@ -39,7 +39,7 @@ export function calculateDurationFromStart( ) { const $endtime = typeof endTime === "string" ? new Date(endTime) : endTime; - const duration = Number(BigInt($endtime.getTime() * 1_000_000) - startTime); + const duration = Number(BigInt($endtime.getTime()) * 1_000_000n - startTime); if (minimumDuration && duration < minimumDuration) { return minimumDuration; diff --git a/apps/webapp/app/v3/eventRepository/index.server.ts b/apps/webapp/app/v3/eventRepository/index.server.ts index f0687d2a7ce..e8ff68be6f8 100644 --- a/apps/webapp/app/v3/eventRepository/index.server.ts +++ b/apps/webapp/app/v3/eventRepository/index.server.ts @@ -240,7 +240,7 @@ async function recordRunEvent( runId: foundRun.friendlyId, ...attributes, }, - startTime: BigInt((startTime?.getTime() ?? Date.now()) * 1_000_000), + startTime: BigInt(startTime?.getTime() ?? Date.now()) * 1_000_000n, ...optionsRest, }); diff --git a/apps/webapp/app/v3/runEngineHandlers.server.ts b/apps/webapp/app/v3/runEngineHandlers.server.ts index c44bcc54cec..f03a24e47ed 100644 --- a/apps/webapp/app/v3/runEngineHandlers.server.ts +++ b/apps/webapp/app/v3/runEngineHandlers.server.ts @@ -555,7 +555,7 @@ export function registerRunEngineEventBusHandlers() { ); await eventRepository.recordEvent(retryMessage, { - startTime: BigInt(time.getTime() * 1000000), + startTime: BigInt(time.getTime()) * 1_000_000n, taskSlug: run.taskIdentifier, environment, attributes: { diff --git a/apps/webapp/seed-ai-spans.mts b/apps/webapp/seed-ai-spans.mts index 5297171f955..dd9773ed097 100644 --- a/apps/webapp/seed-ai-spans.mts +++ b/apps/webapp/seed-ai-spans.mts @@ -474,7 +474,7 @@ function buildAiSpanTree(params: SpanTreeParams): CreateEventInput[] { attemptNumber?: number; }): CreateEventInput { const startNs = BigInt(opts.startMs) * BigInt(1_000_000); - const durationNs = opts.durationMs * 1_000_000; + const durationNs = BigInt(opts.durationMs) * BigInt(1_000_000); return { traceId, spanId: opts.spanId, diff --git a/packages/core/src/v3/utils/durations.ts b/packages/core/src/v3/utils/durations.ts index d1290d93044..5aed38aece2 100644 --- a/packages/core/src/v3/utils/durations.ts +++ b/packages/core/src/v3/utils/durations.ts @@ -32,6 +32,11 @@ export function millisecondsToNanoseconds(milliseconds: number): number { return milliseconds * 1_000_000; } +/** Convert epoch milliseconds to nanoseconds using BigInt to avoid overflow. */ +export function epochMsToNanoseconds(ms: number): bigint { + return BigInt(ms) * 1_000_000n; +} + export function formatDurationNanoseconds(nanoseconds: number, options?: DurationOptions): string { return formatDurationMilliseconds(nanosecondsToMilliseconds(nanoseconds), options); }