{"record":{"id":"856703546319bf26","repo":"Hmbown/CodeWhale","slug":"field-lost-precision-encode-otlp-nanoseconds-as-a-decimal","errorCode":null,"errorMessage":"${field} lost precision: encode OTLP nanoseconds as a decimal string.","messagePattern":"(.+?) lost precision: encode OTLP nanoseconds as a decimal string\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/ingest.ts","lineNumber":115,"sourceCode":"  const a = obj(v);\n  if ('stringValue' in a) return a.stringValue;\n  if ('boolValue' in a) return a.boolValue;\n  if ('doubleValue' in a) return a.doubleValue;\n  if ('intValue' in a) {\n    const n = Number(a.intValue);\n    return Number.isSafeInteger(n) ? n : String(a.intValue);\n  }\n  if ('bytesValue' in a) return a.bytesValue;\n  if ('arrayValue' in a) return list(obj(a.arrayValue).values).map(decodeAnyValue);\n  if ('kvlistValue' in a) return attributes(obj(a.kvlistValue).values);\n  return v;\n}\nexport function attributes(value: unknown): Obj {\n  if (!Array.isArray(value)) return obj(value);\n  return Object.fromEntries(value.filter(x => typeof x?.key === 'string').map(x => [x.key, decodeAnyValue(x.value)]));\n}\nfunction ns(v: unknown, field: string): bigint {\n  if (typeof v === 'number' && !Number.isSafeInteger(v)) throw new Error(`${field} lost precision: encode OTLP nanoseconds as a decimal string.`);\n  const s = String(v ?? '');\n  if (!/^\\d{1,20}$/.test(s) || BigInt(s) > 18446744073709551615n) throw new Error(`Invalid ${field}: expected an OTLP nanosecond integer string.`);\n  return BigInt(s);\n}\nfunction otelStatus(v: unknown): Status {\n  const c = obj(v).code;\n  return c === 2 || c === 'STATUS_CODE_ERROR' ? 'error' : c === 1 || c === 'STATUS_CODE_OK' ? 'success' : 'unknown';\n}\nfunction normalizedEvent(v: unknown, index: number): WhaleEvent {\n  const a = obj(v), id = str(a.id), traceId = str(a.traceId), name = str(a.name);\n  if (!id || !traceId || !name) throw new Error(`Record ${index + 1} requires nonempty id, traceId, and name.`);\n  if (a.schemaVersion !== undefined && a.schemaVersion !== 1) throw new Error(`Record ${index + 1}: unsupported schemaVersion ${a.schemaVersion}.`);\n  const startTime = number(a.startTime, 'startTime', false)!;\n  const endTime = number(a.endTime, 'endTime') ?? startTime;\n  if (endTime < startTime) throw new Error(`Record ${index + 1}: endTime precedes startTime.`);\n  if (a.category !== undefined && !CATEGORIES.includes(a.category)) throw new Error(`Unknown category \"${a.category}\". Use \"other\" plus subtype for extensions.`);\n  const allowed: Status[] = ['pending', 'running', 'success', 'error', 'unknown'];\n  if (a.status !== undefined && !allowed.includes(a.status)) throw new Error(`Record ${index + 1}: invalid status.`);","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/ingest.ts#L97-L133","documentation":"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).","triggerScenarios":"Passing startTimeUnixNano/endTimeUnixNano/timeUnixNano as a JS number too large for Number.isSafeInteger (roughly > 9e15, easily hit by nanosecond epochs).","commonSituations":"JSON parsers decoding large OTLP nanosecond integers as numbers; exporters emitting numbers instead of strings; naive Number(...) conversion of a nanosecond string before ingest.","solutions":["Encode OTLP nanoseconds as decimal strings (as the OTLP JSON spec recommends) e.g. startTimeUnixNano:'1700000000000000000'","If you have a safe integer number, pass it as-is; otherwise keep the string form end-to-end","Fix the upstream serializer to quote nanosecond fields"],"exampleFix":"// before\nstartTimeUnixNano: 1700000000000000000\n// after\nstartTimeUnixNano: '1700000000000000000'","handlingStrategy":"validation","validationCode":"const safeNs=(v:unknown)=>typeof v!=='number'||Number.isSafeInteger(v)?null:'encode as decimal string';","typeGuard":"const isSafeNsNumber=(v:unknown): v is number => typeof v==='number'&&Number.isSafeInteger(v);","tryCatchPattern":"try { ingest(records); } catch (e) { if (/lost precision/.test(e.message)) { /* re-serialize nanos as strings upstream */ } else throw e; }","preventionTips":["Always serialize OTLP nanosecond fields as decimal strings in JSON","Never round-trip nanosecond epochs through JS numbers","Enable lossless JSON options (e.g. json-bigint) on transport"],"tags":["otlp","precision","bigint","timestamps"],"backgroundTag":"invalid-argument-value","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T16:17:23.217Z"}