mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
Addresses review comments on #982: - studio shouldTrack(): adds VITE_HYPERFRAMES_NO_TELEMETRY (mirrors CLI's HYPERFRAMES_NO_TELEMETRY) and import.meta.env.DEV gates so dev / CI studio builds don't pollute production telemetry. shouldTrack() is now exported for testability. - App.tsx session dedupe: moves the once-per-session check from a useRef (which resets on HMR / remount) to sessionStorage via new hasFiredSessionStart / markSessionStartFired helpers in config.ts. - studioRenderTelemetry.ts: documents why `workers` is intentionally omitted from emitStudioRenderError (studio renders don't accept a user-supplied worker count, so early failures genuinely don't know one). - client.ts flush(): documents fire-and-forget no-retry design so future hands don't accidentally add retry logic that double-counts. Tests: - studioRenderTelemetry.test.ts (8 tests): perfPayload mapping for every RenderPerfSummary field, undefined-perf path, missing-extract path, zero-elapsed edge case, error event shape. - studio/telemetry/events.test.ts (4 tests): pin event names (studio_session_start, studio_render_start) and payload shape. - studio/telemetry/client.test.ts (9 tests): shouldTrack() returns false for non-phc_ key, opt-out, doNotTrack, build-time env, vite dev mode; memoization.
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// Lightweight PostHog client for the studio browser bundle.
|
|
// Mirrors `packages/cli/src/telemetry/client.ts` but uses fetch/sendBeacon.
|
|
// All calls are fire-and-forget; telemetry must never break the studio UI.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { getAnonymousId, hasShownNotice, isOptedOut, markNoticeShown } from "./config";
|
|
import { getBrowserSystemMeta } from "./system";
|
|
|
|
// HeyGen's PostHog project key — write-only, safe to embed in client code.
|
|
// OSS builds can override via `VITE_HYPERFRAMES_POSTHOG_KEY` at build time,
|
|
// or set it to an empty string to disable telemetry entirely.
|
|
const POSTHOG_API_KEY =
|
|
(import.meta.env.VITE_HYPERFRAMES_POSTHOG_KEY as string | undefined) ??
|
|
"phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
|
|
const POSTHOG_HOST =
|
|
(import.meta.env.VITE_HYPERFRAMES_POSTHOG_HOST as string | undefined) ??
|
|
"https://us.i.posthog.com";
|
|
const FLUSH_INTERVAL_MS = 1_000;
|
|
|
|
type EventProperties = Record<string, string | number | boolean | undefined>;
|
|
|
|
interface QueuedEvent {
|
|
event: string;
|
|
properties: EventProperties;
|
|
timestamp: string;
|
|
}
|
|
|
|
let eventQueue: QueuedEvent[] = [];
|
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let telemetryEnabled: boolean | null = null;
|
|
|
|
function isDoNotTrackOn(): boolean {
|
|
return typeof navigator !== "undefined" && navigator.doNotTrack === "1";
|
|
}
|
|
|
|
function isApiKeyConfigured(): boolean {
|
|
return POSTHOG_API_KEY.startsWith("phc_");
|
|
}
|
|
|
|
// VITE_HYPERFRAMES_NO_TELEMETRY mirrors the CLI's HYPERFRAMES_NO_TELEMETRY=1
|
|
// opt-out so HeyGen's own dev/CI builds can suppress telemetry from the studio
|
|
// bundle the same way. Vite injects it at build time. Accepts "1" or "true".
|
|
function isBuildTimeOptOut(): boolean {
|
|
const v = import.meta.env.VITE_HYPERFRAMES_NO_TELEMETRY as string | undefined;
|
|
return v === "1" || v === "true";
|
|
}
|
|
|
|
// `import.meta.env.DEV` is true under `vite dev` / `vite preview`. Auto-suppress
|
|
// so developers running `hyperframes preview` don't pollute production telemetry.
|
|
function isViteDevMode(): boolean {
|
|
return import.meta.env.DEV === true;
|
|
}
|
|
|
|
export function shouldTrack(): boolean {
|
|
if (telemetryEnabled !== null) return telemetryEnabled;
|
|
telemetryEnabled =
|
|
isApiKeyConfigured() &&
|
|
!isBuildTimeOptOut() &&
|
|
!isViteDevMode() &&
|
|
!isOptedOut() &&
|
|
!isDoNotTrackOn();
|
|
return telemetryEnabled;
|
|
}
|
|
|
|
export function trackEvent(event: string, properties: EventProperties = {}): void {
|
|
if (!shouldTrack()) return;
|
|
|
|
const sys = getBrowserSystemMeta();
|
|
eventQueue.push({
|
|
event,
|
|
properties: { ...properties, ...sys },
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
if (flushTimer === null) {
|
|
flushTimer = setTimeout(() => {
|
|
flushTimer = null;
|
|
flush();
|
|
}, FLUSH_INTERVAL_MS);
|
|
}
|
|
showNoticeOnce();
|
|
}
|
|
|
|
// Fire-and-forget: the queue is cleared before `send()` resolves, so a network
|
|
// failure drops the batch rather than retrying. Matches the CLI client's
|
|
// design. Do NOT add retry logic here — a retry without cross-batch dedup
|
|
// would risk double-counting events on transient PostHog 5xx responses.
|
|
function flush(): void {
|
|
if (eventQueue.length === 0) return;
|
|
const distinctId = getAnonymousId();
|
|
const batch = eventQueue.map((e) => ({
|
|
event: e.event,
|
|
// $ip: null tells PostHog to not record the request IP.
|
|
properties: { ...e.properties, $ip: null },
|
|
distinct_id: distinctId,
|
|
timestamp: e.timestamp,
|
|
}));
|
|
eventQueue = [];
|
|
send(`${POSTHOG_HOST}/batch/`, JSON.stringify({ api_key: POSTHOG_API_KEY, batch }));
|
|
}
|
|
|
|
function send(url: string, payload: string): void {
|
|
// Prefer fetch with keepalive (survives page navigation). sendBeacon is a
|
|
// fallback for older runtimes where fetch isn't available.
|
|
try {
|
|
void fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: payload,
|
|
keepalive: true,
|
|
}).catch(() => {
|
|
/* silent */
|
|
});
|
|
return;
|
|
} catch {
|
|
/* fall through */
|
|
}
|
|
try {
|
|
navigator.sendBeacon(url, new Blob([payload], { type: "application/json" }));
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
}
|
|
|
|
function showNoticeOnce(): void {
|
|
if (hasShownNotice()) return;
|
|
markNoticeShown();
|
|
// eslint-disable-next-line no-console
|
|
console.info(
|
|
"%c[HyperFrames]%c Anonymous studio usage analytics enabled. " +
|
|
"Disable: localStorage.setItem('hyperframes-studio:telemetryDisabled','1') (then reload).",
|
|
"color:#7c3aed;font-weight:bold",
|
|
"color:inherit",
|
|
);
|
|
}
|
|
|
|
// Flush queued events when the tab is being hidden or closed so tail events
|
|
// (e.g. a render_start fired moments before the user navigates away) aren't lost.
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("pagehide", () => flush(), { capture: true });
|
|
window.addEventListener("visibilitychange", () => {
|
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") flush();
|
|
});
|
|
}
|