Hmbown/CodeWhale · error · Error

Record : endTime precedes startTime.

Error message

Record ${index + 1}: endTime precedes startTime.

What it means

normalizedEvent validates each ingested trace event record. When an explicit endTime is present, it must be greater than or equal to startTime; otherwise the event's interval is nonsensical. The library throws this error to reject the malformed record rather than silently normalizing the timestamps.

Solutions

  1. Fix the source record so endTime >= startTime (or omit endTime to inherit startTime and mark it open-ended).
  2. Check producer units: convert seconds to milliseconds (or ns) so both timestamps share one unit.
  3. Clamp or drop offending records upstream before calling ingest, if you want lenient importing.

Example fix

// before
{ "id": "a1", "traceId": "t1", "name": "db.query", "startTime": 1712345678900, "endTime": 1712345678800 }
// after
{ "id": "a1", "traceId": "t1", "name": "db.query", "startTime": 1712345678800, "endTime": 1712345678900 }
Defensive patterns

Strategy: validation

Validate before calling

function validInterval(rec) {
  if (typeof rec.startTime !== 'number' || typeof rec.endTime !== 'number') return true; // endTime optional
  return rec.endTime >= rec.startTime;
}
if (!validInterval(record)) throw new Error('endTime must be >= startTime');

Type guard

const hasValidInterval = (r: { startTime?: unknown; endTime?: unknown }): r is { startTime: number; endTime: number } =>
  typeof r.startTime === 'number' && typeof r.endTime === 'number' && r.endTime >= r.startTime;

Try / catch

try {
  traces = importTrace(text, file);
} catch (e) {
  if (e instanceof Error && e.message.includes('endTime precedes startTime')) {
    console.error(`Bad interval: ${e.message}`); // drop/fix the record
  } else throw e;
}

Prevention

When it happens

Trigger: Calling ingest with a JSON record where a.endTime is defined and numeric but strictly less than a.startTime, e.g. {id:'1', traceId:'t', name:'span', startTime:100, endTime:50}. endTime may come from number(a.endTime,'endTime') or default to startTime (which never trips this).

Common situations: Producers emitting relative vs absolute clocks inconsistently; unit mismatch (seconds vs milliseconds) between the two timestamps; clocks resynchronized mid-span on the emitting host; hand-written test fixtures with swapped 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


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

Appendix: source

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

  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.`);
  const at = obj(a.attributes);
  return {
    schemaVersion: 1, id, traceId, name, parentId: str(a.parentId), startTime, endTime,
    openEnded: a.endTime === undefined || a.openEnded === true,
    agentId: str(a.agentId) ?? 'unattributed', agentType: str(a.agentType), parentAgentId: str(a.parentAgentId),
    category: a.category ?? categoryFor(name, at), subtype: str(a.subtype),
    model: str(a.model), provider: str(a.provider), tool: str(a.tool),
    inputTokens: nonnegative(a.inputTokens, 'inputTokens'), outputTokens: nonnegative(a.outputTokens, 'outputTokens'),
    cachedTokens: nonnegative(a.cachedTokens, 'cachedTokens'), cost: nonnegative(a.cost, 'cost'),
    costCurrency: str(a.costCurrency), latency: nonnegative(a.latency, 'latency'),
    contextTokens: nonnegative(a.contextTokens, 'contextTokens'), contextLimit: nonnegative(a.contextLimit, 'contextLimit'),
    retry: nonnegative(a.retry, 'retry'), status: a.status ?? 'unknown',
    sourceId: str(a.sourceId), targetId: str(a.targetId), targetType: str(a.targetType),
    links: list(a.links).filter(x => typeof x?.traceId === 'string' && typeof x?.spanId === 'string'),
    attributes: at, payload: a.payload, raw: a.raw ?? v,

View on GitHub (pinned to 433685b202)