{"record":{"id":"9f73d13251284364","repo":"Hmbown/CodeWhale","slug":"otlp-span-s-spanid-end-precedes-start","errorCode":null,"errorMessage":"OTLP span ${s.spanId}: end precedes start.","messagePattern":"OTLP span (.+?): end precedes start\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/ingest.ts","lineNumber":170,"sourceCode":"interface OtlpRecord { span: Obj; resource: Obj; scope: Obj; resourceSchema?: string; scopeSchema?: string }\nfunction otlpRecords(doc: Obj): OtlpRecord[] {\n  const out: OtlpRecord[] = [];\n  for (const r of list(doc.resourceSpans)) {\n    for (const s of list(r.scopeSpans ?? r.instrumentationLibrarySpans)) {\n      for (const span of list(s.spans)) out.push({ span: obj(span), resource: obj(r.resource), scope: obj(s.scope ?? s.instrumentationLibrary), resourceSchema: r.schemaUrl, scopeSchema: s.schemaUrl });\n    }\n  }\n  return out;\n}\nfunction fromOTLP(doc: Obj, maxEvents: number): { events: WhaleEvent[]; origins: Map<string, string>; warnings: string[] } {\n  const records = otlpRecords(doc), bases = new Map<string, bigint>(), warnings: string[] = [];\n  if (!records.length) throw new Error('No spans found in resourceSpans[].scopeSpans[].spans[].');\n  if (records.length > maxEvents) throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit.`);\n  for (const { span: s } of records) {\n    if (!str(s.traceId) || !str(s.spanId)) throw new Error('Every OTLP span requires traceId and spanId.');\n    const start = ns(s.startTimeUnixNano, 'startTimeUnixNano');\n    const end = s.endTimeUnixNano === undefined ? start : ns(s.endTimeUnixNano, 'endTimeUnixNano');\n    if (end < start) throw new Error(`OTLP span ${s.spanId}: end precedes start.`);\n    let earliest = start;\n    for (const e of list(s.events)) { const t = ns(e.timeUnixNano, 'event.timeUnixNano'); if (t < earliest) earliest = t; }\n    if (!bases.has(s.traceId) || earliest < bases.get(s.traceId)!) bases.set(s.traceId, earliest);\n    if ((s.droppedEventsCount ?? 0) > 0) warnings.push(`Span ${s.spanId} reports ${s.droppedEventsCount} dropped events; coverage is incomplete.`);\n  }\n  const events: WhaleEvent[] = [];\n  for (const rec of records) {\n    const s = rec.span, a = { ...attributes(rec.resource.attributes), ...attributes(s.attributes) };\n    const origin = bases.get(s.traceId)!, start = ns(s.startTimeUnixNano, 'startTimeUnixNano');\n    const end = s.endTimeUnixNano === undefined ? start : ns(s.endTimeUnixNano, 'endTimeUnixNano');\n    const name = String(s.name ?? 'unnamed span');\n    const e: WhaleEvent = {\n      schemaVersion: 1, id: s.spanId, traceId: s.traceId, parentId: str(s.parentSpanId),\n      name, startTime: Number(start - origin) / 1e6, endTime: Number(end - origin) / 1e6,\n      openEnded: s.endTimeUnixNano === undefined,\n      agentId: String(a['whalesong.agent_id'] ?? a['gen_ai.agent.id'] ?? a['agent.id'] ?? a['service.name'] ?? 'unattributed'),\n      parentAgentId: str(a['agent.parent_id']), agentType: str(a['gen_ai.agent.name']),\n      category: categoryFor(name, a), model: str(a['gen_ai.request.model'] ?? a['gen_ai.response.model'] ?? a['llm.model_name']),","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/ingest.ts#L152-L188","documentation":"fromOTLP validates that each span's endTimeUnixNano is greater than or equal to startTimeUnixNano. A span ending before it starts is temporally invalid, so the import is rejected with the offending spanId named.","triggerScenarios":"Importing an OTLP document where a span has endTimeUnixNano < startTimeUnixNano (e.g. start='1712345678900000000', end='1712345678800000000'). Spans without endTimeUnixNano default end=start and do not trigger this.","commonSituations":"Clock adjustments (NTP step) on the emitting host between span start and end; producers recording start/end from different clocks or mixed units; corrupted or hand-edited export values.","solutions":["Fix the span's timestamps at the source so end >= start.","Check the emitting SDK/host for clock skew or NTP jumps around the span's time window.","If end is unknowable, omit endTimeUnixNano so the importer treats the span as zero-duration ending at start."],"exampleFix":"// before\n{ \"traceId\": \"5b8efff7...\", \"spanId\": \"eee19b7e...\", \"startTimeUnixNano\": \"1712345678900000000\", \"endTimeUnixNano\": \"1712345678800000000\" }\n// after\n{ \"traceId\": \"5b8efff7...\", \"spanId\": \"eee19b7e...\", \"startTimeUnixNano\": \"1712345678800000000\", \"endTimeUnixNano\": \"1712345678900000000\" }","handlingStrategy":"validation","validationCode":"for (const span of allSpans(doc)) {\n  const s = BigInt(span.startTimeUnixNano), e = BigInt(span.endTimeUnixNano ?? span.startTimeUnixNano);\n  if (e < s) throw new Error(`Span ${span.spanId}: end < start`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  traces = importTrace(text, file);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('end precedes start')) {\n    const spanId = /span (\\S+):/.exec(e.message)?.[1];\n    console.error(`Fix or drop span ${spanId}: endTimeUnixNano < startTimeUnixNano`);\n  } else throw e;\n}","preventionTips":["Sync clocks (NTP) on emitting hosts and prefer monotonic clock for span duration.","Compute end = start + duration rather than two independent wall-clock reads.","Validate BigInt ordering in exports before import."],"tags":["otlp","timestamps","ingest"],"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"}