mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
fix(studio): correct save failure telemetry (#3499)
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
consumeStudioWriteToken,
|
||||
createStudioWriteToken,
|
||||
markStudioWriteToken,
|
||||
resetStudioWriteTokens,
|
||||
studioExpectedFileVersion,
|
||||
studioFileContentVersion,
|
||||
studioWriteHeaders,
|
||||
} from "./studioFileVersion";
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("studioFileContentVersion", () => {
|
||||
it("matches the strong SHA-256 ETag format used by studio-server", async () => {
|
||||
await expect(studioFileContentVersion("abc")).resolves.toBe(
|
||||
@@ -42,6 +46,38 @@ describe("studioFileContentVersion", () => {
|
||||
});
|
||||
|
||||
describe("studio write-token echo identity", () => {
|
||||
it("prefers the platform randomUUID implementation", () => {
|
||||
const randomUUID = vi.fn(() => "11111111-2222-4333-8444-555555555555");
|
||||
vi.stubGlobal("crypto", { randomUUID });
|
||||
resetStudioWriteTokens();
|
||||
|
||||
expect(studioWriteHeaders()).toEqual({
|
||||
"X-Hyperframes-Write-Token": "11111111-2222-4333-8444-555555555555",
|
||||
});
|
||||
expect(randomUUID).toHaveBeenCalledOnce();
|
||||
expect(consumeStudioWriteToken("11111111-2222-4333-8444-555555555555")).toBe(true);
|
||||
});
|
||||
|
||||
it("creates an RFC 4122 UUID-v4 token from getRandomValues when randomUUID is unavailable", () => {
|
||||
const source = Uint8Array.from({ length: 16 }, (_, index) => index);
|
||||
vi.stubGlobal("crypto", {
|
||||
getRandomValues: vi.fn((target: Uint8Array) => {
|
||||
target.set(source);
|
||||
return target;
|
||||
}),
|
||||
});
|
||||
|
||||
expect(createStudioWriteToken()).toBe("00010203-0405-4607-8809-0a0b0c0d0e0f");
|
||||
});
|
||||
|
||||
it("fails explicitly when Web Crypto cannot provide secure random bytes", () => {
|
||||
vi.stubGlobal("crypto", {});
|
||||
|
||||
expect(() => createStudioWriteToken()).toThrow(
|
||||
"Web Crypto getRandomValues is required for Studio write identity",
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses exactly one matching API write receipt without hiding path-only external writes", () => {
|
||||
resetStudioWriteTokens();
|
||||
markStudioWriteToken("studio-write-1");
|
||||
|
||||
@@ -48,8 +48,18 @@ export async function studioExpectedFileVersion(
|
||||
return versions.get(path);
|
||||
}
|
||||
|
||||
function createStudioWriteToken(): string {
|
||||
return globalThis.crypto.randomUUID();
|
||||
export function createStudioWriteToken(): string {
|
||||
const webCrypto = globalThis.crypto;
|
||||
if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
|
||||
if (typeof webCrypto?.getRandomValues !== "function") {
|
||||
throw new Error("Web Crypto getRandomValues is required for Studio write identity");
|
||||
}
|
||||
|
||||
const bytes = webCrypto.getRandomValues(new Uint8Array(16));
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const trackStudioEvent = vi.hoisted(() => vi.fn());
|
||||
vi.mock("./studioTelemetry", () => ({ trackStudioEvent }));
|
||||
|
||||
import {
|
||||
StudioFileConflictError,
|
||||
StudioSaveHttpError,
|
||||
@@ -6,6 +10,7 @@ import {
|
||||
buildStudioSaveFailureProperties,
|
||||
getStudioSaveStatusCode,
|
||||
retryStudioSave,
|
||||
trackStudioEditBlocked,
|
||||
} from "./studioSaveDiagnostics";
|
||||
|
||||
describe("studio save diagnostics", () => {
|
||||
@@ -51,6 +56,30 @@ describe("studio save diagnostics", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("emits expected direct-edit refusals on edit_blocked", () => {
|
||||
const error = new Error("This animation is computed at runtime");
|
||||
|
||||
trackStudioEditBlocked({
|
||||
source: "gsap_commit",
|
||||
error,
|
||||
filePath: "index.html",
|
||||
mutationType: "drag",
|
||||
});
|
||||
|
||||
expect(trackStudioEvent).toHaveBeenCalledWith("edit_blocked", {
|
||||
source: "gsap_commit",
|
||||
error_message: error.message,
|
||||
status_code: null,
|
||||
file_path: "index.html",
|
||||
mutation_type: "drag",
|
||||
attempt: undefined,
|
||||
label: undefined,
|
||||
target_id: undefined,
|
||||
target_selector: undefined,
|
||||
target_source_file: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("reads nested status codes from error causes", () => {
|
||||
const cause = new StudioSaveHttpError("Too many requests", 429);
|
||||
const error = new Error("retry wrapper") as Error & { cause?: unknown };
|
||||
|
||||
@@ -172,6 +172,10 @@ export function trackStudioSaveFailure(input: StudioSaveFailureInput): void {
|
||||
trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
|
||||
}
|
||||
|
||||
export function trackStudioEditBlocked(input: StudioSaveFailureInput): void {
|
||||
trackStudioEvent("edit_blocked", buildStudioSaveFailureProperties(input));
|
||||
}
|
||||
|
||||
export async function createStudioSaveHttpError(
|
||||
response: Response,
|
||||
fallbackMessage: string,
|
||||
|
||||
Reference in New Issue
Block a user