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
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:
143
apps/analytics-mailer/src/index.ts
Normal file
143
apps/analytics-mailer/src/index.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user