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
|
onTrySdkPersist: sdkSession
|
||||||
? (selection, operations, originalContent, targetPath, options) => {
|
? (selection, operations, originalContent, targetPath, options) => {
|
||||||
// Resolver shadow runs regardless of the cutover flag — decoupled tripwire.
|
// 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(
|
return sdkCutoverPersist(
|
||||||
selection,
|
selection,
|
||||||
operations,
|
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 () => {
|
it("C10: unmappable op type produces no mismatch (excluded, not flagged)", async () => {
|
||||||
const session = await openComposition(BASE_HTML);
|
const session = await openComposition(BASE_HTML);
|
||||||
// "unknown-op" is not in MAPPED_OP_TYPES, so it must be silently excluded.
|
// "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
|
* Mirror resolveScoped here: exact scoped-path match, then canonical bare
|
||||||
* match, then first bare match — the resolvability dispatch actually has.
|
* 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 {
|
function resolveSnapshot(session: Composition, id: string): FlatEl | null {
|
||||||
const els = session.getElements();
|
const els = session.getElements();
|
||||||
const exact = els.find((el) => el.scopedId === id);
|
const exact = els.find((el) => el.scopedId === id);
|
||||||
@@ -164,8 +172,17 @@ export function sdkResolverShadowCheck(
|
|||||||
session: Composition,
|
session: Composition,
|
||||||
hfId: string,
|
hfId: string,
|
||||||
ops: PatchOperation[],
|
ops: PatchOperation[],
|
||||||
|
sourceContent?: string,
|
||||||
): SdkResolverMismatch[] {
|
): SdkResolverMismatch[] {
|
||||||
if (!resolveSnapshot(session, hfId)) {
|
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 }];
|
return [{ kind: "element_not_found", hfId }];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,17 +258,30 @@ export function runResolverShadow(
|
|||||||
session: Composition,
|
session: Composition,
|
||||||
hfId: string | null | undefined,
|
hfId: string | null | undefined,
|
||||||
ops: PatchOperation[],
|
ops: PatchOperation[],
|
||||||
|
sourceContent?: string,
|
||||||
): void {
|
): void {
|
||||||
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
||||||
if (!hfId) return;
|
if (!hfId) return;
|
||||||
try {
|
try {
|
||||||
const mismatches = sdkResolverShadowCheck(session, hfId, ops);
|
const mismatches = sdkResolverShadowCheck(session, hfId, ops, sourceContent);
|
||||||
// Emit only on divergence — parity is silent, matching recordResolverParity
|
// Emit only on divergence — parity is silent, matching recordResolverParity
|
||||||
// and recordAnimationResolverParity. Otherwise this fires a PostHog event on
|
// and recordAnimationResolverParity. Otherwise this fires a PostHog event on
|
||||||
// every style/text/attr edit (the editor's chattiest path) at default-ON.
|
// every style/text/attr edit (the editor's chattiest path) at default-ON.
|
||||||
if (mismatches.length === 0) return;
|
if (mismatches.length === 0) return;
|
||||||
|
const isElementNotFound = mismatches.some((m) => m.kind === "element_not_found");
|
||||||
trackStudioEvent("sdk_resolver_shadow", {
|
trackStudioEvent("sdk_resolver_shadow", {
|
||||||
hfId,
|
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,
|
mismatchCount: mismatches.length,
|
||||||
mismatches: JSON.stringify(redactMismatches(mismatches)),
|
mismatches: JSON.stringify(redactMismatches(mismatches)),
|
||||||
});
|
});
|
||||||
@@ -282,6 +312,7 @@ export function recordResolverParity(
|
|||||||
trackStudioEvent("sdk_resolver_shadow", {
|
trackStudioEvent("sdk_resolver_shadow", {
|
||||||
hfId,
|
hfId,
|
||||||
opLabel,
|
opLabel,
|
||||||
|
sessionElementCount: session.getElements().length,
|
||||||
mismatchCount: 1,
|
mismatchCount: 1,
|
||||||
mismatches: JSON.stringify([
|
mismatches: JSON.stringify([
|
||||||
{ kind: "element_not_found", hfId } satisfies SdkResolverMismatch,
|
{ kind: "element_not_found", hfId } satisfies SdkResolverMismatch,
|
||||||
@@ -310,11 +341,13 @@ export function recordAnimationResolverParity(
|
|||||||
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
if (!STUDIO_SDK_RESOLVER_SHADOW_ENABLED) return;
|
||||||
if (!session || !animationId) return;
|
if (!session || !animationId) return;
|
||||||
try {
|
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
|
if (resolves) return; // SDK locates the animation — parity
|
||||||
trackStudioEvent("sdk_resolver_shadow", {
|
trackStudioEvent("sdk_resolver_shadow", {
|
||||||
animationId,
|
animationId,
|
||||||
opLabel,
|
opLabel,
|
||||||
|
sessionElementCount: elements.length,
|
||||||
mismatchCount: 1,
|
mismatchCount: 1,
|
||||||
mismatches: JSON.stringify([
|
mismatches: JSON.stringify([
|
||||||
{ kind: "animation_not_found", animationId } satisfies SdkResolverMismatch,
|
{ kind: "animation_not_found", animationId } satisfies SdkResolverMismatch,
|
||||||
|
|||||||
Reference in New Issue
Block a user