fix(core): guard timeline method calls for non-conformant objects#1098
Conversation
User compositions can register timeline-like objects on window.__timeline where .duration is a number property (not a function) and .pause/.play may be missing entirely. The runtime player called these unconditionally, causing ~166 "duration is not a function" and ~38 "pause is not a function" errors per day. Add safeNum() and safeVoid() helpers that check typeof before calling, falling back to reading numbers as properties and silently skipping missing void methods. Applied consistently across all timeline method call sites in player.ts.
vanceingalls
left a comment
There was a problem hiding this comment.
Review: fix(core): guard timeline method calls for non-conformant objects
Design is sound. The two helpers are well-scoped, the call sites are exhaustive, and the test coverage closes the cases that caused the PostHog noise. Approving — this ships the P0. Two items to carry forward:
[important] safeNum / safeVoid produce zero observability for non-conformant timelines
After this lands, the ~204 errors/day vanish from PostHog. That's the goal. But the team will now have no signal on:
- Which user compositions are non-conformant and which properties are missing
- Whether that population is growing or regressing
- If rate doubles tomorrow
The diagnostics.ts swallow() helper exists exactly for this — its own JSDoc says "emitting nothing makes silent failures invisible to anyone debugging a genuinely broken composition." It's already used two lines down for sibling errors (runtime.player.site1, runtime.player.site2), and it's already imported in this file.
A minimal fix inside each helper:
// safeNum — when the property is not a function and not a number
swallow("runtime.player.nonConformantNum", { prop, actual: typeof val });
return fallback;
// safeVoid — when the method is not callable
swallow("runtime.player.nonConformantVoid", { method, actual: typeof fn });This costs nothing in production (the swallow path is silent unless __hfDebug is set or __hf.onSwallowed is installed), but gives the studio and observability pipeline a hook to surface which compositions are hitting this. The error-rate chart on the AI Studio Health dashboard would then show a real "non-conformant calls silenced" counter instead of a gap. Can be a follow-up PR.
[nit] safeNum: number branch doesn't guard NaN
if (typeof val === "number") return val;typeof NaN === "number" is true. So duration = NaN flows straight through, while the function branch does Number(val.call(obj)) || fallback which correctly maps NaN to fallback. Current call sites are all wrapped in Math.max(0, ...) || 0 so NaN can't escape, but the helper itself is inconsistent. Suggest:
if (typeof val === "number" && Number.isFinite(val)) return val;Everything else checks out — sibling propagation is preserved, all 35 existing tests still pass, the 4 new ones cover every non-conformant shape observed in the PostHog errors, and all required CI is green. Ship it.
— Vai
Summary
window.__timelinewhere.durationis a number property (not a function) and.pause/.playmay be missing entirely. The runtime player called these unconditionally, producing ~166t.getTimeline(...).duration is not a functionand ~38t.pause is not a functionerrors per day.safeNum()andsafeVoid()helpers inplayer.tsthat checktypeofbefore calling — reading numbers as direct property values when not functions, and silently skipping missing void methods. Applied across all timeline call sites.Test plan
bun test packages/core/src/runtime/player.test.ts— 39 tests pass (35 existing + 4 new)bun run build— cleanbunx oxlint+bunx oxfmt --check— clean