chore: add notebook/MCP telemetry events - #593
Open
emrberk wants to merge 2 commits into
Open
Conversation
Collaborator
Author
Review: PR #593 —
|
| # | Issue | Category | Severity | Location | Description | Repro | Suggested fix |
|---|---|---|---|---|---|---|---|
| 1 | Pending-queue widens start()/stop() pipeline race |
Async, timers & cancellation | Moderate | in-diff — ConsoleEventTracker/index.ts:31-45; enabler out-of-diff in sendPipeline.ts:161-173 (non-idempotent startPipeline) |
start() sets started=true, then awaits trimToMaxRows + a new loop of up-to-100 await putEvent, then calls startPipeline(config) with no started re-check. stop() is synchronous. If a telemetry-disable (or config-id change) lands inside that now-~100×-wider window: (a) the uploader starts after opt-out (uploads events despite disabled telemetry), and (b) startPipeline sets intervalId=setInterval without clearing the previous one, so an enable→disable→enable leaks the first 5-second interval permanently (duplicate uploads). Queued events can also be written to IDB after stop(). |
Enable telemetry; while start() is still flushing (large IDB store widens this), toggle telemetry off (and on). Uploader runs post-disable / two intervals run. |
Re-check liveness after each await before startPipeline (capture a generation token at entry or if (!started) return after the trim, inside the loop, and before startPipeline); and have startPipeline clear any existing intervalId first. |
| 2 | tab.export payload type value changed "notebook"→"Notebook" |
Cross-context caller impact | Moderate | in-diff — NotebookToolbar.tsx:284 (BufferType.NOTEBOOK="Notebook", buffers.ts:39) |
Old code emitted { type: "notebook" } (lowercase literal); new code emits { type: BufferType.NOTEBOOK } = "Notebook". Any downstream analytics query grouping/filtering tab.export by type=='notebook' silently drops to zero — a discontinuity in an existing time series, with no compile-time signal. Likely an intentional normalization (now matches tab.add/tab.archive/tab.rename), but it's a wire-value change shipped under "no functional changes." (tabs.tsx:131 still emits TAB_EXPORT with no type, so the dimension stays partly inconsistent.) |
Open a notebook → Export in the notebook toolbar → inspect queued tab.export: props.type is now "Notebook". |
Keep the enum (good for consistency) but call out the value change in the PR description so analytics owners union type IN ('notebook','Notebook'); consider adding type to the tabs.tsx export-all emit too. |
| 3 | MCP connection state-machine telemetry has zero test coverage | Test review & coverage | Moderate | in-diff — MCPBridgeClient.test.ts not updated |
The PR's most bug-prone change — the hadSession/everHadSession machine and the three emits (MCP_CONNECTED/MCP_DISCONNECTED/MCP_CONNECT_FAILED) — ships with no assertions, though the test file already exercises every branch and mocking trackEvent is trivial. Connect-once, disconnect-exclusion-on-explicit-close, and the failed-vs-disconnected split could all regress silently. |
N/A (coverage gap). | Add vi.mock("../../modules/ConsoleEventTracker") cases: two handshakes on one client → MCP_CONNECTED once; disconnect() → no MCP_DISCONNECTED; terminal close before any hello → MCP_CONNECT_FAILED only; terminal close after handshake → MCP_DISCONNECTED{terminal:true}; give-up after MAX reconnects → MCP_CONNECT_FAILED. |
| 4 | mcp.connected/disconnected asymmetry + connect_failed conflation |
Query execution & data integrity (telemetry semantics) | Minor | in-diff — MCPBridgeClient.ts:345-346,364-370,455-457 |
MCP_CONNECTED is gated by everHadSession → fires once per client, but a client auto-reconnects without a new instance, so reconnects emit no CONNECTED while each drop emits MCP_DISCONNECTED. Result: disconnected count exceeds connected; the two can't be paired. Separately, MCP_CONNECT_FAILED fires both on a never-connected terminal close and on reconnect-budget exhaustion after a real session, so it can't be read as an initial-connect failure rate. |
Flaky bridge: connect → drop → reconnect → drop yields CONNECTED×1, DISCONNECTED×2, and a later give-up logs CONNECT_FAILED. | Confirm intended. If reconnects should be visible, emit MCP_CONNECTED on every hello_ack (or add a reconnect flag); split the post-session give-up into a distinct event (e.g. mcp.reconnect_failed). |
| 5 | Consent-window events buffered and flushed on later enable | Persistence & security/privacy | Minor | in-diff — ConsoleEventTracker/index.ts:16-19 + gate QuestProvider/index.tsx:143-150 |
Pre-PR, trackEvent before start() was a hard no-op (dropped). Now events buffer (≤100) and are flushed to IDB + sent if telemetry later transitions disabled→enabled (enabled/id deps can flip at runtime). Activity from a telemetry-off/undetermined window can be transmitted retroactively. Payloads are bounded enums/counts (no sensitive data), so severity is low, but it's a consent-semantics change. Also: queued events get their created stamped at flush time (db.ts:15), losing original timing. |
Disable telemetry → perform notebook/MCP actions → re-enable → buffered off-window events are flushed. | Confirm with privacy. If off-window events must not be sent, drop pending on any disabled state (not just stop()), or gate buffering on a "telemetry not yet determined" flag rather than !started. |
| 6 | notebook.duplicate counts attempts; notebook.create counts successes |
Cross-context (telemetry semantics) | Minor | in-diff — NotebookToolbar.tsx:270-276 |
NOTEBOOK_DUPLICATE fires before the await duplicateNotebook, so it counts even when duplication fails (e.g. MAX_TABS → toast, no notebook). NOTEBOOK_CREATE fires only inside addBuffer(...).then(b => if(b)) (success-only). The two "notebook made" events mean different things (attempt vs. success). |
At the tab limit, click Duplicate → notebook.duplicate fires but no notebook is created. |
Move NOTEBOOK_DUPLICATE into the success branch after duplicateNotebook resolves truthy, mirroring NOTEBOOK_CREATE. |
| 7 | notebook.markdown_apply over-counts on blur |
React correctness (telemetry semantics) | Minor | in-diff — MarkdownCell.tsx:288-320 |
apply("blur") fires on any focused→blur while editing, including when nothing was typed and for freshly-added empty markdown cells (which open in edit mode). Inflates the "apply" metric. Mirrors the pre-existing unconditional user_updated_cell emit, so behavior is consistent, but the metric is noisy. |
Add a markdown cell, click away without typing → notebook.markdown_apply{method:"blur"} fires. |
If "apply" should mean a real edit, gate the blur emit on actual content change (compare draft vs. committed) before trackEvent. |
| 8 | Code-quality nits | Code structure, readability & types | Minor | in-diff | (a) MCPBridgeProvider repeats the identical {outcome:"aborted", durationMs: Date.now()-startedAt} block twice and durationMs 4× in one handler — extract a local trackOutcome(extra) closure. (b) classifyToolResult's return type {outcome; reasonCode?} structurally permits {outcome:"ok", reasonCode:"stale"} it never builds — a discriminated union would encode the invariant. (c) ChartSettingsDrawer Escape path inlines its own cancel emit instead of reusing dismiss(), and the dismiss method union omits "escape". (d) hadSession is a present-state flag with a past-tense name (hasSession/sessionLive reads clearer). |
N/A | Apply the four small refactors above. |
False-positives
Verified against source and dismissed (draft findings that did not survive):
start()flush loop wedges telemetry if aputEventrejects (fresh-adversarial add test for query run when cursor position is next to ending semicolon #4):putEventandtrimToMaxRowswrap their bodies intry/catchand return a boolean (db.ts:10-21,57-70) — they never reject, sostart()cannot abort mid-flush and always reachesstartPipeline. (The interleaving race in wip test for error range #1 is real; this rejection-wedge is not.)Cell.tsxonResizeEnd/onDoubleClickinline arrows cause a re-render regression: the receivingResizeHandleis a plainReact.FC, notReact.memo, and the previous callbacks (maximizedChartResizeEnd,resetToDefaults) were recreated every render anyway. No memoization to break.getCellsSnapshotadded touseCellWrapperInteractionsdeps destabilizesonKeyDown:useNotebookActions()returns acreateStableActionProxymemoized with[];getCellsSnapshotis a stable proxy key. Reference is constant for the provider's life.handleDragStop/handleResizeStopmissing deps → stale layout write: deps are complete;cellsis in the resize deps;react-hooks/exhaustive-depslint passes.MarkdownCell.applystale closure / button+blur double-fire: deps[bufferIdForEvents, cell.id]are complete; the button/shortcut paths don't changeisFocused, andsetEditing(false)disarms the blur effect — single fire per real transition.setStateafter unmount inaddBuffer().then(): the.thenbodies contain onlyvoid trackEvent(...); nosetState.- MCP
toolCalltelemetry alters abort/deadline logic or the bridge result:deadlineTimer/inflightAborterscleanup infinallyis unchanged,sendToolResultpayload is byte-identical, andclassifyToolResultcan't throw, so the success path can't divert intocatch. classifyToolResultthrows on empty/non-text content:contentis alwaysToolContent[];firstText'sfor…ofis empty-safe →"".classifyToolResultmisclassifies due to permission-notice prepend/append: the prepend is gated on!result.isErrorand every denial isisError:true, so the deny prefix always stays at index 0; the<since_last_check>append only adds at the end, preservingstartsWith.MCP_CONNECT_FAILEDdouble-fires for one failure: the terminal-close branchreturns beforescheduleReconnect;scheduleReconnectemits once at the threshold and then stops scheduling.- Privacy leak in a payload / token leak in MCP telemetry: audited all ~55 added payloads — bounded enums/counts/booleans only;
classifyToolResultandapplyNotebookStateLayoutModeconstruct new bounded objects (never spread result text or tool args);MCP_DISCONNECTEDcarries only{closeCode, terminal}; the pairing token is never referenced by anytrackEvent. - Grid-mode resize
regiontelemetry never fires (grid item lacksdata-axis):renderEdgeHandleattaches the RGLrefto the same<Handle>element that carriesdata-axis(EdgeHandle.tsx:141,144), and RGL forwards that node asonResizeStop's 6th arg (react-grid-layoutdist confirmed) —element.dataset.axisresolves correctly. - Dexie schema/version bump needed / MAX_EVENTS overshoot: no table-shape change (the
eventstable pre-exists); the transient 10,100-row overshoot is self-correcting and consistent with the existing startup-only trim. - Context provider re-render storm introduced:
MCPBridgeProvider'svaluememo is untouched; no new state feeds any context value; module-level classifierSets are built once;NotebookToolbar's addedcellsdestructure reads an already-subscribed full-object context. NOTEBOOK_CELL_RUN/NOTEBOOK_CELL_SIZE_RESET/ onboarding open+close double-fire: verified mutually-exclusive early-returns and that the customCloseButtonis a plainstyled.button(programmaticsetOpen(false)doesn't trigger RadixonOpenChange).
Summary
Verdict: Approve with minor changes (nothing blocking; two Moderate items worth addressing before merge).
- Regressions/tradeoffs: The PR's "no functional changes" claim is almost accurate. The two real behavioral deltas are (init react components package #2) the
tab.exporttelemetry value change and (Console autocomplete tests #5) the consent-window buffering change — both are analytics/consent-facing, not user-facing. The only genuine defect risk is (wip test for error range #1), a latentstart()/stop()race the pending queue amplifies; reachability is narrow (fast telemetry toggle at startup) but it's consent-relevant and the fix is trivial. The privacy contract holds fully. - Verification: 20 draft findings verified → 8 confirmed, 12+ dismissed as false positives (listed above). The fresh-context adversarial pass independently corroborated wip test for error range #1, init react components package #2, add test for query run when cursor position is next to ending semicolon #4, Console autocomplete tests #5 and added add
web-consolepackage #6, raising confidence the structured review didn't miss a major defect. - In-diff vs out-of-diff: 8 in-diff, 0 in-code out-of-diff. This is expected here and not a cross-context underrun — Agent 1+12 walked every changed exported symbol's callsites (
bufferTypeOf×3,McpSetupCommand,permissionLevelOf, themcpToolEventsutil, the 4 newly-exporteddenyReason*fns) and confirmed each is safe; the only cross-boundary impact is on downstream analytics dashboards (non-code), captured in init react components package #2. - Quality gates:
yarn typecheck✅ ·yarn lint✅ ·yarn build✅ ·yarn test:unit✅ (83 files, 1738 tests). No quality-check findings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds console telemetry for notebooks and the MCP bridge so we can see how these features are actually used. No functional changes — every emit is a fire-and-forget
trackEventwrapped around existing handlers, batched through the existing IndexedDB send pipeline.What's tracked
Notebook interactions — create/duplicate, layout mode, cell add/delete/duplicate/move/rename/run/draw, resize and size-reset (gesture end only, never per tick), view changes and maximize, chart settings (open/save/cancel, save-blocked, type change, reset-to-auto), markdown apply, variables popover, result-tab switching, onboarding modal funnel (open/step/close, including Escape/overlay dismissals).
MCP lifecycle — pairing prompted/accepted/declined (with granted permission level), manual pair, connect/disconnect/connect-failed, permissions changes, setup-command copy, agent-changes popper, status popover. Intentional teardowns (user disconnect, re-pair) are excluded from
mcp.disconnectedso the event only counts real drops.MCP tool calls — one event per call, named per tool (
mcp.add_cell,mcp.run_query, …), emitted after dispatch with an outcome (ok/denied/tool_error/dispatch_error/aborted), a denial reason code where applicable (permission vs. freshness gate), and duration. Unrecognized tool names emitmcp.unknown_toolwith the name capped at 100 chars.Implementation notes
ConsoleEventTracker/mcpToolEvents.ts): it prefix-matches the known denial message constants and deliberately does not parse serialized tool results.🤖 Generated with Claude Code