Merge pull request #1049 from heygen-com/fix/studio-import-meta-env-guard

fix(studio): guard import.meta.env for non-Vite bundlers
This commit is contained in:
Miguel Ángel
2026-05-23 14:00:03 -04:00
committed by GitHub
2 changed files with 18 additions and 38 deletions
+3 -26
View File
@@ -2,20 +2,11 @@
import { describe, expect, it, vi, beforeEach } from "vitest"; import { describe, expect, it, vi, beforeEach } from "vitest";
// `shouldTrack()` reads `POSTHOG_API_KEY` from module-level const that's // `shouldTrack()` reads module-level constants evaluated at module load time,
// evaluated at module load time, so changing `import.meta.env` after import // so changing env after import has no effect. Each test resets module cache.
// has no effect on the key. Each test resets module cache and re-imports.
const OPT_OUT_KEY = "hyperframes-studio:telemetryDisabled"; const OPT_OUT_KEY = "hyperframes-studio:telemetryDisabled";
function setKey(value: string | undefined): void {
if (value === undefined) {
delete (import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_POSTHOG_KEY;
} else {
(import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_POSTHOG_KEY = value;
}
}
function setNoTelemetry(value: string | undefined): void { function setNoTelemetry(value: string | undefined): void {
if (value === undefined) { if (value === undefined) {
delete (import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_NO_TELEMETRY; delete (import.meta.env as Record<string, unknown>).VITE_HYPERFRAMES_NO_TELEMETRY;
@@ -37,29 +28,16 @@ async function loadShouldTrack(): Promise<() => boolean> {
describe("studio client shouldTrack", () => { describe("studio client shouldTrack", () => {
beforeEach(() => { beforeEach(() => {
setDev(false); setDev(false);
setKey("phc_test_key");
setNoTelemetry(undefined); setNoTelemetry(undefined);
localStorage.clear(); localStorage.clear();
vi.unstubAllGlobals(); vi.unstubAllGlobals();
}); });
it("returns true when key is configured, not in dev mode, and no opt-outs", async () => { it("returns true when not in dev mode and no opt-outs", async () => {
const shouldTrack = await loadShouldTrack(); const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(true); expect(shouldTrack()).toBe(true);
}); });
it("returns false when API key does not start with phc_", async () => {
setKey("not_a_real_key");
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false when API key is empty string", async () => {
setKey("");
const shouldTrack = await loadShouldTrack();
expect(shouldTrack()).toBe(false);
});
it("returns false when user has opted out via localStorage", async () => { it("returns false when user has opted out via localStorage", async () => {
localStorage.setItem(OPT_OUT_KEY, "1"); localStorage.setItem(OPT_OUT_KEY, "1");
const shouldTrack = await loadShouldTrack(); const shouldTrack = await loadShouldTrack();
@@ -93,7 +71,6 @@ describe("studio client shouldTrack", () => {
it("memoizes its decision after the first call", async () => { it("memoizes its decision after the first call", async () => {
const shouldTrack = await loadShouldTrack(); const shouldTrack = await loadShouldTrack();
const first = shouldTrack(); const first = shouldTrack();
// Flip an underlying input — memoized return must not change.
localStorage.setItem(OPT_OUT_KEY, "1"); localStorage.setItem(OPT_OUT_KEY, "1");
expect(shouldTrack()).toBe(first); expect(shouldTrack()).toBe(first);
}); });
+15 -12
View File
@@ -7,15 +7,9 @@
import { getAnonymousId, hasShownNotice, isOptedOut, markNoticeShown } from "./config"; import { getAnonymousId, hasShownNotice, isOptedOut, markNoticeShown } from "./config";
import { getBrowserSystemMeta } from "./system"; import { getBrowserSystemMeta } from "./system";
// HeyGen's PostHog project key — write-only, safe to embed in client code. // Write-only PostHog project key, safe to embed in client code.
// OSS builds can override via `VITE_HYPERFRAMES_POSTHOG_KEY` at build time, const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
// or set it to an empty string to disable telemetry entirely. const POSTHOG_HOST = "https://us.i.posthog.com";
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; const FLUSH_INTERVAL_MS = 1_000;
type EventProperties = Record<string, string | number | boolean | undefined>; type EventProperties = Record<string, string | number | boolean | undefined>;
@@ -41,15 +35,24 @@ function isApiKeyConfigured(): boolean {
// VITE_HYPERFRAMES_NO_TELEMETRY mirrors the CLI's HYPERFRAMES_NO_TELEMETRY=1 // 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 // 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". // bundle the same way. Vite injects it at build time. Accepts "1" or "true".
// `import.meta.env` may be undefined in non-Vite bundlers (Next.js Turbopack).
function isBuildTimeOptOut(): boolean { function isBuildTimeOptOut(): boolean {
const v = import.meta.env.VITE_HYPERFRAMES_NO_TELEMETRY as string | undefined; try {
return v === "1" || v === "true"; const v = import.meta.env.VITE_HYPERFRAMES_NO_TELEMETRY as string | undefined;
return v === "1" || v === "true";
} catch {
return false;
}
} }
// `import.meta.env.DEV` is true under `vite dev` / `vite preview`. Auto-suppress // `import.meta.env.DEV` is true under `vite dev` / `vite preview`. Auto-suppress
// so developers running `hyperframes preview` don't pollute production telemetry. // so developers running `hyperframes preview` don't pollute production telemetry.
function isViteDevMode(): boolean { function isViteDevMode(): boolean {
return import.meta.env.DEV === true; try {
return import.meta.env.DEV === true;
} catch {
return false;
}
} }
export function shouldTrack(): boolean { export function shouldTrack(): boolean {