Hmbown/CodeWhale · error · Error
OTLP span : end precedes start.
Error message
OTLP span ${s.spanId}: end precedes start. What it means
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.
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.
Example fix
// before
{ "traceId": "5b8efff7...", "spanId": "eee19b7e...", "startTimeUnixNano": "1712345678900000000", "endTimeUnixNano": "1712345678800000000" }
// after
{ "traceId": "5b8efff7...", "spanId": "eee19b7e...", "startTimeUnixNano": "1712345678800000000", "endTimeUnixNano": "1712345678900000000" } Defensive patterns
Strategy: validation
Validate before calling
for (const span of allSpans(doc)) {
const s = BigInt(span.startTimeUnixNano), e = BigInt(span.endTimeUnixNano ?? span.startTimeUnixNano);
if (e < s) throw new Error(`Span ${span.spanId}: end < start`);
} Try / catch
try {
traces = importTrace(text, file);
} catch (e) {
if (e instanceof Error && e.message.includes('end precedes start')) {
const spanId = /span (\S+):/.exec(e.message)?.[1];
console.error(`Fix or drop span ${spanId}: endTimeUnixNano < startTimeUnixNano`);
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
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
- Every OTLP span requires traceId and spanId.
- lost precision: encode OTLP nanoseconds as a decimal string.
- Import exceeds the event limit, including span events.
- Import exceeds the event limit.
- Invalid : expected an OTLP nanosecond integer string.
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/9f73d13251284364.
Report an issue: GitHub.
Appendix: source
Thrown at pet/src/core/ingest.ts:170
interface OtlpRecord { span: Obj; resource: Obj; scope: Obj; resourceSchema?: string; scopeSchema?: string }
function otlpRecords(doc: Obj): OtlpRecord[] {
const out: OtlpRecord[] = [];
for (const r of list(doc.resourceSpans)) {
for (const s of list(r.scopeSpans ?? r.instrumentationLibrarySpans)) {
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 });
}
}
return out;
}
function fromOTLP(doc: Obj, maxEvents: number): { events: WhaleEvent[]; origins: Map<string, string>; warnings: string[] } {
const records = otlpRecords(doc), bases = new Map<string, bigint>(), warnings: string[] = [];
if (!records.length) throw new Error('No spans found in resourceSpans[].scopeSpans[].spans[].');
if (records.length > maxEvents) throw new Error(`Import exceeds the ${maxEvents.toLocaleString()} event limit.`);
for (const { span: s } of records) {
if (!str(s.traceId) || !str(s.spanId)) throw new Error('Every OTLP span requires traceId and spanId.');
const start = ns(s.startTimeUnixNano, 'startTimeUnixNano');
const end = s.endTimeUnixNano === undefined ? start : ns(s.endTimeUnixNano, 'endTimeUnixNano');
if (end < start) throw new Error(`OTLP span ${s.spanId}: end precedes start.`);
let earliest = start;
for (const e of list(s.events)) { const t = ns(e.timeUnixNano, 'event.timeUnixNano'); if (t < earliest) earliest = t; }
if (!bases.has(s.traceId) || earliest < bases.get(s.traceId)!) bases.set(s.traceId, earliest);
if ((s.droppedEventsCount ?? 0) > 0) warnings.push(`Span ${s.spanId} reports ${s.droppedEventsCount} dropped events; coverage is incomplete.`);
}
const events: WhaleEvent[] = [];
for (const rec of records) {
const s = rec.span, a = { ...attributes(rec.resource.attributes), ...attributes(s.attributes) };
const origin = bases.get(s.traceId)!, start = ns(s.startTimeUnixNano, 'startTimeUnixNano');
const end = s.endTimeUnixNano === undefined ? start : ns(s.endTimeUnixNano, 'endTimeUnixNano');
const name = String(s.name ?? 'unnamed span');
const e: WhaleEvent = {
schemaVersion: 1, id: s.spanId, traceId: s.traceId, parentId: str(s.parentSpanId),
name, startTime: Number(start - origin) / 1e6, endTime: Number(end - origin) / 1e6,
openEnded: s.endTimeUnixNano === undefined,
agentId: String(a['whalesong.agent_id'] ?? a['gen_ai.agent.id'] ?? a['agent.id'] ?? a['service.name'] ?? 'unattributed'),
parentAgentId: str(a['agent.parent_id']), agentType: str(a['gen_ai.agent.name']),
category: categoryFor(name, a), model: str(a['gen_ai.request.model'] ?? a['gen_ai.response.model'] ?? a['llm.model_name']),View on GitHub (pinned to 433685b202)