Skip to content

Instantly share code, notes, and snippets.

@ItzaMi
Created April 27, 2026 11:36
Show Gist options
  • Select an option

  • Save ItzaMi/cfc54c574994c7a0246d90c2416fb65f to your computer and use it in GitHub Desktop.

Select an option

Save ItzaMi/cfc54c574994c7a0246d90c2416fb65f to your computer and use it in GitHub Desktop.
RevenueCat Analytics & Metrics
#!/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