mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
feat: add opt-in anonymous telemetry reporting
Add an opt-in telemetry system that sends anonymised instance statistics to the public API once per day, which proxies them to PostHog when `POSTHOG_API_KEY` is configured. - Add `packages/core/src/telemetry.ts` — `performTelemetryReport` and `isTelemetryEnabled`; user and title counts are bucketed before sending to avoid exposing exact figures - Add `getInstanceId()` to `settings.ts` — generates and persists a stable UUIDv7 for the instance - Schedule a daily `telemetryReport` cron job (00:30) - Add `POST /v1/telemetry` to `apps/public-api`; forwards payload to PostHog or returns 204 silently if key is absent - Add `admin.telemetry` and `admin.toggleTelemetry` oRPC procedures for inspecting and toggling the setting - Expose `instanceId` on `system.publicInfo`
This commit is contained in:
@@ -23,6 +23,14 @@ export function getUserCount(): number {
|
||||
return result?.count ?? 0;
|
||||
}
|
||||
|
||||
export function getInstanceId(): string {
|
||||
const existing = getSetting("instanceId");
|
||||
if (existing) return existing;
|
||||
const id = Bun.randomUUIDv7();
|
||||
setSetting("instanceId", id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function isRegistrationOpen(): boolean {
|
||||
const userCount = getUserCount();
|
||||
if (userCount === 0) return true;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { db } from "@sofa/db/client";
|
||||
import { count } from "@sofa/db/helpers";
|
||||
import { titles } from "@sofa/db/schema";
|
||||
import { createLogger } from "@sofa/logger";
|
||||
import { imageCacheEnabled } from "./image-cache";
|
||||
import {
|
||||
getInstanceId,
|
||||
getSetting,
|
||||
getUserCount,
|
||||
setSetting,
|
||||
} from "./settings";
|
||||
|
||||
const APP_VERSION = process.env.APP_VERSION || "0.0.0";
|
||||
const PUBLIC_API_URL =
|
||||
process.env.PUBLIC_API_URL || "https://public-api.sofa.watch";
|
||||
const REPORT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
const log = createLogger("telemetry");
|
||||
|
||||
export function isTelemetryEnabled(): boolean {
|
||||
return getSetting("telemetryEnabled") === "true";
|
||||
}
|
||||
|
||||
function bucketUsers(n: number): string {
|
||||
if (n <= 1) return "1";
|
||||
if (n <= 5) return "2-5";
|
||||
if (n <= 10) return "6-10";
|
||||
if (n <= 25) return "11-25";
|
||||
return "26+";
|
||||
}
|
||||
|
||||
function bucketTitles(n: number): string {
|
||||
if (n === 0) return "0";
|
||||
if (n <= 50) return "1-50";
|
||||
if (n <= 200) return "51-200";
|
||||
if (n <= 500) return "201-500";
|
||||
return "501+";
|
||||
}
|
||||
|
||||
function getTitleCount(): number {
|
||||
const result = db.select({ count: count() }).from(titles).get();
|
||||
return result?.count ?? 0;
|
||||
}
|
||||
|
||||
export async function performTelemetryReport(): Promise<void> {
|
||||
if (!isTelemetryEnabled()) {
|
||||
log.debug("Telemetry disabled, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect report interval
|
||||
const lastReported = getSetting("telemetryLastReportedAt");
|
||||
if (lastReported) {
|
||||
const elapsed = Date.now() - new Date(lastReported).getTime();
|
||||
if (elapsed < REPORT_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const body = {
|
||||
instanceId: getInstanceId(),
|
||||
version: APP_VERSION,
|
||||
arch: `${process.platform}-${process.arch}`,
|
||||
users: bucketUsers(getUserCount()),
|
||||
titles: bucketTitles(getTitleCount()),
|
||||
features: {
|
||||
imageCache: imageCacheEnabled(),
|
||||
oidc: !!(
|
||||
process.env.OIDC_CLIENT_ID &&
|
||||
process.env.OIDC_CLIENT_SECRET &&
|
||||
process.env.OIDC_ISSUER_URL
|
||||
),
|
||||
scheduledBackups: getSetting("scheduledBackups") === "true",
|
||||
},
|
||||
};
|
||||
|
||||
const res = await fetch(`${PUBLIC_API_URL}/v1/telemetry`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "sofa-telemetry",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`Public API ${res.status}`);
|
||||
|
||||
setSetting("telemetryLastReportedAt", new Date().toISOString());
|
||||
log.info("Telemetry report sent");
|
||||
} catch (err) {
|
||||
log.warn("Telemetry report failed:", err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user