From d74afc7b7dc620036439b398c6bca823fb3911b4 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 29 Jul 2026 22:55:12 -0700 Subject: [PATCH 1/3] feat(engine): measure live DOM size on every render, not just probed ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The short-comp routing gate can only read a live element count when a probe session exists, and the first v0.7.83 data shows that is far rarer than estimated: 17% of renders (86/503), not the ">=28%" the video-presence proxy suggested. The other 83% fall back to a static source scan, which is exactly blind to the shape that motivated the live count — small markup, thousands of script-created nodes. That leaves the fleet element-count distribution unknowable for most renders, and the observed distribution is already surprising: p99 ~900, max 1,420 against a 2,500 ceiling calibrated on 7k/20k/40k synthetic nodes. Either the ceiling is close to irrelevant, or the large-DOM tail is hiding in the 83% we cannot see. Both readings change what PR B should do, and neither is decidable from probed renders alone (they are a biased sample — they got a probe *because* they carry media or unresolved compositions). So measure it where every render already goes: capture-session init. `collectSessionInitTelemetry` gains a querySelectorAll("*") count beside the tween count it already collects, riding the same channel to `observability_init_element_count`. This is observational only — capture has begun, far too late to route on — and it deliberately does not feed the gate. It answers the distribution question the gate cannot. Coverage for this channel is proven rather than assumed: the tween-count fix that shipped in v0.7.83 took the clamped-parallel bucket from 0/272 renders to 217/217, and 23.1% -> 100% overall. Co-Authored-By: Claude Opus 5 (1M context) --- packages/cli/src/telemetry/events.ts | 2 ++ .../cli/src/telemetry/renderObservability.ts | 1 + packages/engine/src/services/frameCapture.ts | 25 ++++++++++++++++--- packages/engine/src/types.ts | 8 ++++++ .../src/services/render/observability.test.ts | 25 +++++++++++++++---- .../src/services/render/observability.ts | 17 +++++++++++-- .../src/services/renderOrchestrator.test.ts | 14 ++++++++--- .../src/services/renderOrchestrator.ts | 24 +++++++++++++++--- 8 files changed, 99 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/telemetry/events.ts b/packages/cli/src/telemetry/events.ts index 3bc3db5e84..ad7bd9e1bb 100644 --- a/packages/cli/src/telemetry/events.ts +++ b/packages/cli/src/telemetry/events.ts @@ -98,6 +98,7 @@ export interface RenderObservabilityTelemetryPayload { observabilityExtractCacheMisses?: number; observabilityInitDurationMs?: number; observabilityInitTweenCount?: number; + observabilityInitElementCount?: number; } function renderObservabilityEventProperties(props: RenderObservabilityTelemetryPayload) { @@ -157,6 +158,7 @@ function renderObservabilityEventProperties(props: RenderObservabilityTelemetryP observability_extract_cache_misses: props.observabilityExtractCacheMisses, observability_init_duration_ms: props.observabilityInitDurationMs, observability_init_tween_count: props.observabilityInitTweenCount, + observability_init_element_count: props.observabilityInitElementCount, }; } diff --git a/packages/cli/src/telemetry/renderObservability.ts b/packages/cli/src/telemetry/renderObservability.ts index 362837ae8a..c802edf6c1 100644 --- a/packages/cli/src/telemetry/renderObservability.ts +++ b/packages/cli/src/telemetry/renderObservability.ts @@ -66,6 +66,7 @@ export function renderObservabilityTelemetryPayload( observabilityExtractCacheMisses: extraction?.cacheMisses, observabilityInitDurationMs: init?.initDurationMs, observabilityInitTweenCount: init?.tweenCount, + observabilityInitElementCount: init?.elementCount, }; } diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index 0a8e9bc55c..1d5f796b9d 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -119,6 +119,8 @@ export interface CaptureSession { initTelemetry?: { initDurationMs: number; tweenCount: number; + /** Live DOM element count at end of init — observational; see collectSessionInitTelemetry. */ + elementCount: number; }; capturePerf: { frames: number; @@ -406,8 +408,24 @@ function appendBrowserDiagnostic(session: CaptureSession, text: string): void { async function collectSessionInitTelemetry( page: Page, initStart: number, -): Promise<{ initDurationMs: number; tweenCount: number }> { +): Promise<{ initDurationMs: number; tweenCount: number; elementCount: number }> { const initDurationMs = Date.now() - initStart; + // Live DOM size, measured once the init sequence has completed so + // script-generated elements are present. This is the SAME quantity the + // short-comp routing gate wants, but measured here it is observational + // only — capture has already started, so it is far too late to route on. + // Its job is coverage: the routing gate can only read a live count on the + // ~17% of renders that get a probe session, which leaves the fleet + // element-count distribution unknowable for the rest (and hides exactly + // the dangerous shape — small source markup, huge runtime DOM). Every + // render reaches this path, so the distribution becomes readable even + // where the gate stays blind. + let elementCount = 0; + try { + elementCount = await page.evaluate(() => document.querySelectorAll("*").length); + } catch { + elementCount = 0; + } let tweenCount = 0; try { tweenCount = await page.evaluate(() => { @@ -431,7 +449,7 @@ async function collectSessionInitTelemetry( } catch { tweenCount = 0; } - return { initDurationMs, tweenCount }; + return { initDurationMs, tweenCount, elementCount }; } async function recordSessionInitTelemetry( @@ -442,7 +460,7 @@ async function recordSessionInitTelemetry( session.initTelemetry = telemetry; appendBrowserDiagnostic( session, - `[FrameCapture:INIT] complete initDurationMs=${telemetry.initDurationMs} tweenCount=${telemetry.tweenCount}`, + `[FrameCapture:INIT] complete initDurationMs=${telemetry.initDurationMs} tweenCount=${telemetry.tweenCount} elementCount=${telemetry.elementCount}`, ); } @@ -3788,6 +3806,7 @@ export function getCapturePerfSummary(session: CaptureSession): CapturePerfSumma subTimelineWaitOutcome: session.subTimelineWaitOutcome, initDurationMs: session.initTelemetry?.initDurationMs, initTweenCount: session.initTelemetry?.tweenCount, + initElementCount: session.initTelemetry?.elementCount, warnings: cloneCaptureWarnings(session.warnings), staticDedupReused: session.staticDedupCount ?? 0, staticDedupEnabled: session.staticDedupEnabled ?? false, diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts index dd5faa5e6b..c25e2a6cad 100644 --- a/packages/engine/src/types.ts +++ b/packages/engine/src/types.ts @@ -253,6 +253,14 @@ export interface CapturePerfSummary { initDurationMs?: number; /** GSAP tween count at init — the motion-axis signal for capture routing analysis. */ initTweenCount?: number; + /** + * Live DOM element count at end of init. Observational counterpart to the + * short-comp routing gate's own count: the gate can only measure the ~17% + * of renders that get a probe session, so without this the fleet + * element-count distribution — and any large-runtime-DOM tail — stays + * invisible for the rest. + */ + initElementCount?: number; /** Correctness warnings observed before or during capture. */ warnings?: CaptureWarning[]; /** diff --git a/packages/producer/src/services/render/observability.test.ts b/packages/producer/src/services/render/observability.test.ts index 3e9070c63d..8210a2e3d2 100644 --- a/packages/producer/src/services/render/observability.test.ts +++ b/packages/producer/src/services/render/observability.test.ts @@ -416,18 +416,33 @@ describe("init observability fallback (parallel workers)", () => { const summary = makeRecorder().summary({ lastBrowserConsole: ["[FrameCapture:NAV] page.goto start"], capture: { forceScreenshot: false, captureMode: "screenshot" }, - initFallback: { initDurationMs: 850, tweenCount: 1200 }, + initFallback: { initDurationMs: 850, tweenCount: 1200, elementCount: 3400 }, }); - expect(summary.init).toEqual({ initDurationMs: 850, tweenCount: 1200 }); + expect(summary.init).toEqual({ initDurationMs: 850, tweenCount: 1200, elementCount: 3400 }); }); it("max-merges console INIT lines over the fallback, matching multi-session semantics", () => { const summary = makeRecorder().summary({ - lastBrowserConsole: ["[FrameCapture:INIT] complete initDurationMs=1234 tweenCount=42"], + lastBrowserConsole: [ + "[FrameCapture:INIT] complete initDurationMs=1234 tweenCount=42 elementCount=5000", + ], + capture: { forceScreenshot: false, captureMode: "screenshot" }, + initFallback: { initDurationMs: 850, tweenCount: 1200, elementCount: 3400 }, + }); + expect(summary.init).toEqual({ initDurationMs: 1234, tweenCount: 1200, elementCount: 5000 }); + }); + + // The single-session path has no structured fallback — it parses the console + // line only. This is the path that covers renders the routing gate cannot + // measure, so the element count must survive it. + it("parses elementCount from the console INIT line with no fallback at all", () => { + const summary = makeRecorder().summary({ + lastBrowserConsole: [ + "[FrameCapture:INIT] complete initDurationMs=90 tweenCount=7 elementCount=1420", + ], capture: { forceScreenshot: false, captureMode: "screenshot" }, - initFallback: { initDurationMs: 850, tweenCount: 1200 }, }); - expect(summary.init).toEqual({ initDurationMs: 1234, tweenCount: 1200 }); + expect(summary.init).toEqual({ initDurationMs: 90, tweenCount: 7, elementCount: 1420 }); }); it("stays undefined when neither source has anything", () => { diff --git a/packages/producer/src/services/render/observability.ts b/packages/producer/src/services/render/observability.ts index 925f9fcfb0..5a734e6e66 100644 --- a/packages/producer/src/services/render/observability.ts +++ b/packages/producer/src/services/render/observability.ts @@ -182,6 +182,15 @@ export interface RenderExtractionObservability { export interface RenderInitObservability { initDurationMs?: number; tweenCount?: number; + /** + * Live DOM element count at end of capture-session init. Observational: + * measured after routing has already been decided, so it cannot gate — it + * exists because the routing gate's own count is only available on the + * ~17% of renders that get a probe session, leaving the fleet + * element-count distribution (and any large-runtime-DOM tail) unreadable + * for the rest. + */ + elementCount?: number; } export interface RenderObservabilitySummary { @@ -299,13 +308,17 @@ function summarizeInitObservability( // let the console parse (same max semantics) refine it. let initDurationMs: number | undefined = fallback?.initDurationMs; let tweenCount: number | undefined = fallback?.tweenCount; + let elementCount: number | undefined = fallback?.elementCount; for (const line of lines) { if (!line.includes("[FrameCapture:INIT]")) continue; initDurationMs = maxReading(initDurationMs, readUnsignedIntAfter(line, "initDurationMs=")); tweenCount = maxReading(tweenCount, readUnsignedIntAfter(line, "tweenCount=")); + elementCount = maxReading(elementCount, readUnsignedIntAfter(line, "elementCount=")); + } + if (initDurationMs === undefined && tweenCount === undefined && elementCount === undefined) { + return undefined; } - if (initDurationMs === undefined && tweenCount === undefined) return undefined; - return { initDurationMs, tweenCount }; + return { initDurationMs, tweenCount, elementCount }; } // fallow-ignore-next-line complexity diff --git a/packages/producer/src/services/renderOrchestrator.test.ts b/packages/producer/src/services/renderOrchestrator.test.ts index 752e65d95d..c3213d23e1 100644 --- a/packages/producer/src/services/renderOrchestrator.test.ts +++ b/packages/producer/src/services/renderOrchestrator.test.ts @@ -1936,17 +1936,25 @@ describe("shouldPreferSingleWorkerDrawElement (DE priority inversion)", () => { it("max-merges across workers and ignores workers that reported nothing", () => { expect( mergeWorkerInitObservability([ - { initDurationMs: 400, initTweenCount: 900 }, + { initDurationMs: 400, initTweenCount: 900, initElementCount: 1200 }, {}, - { initDurationMs: 1250, initTweenCount: 880 }, + { initDurationMs: 1250, initTweenCount: 880, initElementCount: 1190 }, ]), - ).toEqual({ initDurationMs: 1250, tweenCount: 900 }); + ).toEqual({ initDurationMs: 1250, tweenCount: 900, elementCount: 1200 }); }); it("returns undefined when no worker reported — summary.init must stay absent, not zeroed", () => { expect(mergeWorkerInitObservability([])).toBeUndefined(); expect(mergeWorkerInitObservability([{}, {}])).toBeUndefined(); }); + + it("surfaces an element count even when a worker reported nothing else", () => { + expect(mergeWorkerInitObservability([{ initElementCount: 4000 }])).toEqual({ + initDurationMs: undefined, + tweenCount: undefined, + elementCount: 4000, + }); + }); }); describe("countElementTags", () => { diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 777771a9bd..63e45932a6 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -1345,10 +1345,15 @@ export async function resolveCompositionElementCount( * initializes before the timeline is fully wired, not an expected disagreement. */ export function mergeWorkerInitObservability( - perfs: ReadonlyArray<{ initDurationMs?: number; initTweenCount?: number }>, -): { initDurationMs?: number; tweenCount?: number } | undefined { + perfs: ReadonlyArray<{ + initDurationMs?: number; + initTweenCount?: number; + initElementCount?: number; + }>, +): { initDurationMs?: number; tweenCount?: number; elementCount?: number } | undefined { let initDurationMs: number | undefined; let tweenCount: number | undefined; + let elementCount: number | undefined; for (const perf of perfs) { if (perf.initDurationMs !== undefined) { initDurationMs = @@ -1360,9 +1365,20 @@ export function mergeWorkerInitObservability( tweenCount = tweenCount === undefined ? perf.initTweenCount : Math.max(tweenCount, perf.initTweenCount); } + // Max across workers: every worker loads the same composition, so they + // should agree — max is defensive against a worker sampled before its + // init script finished populating the DOM. + if (perf.initElementCount !== undefined) { + elementCount = + elementCount === undefined + ? perf.initElementCount + : Math.max(elementCount, perf.initElementCount); + } + } + if (initDurationMs === undefined && tweenCount === undefined && elementCount === undefined) { + return undefined; } - if (initDurationMs === undefined && tweenCount === undefined) return undefined; - return { initDurationMs, tweenCount }; + return { initDurationMs, tweenCount, elementCount }; } /** From c61a24b510b022fb7956caa089ee093f87053f96 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 29 Jul 2026 23:51:14 -0700 Subject: [PATCH 2/3] fix(producer): unbias the static element count and stop zeroing failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on #2891. Two of them bite directly on this PR's own purpose — making the fleet element-count distribution readable — so they are fixed rather than noted. countElementTags counted `"` or a template literal building `` inflated the count once per occurrence. Compiled comps embed large inline scripts, so the bias is systematic, not noise, and it lands entirely on the ~83% of renders with no probe session — precisely the cohort this PR exists to characterize. Script and style bodies are now stripped before matching; losing their own closing tags costs 1-2 counts against a threshold in the thousands. The new elementCount fell back to 0 when its page.evaluate threw, following the tweenCount pattern beside it. For this field that pattern is wrong: evaluate failures concentrate on the huge-DOM compositions the field is meant to observe, and a 0 there is indistinguishable from a legitimately empty comp, so the fleet p50/p99 would absorb both silently. It is now undefined on failure, the INIT console line omits the token entirely rather than emitting a zero, and the parser reports absent — mirroring the live/static provenance split the routing resolver already uses. Also documented: the "every render reaches this path" claim holds only for renders that survive to end of init, so the tail is survivor-biased and should be read as a lower bound; and the two element-count fields now say plainly which is which — composition_element_count gates routing, observability_init_element_count is the observational counterpart — so the follow-up analysis can't query the wrong one. Nits: envInt is integer-only per its name, both live-DOM reads use getElementsByTagName (live collection length, no NodeList materialized on the 40k-node tail), and the attribution block notes that it runs with routing off by design. Fault injection confirms the new tests bite: disabling script stripping fails 4, and the zero-vs-undefined case is pinned separately. Co-Authored-By: Claude Opus 5 (1M context) --- packages/engine/src/services/frameCapture.ts | 39 ++++++++++++++----- packages/engine/src/types.ts | 22 ++++++++--- .../src/services/render/observability.test.ts | 13 +++++++ .../src/services/render/observability.ts | 18 ++++++--- .../src/services/renderOrchestrator.test.ts | 32 +++++++++++++-- .../src/services/renderOrchestrator.ts | 25 ++++++++++-- 6 files changed, 122 insertions(+), 27 deletions(-) diff --git a/packages/engine/src/services/frameCapture.ts b/packages/engine/src/services/frameCapture.ts index 1d5f796b9d..28d6df0d6a 100644 --- a/packages/engine/src/services/frameCapture.ts +++ b/packages/engine/src/services/frameCapture.ts @@ -119,8 +119,8 @@ export interface CaptureSession { initTelemetry?: { initDurationMs: number; tweenCount: number; - /** Live DOM element count at end of init — observational; see collectSessionInitTelemetry. */ - elementCount: number; + /** Live DOM element count at end of init; undefined when the measurement itself failed. Observational — see collectSessionInitTelemetry. */ + elementCount?: number; }; capturePerf: { frames: number; @@ -408,7 +408,7 @@ function appendBrowserDiagnostic(session: CaptureSession, text: string): void { async function collectSessionInitTelemetry( page: Page, initStart: number, -): Promise<{ initDurationMs: number; tweenCount: number; elementCount: number }> { +): Promise<{ initDurationMs: number; tweenCount: number; elementCount?: number }> { const initDurationMs = Date.now() - initStart; // Live DOM size, measured once the init sequence has completed so // script-generated elements are present. This is the SAME quantity the @@ -417,14 +417,30 @@ async function collectSessionInitTelemetry( // Its job is coverage: the routing gate can only read a live count on the // ~17% of renders that get a probe session, which leaves the fleet // element-count distribution unknowable for the rest (and hides exactly - // the dangerous shape — small source markup, huge runtime DOM). Every - // render reaches this path, so the distribution becomes readable even - // where the gate stays blind. - let elementCount = 0; + // the dangerous shape — small source markup, huge runtime DOM). + // + // Coverage caveat, since the "every render reaches this" claim is easy to + // over-read: every render that SURVIVES TO END OF INIT reaches it. Renders + // that die in browser launch, navigation, or OOM before init completes + // never emit — and on the huge-DOM tail this field exists to characterize, + // those are disproportionately the ones that fail early. The distribution + // is therefore survivor-biased toward smaller comps; treat the tail as a + // lower bound (review finding). + // Deliberately `undefined` (not 0) when the measurement fails, breaking + // from the tweenCount fallback pattern directly above. A page.evaluate + // crash during init is most likely on exactly the huge-DOM compositions + // this field exists to observe, and a 0 there is indistinguishable from a + // legitimately empty comp — the fleet p50/p99 would silently absorb both + // (review finding). Mirrors the live/static provenance split the routing + // resolver already uses: "not measured" must not read as "measured small". + // getElementsByTagName over querySelectorAll: a live HTMLCollection's + // length avoids materializing a 40k-node static NodeList on the tail this + // is here to characterize. + let elementCount: number | undefined; try { - elementCount = await page.evaluate(() => document.querySelectorAll("*").length); + elementCount = await page.evaluate(() => document.getElementsByTagName("*").length); } catch { - elementCount = 0; + elementCount = undefined; } let tweenCount = 0; try { @@ -460,7 +476,10 @@ async function recordSessionInitTelemetry( session.initTelemetry = telemetry; appendBrowserDiagnostic( session, - `[FrameCapture:INIT] complete initDurationMs=${telemetry.initDurationMs} tweenCount=${telemetry.tweenCount} elementCount=${telemetry.elementCount}`, + `[FrameCapture:INIT] complete initDurationMs=${telemetry.initDurationMs} tweenCount=${telemetry.tweenCount}` + + // Omitted rather than zeroed when unmeasured, so the parser reports + // absent instead of inventing an empty DOM. + (telemetry.elementCount === undefined ? "" : ` elementCount=${telemetry.elementCount}`), ); } diff --git a/packages/engine/src/types.ts b/packages/engine/src/types.ts index c25e2a6cad..8b33b2df5a 100644 --- a/packages/engine/src/types.ts +++ b/packages/engine/src/types.ts @@ -254,11 +254,23 @@ export interface CapturePerfSummary { /** GSAP tween count at init — the motion-axis signal for capture routing analysis. */ initTweenCount?: number; /** - * Live DOM element count at end of init. Observational counterpart to the - * short-comp routing gate's own count: the gate can only measure the ~17% - * of renders that get a probe session, so without this the fleet - * element-count distribution — and any large-runtime-DOM tail — stays - * invisible for the rest. + * Live DOM element count at end of init; undefined when the measurement + * failed (never 0 — see collectSessionInitTelemetry). + * + * WHICH FIELD TO QUERY — two element counts exist and they answer + * different questions: + * • `composition_element_count` (+ `_source`) is the ROUTING-RELEVANT + * one. Measured from the PROBE session before the routing decision, + * falling back to a static source scan. That is what the short-comp + * band actually gates on. + * • `observability_init_element_count` (this field) is the + * OBSERVATIONAL counterpart. Measured from the capture session's own + * DOM at end of init, on every surviving render — including the ~83% + * with no probe, where the routing signal is a blind static scan. + * They agree for most comps and diverge for one that mutates its DOM + * between probe launch and capture init. Use this for distribution and + * tail questions; use `composition_element_count` for anything about what + * the router did (review finding). */ initElementCount?: number; /** Correctness warnings observed before or during capture. */ diff --git a/packages/producer/src/services/render/observability.test.ts b/packages/producer/src/services/render/observability.test.ts index 8210a2e3d2..ef78948900 100644 --- a/packages/producer/src/services/render/observability.test.ts +++ b/packages/producer/src/services/render/observability.test.ts @@ -435,6 +435,19 @@ describe("init observability fallback (parallel workers)", () => { // The single-session path has no structured fallback — it parses the console // line only. This is the path that covers renders the routing gate cannot // measure, so the element count must survive it. + // The collector omits `elementCount=` entirely when the page.evaluate that + // measures it failed, rather than emitting 0 — a 0 there is + // indistinguishable from a legitimately empty comp, and evaluate failures + // concentrate on exactly the huge-DOM tail this field exists to observe. + it("leaves elementCount absent when the INIT line omits it (measurement failed)", () => { + const summary = makeRecorder().summary({ + lastBrowserConsole: ["[FrameCapture:INIT] complete initDurationMs=90 tweenCount=7"], + capture: { forceScreenshot: false, captureMode: "screenshot" }, + }); + expect(summary.init).toEqual({ initDurationMs: 90, tweenCount: 7, elementCount: undefined }); + expect(summary.init?.elementCount).not.toBe(0); + }); + it("parses elementCount from the console INIT line with no fallback at all", () => { const summary = makeRecorder().summary({ lastBrowserConsole: [ diff --git a/packages/producer/src/services/render/observability.ts b/packages/producer/src/services/render/observability.ts index 5a734e6e66..6dbbc7143a 100644 --- a/packages/producer/src/services/render/observability.ts +++ b/packages/producer/src/services/render/observability.ts @@ -183,12 +183,18 @@ export interface RenderInitObservability { initDurationMs?: number; tweenCount?: number; /** - * Live DOM element count at end of capture-session init. Observational: - * measured after routing has already been decided, so it cannot gate — it - * exists because the routing gate's own count is only available on the - * ~17% of renders that get a probe session, leaving the fleet - * element-count distribution (and any large-runtime-DOM tail) unreadable - * for the rest. + * Live DOM element count at end of capture-session init; undefined when + * the measurement failed (never 0). Observational: measured after routing + * has already been decided, so it cannot gate — it exists because the + * routing gate's own count is only available on the ~17% of renders that + * get a probe session, leaving the fleet element-count distribution (and + * any large-runtime-DOM tail) unreadable for the rest. + * + * Not interchangeable with `RenderCaptureObservability.compositionElementCount`: + * that one is measured pre-routing from the probe session (or a static + * scan) and is what the band gates on. This one is measured post-routing + * from the capture session and covers renders the gate cannot see. Query + * the former for router behaviour, this for distribution/tail analysis. */ elementCount?: number; } diff --git a/packages/producer/src/services/renderOrchestrator.test.ts b/packages/producer/src/services/renderOrchestrator.test.ts index c3213d23e1..571994f531 100644 --- a/packages/producer/src/services/renderOrchestrator.test.ts +++ b/packages/producer/src/services/renderOrchestrator.test.ts @@ -1994,12 +1994,38 @@ describe("shouldPreferSingleWorkerDrawElement (DE priority inversion)", () => { }); it("does not false-positive on inline-script comparisons or void-prefixed words", () => { - // Only the closer counts: "if (a < b && x ")).toBe( + 0, + ); + }); + + // Review finding: the ` { + expect(countElementTags('
')).toBe( 1, ); + expect( + countElementTags("

"), + ).toBe(1); + }); + + it("strips ')).toBe(1); + }); + + it("strips multiple and attributed script blocks, not just the first", () => { + expect( + countElementTags( + '
', + ), + ).toBe(1); }); it("is stable on empty and malformed input rather than throwing", () => { diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 63e45932a6..8622b7d517 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -1243,7 +1243,10 @@ export function envInt(name: string, fallback: number): number { const raw = process.env[name]; if (raw === undefined || raw.trim() === "") return fallback; const parsed = Number(raw); - return Number.isFinite(parsed) ? parsed : fallback; + // Integer-only, per the name: a fractional threshold would compare + // sensibly against integer counts but silently means something the knob + // never promised, so treat it as a typo and fall back (review nit). + return Number.isInteger(parsed) ? parsed : fallback; } /** @@ -1286,7 +1289,16 @@ export function envInt(name: string, fallback: number): number { * live count and uses this only when no such session exists. */ export function countElementTags(html: string): number { - const matches = html.match( + // Strip inline ", + // which pass 2 removes. A single pass would leave a stray tag behind. + expect(countElementTags("
ipt>alert(1)")).toBe(1); + }); + + it("terminates on input with no closing tag rather than looping", () => { + expect(countElementTags("