mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(studio): filter runtime-generated nodes from resolver-shadow telemetry (#1795)
The sdk_resolver_shadow tripwire flagged element_not_found for nodes a composition <script> creates at runtime (caption word/group spans, etc.). These have no static data-hf-id, so the SDK session (a static parse) cannot model them by design; the divergence is noise, not a resolver bug. - Runtime-node filter: suppress element_not_found when the resolved hf-id is absent from the on-disk source. An id PRESENT in source but missing from the session stays flagged (the genuine v0.6.110-class resolver divergence). - Add sessionElementCount to all element_not_found / animation_not_found emits (0 = empty/broken session, >0 = element-specific). - Add sourceHfIdCount to emitted element_not_found: =1 = static node the parse dropped (foreign-content exclusion / sub-comp gap), >1 = duplicate-id resolver ambiguity. Scoped to the DOM-edit path. Telemetry-only; no disk writes, no edit change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f24a1a9ce7
commit
a4eacaec37
@@ -260,7 +260,9 @@ export function useDomEditSession({
|
||||
onTrySdkPersist: sdkSession
|
||||
? (selection, operations, originalContent, targetPath, options) => {
|
||||
// Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
|
||||
runResolverShadow(sdkSession, selection.hfId, operations);
|
||||
// Pass originalContent so the runtime-node filter can suppress hf-ids
|
||||
// absent from source (script-created nodes the SDK can't model).
|
||||
runResolverShadow(sdkSession, selection.hfId, operations, originalContent);
|
||||
return sdkCutoverPersist(
|
||||
selection,
|
||||
operations,
|
||||
|
||||
@@ -226,6 +226,53 @@ describe("C. Resolver-parity detection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("C8 runtime-node filter: hfId absent from source → suppressed (not a resolver bug)", () => {
|
||||
// The studio resolved a live-DOM element to an hf-id that the SDK session
|
||||
// doesn't contain AND that never appears in the on-disk source — it's a
|
||||
// node a composition <script> created at runtime (e.g. caption spans). Not
|
||||
// a resolver divergence; suppress.
|
||||
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
|
||||
typeof sdkResolverShadowCheck
|
||||
>[0];
|
||||
const source = `<div data-hf-id="hf-static">no runtime id here</div>`;
|
||||
const mismatches = sdkResolverShadowCheck(
|
||||
session,
|
||||
"hf-runtimeonly",
|
||||
[{ type: "inline-style", property: "color", value: "red" }],
|
||||
source,
|
||||
);
|
||||
expect(mismatches).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("C8 runtime-node filter: hfId PRESENT in source but missing from session → still flagged (real bug)", () => {
|
||||
const session = { getElement: () => null, getElements: () => [] } as unknown as Parameters<
|
||||
typeof sdkResolverShadowCheck
|
||||
>[0];
|
||||
const source = `<div data-hf-id="hf-realbug">in source, not in SDK session</div>`;
|
||||
const mismatches = sdkResolverShadowCheck(
|
||||
session,
|
||||
"hf-realbug",
|
||||
[{ type: "inline-style", property: "color", value: "red" }],
|
||||
source,
|
||||
);
|
||||
expect(mismatches).toHaveLength(1);
|
||||
expect(mismatches[0]?.kind).toBe("element_not_found");
|
||||
});
|
||||
|
||||
it("C8 sourceHfIdCount: emitted element_not_found carries source occurrence count", async () => {
|
||||
mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
|
||||
const session = { getElement: () => null, getElements: () => [] } as unknown as Composition;
|
||||
// id present twice in source (duplicate-id ambiguity) but absent from session
|
||||
const source = `<div data-hf-id="hf-dup">a</div><div data-hf-id="hf-dup">b</div>`;
|
||||
runResolverShadow(
|
||||
session,
|
||||
"hf-dup",
|
||||
[{ type: "inline-style", property: "color", value: "red" }],
|
||||
source,
|
||||
);
|
||||
expect(lastShadow()?.sourceHfIdCount).toBe(2);
|
||||
});
|
||||
|
||||
it("C10: unmappable op type produces no mismatch (excluded, not flagged)", async () => {
|
||||
const session = await openComposition(BASE_HTML);
|
||||
// "unknown-op" is not in MAPPED_OP_TYPES, so it must be silently excluded.
|
||||
|
||||
@@ -82,6 +82,14 @@ type AttrMap = Record<string, string | null>;
|
||||
* Mirror resolveScoped here: exact scoped-path match, then canonical bare
|
||||
* match, then first bare match — the resolvability dispatch actually has.
|
||||
*/
|
||||
// Count static `data-hf-id="<id>"` occurrences (both quote styles) in source.
|
||||
// Substring split, not regex — no escaping, and the id never contains a quote.
|
||||
function countHfIdInSource(source: string, id: string): number {
|
||||
return (
|
||||
source.split(`data-hf-id="${id}"`).length - 1 + (source.split(`data-hf-id='${id}'`).length - 1)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSnapshot(session: Composition, id: string): FlatEl | null {
|
||||
const els = session.getElements();
|
||||
const exact = els.find((el) => el.scopedId === id);
|
||||
@@ -164,8 +172,17 @@ export function sdkResolverShadowCheck(
|
||||
session: Composition,
|
||||
hfId: string,
|
||||
ops: PatchOperation[],
|
||||
sourceContent?: string,
|
||||
): SdkResolverMismatch[] {
|
||||
if (!resolveSnapshot(session, hfId)) {
|
||||
// Runtime-node filter: an hf-id absent from the on-disk source the SDK
|
||||
// parsed was never in the static DOM — it belongs to an element a
|
||||
// composition <script> creates at runtime (e.g. caption word/group spans),
|
||||
// which the SDK session cannot model by design. That is NOT a resolver bug,
|
||||
// so suppress it. An hf-id PRESENT in source but missing from the session IS
|
||||
// a genuine resolver divergence (the v0.6.110 class) — keep emitting that.
|
||||
// ponytail: substring match; biases toward keeping signal on a loose hit.
|
||||
if (sourceContent !== undefined && !sourceContent.includes(hfId)) return [];
|
||||
return [{ kind: "element_not_found", hfId }];
|
||||
}
|
||||
|
||||
@@ -241,17 +258,30 @@ export function runResolverShadow(
|
||||
session: Composition,
|
||||
hfId: string | null | undefined,
|
||||
ops: PatchOperation[],
|
||||
sourceContent?: string,
|
||||
): void {
|
||||
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
||||
if (!hfId) return;
|
||||
try {
|
||||
const mismatches = sdkResolverShadowCheck(session, hfId, ops);
|
||||
const mismatches = sdkResolverShadowCheck(session, hfId, ops, sourceContent);
|
||||
// Emit only on divergence — parity is silent, matching recordResolverParity
|
||||
// and recordAnimationResolverParity. Otherwise this fires a PostHog event on
|
||||
// every style/text/attr edit (the editor's chattiest path) at default-ON.
|
||||
if (mismatches.length === 0) return;
|
||||
const isElementNotFound = mismatches.some((m) => m.kind === "element_not_found");
|
||||
trackStudioEvent("sdk_resolver_shadow", {
|
||||
hfId,
|
||||
// sessionElementCount > 0 + element_not_found = runtime-only element;
|
||||
// sessionElementCount === 0 = session is empty/broken (actionable).
|
||||
sessionElementCount: session.getElements().length,
|
||||
// Count of data-hf-id="<id>" occurrences in source for an emitted
|
||||
// element_not_found (the runtime-node filter already dropped absent-from-
|
||||
// source ids, so an emitted one is in source ≥1×). >1 = duplicate ids →
|
||||
// resolver picked the wrong instance; =1 = single static node the SDK
|
||||
// parse dropped (foreign-content exclusion / sub-comp inlining gap).
|
||||
...(isElementNotFound && sourceContent !== undefined
|
||||
? { sourceHfIdCount: countHfIdInSource(sourceContent, hfId) }
|
||||
: {}),
|
||||
mismatchCount: mismatches.length,
|
||||
mismatches: JSON.stringify(redactMismatches(mismatches)),
|
||||
});
|
||||
@@ -282,6 +312,7 @@ export function recordResolverParity(
|
||||
trackStudioEvent("sdk_resolver_shadow", {
|
||||
hfId,
|
||||
opLabel,
|
||||
sessionElementCount: session.getElements().length,
|
||||
mismatchCount: 1,
|
||||
mismatches: JSON.stringify([
|
||||
{ kind: "element_not_found", hfId } satisfies SdkResolverMismatch,
|
||||
@@ -310,11 +341,13 @@ export function recordAnimationResolverParity(
|
||||
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
||||
if (!session || !animationId) return;
|
||||
try {
|
||||
const resolves = session.getElements().some((el) => el.animationIds.includes(animationId));
|
||||
const elements = session.getElements();
|
||||
const resolves = elements.some((el) => el.animationIds.includes(animationId));
|
||||
if (resolves) return; // SDK locates the animation — parity
|
||||
trackStudioEvent("sdk_resolver_shadow", {
|
||||
animationId,
|
||||
opLabel,
|
||||
sessionElementCount: elements.length,
|
||||
mismatchCount: 1,
|
||||
mismatches: JSON.stringify([
|
||||
{ kind: "animation_not_found", animationId } satisfies SdkResolverMismatch,
|
||||
|
||||
Reference in New Issue
Block a user