From c4d366c555898baccaa3a46ca695f47daca1e024 Mon Sep 17 00:00:00 2001 From: DragunovX16 <75143900+DragunovX16@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:28:38 +0200 Subject: [PATCH 1/2] feat(dlq): add dead-letter job-type breakdown helper --- src/db/repositories.ts | 19 +++++++ test/unit/db-parsers.test.ts | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/src/db/repositories.ts b/src/db/repositories.ts index ef4dd93a82..c651faf6d5 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -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> { + const db = getDb(env.DB); + const jobTypeExpr = + sql`coalesce(nullif(trim(cast(json_extract(${auditEvents.metadataJson}, '$.jobType') as text)), ''), 'unknown')`; + const rows = await db + .select({ + jobType: jobTypeExpr, + count: sql`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 ?? 0)])); +} + export type PrVisibilitySkipAuditEvent = { repoFullName: string; pullNumber: number; diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 58d729c88f..4a6a7432d2 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { claimRegateFanoutSlot, countRecentDeadLetters, + countRecentDeadLettersByType, countRecentAuditEventsForActorAndTarget, getLatestScorePreview, getRepoAuthorPullRequestHistory, @@ -373,6 +374,101 @@ 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: "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" }); From 144fd355804965098b5374d0ed5f12c75185388b Mon Sep 17 00:00:00 2001 From: DragunovX16 <75143900+DragunovX16@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:54:50 +0200 Subject: [PATCH 2/2] fix(dlq): cover recent dead-letter cutoff branch --- src/db/repositories.ts | 2 +- test/unit/db-parsers.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/db/repositories.ts b/src/db/repositories.ts index c651faf6d5..722f8da4e9 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -2254,7 +2254,7 @@ export async function countRecentDeadLettersByType(env: Env, sinceIso: string): .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 ?? 0)])); + return Object.fromEntries(rows.map((row) => [row.jobType, Number(row.count)])); } export type PrVisibilitySkipAuditEvent = { diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 4a6a7432d2..4ff5bb26e3 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -435,6 +435,14 @@ describe("database row parser hardening", () => { 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",