mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat(stats): count emails forwarded to the catch-all fallback
Adds an emails_forwarded counter (a subset of emails_rejected) bumped on a successful FALLBACK_FORWARD_ADDRESS forward. Dropped = rejected − forwarded. Surfaced in the /api/v1/stats response and the public status page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ const EMPTY_COUNTERS: Counters = {
|
||||
feeds_deleted: 0,
|
||||
emails_received: 0,
|
||||
emails_rejected: 0,
|
||||
emails_forwarded: 0,
|
||||
unsubscribes_sent: 0,
|
||||
};
|
||||
|
||||
@@ -41,6 +42,7 @@ export async function bumpCounters(
|
||||
current.feeds_deleted += changes.feeds_deleted ?? 0;
|
||||
current.emails_received += changes.emails_received ?? 0;
|
||||
current.emails_rejected += changes.emails_rejected ?? 0;
|
||||
current.emails_forwarded += changes.emails_forwarded ?? 0;
|
||||
current.unsubscribes_sent += changes.unsubscribes_sent ?? 0;
|
||||
if (changes.last_email_at) current.last_email_at = changes.last_email_at;
|
||||
if (changes.last_feed_created_at)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest";
|
||||
import "../test/setup";
|
||||
import { createMockEnv } from "../test/setup";
|
||||
import { handleCloudflareEmail } from "./cloudflare-email";
|
||||
import { getCounters } from "../application/stats";
|
||||
|
||||
const VALID_FEED_ID = "apple.mountain.42";
|
||||
const DOMAIN = "test.getmynews.app";
|
||||
@@ -245,5 +246,53 @@ describe("handleCloudflareEmail", () => {
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("increments the emails_forwarded counter on a successful forward", async () => {
|
||||
const { forward } = spyForward();
|
||||
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
|
||||
|
||||
await handleCloudflareEmail(
|
||||
makeMessage({ forward }),
|
||||
env as any,
|
||||
{ waitUntil: () => {} } as any,
|
||||
);
|
||||
|
||||
const counters = await getCounters(env.EMAIL_STORAGE as any);
|
||||
expect(counters.emails_forwarded).toBe(1);
|
||||
});
|
||||
|
||||
it("does not increment emails_forwarded when the forward fails", async () => {
|
||||
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
|
||||
const forward = async () => {
|
||||
throw new Error("destination address not verified");
|
||||
};
|
||||
|
||||
await handleCloudflareEmail(
|
||||
makeMessage({ forward }),
|
||||
env as any,
|
||||
{ waitUntil: () => {} } as any,
|
||||
);
|
||||
|
||||
const counters = await getCounters(env.EMAIL_STORAGE as any);
|
||||
expect(counters.emails_forwarded).toBe(0);
|
||||
});
|
||||
|
||||
it("does not increment emails_forwarded for dropped reasons", async () => {
|
||||
const { forward } = spyForward();
|
||||
env.FALLBACK_FORWARD_ADDRESS = FALLBACK;
|
||||
await env.EMAIL_STORAGE.put(
|
||||
`feed:${VALID_FEED_ID}:config`,
|
||||
JSON.stringify({ expires_at: Date.now() - 1000 }),
|
||||
);
|
||||
|
||||
await handleCloudflareEmail(
|
||||
makeMessage({ forward }),
|
||||
env as any,
|
||||
{ waitUntil: () => {} } as any,
|
||||
);
|
||||
|
||||
const counters = await getCounters(env.EMAIL_STORAGE as any);
|
||||
expect(counters.emails_forwarded).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
RawAttachment,
|
||||
IngestRejectionReason,
|
||||
} from "../application/email-processor";
|
||||
import { bumpCounters } from "../application/stats";
|
||||
import { normalizeCid } from "../infrastructure/html-processor";
|
||||
import { logger } from "./logger";
|
||||
|
||||
@@ -80,6 +81,9 @@ async function maybeForwardFallback(
|
||||
|
||||
try {
|
||||
await message.forward(fallback);
|
||||
// Counted as a subset of emails_rejected (already bumped in processEmail);
|
||||
// the dropped count is derived as emails_rejected − emails_forwarded.
|
||||
await bumpCounters(env.EMAIL_STORAGE, { emails_forwarded: 1 });
|
||||
} catch (error) {
|
||||
logger.warn("Fallback forward failed", {
|
||||
to: message.to,
|
||||
|
||||
@@ -14,6 +14,7 @@ describe("CountersRepository", () => {
|
||||
feeds_deleted: 0,
|
||||
emails_received: 2,
|
||||
emails_rejected: 0,
|
||||
emails_forwarded: 0,
|
||||
unsubscribes_sent: 0,
|
||||
});
|
||||
expect(await repo.getRaw()).toMatchObject({ emails_received: 2 });
|
||||
|
||||
@@ -138,6 +138,7 @@ export const StatsSchema = z
|
||||
feeds_deleted: z.number(),
|
||||
emails_received: z.number(),
|
||||
emails_rejected: z.number(),
|
||||
emails_forwarded: z.number(),
|
||||
unsubscribes_sent: z.number(),
|
||||
active_feeds: z.number(),
|
||||
websub_subscriptions_active: z.number(),
|
||||
|
||||
@@ -162,6 +162,10 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
|
||||
value={stats.emails_rejected}
|
||||
tone="danger"
|
||||
/>
|
||||
<Stat
|
||||
label="Forwarded (catch-all)"
|
||||
value={stats.emails_forwarded}
|
||||
/>
|
||||
<Stat
|
||||
label="Acceptance rate"
|
||||
value={acceptanceRate}
|
||||
|
||||
@@ -89,6 +89,9 @@ export interface Counters {
|
||||
feeds_deleted: number;
|
||||
emails_received: number;
|
||||
emails_rejected: number;
|
||||
// Subset of emails_rejected: non-feed mail forwarded to FALLBACK_FORWARD_ADDRESS
|
||||
// instead of dropped. Dropped count = emails_rejected − emails_forwarded.
|
||||
emails_forwarded: number;
|
||||
unsubscribes_sent: number;
|
||||
last_email_at?: string; // ISO 8601
|
||||
last_feed_created_at?: string; // ISO 8601
|
||||
|
||||
Reference in New Issue
Block a user