mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
Merge pull request #2854 from heygen-com/feat/canary-rollouts
feat(core): percentage-based canary rollouts + calibration experiment
This commit is contained in:
@@ -16,7 +16,12 @@ import {
|
||||
loadRuntimeSourceSignature,
|
||||
} from "./runtimeSource.js";
|
||||
import { VERSION as version } from "../version.js";
|
||||
import { buildStudioHeadScripts, resolveCliTelemetryDistinctId } from "./telemetryIdentity.js";
|
||||
import {
|
||||
buildStudioHeadScriptsForHost,
|
||||
identityAllowed,
|
||||
refreshTelemetryPosture,
|
||||
resolveCliTelemetryDistinctId,
|
||||
} from "./telemetryIdentity.js";
|
||||
import { emitStudioRenderComplete, emitStudioRenderError } from "./studioRenderTelemetry.js";
|
||||
import { isDevMode } from "../utils/env.js";
|
||||
import {
|
||||
@@ -424,6 +429,10 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
rendersDir: () => join(projectDir, "renders"),
|
||||
|
||||
startRender(opts): RenderJobState {
|
||||
// The render POST is a request boundary like any other. Without this an
|
||||
// already-open Studio tab keeps rendering under the posture cached when
|
||||
// the server booted.
|
||||
refreshTelemetryPosture();
|
||||
const abortController = new AbortController();
|
||||
const state: RenderJobState = {
|
||||
id: opts.jobId,
|
||||
@@ -503,6 +512,12 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
metaPath,
|
||||
JSON.stringify({ status: "complete", durationMs: Date.now() - startTime }),
|
||||
);
|
||||
// Refreshed HERE, not just at render start: a render can run for
|
||||
// minutes, and `hyperframes telemetry disable` during one must be
|
||||
// honoured by the event that reports it. Studio never polls
|
||||
// /api/telemetry-identity, so this process would otherwise keep its
|
||||
// startup-cached posture for the life of the preview server.
|
||||
refreshTelemetryPosture();
|
||||
emitStudioRenderComplete(opts, Date.now() - startTime, job.perfSummary);
|
||||
} catch (err) {
|
||||
if (abortController.signal.aborted) {
|
||||
@@ -512,6 +527,7 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
state.status = "failed";
|
||||
state.error = err instanceof Error ? err.message : String(err);
|
||||
// fallow-ignore-next-line code-duplication
|
||||
refreshTelemetryPosture();
|
||||
emitStudioRenderError(opts, Date.now() - startTime, state.stage, err, renderJob);
|
||||
try {
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
|
||||
@@ -692,7 +708,26 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
// clients that can't rely on the injected global. Returns the CLI's anonymous
|
||||
// distinct id (no PII) so the browser session can join the CLI's PostHog
|
||||
// person, or `{ distinctId: null }` when CLI telemetry is disabled.
|
||||
//
|
||||
// Deliberately does NOT serve `bucketSeed`. Studio gets its canary answers
|
||||
// from the injected `window.__HF_CLI_CANARY_DECISIONS` (booleans, not the
|
||||
// value cohorts derive from), so nothing needs the seed over HTTP — and an
|
||||
// unauthenticated local endpoint is a strictly worse place for it than an
|
||||
// inline script scoped to Studio's own document.
|
||||
//
|
||||
// Host-guarded against DNS rebinding: a remote page can point a hostname it
|
||||
// controls at 127.0.0.1 and read this response as same-origin. Pinning the
|
||||
// Host header to a loopback name means such a request (which carries the
|
||||
// attacker's hostname) is refused. Same-origin Studio traffic always
|
||||
// presents the bound loopback host.
|
||||
app.get("/api/telemetry-identity", (c) => {
|
||||
if (!identityAllowed(c.req.header("host"))) {
|
||||
return c.json({ error: "forbidden" }, 403);
|
||||
}
|
||||
// Same request-boundary refresh the head-script route does: this endpoint
|
||||
// is polled by a long-lived Studio tab, so a cached posture here outlives
|
||||
// an opt-out run in another terminal just as visibly.
|
||||
refreshTelemetryPosture();
|
||||
return c.json({ distinctId: resolveCliTelemetryDistinctId() });
|
||||
});
|
||||
|
||||
@@ -836,7 +871,17 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
// Inject before the studio bundle runs. Identity script first (see
|
||||
// buildStudioHeadScripts) so the CLI distinct id is on `window` by the time
|
||||
// telemetry init reads it.
|
||||
const headScript = buildStudioHeadScripts(buildRuntimeEnvScript());
|
||||
//
|
||||
// Host-guarded for the same reason /api/telemetry-identity is, and it has
|
||||
// to be checked HERE too: guarding only the endpoint leaves this route as
|
||||
// an open side door, since a rebound origin can simply fetch `/` and read
|
||||
// the same distinct id and seed out of the returned HTML.
|
||||
//
|
||||
// Only IDENTITY is withheld from an untrusted Host. The canary decisions
|
||||
// map still goes out — it is non-identifying, and a LAN/remote Studio
|
||||
// (`HYPERFRAMES_PREVIEW_HOST=0.0.0.0`) needs it to stay in agreement with
|
||||
// the CLI. See buildStudioHeadScriptsForHost.
|
||||
const headScript = buildStudioHeadScriptsForHost(buildRuntimeEnvScript(), c.req.header("host"));
|
||||
if (headScript) {
|
||||
html = html.replace("<head>", `<head>${headScript}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user