mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 00:25:38 -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:
@@ -26,4 +26,41 @@ app.get("/v1/version", async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/v1/telemetry", async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
if (!body.instanceId || !body.version) {
|
||||
return c.json({ error: "Missing required fields" }, 400);
|
||||
}
|
||||
|
||||
const posthogKey = process.env.POSTHOG_API_KEY;
|
||||
if (!posthogKey) {
|
||||
return c.body(null, 204);
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch("https://us.i.posthog.com/i/v0/e/", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
api_key: posthogKey,
|
||||
event: "instance_report",
|
||||
distinct_id: body.instanceId,
|
||||
properties: {
|
||||
version: body.version,
|
||||
arch: body.arch,
|
||||
users: body.users,
|
||||
titles: body.titles,
|
||||
...(body.features ?? {}),
|
||||
},
|
||||
}),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
} catch {
|
||||
// Fire-and-forget — don't fail the request if PostHog is down
|
||||
}
|
||||
|
||||
return c.body(null, 204);
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
refreshTvChildren,
|
||||
} from "@sofa/core/metadata";
|
||||
import { getSetting } from "@sofa/core/settings";
|
||||
import { performTelemetryReport } from "@sofa/core/telemetry";
|
||||
import { performUpdateCheck } from "@sofa/core/update-check";
|
||||
import { db } from "@sofa/db/client";
|
||||
import { and, eq, inArray, isNotNull, lt, or } from "@sofa/db/helpers";
|
||||
@@ -366,6 +367,9 @@ export function startJobs() {
|
||||
schedule("updateCheck", "0 */6 * * *", async () => {
|
||||
await performUpdateCheck();
|
||||
});
|
||||
schedule("telemetryReport", "30 0 * * *", async () => {
|
||||
await performTelemetryReport();
|
||||
});
|
||||
|
||||
log.info(`Started ${jobs.size} jobs`);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
restoreFromBackup,
|
||||
} from "@sofa/core/backup";
|
||||
import { getSetting, setSetting } from "@sofa/core/settings";
|
||||
import { isTelemetryEnabled } from "@sofa/core/telemetry";
|
||||
import {
|
||||
getCachedUpdateCheck,
|
||||
isUpdateCheckEnabled,
|
||||
@@ -132,6 +133,21 @@ export const toggleUpdateCheck = os.admin.toggleUpdateCheck
|
||||
setSetting("updateCheckEnabled", String(input.enabled));
|
||||
});
|
||||
|
||||
// ─── Telemetry ────────────────────────────────────────────────
|
||||
|
||||
export const telemetry = os.admin.telemetry.use(admin).handler(() => {
|
||||
return {
|
||||
enabled: isTelemetryEnabled(),
|
||||
lastReportedAt: getSetting("telemetryLastReportedAt"),
|
||||
};
|
||||
});
|
||||
|
||||
export const toggleTelemetry = os.admin.toggleTelemetry
|
||||
.use(admin)
|
||||
.handler(({ input }) => {
|
||||
setSetting("telemetryEnabled", String(input.enabled));
|
||||
});
|
||||
|
||||
// ─── Jobs ──────────────────────────────────────────────────────
|
||||
|
||||
export const triggerJob = os.admin.triggerJob
|
||||
|
||||
@@ -3,7 +3,11 @@ import {
|
||||
isOidcConfigured,
|
||||
isPasswordLoginDisabled,
|
||||
} from "@sofa/auth/config";
|
||||
import { getUserCount, isRegistrationOpen } from "@sofa/core/settings";
|
||||
import {
|
||||
getInstanceId,
|
||||
getUserCount,
|
||||
isRegistrationOpen,
|
||||
} from "@sofa/core/settings";
|
||||
import { isTmdbConfigured } from "@sofa/tmdb/config";
|
||||
import { tmdbImageUrl } from "@sofa/tmdb/image";
|
||||
import { os } from "../context";
|
||||
@@ -30,6 +34,7 @@ export const publicInfo = os.system.publicInfo.handler(async () => {
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
return {
|
||||
instanceId: getInstanceId(),
|
||||
tmdbConfigured: isTmdbConfigured(),
|
||||
userCount: getUserCount(),
|
||||
registrationOpen: isRegistrationOpen(),
|
||||
|
||||
@@ -78,6 +78,8 @@ export const router = os.router({
|
||||
toggleRegistration: admin.toggleRegistration,
|
||||
updateCheck: admin.updateCheck,
|
||||
toggleUpdateCheck: admin.toggleUpdateCheck,
|
||||
telemetry: admin.telemetry,
|
||||
toggleTelemetry: admin.toggleTelemetry,
|
||||
triggerJob: admin.triggerJob,
|
||||
},
|
||||
account: {
|
||||
|
||||
Reference in New Issue
Block a user