Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/orb/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const MAX_INSTANCE_ID_CHARS = 64;
const MAX_HASH_CHARS = 128;
const MAX_BUCKET_CHARS = 64;
const MAX_VERDICT_CHARS = 32;
const MAX_TIMESTAMP_CHARS = 64;
const VALID_OUTCOMES = new Set(["merged", "closed"]);
const VALID_REVERSALS = new Set(["none", "reopened", "reverted", "superseded"]);
// gate_verdict is read downstream as a CLOSED enum by exact equality (analytics.ts foldInstance branches on
Expand Down Expand Up @@ -120,6 +121,17 @@ function clampReuseCount(value: unknown): number | null {
return rounded;
}

/** Validate a sender-supplied timestamp (#10028). Returns the string unchanged only when it is a
* length-capped, `Date.parse`-able instant; otherwise null. decision_timestamp is the day bucket for the
* public fleet-accuracy trend and the retention rollup key, so a non-instant string would sort above every
* ISO date in the lexicographic window bound, be dropped in JS, and leave a permanent junk `day` in the
* rollup PK. Null lets `COALESCE(decision_timestamp, received_at)` fall back to the server clock, so the
* signal still counts. Mirrors the REUSE_DAY_PATTERN + clampReuseCount pair one field family over. */
export function normalizeIngestTimestamp(value: unknown): string | null {
if (typeof value !== "string" || value.length > MAX_TIMESTAMP_CHARS) return null;
return Number.isFinite(Date.parse(value)) ? value : null;
}

export type OrbIngestResult = { accepted: number } | { error: string };

/** Clamp a sender-supplied cycle time to a plausible range; null for anything implausible/absent. */
Expand Down Expand Up @@ -223,9 +235,9 @@ export async function handleOrbIngest(body: string, db: D1Database, presentedIns
reversal,
typeof event.gate_reasoncode_bucket === "string" && event.gate_reasoncode_bucket.length <= MAX_BUCKET_CHARS && VALID_REASONCODE_BUCKETS.has(event.gate_reasoncode_bucket) ? event.gate_reasoncode_bucket : null,
clampCycleMs(event.time_to_close_ms),
typeof event.decision_timestamp === "string" ? event.decision_timestamp : null,
typeof event.outcome_timestamp === "string" ? event.outcome_timestamp : null,
typeof event.outcome_timestamp === "string" ? event.outcome_timestamp : null,
normalizeIngestTimestamp(event.decision_timestamp),
normalizeIngestTimestamp(event.outcome_timestamp),
normalizeIngestTimestamp(event.outcome_timestamp),
)
.run();
if (result.meta.changes > 0) accepted++;
Expand Down
42 changes: 41 additions & 1 deletion test/integration/orb-ingest.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { createApp } from "../../src/api/routes";
import { handleOrbIngest, MAX_ORB_INGEST_BODY_BYTES, readOrbIngestBody } from "../../src/orb/ingest";
import { handleOrbIngest, MAX_ORB_INGEST_BODY_BYTES, normalizeIngestTimestamp, readOrbIngestBody } from "../../src/orb/ingest";
import { createTestEnv, TestD1Database } from "../helpers/d1";

describe("handleOrbIngest()", () => {
Expand Down Expand Up @@ -222,6 +222,46 @@ describe("handleOrbIngest()", () => {
expect(await ingest(db, [ev()])).toBeTruthy();
expect(call).toBeGreaterThan(0);
});

it("#10028: normalizeIngestTimestamp keeps a parseable capped instant and rejects everything else", () => {
expect(normalizeIngestTimestamp("2026-07-30T12:00:00.000Z")).toBe("2026-07-30T12:00:00.000Z");
expect(normalizeIngestTimestamp("unknown")).toBeNull();
expect(normalizeIngestTimestamp("x".repeat(65))).toBeNull();
expect(normalizeIngestTimestamp(12345)).toBeNull();
expect(normalizeIngestTimestamp(undefined)).toBeNull();
});

it("#10028: a malformed decision_timestamp is stored NULL, the event still accepted", async () => {
const db = makeDb();
expect(await ingest(db, [ev({ pr_hash: "ts1", decision_timestamp: "unknown" })])).toEqual({ accepted: 1 });
expect(await col(db, "ts1", "decision_timestamp")).toBeNull();
});

it("#10028: a well-formed decision_timestamp/outcome_timestamp is stored verbatim in all three columns", async () => {
const db = makeDb();
const iso = "2026-07-30T12:00:00.000Z";
await ingest(db, [ev({ pr_hash: "ts2", decision_timestamp: iso, outcome_timestamp: iso })]);
expect(await col(db, "ts2", "decision_timestamp")).toBe(iso);
expect(await col(db, "ts2", "outcome_timestamp")).toBe(iso);
expect(await col(db, "ts2", "sent_at")).toBe(iso);
});

it("#10028: an over-length (65+ char) timestamp is stored NULL while the event is still accepted", async () => {
const db = makeDb();
// A parseable ISO prefix but past MAX_TIMESTAMP_CHARS (64): the length cap rejects it before Date.parse.
expect(await ingest(db, [ev({ pr_hash: "ts3", decision_timestamp: `2026-07-30T12:00:00.000Z${"0".repeat(50)}` })])).toEqual({ accepted: 1 });
expect(await col(db, "ts3", "decision_timestamp")).toBeNull();
});

it("#10028 REGRESSION: a malformed decision_timestamp must not silently drop the signal from the public trend", async () => {
const db = makeDb();
await ingest(db, [ev({ pr_hash: "ts4", decision_timestamp: "unknown" })]);
// COALESCE(decision_timestamp, received_at) must fall back to the server clock and parse to a finite
// instant, so substr(...,1,10) is a real day bucket and the signal still counts toward the trend.
const coalesced = await col(db, "ts4", "COALESCE(decision_timestamp, received_at)");
expect(typeof coalesced).toBe("string");
expect(Number.isFinite(Date.parse(String(coalesced)))).toBe(true);
});
});

describe("handleOrbIngest() health ping (#4933)", () => {
Expand Down