Hmbown/CodeWhale · error · Error

lost precision: encode OTLP nanoseconds as a decimal string.

Error message

${field} lost precision: encode OTLP nanoseconds as a decimal string.

What it means

The `ns` helper converts OTLP nanosecond timestamps to bigint. If a caller passes a JavaScript number that is not a safe integer (i.e. it already lost nanosecond precision as a double), it refuses the value and demands a decimal string (pet/src/core/ingest.ts:115).

Solutions

  1. Encode OTLP nanoseconds as decimal strings (as the OTLP JSON spec recommends) e.g. startTimeUnixNano:'1700000000000000000'
  2. If you have a safe integer number, pass it as-is; otherwise keep the string form end-to-end
  3. Fix the upstream serializer to quote nanosecond fields

Example fix

// before
startTimeUnixNano: 1700000000000000000
// after
startTimeUnixNano: '1700000000000000000'
Defensive patterns

Strategy: validation

Validate before calling

const safeNs=(v:unknown)=>typeof v!=='number'||Number.isSafeInteger(v)?null:'encode as decimal string';

Type guard

const isSafeNsNumber=(v:unknown): v is number => typeof v==='number'&&Number.isSafeInteger(v);

Try / catch

try { ingest(records); } catch (e) { if (/lost precision/.test(e.message)) { /* re-serialize nanos as strings upstream */ } else throw e; }

Prevention

When it happens

Trigger: Passing startTimeUnixNano/endTimeUnixNano/timeUnixNano as a JS number too large for Number.isSafeInteger (roughly > 9e15, easily hit by nanosecond epochs).

Common situations: JSON parsers decoding large OTLP nanosecond integers as numbers; exporters emitting numbers instead of strings; naive Number(...) conversion of a nanosecond string before ingest.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/856703546319bf26. Report an issue: GitHub.

Appendix: source

Thrown at pet/src/core/ingest.ts:115

  const a = obj(v);
  if ('stringValue' in a) return a.stringValue;
  if ('boolValue' in a) return a.boolValue;
  if ('doubleValue' in a) return a.doubleValue;
  if ('intValue' in a) {
    const n = Number(a.intValue);
    return Number.isSafeInteger(n) ? n : String(a.intValue);
  }
  if ('bytesValue' in a) return a.bytesValue;
  if ('arrayValue' in a) return list(obj(a.arrayValue).values).map(decodeAnyValue);
  if ('kvlistValue' in a) return attributes(obj(a.kvlistValue).values);
  return v;
}
export function attributes(value: unknown): Obj {
  if (!Array.isArray(value)) return obj(value);
  return Object.fromEntries(value.filter(x => typeof x?.key === 'string').map(x => [x.key, decodeAnyValue(x.value)]));
}
function ns(v: unknown, field: string): bigint {
  if (typeof v === 'number' && !Number.isSafeInteger(v)) throw new Error(`${field} lost precision: encode OTLP nanoseconds as a decimal string.`);
  const s = String(v ?? '');
  if (!/^\d{1,20}$/.test(s) || BigInt(s) > 18446744073709551615n) throw new Error(`Invalid ${field}: expected an OTLP nanosecond integer string.`);
  return BigInt(s);
}
function otelStatus(v: unknown): Status {
  const c = obj(v).code;
  return c === 2 || c === 'STATUS_CODE_ERROR' ? 'error' : c === 1 || c === 'STATUS_CODE_OK' ? 'success' : 'unknown';
}
function normalizedEvent(v: unknown, index: number): WhaleEvent {
  const a = obj(v), id = str(a.id), traceId = str(a.traceId), name = str(a.name);
  if (!id || !traceId || !name) throw new Error(`Record ${index + 1} requires nonempty id, traceId, and name.`);
  if (a.schemaVersion !== undefined && a.schemaVersion !== 1) throw new Error(`Record ${index + 1}: unsupported schemaVersion ${a.schemaVersion}.`);
  const startTime = number(a.startTime, 'startTime', false)!;
  const endTime = number(a.endTime, 'endTime') ?? startTime;
  if (endTime < startTime) throw new Error(`Record ${index + 1}: endTime precedes startTime.`);
  if (a.category !== undefined && !CATEGORIES.includes(a.category)) throw new Error(`Unknown category "${a.category}". Use "other" plus subtype for extensions.`);
  const allowed: Status[] = ['pending', 'running', 'success', 'error', 'unknown'];
  if (a.status !== undefined && !allowed.includes(a.status)) throw new Error(`Record ${index + 1}: invalid status.`);

View on GitHub (pinned to 433685b202)