Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m24s
Monorepo Pipeline / 🧪 Test (push) Successful in 41s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m4s
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
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 7s
179 lines
6.3 KiB
TypeScript
179 lines
6.3 KiB
TypeScript
import fs from "fs/promises";
|
|
import path from "path";
|
|
import axios from "axios";
|
|
import { render } from "@mintel/mail";
|
|
import { AnalyticsTemplate } from "@mintel/mail";
|
|
import { getLast7Days, getThisMonth, getThisYear } from "./utils/time-calculator";
|
|
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;
|
|
greeting?: string;
|
|
interval?: "weekly" | "monthly";
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
import https from "https";
|
|
|
|
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([
|
|
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: [] } })),
|
|
]);
|
|
|
|
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 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;
|
|
const views = typeof stats.pageviews === "object" ? stats.pageviews?.value || 0 : stats.pageviews || 0;
|
|
const bounces = typeof stats.bounces === "object" ? stats.bounces?.value || 0 : stats.bounces || 0;
|
|
const totaltime = typeof stats.totaltime === "object" ? stats.totaltime?.value || 0 : stats.totaltime || 0;
|
|
|
|
const bounceRate = visits > 0 ? Math.round((bounces / visits) * 100) : 0;
|
|
|
|
let visitDuration = "0s";
|
|
if (visits > 0) {
|
|
const avgSeconds = Math.round(totaltime / visits);
|
|
if (avgSeconds < 60) {
|
|
visitDuration = `${avgSeconds}s`;
|
|
} else {
|
|
visitDuration = `${Math.floor(avgSeconds / 60)}m ${avgSeconds % 60}s`;
|
|
}
|
|
}
|
|
|
|
const chartData = (viewsRes.data?.pageviews || []).map((pv: any) => ({
|
|
label: pv.x,
|
|
value: pv.y || 0
|
|
}));
|
|
|
|
return {
|
|
label,
|
|
visitors,
|
|
visits,
|
|
views,
|
|
bounceRate,
|
|
visitDuration,
|
|
topPages,
|
|
topReferrers,
|
|
chartData,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const isDryRun = process.argv.includes("--dry-run");
|
|
const intervalArg = process.argv.find(a => a.startsWith("--interval="));
|
|
const runInterval = intervalArg ? intervalArg.split("=")[1] : "monthly";
|
|
|
|
if (runInterval !== "weekly" && runInterval !== "monthly") {
|
|
throw new Error("Invalid interval. Use --interval=weekly or --interval=monthly");
|
|
}
|
|
|
|
// 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 httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers, httpsAgent });
|
|
|
|
// Setup Mailgun
|
|
const mailgun = new Mailgun(formData);
|
|
const mg = mailgun.client({ username: "api", key: MAILGUN_API_KEY || "dummy", url: MAILGUN_URL });
|
|
|
|
const timeRanges = [getLast7Days(), getThisMonth(), getThisYear()];
|
|
|
|
console.log(`Generating reports...`);
|
|
|
|
for (const client of clients) {
|
|
if (client.websiteId === "replace-with-umami-website-id") continue;
|
|
const clientInterval = client.interval || "monthly";
|
|
if (clientInterval !== runInterval) {
|
|
console.log(`Skipping ${client.domain} (runs ${clientInterval}, currently executing ${runInterval})`);
|
|
continue;
|
|
}
|
|
|
|
console.log(`Processing ${client.domain} (${client.clientName})...`);
|
|
|
|
try {
|
|
const periods = await Promise.all(
|
|
timeRanges.map((tr) => fetchUmamiData(api, client.websiteId, tr.startAt, tr.endAt, tr.label))
|
|
);
|
|
|
|
const html = await render(AnalyticsTemplate({
|
|
greeting: client.greeting || `Hallo ${client.clientName},`,
|
|
domain: client.domain,
|
|
periods,
|
|
}));
|
|
|
|
const subject = `Performance-Report - ${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);
|
|
});
|