Skip to content

Commit 5e25eac

Browse files
authored
Merge pull request #37 from webstackdev/feature/observability-for-trend-pipeline
observability and metrics
2 parents 71a6e36 + 23dc22a commit 5e25eac

23 files changed

Lines changed: 1213 additions & 2 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ ANYMAIL_WEBHOOK_SECRET=
5959

6060
LOG_LEVEL=INFO
6161

62+
# Bearer token required to access the Prometheus-style /metrics endpoint.
63+
METRICS_TOKEN=
64+
6265
CELERY_TASK_ALWAYS_EAGER=false
6366

6467
DJANGO_SUPERUSER_USERNAME=admin

core/settings_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ class CoreSettings(Protocol):
55
BLUESKY_CREDENTIALS_ENCRYPTION_KEY: str
66
CELERY_TASK_ALWAYS_EAGER: bool
77
DEFAULT_FROM_EMAIL: str
8+
METRICS_TOKEN: str
89
NEWSLETTER_API_BASE_URL: str
910
QDRANT_URL: str
1011
EMBEDDING_MODEL: str

deploy/helm/newsletter-maker/templates/secret.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ stringData:
1010
DATABASE_URL: {{ include "newsletter-maker.databaseUrl" . | quote }}
1111
POSTGRES_PASSWORD: {{ .Values.postgres.password | quote }}
1212
BLUESKY_CREDENTIALS_ENCRYPTION_KEY: {{ .Values.secrets.blueskyCredentialsEncryptionKey | quote }}
13+
METRICS_TOKEN: {{ .Values.secrets.metricsToken | quote }}
1314
OPENROUTER_API_KEY: {{ .Values.secrets.openrouterApiKey | quote }}
1415
REDDIT_CLIENT_ID: {{ .Values.secrets.redditClientId | quote }}
1516
REDDIT_CLIENT_SECRET: {{ .Values.secrets.redditClientSecret | quote }}

deploy/helm/newsletter-maker/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ env:
4040
secrets:
4141
secretKey: change-me
4242
blueskyCredentialsEncryptionKey: ""
43+
metricsToken: ""
4344
openrouterApiKey: ""
4445
redditClientId: ""
4546
redditClientSecret: ""

frontend/src/app/admin/health/page.test.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
SourceDiversitySnapshot,
1111
TopicCentroidObservabilitySummary,
1212
TopicCentroidSnapshot,
13+
TrendTaskRun,
14+
TrendTaskRunObservabilitySummary,
1315
} from "@/lib/types"
1416

1517
const {
@@ -18,17 +20,21 @@ const {
1820
getProjectSourceDiversitySnapshotsMock,
1921
getProjectSourceDiversitySummaryMock,
2022
getProjectSourceConfigsMock,
23+
getProjectTrendTaskRunsMock,
2124
getProjectTopicCentroidSnapshotsMock,
2225
getProjectTopicCentroidSummaryMock,
26+
getProjectTrendTaskRunSummaryMock,
2327
selectProjectMock,
2428
} = vi.hoisted(() => ({
2529
getProjectIngestionRunsMock: vi.fn(),
2630
getProjectsMock: vi.fn(),
2731
getProjectSourceDiversitySnapshotsMock: vi.fn(),
2832
getProjectSourceDiversitySummaryMock: vi.fn(),
2933
getProjectSourceConfigsMock: vi.fn(),
34+
getProjectTrendTaskRunsMock: vi.fn(),
3035
getProjectTopicCentroidSnapshotsMock: vi.fn(),
3136
getProjectTopicCentroidSummaryMock: vi.fn(),
37+
getProjectTrendTaskRunSummaryMock: vi.fn(),
3238
selectProjectMock: vi.fn(),
3339
}))
3440

@@ -70,8 +76,10 @@ vi.mock("@/lib/api", () => ({
7076
getProjectSourceDiversitySnapshots: getProjectSourceDiversitySnapshotsMock,
7177
getProjectSourceDiversitySummary: getProjectSourceDiversitySummaryMock,
7278
getProjectSourceConfigs: getProjectSourceConfigsMock,
79+
getProjectTrendTaskRuns: getProjectTrendTaskRunsMock,
7380
getProjectTopicCentroidSnapshots: getProjectTopicCentroidSnapshotsMock,
7481
getProjectTopicCentroidSummary: getProjectTopicCentroidSummaryMock,
82+
getProjectTrendTaskRunSummary: getProjectTrendTaskRunSummaryMock,
7583
}))
7684

7785
vi.mock("@/lib/view-helpers", async () => {
@@ -200,6 +208,42 @@ function createSourceDiversitySummary(
200208
}
201209
}
202210

211+
function createTrendTaskRun(
212+
overrides: Partial<TrendTaskRun> = {},
213+
): TrendTaskRun {
214+
return {
215+
id: 41,
216+
project: 1,
217+
task_name: "recompute_topic_centroid",
218+
task_run_id: "95ae5b14-5d7d-498e-9adc-1dbaab4dd4b8",
219+
status: "completed",
220+
started_at: "2026-04-28T08:00:00Z",
221+
finished_at: "2026-04-28T08:00:01Z",
222+
latency_ms: 523,
223+
error_message: "",
224+
summary: {
225+
project_id: 1,
226+
feedback_count: 12,
227+
upvote_count: 10,
228+
downvote_count: 2,
229+
centroid_active: true,
230+
},
231+
...overrides,
232+
}
233+
}
234+
235+
function createTrendTaskRunSummary(
236+
overrides: Partial<TrendTaskRunObservabilitySummary> = {},
237+
): TrendTaskRunObservabilitySummary {
238+
return {
239+
project: 1,
240+
run_count: 0,
241+
failed_run_count: 0,
242+
latest_runs: [],
243+
...overrides,
244+
}
245+
}
246+
203247
async function loadHealthPageModule() {
204248
return import("./page")
205249
}
@@ -288,8 +332,10 @@ describe("HealthPage", () => {
288332
getProjectIngestionRunsMock.mockReset()
289333
getProjectSourceDiversitySnapshotsMock.mockReset()
290334
getProjectSourceDiversitySummaryMock.mockReset()
335+
getProjectTrendTaskRunsMock.mockReset()
291336
getProjectTopicCentroidSnapshotsMock.mockReset()
292337
getProjectTopicCentroidSummaryMock.mockReset()
338+
getProjectTrendTaskRunSummaryMock.mockReset()
293339
selectProjectMock.mockReset()
294340

295341
getProjectsMock.mockResolvedValue([defaultProject])
@@ -299,10 +345,14 @@ describe("HealthPage", () => {
299345
getProjectSourceDiversitySummaryMock.mockResolvedValue(
300346
createSourceDiversitySummary(),
301347
)
348+
getProjectTrendTaskRunsMock.mockResolvedValue([])
302349
getProjectTopicCentroidSnapshotsMock.mockResolvedValue([])
303350
getProjectTopicCentroidSummaryMock.mockResolvedValue(
304351
createTopicCentroidSummary(),
305352
)
353+
getProjectTrendTaskRunSummaryMock.mockResolvedValue(
354+
createTrendTaskRunSummary(),
355+
)
306356
selectProjectMock.mockImplementation((projects: Project[]) => {
307357
return projects[0] ?? null
308358
})
@@ -325,8 +375,10 @@ describe("HealthPage", () => {
325375
expect(getProjectIngestionRunsMock).not.toHaveBeenCalled()
326376
expect(getProjectSourceDiversitySnapshotsMock).not.toHaveBeenCalled()
327377
expect(getProjectSourceDiversitySummaryMock).not.toHaveBeenCalled()
378+
expect(getProjectTrendTaskRunsMock).not.toHaveBeenCalled()
328379
expect(getProjectTopicCentroidSnapshotsMock).not.toHaveBeenCalled()
329380
expect(getProjectTopicCentroidSummaryMock).not.toHaveBeenCalled()
381+
expect(getProjectTrendTaskRunSummaryMock).not.toHaveBeenCalled()
330382
})
331383

332384
it("renders an empty source-configurations row when the project has no sources", async () => {
@@ -341,12 +393,20 @@ describe("HealthPage", () => {
341393
expect(
342394
screen.getByText("No source configurations exist for this project yet."),
343395
).toBeInTheDocument()
396+
expect(
397+
screen.getByText("No trend pipeline runs have been persisted for this project yet."),
398+
).toBeInTheDocument()
399+
expect(
400+
screen.getByText("No trend task run history exists for this project yet."),
401+
).toBeInTheDocument()
344402
expect(getProjectSourceConfigsMock).toHaveBeenCalledWith(1)
345403
expect(getProjectIngestionRunsMock).toHaveBeenCalledWith(1)
346404
expect(getProjectSourceDiversitySnapshotsMock).toHaveBeenCalledWith(1)
347405
expect(getProjectSourceDiversitySummaryMock).toHaveBeenCalledWith(1)
406+
expect(getProjectTrendTaskRunsMock).toHaveBeenCalledWith(1)
348407
expect(getProjectTopicCentroidSnapshotsMock).toHaveBeenCalledWith(1)
349408
expect(getProjectTopicCentroidSummaryMock).toHaveBeenCalledWith(1)
409+
expect(getProjectTrendTaskRunSummaryMock).toHaveBeenCalledWith(1)
350410
})
351411

352412
it("shows a no-runs message for sources without ingestion history", async () => {
@@ -519,4 +579,65 @@ describe("HealthPage", () => {
519579
expect(screen.getByText("View raw breakdown JSON")).toBeInTheDocument()
520580
expect(screen.getByLabelText("Source diversity trend")).toBeInTheDocument()
521581
})
582+
583+
it("renders the latest trend pipeline task runs", async () => {
584+
getProjectTrendTaskRunsMock.mockResolvedValue([
585+
createTrendTaskRun({
586+
id: 43,
587+
task_name: "generate_theme_suggestions",
588+
started_at: "2026-04-28T08:20:00Z",
589+
finished_at: "2026-04-28T08:20:01Z",
590+
latency_ms: 1480,
591+
status: "failed",
592+
error_message: "OpenRouter timeout",
593+
summary: { project_id: 1, created: 0, updated: 0, skipped: 2 },
594+
}),
595+
createTrendTaskRun({
596+
id: 41,
597+
started_at: "2026-04-28T08:00:00Z",
598+
finished_at: "2026-04-28T08:00:01Z",
599+
}),
600+
])
601+
getProjectTrendTaskRunSummaryMock.mockResolvedValue(
602+
createTrendTaskRunSummary({
603+
run_count: 8,
604+
failed_run_count: 1,
605+
latest_runs: [
606+
createTrendTaskRun(),
607+
createTrendTaskRun({
608+
id: 42,
609+
task_name: "generate_theme_suggestions",
610+
status: "failed",
611+
latency_ms: 1480,
612+
error_message: "OpenRouter timeout",
613+
summary: { project_id: 1, created: 0, updated: 0, skipped: 2 },
614+
}),
615+
],
616+
}),
617+
)
618+
619+
await renderHealthPage()
620+
621+
expect(
622+
screen.getByRole("heading", { level: 2, name: "Trend pipeline runs" }),
623+
).toBeInTheDocument()
624+
expect(screen.getAllByText("Topic centroid").length).toBeGreaterThan(0)
625+
expect(screen.getAllByText("Theme suggestions").length).toBeGreaterThan(0)
626+
expect(
627+
screen.getAllByText("feedback 12 • upvotes 10 • downvotes 2").length,
628+
).toBeGreaterThan(0)
629+
expect(screen.getAllByText("OpenRouter timeout").length).toBeGreaterThan(0)
630+
expect(screen.getAllByText("1.5s").length).toBeGreaterThan(0)
631+
expect(screen.getByText("8")).toBeInTheDocument()
632+
expect(
633+
screen.getByRole("link", { name: "Open trend task run history" }),
634+
).toHaveAttribute(
635+
"href",
636+
"/admin/health?project=1#trend-task-run-history",
637+
)
638+
expect(
639+
screen.getByRole("heading", { level: 2, name: "Trend task run history" }),
640+
).toBeInTheDocument()
641+
expect(screen.getByText("Showing 2 of 8 runs")).toBeInTheDocument()
642+
})
522643
})

0 commit comments

Comments
 (0)