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:
2026-03-13 21:14:56 -04:00
parent b7bacb352f
commit 9278ea6e73
10 changed files with 185 additions and 1 deletions
+16
View File
@@ -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
+6 -1
View File
@@ -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(),