309 lines · 13.1 KB
/**
 * Browser analytics (PostHog). Everything here is a no-op unless the server sent a config, which it only does
 * when POSTHOG_KEY is set. posthog-js is loaded lazily, so instances without analytics never download it.
 *
 * Privacy rules enforced here:
 * - Honors Do Not Track and Global Privacy Control, and a per-browser opt-out (setAnalyticsOptOut).
 * - Runs only on the forge's own origin, never on editor, user-content, sites or pages origins.
 * - URLs lose query strings and fragments; repository paths are redacted unless the repo is known public.
 * - Autocapture never records element text or attributes; inputs are masked in replays, and replays pause on
 *   private repositories and on account, settings and auth pages.
 */
import type { PostHog } from "posthog-js";
import { scrubErrorMessage } from "./scrub-error";
import { APP_ROOTS, isRouteTemplate, routeTemplate } from "./route-template";

export type ClientAnalyticsConfig = { key: string; host: string; origin: string; personEmail: boolean };
export type AnalyticsViewer = { id: string; email?: string; isAdmin?: boolean } | null;

const OPT_OUT_KEY = "gitfor:analytics-opt-out";
export const INGEST_PREFIX = "/ingest";

/** PostHog's app host for toolbar links: us.i.posthog.com -> us.posthog.com. */
function uiHost(host: string): string {
  return host.replace(/^https:\/\/(us|eu)\.i\.posthog\.com$/, "https://$1.posthog.com");
}
const REDACTED = "[private]";

const NO_RECORDING = [
  /^\/settings(\/|$)/,
  /^\/admin(\/|$)/,
  /^\/(login|signup|reset-password|forgot-password|verify-email)/,
  /^\/[^/]+\/[^/]+\/settings(\/|$)/,
  /^\/orgs\/[^/]+\/settings/,
];

let client: PostHog | null = null;
let loading: Promise<PostHog | null> | null = null;
const publicRepos = new Set<string>();

function repoKey(owner: string, repo: string): string {