fix(studio): gracefully handle visual edits on runtime-generated elements (#1150)

* fix(studio): gracefully handle visual edits on runtime-generated elements

When the DOM patcher can't find an element in source HTML (e.g. elements
created by JavaScript at runtime like #arrows-svg, .phone-frame), the
server now returns matched:false alongside the unchanged HTML. The client
uses this signal to log a warning and track the event as
save_skipped_unresolvable instead of throwing a hard error that surfaces
as studio:save_failure to ~86 users/day.

Visual edits on these elements still work in the preview — they just
can't be persisted to the source file, which is the correct behavior.

* fix(studio): throttle save_skipped_unresolvable and add composition context

Deduplicate telemetry — fire once per selector per session instead of on
every RAF tick during drag. Add composition path to the event payload for
dashboard pivoting.
This commit is contained in:
Miguel Ángel
2026-06-01 16:44:53 -04:00
committed by GitHub
parent 5697e4adc3
commit 8c7068aa42
4 changed files with 72 additions and 40 deletions
+21 -2
View File
@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useRef } from "react";
import { usePlayerStore } from "../player";
import { FONT_EXT } from "../utils/mediaTypes";
import type { PatchOperation } from "../utils/sourcePatcher";
@@ -128,6 +128,8 @@ export function useDomEditCommits({
[fileTree, projectId, importedFontAssetsRef],
);
const reportedUnresolvableRef = useRef(new Set<string>());
// fallow-ignore-next-line complexity
const persistDomEditOperations: PersistDomEditOperations = useCallback(
async (selection, operations, options) => {
@@ -173,11 +175,28 @@ export function useDomEditCommits({
const patchData = (await patchResponse.json()) as {
ok?: boolean;
changed?: boolean;
matched?: boolean;
content?: string;
};
if (!patchData.changed) {
throw new Error(`Unable to patch ${selection.selector ?? selection.id ?? "selection"}`);
if (patchData.matched === false) {
const targetKey = selection.selector ?? selection.id ?? "selection";
if (!reportedUnresolvableRef.current.has(targetKey)) {
reportedUnresolvableRef.current.add(targetKey);
trackStudioEvent("save_skipped_unresolvable", {
target_id: selection.id ?? undefined,
target_selector: selection.selector ?? undefined,
target_source_file: selection.sourceFile ?? undefined,
composition: activeCompPath ?? undefined,
});
console.warn(
`[studio] Element not found in source: ${targetKey}. ` +
"This element may be generated at runtime and cannot be persisted.",
);
}
}
return;
}
const patchedContent =