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 { 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); });