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,41 @@
import { render } from "@react-email/components";
import { describe, it, expect } from "vitest";
import { AnalyticsTemplate, PeriodData } from "./AnalyticsTemplate";
import * as React from "react";
describe("AnalyticsTemplate", () => {
it("renders entry and exit pages correctly", async () => {
const period: PeriodData = {
label: "Test Period",
visitors: 100,
visits: 120,
views: 500,
bounceRate: 50,
visitDuration: "2m",
topPages: [{ url: "/home", views: 50 }],
topReferrers: [{ url: "google.com", visitors: 30 }],
// These fields will cause a TS error because they don't exist yet,
// but we force it to see it fail.
topEntryPages: [{ url: "/entry", visitors: 40 }],
topExitPages: [{ url: "/exit", visitors: 20 }],
chartData: [],
} as any; // Cast as any to bypass TS compilation before we update the type
const html = await render(
<AnalyticsTemplate
greeting="Hallo Kunde,"
domain="example.com"
periods={[period]}
/>
);
// Assert that the new sections for entry and exit pages exist.
expect(html).toContain("Häufigste Einstiege");
expect(html).toContain("Häufigste Ausstiege");
expect(html).toContain("/entry");
expect(html).toContain("/exit");
// Assert that the badges have updated line heights
expect(html).toContain('line-height:1.5');
});
});

View File

@@ -10,6 +10,8 @@ export interface PeriodData {
visitDuration: string;
topPages: { url: string; views: number }[];
topReferrers: { url: string; visitors: number }[];
topEntryPages: { url: string; visitors: number }[];
topExitPages: { url: string; visitors: number }[];
chartData: { label: string; value: number }[];
}
@@ -135,8 +137,8 @@ export const AnalyticsTemplate = ({
<tr>
<td width="48%" valign="top">
<div style={listContainer}>
<Text style={listTitle}>Seiten</Text>
<Text style={listSubtitle}>Die beliebtesten Unterseiten sortiert nach Aufrufen.</Text>
<Text style={listTitle}>Beliebteste Seiten</Text>
<Text style={listSubtitle}>Was schauen sich Ihre Besucher am häufigsten an?</Text>
{period.topPages.map((p, idx) => (
<Row key={idx} style={listRow}>
<Column><Text style={listText}>{p.url}</Text></Column>
@@ -148,8 +150,8 @@ export const AnalyticsTemplate = ({
<td width="4%" />
<td width="48%" valign="top">
<div style={listContainer}>
<Text style={listTitle}>Quellen</Text>
<Text style={listSubtitle}>Woher Ihre Besucher auf die Website kommen.</Text>
<Text style={listTitle}>Woher kommen Ihre Besucher?</Text>
<Text style={listSubtitle}>Wie wurden Sie gefunden? (z.B. Google, Direkt)</Text>
{period.topReferrers.map((r, idx) => (
<Row key={idx} style={listRow}>
<Column><Text style={listText}>{r.url || "Direktaufruf"}</Text></Column>
@@ -161,6 +163,38 @@ export const AnalyticsTemplate = ({
</tr>
</tbody>
</table>
<table width="100%" border={0} cellPadding="0" cellSpacing="0" style={tablesContainer}>
<tbody>
<tr>
<td width="48%" valign="top">
<div style={listContainer}>
<Text style={listTitle}>Häufigste Einstiege</Text>
<Text style={listSubtitle}>Auf welchen Seiten starten Ihre Besucher?</Text>
{period.topEntryPages?.map((p, idx) => (
<Row key={idx} style={listRow}>
<Column><Text style={listText}>{p.url}</Text></Column>
<Column align="right"><Text style={listTextBold}>{p.visitors.toLocaleString("de-DE")}</Text></Column>
</Row>
))}
</div>
</td>
<td width="4%" />
<td width="48%" valign="top">
<div style={listContainer}>
<Text style={listTitle}>Häufigste Ausstiege</Text>
<Text style={listSubtitle}>Wo verlassen Besucher Ihre Website am häufigsten?</Text>
{period.topExitPages?.map((p, idx) => (
<Row key={idx} style={listRow}>
<Column><Text style={listText}>{p.url}</Text></Column>
<Column align="right"><Text style={listTextBold}>{p.visitors.toLocaleString("de-DE")}</Text></Column>
</Row>
))}
</div>
</td>
</tr>
</tbody>
</table>
<hr style={divider} />
</Section>
))}
@@ -332,6 +366,7 @@ const badgePositive = {
fontWeight: "bold",
color: "#16a34a",
margin: "4px 0 0 0",
lineHeight: "1.5",
};
const badgeNeutral = {
@@ -339,4 +374,5 @@ const badgeNeutral = {
fontWeight: "bold",
color: "#6b7280",
margin: "4px 0 0 0",
lineHeight: "1.5",
};