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
19 changes: 19 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2238,6 +2238,25 @@ export async function countRecentDeadLetters(env: Env, sinceIso: string): Promis
return row.count;
}

/** Observability for the DLQ dashboard (#1208): recent dead letters grouped by job type, using the jobType stored
* in each `github_app.dlq_dead_lettered` audit event's metadata. Missing/blank jobType falls back to `unknown`,
* and the returned object's keys are sorted deterministically for stable consumers/tests. */
export async function countRecentDeadLettersByType(env: Env, sinceIso: string): Promise<Record<string, number>> {
const db = getDb(env.DB);
const jobTypeExpr =
sql<string>`coalesce(nullif(trim(cast(json_extract(${auditEvents.metadataJson}, '$.jobType') as text)), ''), 'unknown')`;
const rows = await db
.select({
jobType: jobTypeExpr,
count: sql<number>`count(*)`,
})
.from(auditEvents)
.where(and(eq(auditEvents.eventType, "github_app.dlq_dead_lettered"), gte(auditEvents.createdAt, sinceIso)))
.groupBy(jobTypeExpr)
.orderBy(asc(jobTypeExpr));
return Object.fromEntries(rows.map((row) => [row.jobType, Number(row.count)]));
}

export type PrVisibilitySkipAuditEvent = {
repoFullName: string;
pullNumber: number;
Expand Down
104 changes: 104 additions & 0 deletions test/unit/db-parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import {
claimRegateFanoutSlot,
countRecentDeadLetters,
countRecentDeadLettersByType,
countRecentAuditEventsForActorAndTarget,
getLatestScorePreview,
getRepoAuthorPullRequestHistory,
Expand Down Expand Up @@ -373,6 +374,109 @@ describe("database row parser hardening", () => {
expect(await countRecentDeadLetters(env, "2026-06-24T13:00:00.000Z")).toBe(0); // none after the cutoff → count(*) returns 0
});

it("countRecentDeadLettersByType groups recent dead letters by job type in deterministic key order (#1208)", async () => {
const env = createTestEnv();
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:github-webhook:a",
outcome: "error",
createdAt: "2026-06-24T12:00:00.000Z",
metadata: { jobType: "github-webhook" },
});
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:backfill-repo-segment:b",
outcome: "error",
createdAt: "2026-06-24T10:00:00.000Z",
metadata: { jobType: "backfill-repo-segment" },
});
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:github-webhook:c",
outcome: "error",
createdAt: "2026-06-24T14:00:00.000Z",
metadata: { jobType: "github-webhook" },
});

const counts = await countRecentDeadLettersByType(env, "2026-06-24T09:00:00.000Z");
expect(counts).toEqual({
"backfill-repo-segment": 1,
"github-webhook": 2,
});
expect(Object.keys(counts)).toEqual(["backfill-repo-segment", "github-webhook"]);
});

it("countRecentDeadLettersByType returns a single grouped key when only one job type is present", async () => {
const env = createTestEnv();
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:refresh-registry:a",
outcome: "error",
createdAt: "2026-06-24T10:00:00.000Z",
metadata: { jobType: "refresh-registry" },
});
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:refresh-registry:b",
outcome: "error",
createdAt: "2026-06-24T11:00:00.000Z",
metadata: { jobType: "refresh-registry" },
});

expect(await countRecentDeadLettersByType(env, "2026-06-24T09:00:00.000Z")).toEqual({
"refresh-registry": 2,
});
});

it("countRecentDeadLettersByType returns an empty object when no recent dead letters exist", async () => {
const env = createTestEnv();
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:github-webhook:stale",
outcome: "error",
createdAt: "2026-06-24T08:59:59.000Z",
metadata: { jobType: "github-webhook" },
});
await recordAuditEvent(env, {
eventType: "agent.sweep.regate",
actor: "gittensory",
targetKey: "owner/repo",
outcome: "completed",
createdAt: "2026-06-24T12:00:00.000Z",
});

expect(await countRecentDeadLettersByType(env, "2026-06-24T09:00:00.000Z")).toEqual({});
});

it("countRecentDeadLettersByType falls back missing or blank job types to unknown", async () => {
const env = createTestEnv();
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:unknown:a",
outcome: "error",
createdAt: "2026-06-24T10:00:00.000Z",
});
await recordAuditEvent(env, {
eventType: "github_app.dlq_dead_lettered",
actor: "gittensory",
targetKey: "dlq:unknown:b",
outcome: "error",
createdAt: "2026-06-24T11:00:00.000Z",
metadata: { jobType: " " },
});

expect(await countRecentDeadLettersByType(env, "2026-06-24T09:00:00.000Z")).toEqual({
unknown: 2,
});
});

it("countRecentAuditEventsForActorAndTarget counts events scoped to ONE actor+eventType+targetKey since a cutoff (#2463)", async () => {
const env = createTestEnv();
await recordAuditEvent(env, { eventType: "github_app.review_nag_ping", actor: "chatty", targetKey: "owner/repo#1", outcome: "completed", createdAt: "2026-06-24T10:00:00.000Z" });
Expand Down
Loading