VIEW-003: Trigger Chain Debugger
View Type: 🗺️ Meta View (replaces canvas with timeline view)
Overview
An interactive timeline/sequence view that shows the chain of events from a user action through all the nodes it triggers, across component boundaries. Like a "call stack" for visual programming, with the ability to record live interactions or simulate paths.
Estimate: 5-7 days
Priority: HIGH
Complexity: High
Dependencies: VIEW-000 (Foundation), PREREQ-002 (React 19 Debug Fixes)
The Problem
When something goes wrong in a Noodl app:
- You don't know what triggered what
- Events cross component boundaries invisibly
- Race conditions are nearly impossible to debug
- You can't see when things happened relative to each other
- Tracing a click through 5 components is manual detective work
The classic question: "The login isn't working. What's happening between the button click and the API call?"
The Solution
A debugger that:
- Records actual runtime events as they happen (live mode)
- Traces potential paths through the graph (static analysis mode)
- Shows timing - when each step happened
- Crosses boundaries - follows data/signals into child components
- Highlights failures - shows where things stopped or errored
User Stories
-
As a developer debugging, I want to see exactly what happened when I clicked a button, step by step.
-
As a developer debugging race conditions, I want to see the timing of events to understand ordering.
-
As a developer understanding code, I want to trace what would happen from a given trigger without running the app.
-
As a developer fixing bugs, I want to see where in the chain something failed or got blocked.
UI Design
Main View - Recorded Chain
┌─────────────────────────────────────────────────────────────────┐
│ Trigger Chain Debugger [🔴 Record] │
├─────────────────────────────────────────────────────────────────┤
│ Chain: "Login Button Click" Total: 847ms │
│ Started: 14:32:05.123 Steps: 12 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ Login Page ────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 1 14:32:05.123 [Button] "Login" clicked │ │ │
│ │ │ └─ onClick signal fired │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 2ms │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 2 14:32:05.125 [Function] validateForm │ │ │
│ │ │ ├─ Input: { email: "test@...", password: "***" } │ │ │
│ │ │ └─ Output: Success ✓ │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 1ms │ │
│ └─────────────────────────┼───────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┴─────────────────┐ │
│ │ ↓ Entering: AuthFlow Component │ │
│ └─────────────────┬─────────────────┘ │
│ │ │
│ ┌─ AuthFlow Component ────┼───────────────────────────────────┐ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 3 14:32:05.126 [Component Input] loginTriggered │ │ │
│ │ │ └─ Signal received from parent │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ 0ms │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 4 14:32:05.126 [Variable] isLoading ← true │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────┴──────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ 5 [States] │ │ 6 [REST] POST │ │ │
│ │ │ → "loading" │ │ /api/auth/login │ │ │
│ │ └────────────────────┘ └────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ ⏳ 523ms │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 7 14:32:05.649 [REST] Response received │ │ │
│ │ │ ├─ Status: 200 OK │ │ │
│ │ │ └─ Body: { user: { id: 123, name: "..." } } │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ └─────────────────────────┼───────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┴─────────────────┐ │
│ │ ↑ Exiting: AuthFlow Component │ │
│ └─────────────────┬─────────────────┘ │
│ │ │
│ ┌─ Login Page ────────────┼───────────────────────────────────┐ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ 8 14:32:05.651 [Navigate] → /home │ │ │
│ │ │ └─ ✓ Navigation successful │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ [< Prev] Step 8 of 8 [Next >] [Clear] [↗ Jump to Node] │
└─────────────────────────────────────────────────────────────────┘
Error State View
┌──────────────────────────────────────────────────────────────┐
│ 7 14:32:05.649 [REST] Response received │
│ ├─ Status: 401 Unauthorized │
│ └─ Body: { error: "Invalid credentials" } │
│ │
│ ❌ ERROR: Request failed │
│ Chain terminated here │
│ │
│ Expected next: [Variable] currentUser │
│ But: Error output triggered instead │
└──────────────────────────────────────────────────────────────┘
Race Condition View
┌─────────────────────────────────────────────────────────────────┐
│ ⚠️ POTENTIAL RACE CONDITION DETECTED │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Timeline: │
│ │
│ Chain A (Login) Chain B (Auto-refresh) │
│ ────────────── ─────────────────────── │
│ │
│ 05.123 Button click │
│ 05.125 │ 05.130 Timer fired │
│ 05.126 │ REST call 05.131 │ REST call │
│ │ started │ started │
│ ▼ ▼ │
│ 05.649 │ Response ────────05.645 │ Response │
│ │ │ │
│ 05.651 │ Set user ←───────05.647 │ Set user ← CONFLICT! │
│ ▼ ▼ │
│ │
│ Both chains write to 'currentUser' within 4ms │
│ Final value depends on timing │
│ │
└─────────────────────────────────────────────────────────────────┘
Controls
- [🔴 Record] - Start/stop recording live events
- [< Prev] [Next >] - Step through the chain
- [Clear] - Clear recorded chain
- [↗ Jump to Node] - Jump to the selected step's node in canvas
- Click any step - Select it, show details
- Hover step - Preview the node
Technical Design
Recording Architecture
┌─────────────────────────────────────────────────────────────────┐
│ RUNTIME │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Node A │───▶│ Node B │───▶│ Node C │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ event │ event │ event │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ TriggerChainRecorder (singleton) ││
│ │ ││
│ │ ┌─────────────────────────────────────────────────────┐ ││
│ │ │ events: [ │ ││
│ │ │ { timestamp, nodeId, type, data, component }, │ ││
│ │ │ { timestamp, nodeId, type, data, component }, │ ││
│ │ │ ... │ ││
│ │ │ ] │ ││
│ │ └─────────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────────┘│
│ │ │
└──────────────────────────────┼───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ EDITOR │
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ TriggerChainDebuggerView ││
│ │ ││
│ │ - Subscribes to recorder ││
│ │ - Builds visual chain from events ││
│ │ - Groups by component ││
│ │ - Calculates timing ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────┘
Data Model
interface TriggerEvent {
id: string;
timestamp: number; // High-resolution timestamp
// What happened
nodeId: string;
nodeType: string;
nodeLabel: string;
eventType: 'signal' | 'value-change' | 'api-call' | 'api-response' | 'navigation' | 'error';
// Context
componentName: string;
componentPath: string[]; // Full path for nested components
// Data
inputData?: Record<string, unknown>;
outputData?: Record<string, unknown>;
error?: { message: string; stack?: string };
// Chain info
triggeredBy?: string; // ID of event that caused this one
triggers?: string[]; // IDs of events this one caused
}
interface TriggerChain {
id: string;
name: string; // User-assigned or auto-generated
startTime: number;
endTime: number;
duration: number;
events: TriggerEvent[];
// Organized view
byComponent: Map<string, TriggerEvent[]>;
tree: TriggerEventNode; // Tree structure for rendering
// Analysis
warnings: ChainWarning[];
errors: ChainError[];
}
interface TriggerEventNode {
event: TriggerEvent;
children: TriggerEventNode[];
componentBoundary?: 'enter' | 'exit';
}
Static Analysis Mode
For tracing without running:
interface StaticTraceResult {
possiblePaths: TracePath[];
warnings: string[];
}
interface TracePath {
steps: TraceStep[];
probability: 'certain' | 'conditional' | 'unlikely';
conditions: string[]; // What conditions must be true for this path
}
interface TraceStep {
node: NodeGraphNode;
component: ComponentModel;
inputPort?: string;
outputPort?: string;
crossesComponent?: {
from: string;
to: string;
direction: 'into' | 'outof';
};
}
/**
* Trace all possible paths from a starting node.
* Uses graph traversal + condition analysis.
*/
function traceStaticPaths(
project: ProjectModel,
startComponent: ComponentModel,
startNodeId: string,
startPort: string
): StaticTraceResult;
Implementation Phases
Phase 1: Recording Infrastructure (2 days)
- Create
TriggerChainRecordersingleton - Hook into runtime event system
- Capture node activations with timestamps
- Track causal relationships (what triggered what)
- Handle component boundary crossings
- Store events in memory (with size limits)
Verification:
- Recorder captures node events
- Timestamps are accurate
- Causal chains are tracked
- Component boundaries marked
Phase 2: Chain Builder (1 day)
- Implement chain construction from raw events
- Group events by component
- Build tree structure for rendering
- Calculate timing between steps
- Detect parallel branches
Verification:
- Chain builds correctly from events
- Component grouping works
- Timing calculations accurate
Phase 3: Basic UI (1.5 days)
- Create
TriggerChainDebuggerViewcomponent - Render event timeline
- Show component boundaries
- Display event details
- Implement step navigation
Verification:
- Timeline renders correctly
- Component sections visible
- Step details shown
- Navigation works
Phase 4: Recording Controls (0.5 days)
- Add Record/Stop button
- Add Clear button
- Show recording indicator
- Handle multiple chains
- Auto-name chains
Verification:
- Can start/stop recording
- Multiple chains supported
- UI updates in real-time during recording
Phase 5: Error & Race Detection (1 day)
- Detect and highlight errors in chain
- Implement race condition detection
- Show warnings for potential issues
- Add conflict highlighting
Verification:
- Errors clearly shown
- Race conditions detected
- Warnings displayed
Phase 6: Static Analysis Mode (1 day)
- Implement
traceStaticPaths() - Add "Trace from here" context menu
- Show possible paths without running
- Indicate conditional branches
Verification:
- Static trace works
- Conditional paths shown
- Context menu integration works
Files to Create
packages/noodl-runtime/src/
└── debug/
└── TriggerChainRecorder.ts
packages/noodl-editor/src/editor/src/views/AnalysisPanel/
└── TriggerChainDebuggerView/
├── index.ts
├── TriggerChainDebuggerView.tsx
├── TriggerChainDebuggerView.module.scss
├── ChainTimeline.tsx
├── EventStep.tsx
├── ComponentBoundary.tsx
├── RecordingControls.tsx
├── ChainWarnings.tsx
├── useTriggerChainRecorder.ts
└── staticAnalysis.ts
Success Criteria
- Can record a live interaction chain
- Shows accurate timing between events
- Component boundaries clearly marked
- Can step through chain
- Can jump to any node in canvas
- Errors highlighted clearly
- Race conditions detected
- Static analysis provides useful paths
- Performance acceptable (< 5% overhead when recording)
Future Enhancements
- Export chain - Export as JSON for bug reports
- Replay - Actually replay a recorded chain
- Breakpoints - Pause execution at certain nodes
- Conditional breakpoints - Break when condition is met
- Time travel - Step backwards through chain
- Chain comparison - Compare two chains side by side
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Performance overhead from recording | Make recording opt-in, use sampling |
| Too many events to display | Filtering, collapsing, pagination |
| Causal tracking misses relationships | Test extensively, allow manual linking |
| Runtime integration complex | Start with key node types, expand |
Dependencies
- VIEW-000 Foundation
- Runtime integration (may require runtime changes)
Blocks
- None (independent view)
Runtime Integration (CRITICAL)
This view REQUIRES tight integration with the app preview runtime. It builds on the same infrastructure that makes nodes "light up" when you interact with the preview.
Existing Debug Infrastructure
Noodl already has powerful runtime debugging:
- DebugInspector - Shows live values when hovering over connections
- Node highlighting - Nodes flash/highlight when they fire
- Data flow visualization - Connections animate when data flows
The Trigger Chain Debugger extends this by:
- Recording these events instead of just displaying them momentarily
- Correlating events across time to build causal chains
- Crossing component boundaries to show the full picture
- Persisting the chain for step-by-step review
Integration Points
// Existing infrastructure to leverage:
// 1. DebugInspector.InspectorsModel - already tracks pinned values
import { DebugInspector } from '@noodl-models/DebugInspector';
// 2. Node execution events - the runtime emits these
nodeInstance.on('outputChanged', (port, value) => { ... });
nodeInstance.on('signalSent', (port) => { ... });
// 3. Connection highlighting - already exists in NodeGraphEditor
this.highlightConnection(connection, duration);
this.highlightNode(node, duration);
What We Need from Runtime
The recorder needs to subscribe to events that the runtime already emits (or can easily emit):
| Event | Currently Available? | Notes |
|---|---|---|
| Node output changed | ✅ Yes | Used for debug inspector |
| Signal fired | ✅ Yes | Used for node highlighting |
| Component entered | ⚠️ Partial | May need enhancement |
| Component exited | ⚠️ Partial | May need enhancement |
| REST call started | ✅ Yes | Visible in network tab |
| REST call completed | ✅ Yes | Visible in network tab |
| Error occurred | ✅ Yes | Shown in console |
Key Difference from Canvas Highlighting
The existing canvas highlighting shows you what's happening right now in one component at a time. You have to manually navigate between components to see different parts of the flow.
The Trigger Chain Debugger shows you what happened over time across all components simultaneously. It's the difference between:
- 🎥 Existing: Live TV with one camera angle, you switch channels manually
- 🎬 New: Recorded footage with all camera angles synced, scrub through timeline
Synchronization with Preview
When recording:
- User clicks [Record] in the debugger view
- Debugger subscribes to runtime events via existing debug infrastructure
- User interacts with preview window
- Events stream into the debugger and build the chain
- User clicks [Stop]
- Chain is available for step-through review
When reviewing:
- User steps through recorded chain
- Each step highlights the corresponding node on canvas (using existing highlighting)
- If user clicks "Jump to Node", canvas navigates and selects that node
- Optionally: replay the highlighting animation at each step
Implementation Priority
Phase 1 of this task should focus on discovering and documenting the existing debug event infrastructure before building UI. Key questions:
- What events does the runtime already emit?
- How does DebugInspector subscribe to them?
- How does node highlighting currently work?
- What's missing for cross-component tracking?