feat: enhance analytics mailer with umami entry/exit pages and beginner-friendly UI
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m23s
Monorepo Pipeline / 🧪 Test (push) Successful in 41s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m20s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Analytics Mailer / 📊 Send Analytics Reports (push) Successful in 48s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s

This commit is contained in:
2026-07-27 10:43:44 +02:00
parent 9a92874011
commit 24625c20f2
4 changed files with 125 additions and 9 deletions

View File

@@ -0,0 +1,28 @@
import { describe, it, expect, vi } from "vitest";
import { fetchUmamiData } from "./index";
describe("fetchUmamiData", () => {
it("fetches entry and exit pages correctly", async () => {
const apiMock = {
get: vi.fn().mockImplementation((url, config) => {
if (url.includes("/stats")) return Promise.resolve({ data: { visitors: 10, visits: 10, pageviews: 10, bounces: 5, totaltime: 100 } });
if (url.includes("/pageviews")) return Promise.resolve({ data: { pageviews: [] } });
if (url.includes("/metrics")) {
if (config.params.type === "path") return Promise.resolve({ data: [{ x: "/path", y: 5 }] });
if (config.params.type === "referrer") return Promise.resolve({ data: [{ x: "google", y: 3 }] });
if (config.params.type === "entry") return Promise.resolve({ data: [{ x: "/entry", y: 4 }] });
if (config.params.type === "exit") return Promise.resolve({ data: [{ x: "/exit", y: 2 }] });
}
return Promise.resolve({ data: {} });
})
};
const result = await fetchUmamiData(apiMock, "test-id", 0, 1000, "Test");
// We expect the new entry and exit pages to be parsed and returned correctly
expect(result.topEntryPages).toBeDefined();
expect(result.topEntryPages[0]).toEqual({ url: "/entry", visitors: 4 });
expect(result.topExitPages).toBeDefined();
expect(result.topExitPages[0]).toEqual({ url: "/exit", visitors: 2 });
});
});

View File

@@ -48,20 +48,24 @@ async function getUmamiToken(): Promise<string> {
import https from "https";
async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number, label: string) {
export async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number, label: string) {
let unit = "day";
if (label === "Dieses Jahr") unit = "month";
const [statsRes, pagesRes, refRes, viewsRes] = await Promise.all([
const [statsRes, pagesRes, refRes, viewsRes, entryRes, exitRes] = await Promise.all([
api.get(`/websites/${websiteId}/stats`, { params: { startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "path", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/pageviews`, { params: { startAt, endAt, unit, timezone: "Europe/Berlin" } }).catch(() => ({ data: { pageviews: [] } })),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "entry", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "exit", limit: 5, startAt, endAt } }),
]);
const stats = statsRes.data;
const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y || 0 }));
const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y || 0 }));
const topEntryPages = (entryRes.data || []).map((p: any) => ({ url: p.x, visitors: p.y || 0 }));
const topExitPages = (exitRes.data || []).map((p: any) => ({ url: p.x, visitors: p.y || 0 }));
const visitors = typeof stats.visitors === "object" ? stats.visitors?.value || 0 : stats.visitors || 0;
const visits = typeof stats.visits === "object" ? stats.visits?.value || 0 : stats.visits || 0;
@@ -95,6 +99,8 @@ async function fetchUmamiData(api: any, websiteId: string, startAt: number, endA
visitDuration,
topPages,
topReferrers,
topEntryPages,
topExitPages,
chartData,
};
}
@@ -130,7 +136,9 @@ async function main() {
} : undefined,
});
const timeRanges = [getLast7Days(), getThisMonth(), getThisYear()];
const timeRanges = runInterval === "weekly"
? [getLast7Days(), getThisMonth(), getThisYear()]
: [getThisMonth(), getThisYear()];
console.log(`Generating reports...`);
@@ -157,8 +165,12 @@ async function main() {
const subject = `Performance-Report - ${client.domain}`;
const recipients = client.clientEmail.split(',').map(e => e.trim()).filter(e => e.length > 0);
if (isDryRun) {
console.log(`[DRY-RUN] Would send email to ${client.clientEmail} for ${client.domain}`);
for (const recipient of recipients) {
console.log(`[DRY-RUN] Would send individual email to: ${recipient} (for ${client.domain})`);
}
const debugPath = path.resolve(process.cwd(), `dry-run-${client.domain}.html`);
await fs.writeFile(debugPath, html);
console.log(`[DRY-RUN] Saved HTML preview to ${debugPath}`);
@@ -167,7 +179,6 @@ async function main() {
throw new Error("SMTP_HOST is missing for live run.");
}
const recipients = client.clientEmail.split(',').map(e => e.trim()).filter(e => e.length > 0);
for (const recipient of recipients) {
await transporter.sendMail({
from: SMTP_SENDER,