feat: setup analytics-mailer with gitea cron pipeline for e-tib and klz
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m42s
Monorepo Pipeline / 🧪 Test (push) Successful in 56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
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
Monthly Analytics Mailer / 📊 Send Monthly Reports (push) Failing after 59s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s

This commit is contained in:
2026-06-21 20:52:31 +02:00
parent 985e532c61
commit 7a9dd06fc6
8 changed files with 509 additions and 7 deletions

View File

@@ -0,0 +1,143 @@
import fs from "fs/promises";
import path from "path";
import axios from "axios";
import { render } from "@mintel/mail";
import { MonthlyAnalyticsTemplate } from "@mintel/mail";
import Mailgun from "mailgun.js";
import formData from "form-data";
import * as dotenv from "dotenv";
dotenv.config();
const UMAMI_BASE_URL = process.env.UMAMI_BASE_URL || "https://umami.infra.mintel.me";
const UMAMI_USERNAME = process.env.UMAMI_USERNAME;
const UMAMI_PASSWORD = process.env.UMAMI_PASSWORD;
const UMAMI_API_KEY = process.env.UMAMI_API_KEY;
const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY;
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN;
const MAILGUN_SENDER = process.env.MAILGUN_SENDER || "reports@mintel.me";
const MAILGUN_URL = process.env.MAILGUN_URL || "https://api.eu.mailgun.net";
interface ConfigClient {
websiteId: string;
domain: string;
clientEmail: string;
clientName: string;
}
async function getUmamiToken(): Promise<string> {
if (UMAMI_API_KEY) {
return UMAMI_API_KEY;
}
if (!UMAMI_USERNAME || !UMAMI_PASSWORD) {
throw new Error("Missing UMAMI_USERNAME and UMAMI_PASSWORD or UMAMI_API_KEY");
}
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
username: UMAMI_USERNAME,
password: UMAMI_PASSWORD,
});
return res.data.token;
}
async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number) {
const [statsRes, pagesRes, refRes] = await Promise.all([
api.get(`/websites/${websiteId}/stats`, { params: { startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "url", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 3, startAt, endAt } }),
]);
const stats = statsRes.data;
const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y }));
const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y }));
return {
totalVisitors: stats.visitors?.value || 0,
totalPageviews: stats.pageviews?.value || 0,
topPages,
topReferrers,
};
}
async function main() {
const isDryRun = process.argv.includes("--dry-run");
// Load config
const configPath = path.resolve(process.cwd(), "config.json");
const configRaw = await fs.readFile(configPath, "utf-8");
const clients: ConfigClient[] = JSON.parse(configRaw);
// Setup Umami API
const token = await getUmamiToken();
const headers = UMAMI_API_KEY ? { "x-umami-api-key": token } : { Authorization: `Bearer ${token}` };
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers });
// Setup Mailgun
const mailgun = new Mailgun(formData);
const mg = mailgun.client({ username: "api", key: MAILGUN_API_KEY!, url: MAILGUN_URL });
// Time range calculation (previous month)
const now = new Date();
// Ensure we get the correct previous month relative to current local time, but use UTC boundary or local? Umami uses ms timestamps.
// Standard way: First day of previous month, last day of previous month.
const startOfPrevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const endOfPrevMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
const startAt = startOfPrevMonth.getTime();
const endAt = endOfPrevMonth.getTime();
const monthName = startOfPrevMonth.toLocaleString("de-DE", { month: "long" });
const year = startOfPrevMonth.getFullYear().toString();
console.log(`Generating reports for ${monthName} ${year}...`);
for (const client of clients) {
if (client.websiteId === "replace-with-umami-website-id") continue;
console.log(`Processing ${client.domain} (${client.clientName})...`);
try {
const data = await fetchUmamiData(api, client.websiteId, startAt, endAt);
const html = await render(MonthlyAnalyticsTemplate({
clientName: client.clientName,
domain: client.domain,
month: monthName,
year,
totalVisitors: data.totalVisitors,
totalPageviews: data.totalPageviews,
topPages: data.topPages,
topReferrers: data.topReferrers,
}));
const subject = `Ihr Website-Report für ${monthName} ${year} - ${client.domain}`;
if (isDryRun) {
console.log(`[DRY-RUN] Would send email to ${client.clientEmail} 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}`);
} else {
if (!MAILGUN_API_KEY) {
throw new Error("MAILGUN_API_KEY is missing for live run.");
}
await mg.messages.create(MAILGUN_DOMAIN!, {
from: MAILGUN_SENDER,
to: [client.clientEmail],
subject,
html,
});
console.log(`Successfully sent to ${client.clientEmail}`);
}
} catch (e: any) {
console.error(`Failed to process client ${client.domain}:`, e.response?.data || e.message);
}
}
console.log("Done.");
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});