fix(studio): make SDK shadow telemetry fire + be correct (5 fixes, E2E-verified)#1491
Conversation
useSdkSession(projectId, activeCompPath) received activeCompPath=null in the master/entry view — the studio's convention where null means index.html (isMasterView = !activeCompPath || activeCompPath === "index.html"). The hook's guard `if (!projectId || !activeCompPath) return` then bailed, so the SDK session never opened in the default editing surface. Result: sdkSession was null there → every shadow tap (onDomEditPersisted, onElementDeleted, timing, gsap) was undefined/no-op → zero sdk_shadow_dispatch telemetry for master-view edits (the common case). Shadow only fired when a sub-comp was explicitly opened (which sets activeCompPath). Resolve null → "index.html" (matching the existing convention used by isMasterView and blockInstaller) so the session opens in master view. Verified live: instrumenting the hook showed phase "skipped_no_ids" (activeCompPath null) before, "opened" after. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ebab
The inline-style parity resolver read flat.styles[op.property] with the
kebab-case PatchOperation key ("background-color"), but ElementSnapshot
inlineStyles are camelCase ("backgroundColor"), so the read-back was always
null → a false value_mismatch on every hyphenated CSS property. Single-word
props (color, opacity) coincide, so unit tests missed it.
Found live: a color edit on a box emitted op:property mismatchCount:1 with
{property:"background-color", expected:"rgb(255,79,88)", actual:null}. Convert
kebab→camel for the read-back (fall back to the raw key).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Delete/Backspace hotkey routes to handleTimelineElementDelete whenever a
timeline element is selected (useAppHotkeys: `if (selectedElementId) {
handleTimelineElementDelete(el); return; }`), returning before the
shadow-wired handleDomEditElementDelete. Every clip is a timeline element, so
clip deletes — the common case — emitted no op:delete; the delete shadow only
fired for a non-timed DOM selection.
Add runShadowDelete(sdkSession, element.hfId) to handleTimelineElementDelete's
success path, mirroring the move/resize timing taps.
Verified live (browser-use): deleting a clip now emits
sdk_shadow_dispatch op:delete dispatched:true mismatchCount:0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…selector
gsap_fidelity keyed tweens by id (targetSelector-method-position). On tween
ADD, the SDK writer emits [data-hf-id="X"] selectors while the server emits
class selectors (.x) for the same element — different ids → false
present/absent mismatch (mc:2) on every add. Update/remove were clean (the
tween already existed with one consistent selector).
Key by resolved element (selector → data-hf-id via the pre-op DOM) + method +
position, so equivalent tweens match and only real value drift registers.
Falls back to raw selector when resolution isn't possible.
Found live (browser-use): adding a tween emitted gsap_fidelity mc:2 with
{[data-hf-id="hf-b"]-to-0 present-only} + {.b-to-0 present-only}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Property-path parity false-mismatched on canvas-drag (path-offset) edits, which
emit attribute ops like {property:"data-hf-studio-path-offset"}:
1. The name was built as `data-${op.property}` → double-prefix
"data-data-hf-studio-path-offset".
2. The SDK model excludes all data-hf-* attributes, so even the right name
reads back null → false value_mismatch.
attrName() prefixes only when needed; isShadowableOp() drops data-hf-* attribute
ops (studio-internal markers the SDK can't represent), filtered in
sdkShadowDispatch before dispatch + parity.
Code-confirmed via handleDomPathOffsetCommit → commitPositionPatchToHtml →
persistDomEditOperations → onDomEditPersisted; live repro blocked because the
test comp's elements were GSAP-animated (drags route to the GSAP path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
miga-heygen
left a comment
There was a problem hiding this comment.
Thorough review of all five fixes. Each one is targeted, correctly identified at the root cause, and test-covered. CI (40+ checks including 8 regression shards) all green.
Fix-by-fix:
-
activeCompPath ?? "index.html"— Master view hadnullpath, guard bailed, zero telemetry. The fallback to"index.html"is the right convention for the entry comp. Clean. -
Kebab→camel read-back —
flat.styles["background-color"]was always null becauseCSSStyleDeclarationkeys are camelCase.kebabToCamel+ fallback chain (camel ?? raw ?? null) handles both forms gracefully. Good defensive coding. -
runShadowDeletein timeline path — The Delete hotkey routed throughhandleTimelineElementDeletewhich was never shadow-wired. Single-line addition,sdkSessioncorrectly added to theuseCallbackdeps array. Tight. -
GSAP fidelity selector resolution — This was the most substantial fix.
tweenKey(anim, resolveSelector)normalizes[data-hf-id="X"]vs.classselectors to the same canonical element id viamakeSelectorResolver(DOMParser on pre-op HTML, graceful fallback). Since method + position are now part of the key, they're correctly removed from the field-by-field comparison. TheposKeynumeric normalization handles string/number position ambiguity. -
data-hf-*marker filtering —isShadowableOp()drops attributes the SDK model explicitly excludes, andattrName()prevents the double-prefix (data-data-hf-...). Applied before both dispatch and parity check — correct, since filtering after dispatch would still report false mismatches.
Tests cover the three most subtle failure modes (kebab-case styles, data-hf-* markers, cross-selector-form matching). The selector resolver test is nicely structured — asserts that without a resolver you GET mismatches, with one you don't.
No issues found. Ship it.
— Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Review at HEAD 02cdd37c.
This PR addresses separate scope from my held #1473 (double-writer race) and #1474 (shadowGsapOp per-property gap) — it's five downstream fixes that make the shadow telemetry from those PRs actually fire + report correctly under live E2E. Each fix is targeted at a real root cause the unit tests missed, all CI green, and Miga already approved with a thorough fix-by-fix breakdown. I agree with the approval on substance; layering a few smaller observations + two questions on the gsap-fidelity selector resolver.
Re-reading my #1473 hold: the race I held on doesn't exist at HEAD — useSdkSession.ts:25-32 confirms the session is opened WITHOUT persist ("shadow-telemetry + selection-sync only — reads from the server but must NEVER write back"). That's resolution path (a) from my hold. I'd missed that #1473's session-open path landed without persist; the load-bearing TODO I quoted was rewritten in-PR to clarify the no-persist contract. Apologies for the false-flag in that review round. #1491 is NOT contingent on the #1473 hold — the race never materialized.
#1474's shadowGsapOp per-property gap is also untouched here; this PR only touches the fidelity diff key, not the per-property emission shape, so that hold is independent and remains open.
Fix-by-fix agreement with Miga:
activeCompPath ?? "index.html"— correct entry-comp fallback. (App.tsx:178)kebabToCamel+flat.styles[camel] ?? flat.styles[raw] ?? null— handles both forms gracefully; empty-string CSS values are not nullish so they short-circuit cleanly. (sdkShadow.ts:118-128)runShadowDeleteinhandleTimelineElementDeletewithsdkSessioncorrectly added to theuseCallbackdeps array. (useTimelineEditing.ts:291,304)- GSAP fidelity selector resolution via
tweenKey(anim, resolveSelector)— element-keyed match across[data-hf-id="X"]vs.xselector forms. Method + position correctly removed from field comparison since they're now part of the key. (sdkShadowGsapFidelity.ts:48-58) attrName()+isShadowableOp()— correct filter applied before BOTH dispatch and parity check;html-attributeandattributepaths both covered. (sdkShadow.ts:29-40,185-189)
Test coverage is right where it should be: kebab-style, data-hf-* marker, and the cross-selector-form match test that asserts the resolver-on vs resolver-off behavior explicitly.
Questions / small notes (non-blocking):
• sdkShadowGsapFidelity.ts:184-201 makeSelectorResolver — doc.querySelector(sel) returns the first match; if the pre-op HTML happens to have two elements sharing .x class and the SDK side targets one of them via [data-hf-id="hf-X"], the resolver may map them to different ids and the diff still flags present/absent. Probably rare for studio-emitted templates (one tween per data-hf-id), but worth a sentence in the docstring noting "first-match heuristic — ambiguous selectors may not unify." Or upgrade to querySelectorAll + uniqueness check. Question, not blocker.
• sdkShadowGsapFidelity.ts:48-58 tweenKey collision risk — two tweens on the same resolved element + method + position with different properties (e.g. tl.to(x, {opacity:1, duration:0.5}, 0) + tl.to(x, {scale:2, duration:1}, 0)) would map to the same key; the second map.set overwrites the first and the fidelity diff under-reports. Narrow real-world hit (GSAP authors rarely write coincident tweens on one target), but the fail-mode is silent. Worth a comment noting the assumption "one tween per (element, method, position) — coincident tweens collapse," or a key disambiguator on the props hash if you've seen this in the wild.
• kebabToCamel (sdkShadow.ts:120-122) — regex replace(/-([a-z])/g, ...) handles standard CSS props but doesn't handle vendor prefixes properly (-webkit-transform → WebkitTransform is the correct DOM form, regex gives WebkitTransform too — actually fine). Confirming: -webkit-transform → WebkitTransform via the regex. Works. (Not a finding — just verified.)
• useSdkSession.ts:25-32 docstring is the right shape — explicitly calls out "must re-add persist TOGETHER WITH self-write suppression" for any future cutover. Excellent prereq fencing for whoever picks up Step 3c.
Per established lane split with @rames Jusso (the OG): I review at --comment; he handles stamps. He has the go-ahead to approve from me — Miga's approve already satisfies the human-reviewer bar, and the two questions above are non-blocking design notes the author can address in a follow-up or wave off.
— Rames D Jusso
Per review (Rames, non-blocking): name the two silent fail-modes in the GSAP fidelity diff rather than build speculative disambiguators (not observed in studio-emitted templates). - tweenKey: coincident tweens (same element+method+position) collapse, last wins. Props can't join the key — a matched pair must share a key for the field-diff to run. Upgrade path: property-name hash. - makeSelectorResolver: first-match heuristic; ambiguous shared-class selectors may misunify. Upgrade path: querySelectorAll + uniqueness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @james-russo-rames-d-jusso — both notes addressed in
vendor-prefix + |
Five fixes that make SDK shadow telemetry (#1473/#1474) work end-to-end. All found via live E2E testing of
hyperframes preview(browser-use driving the studio + a PostHog network-capture shim); none caught by the unit tests. Every fix verified live.Session never opened in master view —
useSdkSessiongotactiveCompPath=null(master = entry comp); guard bailed → SDK session never opened in the default editing surface → all shadow taps no-op → zero telemetry. Fix:activeCompPath ?? "index.html". (phase: skipped_no_ids → opened)Property kebab/camel false-mismatch — inline-style parity read
flat.styles[op.property](kebab) butinlineStylesare camelCase → null read-back → false mismatch on every hyphenated CSS prop (background-color, …). Fix: kebab→camel read-back. (live color editmc:1 → 0)op:deletenever fired for clips — the Delete hotkey routes tohandleTimelineElementDelete(not shadow-wired) for any timeline element, returning beforehandleDomEditElementDelete. Fix: taprunShadowDeletein the timeline-delete path. (clip delete →op:delete mc:0)gsap_fidelityselector-form divergence on add — the SDK writes[data-hf-id="X"]selectors, the server writes.class; id-keyed diff saw them as different tweens →mc:2on every add. Fix: key by resolved element + method + position. (addmc:2 → 0)Property false-mismatch on
data-hf-studio-*markers — canvas-drag/path-offset edits emit attribute ops the parity double-prefixed (data-data-hf-…) and the SDK model excludes (data-hf-*). Fix:attrName()(no double-prefix) +isShadowableOp()dropsdata-hf-*markers. (drag →op:property mc:0)Live op matrix (all clean after fixes)
property (color/bg/path-offset), timing (drag + input), delete (clip), gsap (add/update/remove/keyframe), gsap_fidelity (add/update/remove) — all
dispatched:true mismatchCount:0.🤖 Generated with Claude Code