Created
April 27, 2026 11:36
-
-
Save ItzaMi/cfc54c574994c7a0246d90c2416fb65f to your computer and use it in GitHub Desktop.
RevenueCat Analytics & Metrics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // RevenueCat v2 analytics fetcher. | |
| // Usage: RC_API_KEY=sk_v2_... RC_PROJECT_ID=proj_... node rc-analytics.mjs [lifetime|28d|90d] | |
| // Defaults to 28d. Prints the overview snapshot plus a set of period-aware charts. | |
| const PERIODS = { | |
| lifetime: { days: null, label: "Lifetime" }, | |
| "28d": { days: 28, label: "Last 28 days" }, | |
| "90d": { days: 90, label: "Last 90 days" }, | |
| }; | |
| const arg = (process.argv[2] || "28d").toLowerCase(); | |
| const period = PERIODS[arg]; | |
| if (!period) { | |
| console.error(`Unknown period "${arg}". Use: ${Object.keys(PERIODS).join(", ")}`); | |
| process.exit(1); | |
| } | |
| const { RC_API_KEY, RC_PROJECT_ID } = process.env; | |
| if (!RC_API_KEY || !RC_PROJECT_ID) { | |
| console.error("Set RC_API_KEY (v2 secret key) and RC_PROJECT_ID in env."); | |
| process.exit(1); | |
| } | |
| const today = new Date(); | |
| const endDate = today.toISOString().slice(0, 10); | |
| const startDate = period.days | |
| ? new Date(today.getTime() - period.days * 86_400_000).toISOString().slice(0, 10) | |
| : "2010-01-01"; | |
| const BASE = `https://api.revenuecat.com/v2/projects/${RC_PROJECT_ID}`; | |
| const headers = { | |
| Authorization: `Bearer ${RC_API_KEY}`, | |
| Accept: "application/json", | |
| }; | |
| async function get(path, params = {}) { | |
| const url = new URL(`${BASE}${path}`); | |
| for (const [k, v] of Object.entries(params)) { | |
| if (v != null) url.searchParams.set(k, String(v)); | |
| } | |
| const res = await fetch(url, { headers }); | |
| if (!res.ok) throw new Error(`${res.status} ${path}: ${(await res.text()).slice(0, 200)}`); | |
| return res.json(); | |
| } | |
| // Charts available in RC v2: actives, actives_movement, actives_new, arr, churn, | |
| // conversion_to_paying, customers_new, ltv_per_customer, ltv_per_paying_customer, | |
| // mrr, revenue, etc. Edit this list to taste. | |
| const CHARTS = ["revenue", "mrr", "arr", "actives", "customers_new", "churn", "conversion_to_paying"]; | |
| console.log(`\nRevenueCat — ${period.label} (${startDate} → ${endDate})\n`); | |
| // 1. Overview snapshot (each metric carries its own period: P28D / LIFETIME / etc.) | |
| try { | |
| const overview = await get("/metrics/overview"); | |
| const items = overview.items ?? overview.metrics ?? []; | |
| if (items.length) { | |
| console.log("Snapshot"); | |
| const pad = Math.max(...items.map((m) => (m.name ?? m.id).length)); | |
| for (const m of items) { | |
| const tag = m.period ? ` (${m.period})` : ""; | |
| console.log(` ${(m.name ?? m.id).padEnd(pad)} ${formatValue(m, overview.currency)}${tag}`); | |
| } | |
| console.log(); | |
| } | |
| } catch (e) { | |
| console.error(`overview failed: ${e.message}\n`); | |
| } | |
| // 2. Chart aggregates over the selected window. | |
| console.log("Charts"); | |
| const chartPad = Math.max(...CHARTS.map((c) => c.length)); | |
| const params = { start_date: startDate, end_date: endDate }; | |
| const results = await Promise.allSettled( | |
| CHARTS.map((chart) => get(`/charts/${chart}`, params).then((r) => [chart, r])), | |
| ); | |
| for (let i = 0; i < results.length; i++) { | |
| const r = results[i]; | |
| const name = CHARTS[i]; | |
| if (r.status === "rejected") { | |
| console.log(` ${name.padEnd(chartPad)} error: ${r.reason.message.slice(0, 100)}`); | |
| continue; | |
| } | |
| console.log(` ${name.padEnd(chartPad)} ${summarize(r.value[1])}`); | |
| } | |
| console.log(); | |
| function formatValue(m, currency) { | |
| const v = m.value; | |
| if (m.unit === "currency") return `${v}${currency ? " " + currency : ""}`; | |
| if (m.unit === "percentage" || m.unit === "%") return `${v}%`; | |
| return String(v); | |
| } | |
| // Best-effort summary across the various chart response shapes RC returns. | |
| function summarize(chart) { | |
| const series = chart.series ?? chart.data ?? chart.values ?? null; | |
| if (Array.isArray(series) && series.length) { | |
| const last = series[series.length - 1]; | |
| if (typeof last === "number") return String(last); | |
| if (last && typeof last === "object") { | |
| const v = last.value ?? last.y ?? last.total ?? last.aggregate; | |
| if (v != null) return String(v); | |
| } | |
| } | |
| if (chart.summary?.value != null) return String(chart.summary.value); | |
| return JSON.stringify(chart).slice(0, 120); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment