DataCollector: Debugging & Advanced Features
The definitive technical manual for DC-UI debugging: unified debugging tab, debug navigation trail ("Follow the Rabbit"), expression editor drawer, schema mapping debugging, DebugEventsProvider API, debug badges, and all advanced configuration features.
π Step-by-Step Debugging Workflow
Strategy/System Type Debugging
- Open System Type or System edit page
- Configure connector calls, produces, expressions as needed
- Click Test in header (or in Debugging tab empty state)
- TestConfigModal: Select system (system-type) or confirm (system)
- Optionally select Sample Input (Full, Delta 1h ago) or edit JSON
- Click Run Test
- Switch to Debugging tabβsee streaming state, then results
- Use badge trail: Connector Calls tab β row β section β expression
- Click expression badge or Templates tab "Debug" to open ExpressionEditorDrawer
- View evaluation grid; optionally enable Sandbox to experiment
- Download events (JSON/YAML) if needed
- Clear or run another test
Schema Mapping Debugging
- Open Schema Edit page
- Navigate to Debugging tab
- Select system from dropdown
- Click Run; wait for connector calls to stream
- Click a connector call row β SchemaDebugDrawer opens
- Click cell in transformed grid β Expression editor shows field expression
- Click Check Reliability to verify roundtrip
- Edit expressions; click Evaluate to test
- Use Columns chooser to focus; Add/Remove/Rename fields as needed
π Debugging Overview
DC-UI provides a unified debugging experience across every config page that has something runnable. The Debugging tab and the Test button look and behave the same whether you're editing a Universal-strategy system type, a specific-strategy system type (LDAP Delta, SAP IAS, SAP LDAP VDS, IDM Reference Data VDS, Fieldglass), a system instance, or a schema mapping.
Unified Debugging Tab
The Debugging tab was unified across all entry points. Previously, Universal strategy had
UniversalDebuggingTab while other strategies used inline implementations.
Now all use the same component with consistent behavior.
Tab Restructuring
The orders below are the page-level tab order β i.e., what the user sees in the browser. The page (e.g., SystemTypeEditPage) inserts General, Used By (existing only), and YAML around what the strategy designer's get*DesignerTabs() function returns.
| Context | Tab Order |
|---|---|
| Universal Strategy Designer (System Type) | General | Connection | Settings | Variables | Connector Calls | Before Finish | Debugging | Flow | Used By | YAML |
| Non-Universal System Type | General | ... | Debugging | YAML |
| System Edit Page | General | ... | Debugging | YAML |
| Schema Edit Page | Mapping | Debugging | YAML |
Debugging Tab States
| State | Display |
|---|---|
| No session (default) | Bug icon + "Click 'Test' to start a debug session" + Test button |
| Streaming | Spinner + "Receiving events..." + event count badge + Stop button |
| Has results | DebugResultsPanel with all tabs + Clear button + Stop (if still streaming) |
Test Button Everywhere
- System Type Edit: Test button for ANY system typeβsimplified popup with system selection only
- System Edit: Test button for ANY systemβempty popup (system known), immediate test
- Schema Edit: Test button navigates to Debugging tab (no popupβschema has its own system selector)
The strategy_name in the request body is always builtin.strategies.universal.UniversalStrategy β Universal is the strategy that implements the debug operations. The operation is what differs by entry point: System / System Type tests use run_full_inventory, Schema tests use debug_connector_calls (see "Test buttons β debug operations" further down). Credentials and variables are loaded from the selected system.
π‘ Always Available
The Debugging tab is always visible (not conditional like Flow). Users see it even before running a test, making debugging discoverable. Badges show real numbers with overflowCount={9999999} instead of "99+".
DebugState Interface
interface DebugState {
results: { success: boolean | null; error?: string; events: any[]; inventory_run_id?: string } | null;
isStreaming: boolean;
eventCount: number;
onStop: () => void;
onClear: () => void;
onTest?: () => void;
saveCount?: number; // Incremented on save to trigger auto-clear
}
Props Flow for Universal Strategy
Auto-Clear on Save
When user saves configuration, the debug session is cleared. saveCount is incremented in handleSave; when it changes, the debugging tab resets to empty state.
Keyboard Shortcuts (Debugging Context)
Relevant shortcuts: Ctrl+S Save (where applicable); Escape Close modals/drawers; Ctrl+R Refresh (e.g., Admin Events).
Streaming API (useDebugStream)
Debug sessions use Server-Sent Events (SSE). The useDebugStream hook provides:
const {
startStream, // (params) => Promise - POST to debug/strategy/stream
stopStream, // () => void - abort connection
isStreaming, // boolean
events, // any[] - accumulated events
error, // string | null
clearEvents // () => void
} = useDebugStream({ onComplete?: (e: DebugCompleteEvent) => void });
Request body: { strategy_name, request: { operation, system_name, system_type_yaml?, inventory_input? } }. Response: SSE stream of monitoring events (step_start, connector_call, template_evaluation, change_dispatch, etc.).
π Expression Editor Drawer
The ExpressionEditorDrawer is the primary interface for viewing and editing expressions across connectors, produces, and next calls. It integrates tightly with debug context.
Always Expanded β split layout
The drawer always renders at 90vw with a fixed split:
- Left panel β Debug evaluation grid (or empty state with Test button when no debug session has captured this expression yet); takes the remaining width
- Right panel β Expression editor, fixed at 500px
Selection modes
Above the evaluation grid sits a Radio.Group (button style) that controls which rows the action buttons operate on:
- Selected (N) β only rows the user has explicitly checked. Auto-selected when any row checkbox is ticked.
- Random 10 β Fisher-Yates shuffle of the current page; useful for spot-checking large debug sessions
- All (N) β every row in the current grid
Two action buttons
The left panel exposes two parallel buttons; both run with concurrency 10 via Promise.all:
- Check Reliability β re-runs each selected row's saved
template_yaml. Surfaces drift between what's stored and what re-evaluation produces (e.g., context shape changes after a connector update). - Evaluate β runs the current expression in the right panel against each selected row's captured context. Use this to iterate on a fix before saving.
Stats cards
Two summary cards sit above the grid; each tracks one of the action buttons:
- Reliability β matched / total + percentage from the last Check Reliability run. Tinted green at 100%, orange at β₯95%, red below 95%.
- Evaluation β same shape, separate counters, driven by the last Evaluate run.
Mixed mode
The drawer can be opened with no specific path β typically from the Templates tab's Debug All button. In that mode, the grid shows every captured template_evaluation with a Path column visible; selecting a row loads that row's template_yaml into the editor (parsed back into the right type by detectExpressionType + getExpressionDisplayValue). Lets you sweep across the entire run instead of focusing on a single field.
Read-Only vs Editable
When opened from Debugging β Templates β Debug, expressions are read-only:
- Info alert: "View Only - This expression cannot be edited from the debugging tab"
- Radio.Group (expression type selector) disabled
- TextArea
readOnly={true}with subtle background - Type hints hidden
- Reason: Drawer doesn't know where in YAML the expression livesβcan't save
Sandbox Mode
In read-only mode, a Sandbox toggle enables experimentation without saving:
| State | Behavior |
|---|---|
| Sandbox OFF | Editor readonly, Evaluate disabled, type hints hidden |
| Sandbox ON | Info: "You can experiment... Changes will not affect your configuration." Editor editable, Evaluate enabled, type hints shown. Changes temporaryβnot persisted on close. |
Sandbox resets to OFF on row change in the evaluation grid and when the drawer is reopened β there's no carry-over of unsaved sandbox state across sessions.
Local State
The drawer uses local state instead of deriving from props on every keystroke:
const [localType, setLocalType] = useState<ExpressionType>('value');
const [localContent, setLocalContent] = useState<string>('');
// Initialize from props when drawer opens
useEffect(() => {
if (open) {
const detectedType = detectExpressionType(value);
const displayContent = getExpressionDisplayValue(value, detectedType);
setLocalType(detectedType);
setLocalContent(displayContent);
}
}, [open, value]);
// Save on close only
const handleClose = useCallback(() => {
if (!readOnly) {
const reconstructed = reconstructValue(localContent, localType);
onSave(reconstructed);
}
onCancel();
}, [readOnly, localContent, localType, onSave, onCancel]);
Validation
reconstructValue validates contentβthrows errors for invalid input. UI shows validation error state and Alert. No heuristics: invalid JSON for Literal Value shows error; invalid YAML for YAML Object shows error.
π€ Expression Types
DC-UI exposes expressions in two distinct surfaces, and each surface accepts a different subset of the expression-type catalog. Don't mix the two β what works in the schema-mapping editor is a strict subset of what works in the Universal-designer drawer.
Schema-mapping expressions (3 types)
Used by SchemaFieldEditor and MappingTable in the schema editor and the schema debug drawer. The variable in scope is result β the raw item being transformed.
| Type | Syntax | Purpose |
|---|---|---|
| Direct | Dot-path: id, name.givenName, emails.0.value |
Access nested fields on result with no evaluation |
| Expression | [[ result.get("id") or "unknown" ]] |
Python expression evaluated through the Jinja engine; auto-wrapped / stripped by the editor |
| String Template | {{ result.firstName }} {{ result.lastName }} |
Jinja string interpolation |
Universal-designer expressions (6 types)
Used by ExpressionEditorDrawer on every expression field in the Universal-strategy designer (params, success_check, pre_transform, produces, next_connector_calls, finish.produces). The variable in scope depends on the field β see the Universal Strategy Connector Calls section in day 27.
| Type | Syntax | Purpose |
|---|---|---|
| Literal Value | "hello", 42, true, null |
Static value with full JSON typing |
| Expression | [[ pair.transformed["eid"]["id"] ]] |
Python expression with type preservation |
| Jinja Template | https://{{ system_var["host"] }}/api |
String-only Jinja interpolation (always returns a string) |
!expr tag |
YAML scalar tag β same as Expression but as a YAML tag | Used in YAML files |
!yaml tag |
Renders the template, then YAML-parses the result; uses {{ }} inside, not [[ ]]; supports | quote filter |
For loops, conditionals, dynamic keys producing structured output |
| YAML Object | Multiline YAML, e.g. nested dicts / arrays | Complex nested structures (maps, arrays) |
β οΈ Schema mapping β Universal designer
The schema-mapping editor cannot use !yaml, !expr, YAML Object, or Literal Value β those types live exclusively in the Universal-designer drawer. The schema mapping engine only understands Direct / Expression / String Template, so saving any other type into a schema-mapping field produces a parse error at evaluation time. See system-admins/03-universal-strategy-system-type.md for the YAML-tag semantics.
reconstructValue Strict Rules
export const reconstructValue = (content: string, type: ExpressionType): any => {
const trimmed = content.trim();
if (trimmed === '') return undefined;
switch (type) {
case 'expression': return `[[ ${trimmed} ]]`;
case 'expr_tag': return { __yaml_type: 'expr', __yaml_value: trimmed };
case 'yaml_tag': return { __yaml_type: 'yaml', __yaml_value: trimmed };
case 'jinja': return trimmed;
case 'object': return parseYaml(trimmed); // STRICT: no fallback
case 'value': return JSON.parse(trimmed); // STRICT: must be valid JSON
default: throw new Error(`Unknown expression type: ${type}`);
}
};
π‘ Debug Events Context
DebugEventsProvider provides debug events and counting functions to deeply nested components. Wraps SystemTypeEditPage, SystemEditPage content.
Context Interface
interface DebugEventsContextValue {
events: any[];
getEvaluationCount: (debugPath: string) => number;
getEvaluationCountByPrefix: (prefix: string) => number;
getTotalEvaluationCount: () => number;
getConnectorCallCount: (callName?: string) => number;
getChangeDispatchCount: (callName: string, changeType: string, entityType: string) => number;
getChangeDispatchCountForCall: (callName: string) => number;
getFollowUpDispatchCount: (sourceCall: string, targetCall?: string) => number;
onTest?: () => void;
isStreaming?: boolean;
}
Function Descriptions
| Function | Purpose |
|---|---|
getEvaluationCount(debugPath) |
Count template_evaluation events matching exact path (e.g., produce/fetch_users/account/key1) |
getEvaluationCountByPrefix(prefix) |
Count all events whose debugPath starts with prefixβaggregation at any level |
getConnectorCallCount(callName?) |
Count step_start events correlated to call (via step_idβcall map). Omit callName for total. |
getChangeDispatchCount(call, changeType, entityType) |
Count change_dispatch with compound key. Status maps: entity_confirmed_*βconfirm, entity_deletedβdelete, etc. |
getChangeDispatchCountForCall(callName) |
Total change_dispatch for a call |
getFollowUpDispatchCount(sourceCall, targetCall?) |
Count step_add where labels.status === 'another_step_run', scoped by source and optionally target call |
Provider Placement
DebugEventsProvider
events={streamingEvents}
onTest={() => setTestModalVisible(true)}
isStreaming={isStreaming}
>
<UniversalStrategyDesigner ... />
</DebugEventsProvider>
Pre-computed Counts (Implementation)
Context pre-computes maps for O(1) lookup:
// Step 1: Build step_id β call_name from step_add
const stepIdToCallName = new Map<string, string>();
events.forEach((event) => {
if (event.event_type === 'step_add' &&
event.added_step_name === 'execute' &&
event.parameters?.call) {
stepIdToCallName.set(event.added_step_id, event.parameters.call);
}
});
// Step 2: Count step_start per call
const stepStartCounts = new Map<string, number>();
events.forEach((event) => {
if (event.event_type === 'step_start') {
const callName = stepIdToCallName.get(event.labels?.step_id);
if (callName) {
stepStartCounts.set(callName, (stepStartCounts.get(callName) || 0) + 1);
}
}
});
// Step 3: Count change_dispatch with compound key callName/changeType/entityType
const statusToChangeType = (status: string): string | null => {
if (status?.startsWith('entity_confirmed')) return 'confirm';
if (status === 'entity_deleted') return 'delete';
if (status === 'entities_purge_unseen') return 'purge_unseen';
if (status === 'entities_purge_by_relation') return 'purge_by_relation';
return null;
};
useDebugEvents Hook
Components use useDebugEvents() to access context. Returns full value; components destructure getEvaluationCount, getConnectorCallCount, etc. as needed.
π·οΈ Debug Badges
All debug badges use consistent styling via custom CSS classes in GlobalThemeOverrides.
Badge Styling
/* Solid orange background with BLACK text */
.dc-debug-badge.ant-tag {
background: var(--warning-color, #faad14) !important;
border-color: var(--warning-color, #faad14) !important;
color: #000000 !important;
}
.dc-debug-badge.ant-tag * {
color: #000000 !important;
}
/* Pulsing glow animation when streaming */
@keyframes dc-debug-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(250, 173, 20, 0.4); }
50% { box-shadow: 0 0 0 4px rgba(250, 173, 20, 0.2); }
}
.dc-debug-badge-streaming {
animation: dc-debug-pulse 1.5s ease-in-out infinite !important;
}
Badge Usage
// Expression input badge
<Tag
color="gold"
className={`dc-debug-badge ${isStreaming ? 'dc-debug-badge-streaming' : ''}`}
>
{isStreaming ? <SyncOutlined spin /> : <BugOutlined />}
{' '}{evalCount}
</Tag>
- Icon: Spinning
SyncOutlinedwhen streaming,BugOutlinedotherwise - Animation: Apply
dc-debug-badge-streamingclass whenisStreaming=true - Expression type badge:
dc-expression-badgeβsolid dark background, light text
πΊοΈ Schema Mapping Debugging
The Schema Edit page has its own debugging tab, powered by ConnectorCallsDebugPanel and SchemaDebugDrawer.
ConnectorCallsDebugPanel
- Schema mapping badges showing counts per schema
- Connector calls grid: Connector, Schema, Status, Duration, Items
- Row selection shows detail panel with transformed data
- Raw connector response JSON viewer
- Expression context:
{ result: <source item> }
Schema Edit Debugging Tab Flow
- Open Schema Edit page
/dc-ui/config/schemas/:name - Navigate to Debugging tab
- Select system from dropdown (auto-loads systems using this schema via
listSystems({ schemaMapping: schemaName })) - Click Run to start streaming
- Backend uses
debug_connector_callsoperation with system_name, system_type_name - View connector calls filtered by schema_mapping
- Click row to open SchemaDebugDrawer
connector_call Event Structure
event_type: connector_call
event_id: uuid
connector: fetch_accounts
credential_pointer: secrets://...
schema_mapping: account_sapias # Filter key for Schema tab
duration_ms: 234
labels:
status: success | error
response_transformed_data: # Transformed items array
- { id: "123", name: "..." }
response_correlations:
raw_connector_response: {...}
transformed_pairs: [{raw, transformed}, ...]
response_error_data: {...} # If failed
SchemaDebugDrawer
Opens when clicking a connector call row. Split layout β left panel is the transformed data grid, right panel is the expression editor for the selected column.
Per-store switching
When the captured connector call dispatched to multiple stores, the drawer adds a store switcher above the grid. Each store has its own baseline rows (from the matching store's transformed_data) and its own schema mapping, so switching stores re-keys the grid columns and re-anchors the expression editor against the right schema.
Three-state cell display
Each value cell carries up to three states, each comparable side-by-side:
- Original β the baseline
transformed_datathe engine produced during the captured run - Reliability β re-evaluated with the saved schema (catches drift between stored data and what the saved expressions produce now)
- Evaluated β re-evaluated with the current right-panel expression (the in-progress edit)
Click a cell to open a popover showing all three states with type badges. Strikethrough on Original when Reliability disagrees, so visual scanning surfaces drift fast.
Immediate onChange
Edits inside the drawer call onChange(newMappings) immediately, propagating to the schema page state and the YAML tab in real time β same as the wider immediate-editing pattern across DC-UI. There's no internal Save button on the drawer; saving happens at the schema page's header.
Left panel β transformed data grid
- Cell selection: Click cell β row + column highlighted
- Reliability check: re-apply saved mappings, compare, show β (green) or β (red)
- Evaluate: test edited expressions, show new values alongside originals with strikethrough
- Dynamic columns: from server data only β Original / Reliability / Evaluated columns
- Columns chooser: "Show All" / "Minimal" buttons, checkbox per column
Right panel β expression editor
- Mapping expression for the selected column
- Context display: raw data for the selected row (proper JSON:
{"result": {...}}) - Field management: Add Field, Remove Field, Rename Field
CSS Specificity Workarounds
Global theme overrides require custom classes:
.schema-debug-table .ant-table-tbody > tr.schema-debug-row-selected > td {
background: rgba(24, 144, 255, 0.1) !important;
}
.schema-debug-table .ant-table-tbody > tr > td.schema-debug-col-selected {
background: rgba(24, 144, 255, 0.08) !important;
}
.schema-debug-table .reliability-pass path { fill: rgba(82, 196, 26, 0.8) !important; }
.schema-debug-table .reliability-fail path { fill: rgba(255, 77, 79, 0.8) !important; }
.schema-debug-table .value-original { color: rgba(150, 150, 150, 0.7) !important; }
Type Badges in Popup
| Value | Badge/Tag |
|---|---|
| string | <Tag>string</Tag> |
| int | <Tag>int</Tag> |
| float | <Tag>float</Tag> |
| bool | <Tag>bool</Tag> |
| array | <Tag>array</Tag> |
| object | <Tag>object</Tag> |
| null | <Tag>null</Tag> |
| undefined | <Tag>n/a</Tag> |
SchemaDebugDrawer UI Mockup
The debug API
DC exposes two debug endpoints. The body shape is the same for both: a strategy_name
plus an opaque request dict whose schema is defined by the strategy. The Universal
strategy implements four operations: RUN_FULL_INVENTORY,
EVALUATE_TEMPLATE, DEBUG_CONNECTOR_CALLS, and
APPLY_SCHEMA_MAPPING.
POST /debug/strategy # batch mode β returns all events on completion
POST /debug/strategy/stream # SSE β events stream as they happen, 5s heartbeats during idle
{
"strategy_name": "builtin.strategies.universal.UniversalStrategy",
"request": {
"operation": "apply_schema_mapping",
"system_name": "sapias-prod",
"system_type_name": "sapias",
"schema_name": "account_sapias",
"schema_yaml": null, // optional: override schema YAML inline
"raw_data": [ ... ]
// ... operation-specific fields
}
}
Other strategies can implement their own debug() / debug_streaming()
static methods with their own request shapes β DC routes the opaque request to the strategy
without interpreting it.
Auth: in PDP mode, debug endpoints require op:dc-ui:strategy.manage (same blast radius as editing strategy code). In scope mode, they require datacollector:debug.
Test buttons β debug operations
The Test button in DC-UI is the same control everywhere, but the operation it sends to POST /debug/strategy/stream depends on which page hosts it:
- System and System Type debugging tabs use
operation: run_full_inventoryβ runs an end-to-end DETACHED_READONLY inventory with the unsaved YAML / form state, dispatching every connector call the strategy would normally make. This is the broad "does my whole config work?" test. - Schema Mapping debugging tab uses
operation: debug_connector_callsβ narrows the run to only the connector calls that produce items for this schema, with the schema-mapping engine attached so each captured call has fullresponse_transformed_dataready for the SchemaDebugDrawer. This is the "did my schema-mapping changes apply correctly?" test.
Both POST to /api/datacollector/debug/strategy/stream over SSE; both stream the same connector_call / step_* / template_evaluation events. The difference is operation-side scope, not transport.
π Understanding Step Dependencies
DataCollector strategies execute as directed acyclic graphs (DAGs) of steps. Each step can depend on one or more parent steps, creating a dependency chain that determines execution order.
(Entry step)"] --> FetchUsers["fetch_users
(Depends: entry)"] Entry --> FetchGroups["fetch_groups
(Depends: entry)"] FetchUsers --> Page2["fetch_users_page2
(Depends: fetch_users)"] FetchUsers --> Page3["fetch_users_page3
(Depends: fetch_users)"] FetchGroups --> ProcessGroups["process_groups
(Depends: fetch_groups)"] Page2 --> Purge["_purge
(Depends: all fetch steps)"] Page3 --> Purge ProcessGroups --> Purge Purge --> Finish["finish
(Depends: _purge)"] style Entry fill:#e8f5e9 style Finish fill:#e8f5e9 style Purge fill:#fff3e0
Dependency Resolution
The step dependency system allows for parallel execution of independent steps while ensuring that dependent steps wait for their prerequisites. This is crucial for:
- Data availability: Ensure parent steps complete before child steps access their data
- Performance optimization: Allow independent steps to run concurrently
- Error handling: Prevent execution of dependent steps if prerequisites fail
- Resource management: Control the order of connector calls and API requests
Common Dependency Patterns
| Pattern | Use Case | Example |
|---|---|---|
| Sequential Chain | Each step depends on the previous one | entry β fetch β transform β finish |
| Parallel Branches | Multiple steps depend on same parent | entry β [fetch_users, fetch_groups] |
| Join Point | Step depends on multiple parents | [fetch_users, fetch_groups] β merge |
| Pagination Chain | Sequential API calls for paged data | page1 β page2 β page3 |
Viewing Dependencies in Debug UI
The debug UI shows step dependencies through:
- Performance Gantt chart: Visual timeline showing when steps execute relative to each other
- Step details:
depends_onfield shows parent step names - Follow-up badges: Count of steps dispatched by each connector call
- Step status: Shows which steps are waiting vs executing vs completed
π‘ Tip: Optimizing Dependencies
When designing strategies, minimize sequential dependencies to maximize parallel execution.
For example, if fetching users and groups are independent operations, don't artificially
chain themβlet them run concurrently by both depending on the entry step only.
πΆ Debug Event Badges & Streaming
Custom CSS classes and animation keyframes for streaming state.
Expression Input Badges
Animation Keyframes
@keyframes dc-debug-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(250, 173, 20, 0.4); }
50% { box-shadow: 0 0 0 4px rgba(250, 173, 20, 0.2); }
}
π Debug Path Reference
Complete table of every expression location and its debug path. Paths must match backend evaluation.
| Location | Field | Debug Path |
|---|---|---|
| ConnectorCallDrawer | params | execute/{call}/params |
| ConnectorCallDrawer | pre_transform | execute/{call}/pre_transform |
| ConnectorCallDrawer | success_check | execute/{call}/success_check |
| ProduceEditModal | key1 | produce/{call}/{entity}/key1 |
| ProduceEditModal | relation1_key1, relation2_key1, relation3_key1 | produce/{call}/{entity}/relation1_key1 etc. |
| ProduceEditModal | list | produce/{call}/{entity}/list |
| ProduceEditModal | transformed_data | produce/{call}/{entity}/transformed_data |
| NextCallEditModal | when | next_call/{from}/{to}/when |
| NextCallEditModal | list | next_call/{from}/{to}/list |
| NextCallEditModal | input | next_call/{from}/{to}/input |
| EntryPointsEditor | input | start/{call}/input |
| ProduceEditModal (purge) | relation keys | purge/{call}/{entity}/relation1_key1 etc. |
Health check params have no debugPathβevaluated outside inventory context.
Event Matching Logic
Template evaluation events have event_type: 'template_evaluation' and labels.status set to the debug path. ExpressionInput matches via event.labels.status === debugPath. The backend emits these paths during strategy execution.
πΎ Download Debug Events
DC-UI adds JSON/YAML export of captured events from the Debugging tab.
Implementation
- Download dropdown between Stop and Clear buttons
- Options: "Download as JSON", "Download as YAML"
- Filename:
debug-events-{inventory_run_id}.jsonor.yaml - Disabled when no events
Output Format
// JSON
{
"inventory_run_id": "abc-123",
"events": [
{ "event_type": "connector_call", ... },
{ "event_type": "template_evaluation", ... }
]
}
// YAML
inventory_run_id: abc-123
events:
- event_type: connector_call
...
- event_type: template_evaluation
...
π₯ Sample Inventory Inputs
DC-UI adds predefined inventory input options to the Test Config Modal.
Backend Integration
- API:
GET /configs/plugins/strategies/{name}/sample-inputs SampleInventoryInput:title,input,description_markdown- Universal strategy returns: "Full" (
{}), "Delta 1h ago" ({started_at, full_completed_at})
TestConfigModal Updates
- Added
strategyNameprop for fetching sample inputs onTestsignature:(systemName: string, inventoryInput: Record<string, any>) => void- Fetches sample inputs when modal opens
- Shows selectable preset tiles (Full, Delta 1h ago, etc.)
- Markdown description for selected input
- JSON editor for customizing the input
UI Layout
The debug modal shows: (1) System instance tiles, (2) Sample inventory input tiles, (3) Markdown description, (4) JSON editor for custom input.
SampleInventoryInput Interface
interface SampleInventoryInput {
title: string; // e.g., "Full", "Delta 1h ago"
input: Record<string, any>; // e.g., {} or {started_at, full_completed_at}
description_markdown?: string;
}
Universal Strategy Sample Inputs
| Preset | Input | Use Case |
|---|---|---|
| Full | {} | Complete inventory from scratch |
| Delta 1h ago | {started_at, full_completed_at} | Incremental since last run |
βοΈ Test Config Modal
DC-UI introduced a simplified common TestConfigModal for all strategies.
Modes
| Mode | Context | UI |
|---|---|---|
| system-type | System Type Edit page | System dropdown (filtered by systemTypeName), preselects first matching, single "Run Test" button |
| system | System Edit page | Confirmation text (no choices), single "Run Test" button. Uses systemName prop directly. |
Removed from Old Modal
Credential pointer input, variables table, "specify manually" toggle. Backend loads credentials from selected system.
TestConfigModal Props
interface TestConfigModalProps {
visible: boolean;
onCancel: () => void;
onTest: (systemName: string, inventoryInput?: Record<string, any>) => void;
loading?: boolean;
mode: 'system-type' | 'system';
systemTypeName?: string; // system-type: filter systems
systemName?: string; // system: pre-known
strategyName?: string; // for fetching sample inputs
}
API Request
await startStream({
strategy_name: 'builtin.strategies.universal.UniversalStrategy',
request: {
operation: 'run_full_inventory',
system_name: systemName,
system_type_yaml: currentYaml, // Include unsaved changes
// NO system_type_name when using system_type_yaml
},
});
π DebugResultsPanel - 9 Tabs
DebugResultsPanel displays all captured events across nine sub-tabs: Overview, Steps, Connector Calls, Changes, Logs, Templates, Diffs, Input Changes, Diagrams. Each tab carries a count badge driven by the events stream.
Overview Tab
Summary of run: inventory_run_id, total events, success/failure indicators.
Expandable Row Data
Steps tab rows expand to show contextual data:
- step_start, step_add: Parameters panel (JSON)
- step_finish: Timing breakdown: actions_duration_ms, idle_duration_ms, commit_duration_ms, total_duration_ms, kafka_wait_ms, file_wait_ms
- inventory_finish: Next Inventory Input (if present)
- All step events: IDs panel: step_id, step_run_id, added_step_id, unique_key (copyable)
Steps Tab
| Event Type | Label | Color |
|---|---|---|
| inventory_start | Inventory Start | purple |
| inventory_finish | Inventory Finish | purple |
| step_add | Step Dispatched | orange |
| step_start | Step Start | blue |
| step_actions_complete | Actions Complete | cyan |
| step_commit_start | Commit Start | geekblue |
| step_finish | Step Finish | green |
Columns: Time, Event, Step, #, Outcome, Duration, Message. Expandable rows with parameters, timing breakdown, IDs.
Connector Calls Tab
Connector call events: connector name, schema, status, duration, item count. Badge: green if all OK, red if any failed.
Changes Tab
Change dispatch events. Blue badge.
Templates Tab
Template evaluation events. Debug buttons open ExpressionEditorDrawer (read-only). Red badge if template errors, blue otherwise.
Diffs Tab
Entity diffs. Blue badge whose count combines both change_decision events (CP-emitted decisions) and diffs_publish events (DiffPublisher-routed diffs) β DebugResultsPanel collapses what Past Run Details splits into separate "Diffs" and "Diff Publications" tabs into a single panel here.
Input Changes Tab
NII change events captured during the debug session. Each row shows: timestamp, target stream, command (when the write was triggered by a command rather than finish_inventory), command_id, step name, plus the old / new value presented as a collapsible diff. Useful for verifying that finish_inventory(next_inventory_input=...) and command-driven writes produce the expected next-run state.
Diagrams Tab
Flow visualization. Blue badge.
Logs Tab
Always shows badge. Color: red if errors, orange if warnings, blue otherwise. "View Details" button opens modal with full log record.
Badge Overflow
All badges use overflowCount={9999999} to show real numbers instead of "99+".
Complete Event Type Reference
| event_type | Purpose | Key Fields |
|---|---|---|
| inventory_start | Inventory begins | labels, timestamp |
| inventory_finish | Inventory completes | next_inventory_input, parameters |
| step_add | Step dispatched | added_step_id, added_step_name, parameters.call |
| step_start | Step begins | labels.step_id |
| step_actions_complete | Actions done | labels |
| step_commit_start | Commit phase | labels |
| step_finish | Step complete | actions_duration_ms, idle_duration_ms, commit_duration_ms, total_duration_ms |
| connector_call | API call | response_transformed_data, response_correlations |
| template_evaluation | Expression eval | labels.status (debug path) |
| change_dispatch | Entity change | labels.step_id, entity_type, status |
| log | Log message | message, logger_name, level |
π§ Advanced Configuration
Circuit Breaker
The circuit breaker pattern prevents cascading failures by automatically disabling connector calls when a threshold of failures is reached. This protects both the DataCollector and the target system.
| Field | Type | Description |
|---|---|---|
| enabled | boolean | Enable/disable circuit breaker |
| threshold | number | Failures before opening circuit |
| timeout | number | Seconds before testing recovery (OPEN β HALF_OPEN) |
| success_threshold | number | Successes needed in HALF_OPEN to close |
| window_seconds | number | Time window for counting failures |
| security_mode | boolean | Fail-secure mode |
Inheritance: Global defaults β System Type β System. Empty fields inherit.
Storage Settings
StorageConfigEditor: checkbox matrix with entity types as rows, storage options as columns:
- Disable Changes: Disable change storage (StepRunChange) for entity type
- Enable Entities Transformed: Store in Entity.transformed_data
- Enable Changes Transformed: Store in StepRunChange.transformed_data
System Type: required. System: optional override.
Monitoring Exclusions
GeneralSettingsPanel fetches schema from GET /api/datacollector/monitoring/schema. MonitoringDisableRule: event_type, field, destination, decision_unchanged, change_type, entity_type, template_use_case_regex. Contextual fields based on event type.
Monitoring Schema Response
{
"event_types": [
{"value": "connector_call", "label": "Connector Call", "fields": ["request_params", "..."]}
],
"monitoring_destinations": [
{"value": "kafka", "label": "Kafka"}
],
"change_types": [
{"value": "entity_confirmed_exact", "label": "Entity Confirmed Exact"}
]
}
StorageConfig Type
interface StorageConfig {
disable_changes: string[]; // Entity types to skip change storage
enable_entities_transformed_data: string[]; // Store in Entity.transformed_data
enable_changes_transformed_data: string[]; // Store in StepRunChange.transformed_data
}
Circuit Breaker YAML Output
circuit_breaker:
enabled: true
threshold: 5
timeout: 30
success_threshold: 2
window_seconds: 60
security_mode: false
ποΈ Development Patterns
From datacollector/docs/strategies-development/040-development-patterns.md.
Strategy Development
- Use unit and E2E tests where DC runs headless with
--execute - Monitoring events output to files; tests assert against outputs
- Plugin code hot-reloads with 5-second polling
Connector Design
Multi-Method Connectors: Single class, multiple operations via params['method']. Example: LDAP connector with search, open_connection, paged_search, close_connection.
SDK Boundary
Credentials schema is connector-specific. DC-UI credentials designer adheres to that schema. Strategies define what to collect; connectors define protocol and credentials.
Connector execute() Pattern
async def execute(self, params: Dict, credentials: Dict, timeout_ms: float) -> Any:
method: str = params['method']
if method == 'search':
return await self._execute_search(params, credentials, timeout_ms)
elif method == 'paged_search':
return await self._execute_paged_search(params, timeout_ms)
else:
raise ConnectorCallException(
ConnectorCallStatus.FATAL_ERROR,
{"error": f"Unknown method: {method}"}
)
Plugin Hot-Reload
Changes to custom plugin files (strategies, connectors) are detected automatically with 5-second polling. No DC restart needed. Use unit tests for verification.
β Best Practices
Debugging Workflow
- Run Test with Detached Readonly before deploying config changes
- Use Sample Inventory Inputs (Full, Delta 1h ago) for common scenarios
- Follow badge trail to navigate to expressions with issues
- Use Sandbox mode in Templates tab to experiment without saving
- Download events (JSON/YAML) for offline analysis or team sharing
Schema Mapping Debugging
- Run Reliability Check after loadingβidentifies serialization mismatches
- Use "Minimal" columns first to focus on key fields
- Evaluate after Add/Remove/Rename to see server resultβcolumns come from server data only
- Context panel shows
{ result: rawItem }βuse for expression writing
Expression Editor
- Use Literal Value for static values (quoted JSON: "hello", 42, true)
- Use YAML Object for nested structures; Expression for dynamic values
- Fix validation errors before savingβreconstructValue is strict
- From debugging tab: read-only by default; enable Sandbox for experimentation
β‘ Performance Optimization
Query Patterns
- Use pagination for large result sets (Entities Explorer, Admin Events)
- Server-side filtering reduces payload
- Avoid N+1: batch fetches for related data
Caching Strategies
- Plugin list (strategies, connectors) cached with invalidation on config change
- Monitoring schema cached per session
- Debug events in memory until cleared or page unload
π¬ Advanced Debugging Techniques
Master these advanced techniques to diagnose complex issues and optimize your DataCollector strategies.
Using Debug Badges to Trace Execution
Debug badges provide a visual breadcrumb trail through your strategy execution. Each badge shows contextually relevant metrics at different navigation levels:
-
Tab-level badges: Show aggregate counts across all data (e.g., "Connector Calls: 42"
means 42
step_startevents withadded_step_name === 'execute') - Row-level badges: Show counts for specific calls, entities, or templates (e.g., "fetch_users [π 15]" means that connector was called 15 times)
-
Section-level badges: Show grouped metrics (e.g., Entity Production section shows
total
change_dispatchevents for that call) - Field-level badges: Show expression evaluation counts (e.g., "when [π 3]" means that expression was evaluated 3 times with different contexts)
π‘ Pro Tip: Following the Rabbit Trail
Start at the top level and "follow the rabbit" down through badges. If you see 42 total connector calls but only one call has a badge showing 42, you know all calls went through that single connector. This helps identify bottlenecks and unexpected execution patterns.
Comparing Original vs Re-evaluated Expressions
The Expression Editor Drawer lets you modify and re-evaluate expressions using captured debug context. This is invaluable for testing fixes without redeploying:
- Click a field badge with debug data to open the Expression Editor Drawer
- The left panel shows all captured evaluations with their context and results
- In Sandbox mode, edit the expression in the right panel
- Click Re-evaluate to test your changes
- Compare results: original values appear with strikethrough, new values in color
- Verify your fix works across all captured contexts before saving
β οΈ Context Completeness
Re-evaluation uses the captured context from debug events. If the original evaluation accessed data that wasn't included in the monitoring event (e.g., step context not fully serialized), your re-evaluation may produce different results. Always test changes in a full test run.
Reliability Checks for JSON Serialization
The Schema Debug Drawer includes a Reliability Check feature that verifies your schema mappings survive JSON serialization/deserialization:
- Open Schema Debug Drawer after running a connector call
- Click Reliability Check button
- System re-applies original mappings to deserialized JSON (simulating database round-trip)
- Results show β (green) for matching values, β (red) for mismatches
Common serialization issues detected:
- Date precision loss: Python datetime with microseconds β JSON β loses precision
- Type coercion: Numbers become strings, booleans become 0/1
- Null vs empty:
nullvs""vs[]handling differences - NaN/Infinity: Not valid JSON, become
null - Timezone ambiguity: Timestamps without explicit timezone
π‘ Best Practice: Always Run Reliability Check
Run reliability checks after creating or modifying schema mappings. This catches serialization issues before they cause data inconsistencies in production. Pay special attention to date/time fields, numeric precision, and nullable fields.
Download Debug Events for Offline Analysis
The Download button in the Debug Results Panel exports all captured monitoring events in JSON or YAML format:
- JSON format: Machine-readable, easy to process with scripts or tools like jq
- YAML format: Human-readable, better for documentation and code review
Use Cases for Downloaded Events
| Use Case | How To |
|---|---|
| Team Collaboration | Share debug session with colleagues for review |
| Bug Reports | Attach events to issues for reproducible debugging |
| Performance Analysis | Process timing data with custom scripts |
| Audit Trail | Document behavior for compliance or change tracking |
| Automated Testing | Compare events against expected baselines |
Example: Extracting Connector Timings with jq
# Get all connector call durations
cat debug-events.json | jq -r '.[] |
select(.event_type == "connector_call") |
"\(.connector): \(.duration_ms)ms"'
# Find slowest connectors
cat debug-events.json | jq -r '.[] |
select(.event_type == "connector_call") |
{connector, duration_ms}' |
jq -s 'sort_by(.duration_ms) | reverse | .[0:5]'
Using Sample Inventory Inputs for Testing
Sample inventory inputs let you test strategies with predefined data instead of making real API calls. This is essential for development and testing:
When to Use Sample Inputs
- Offline development: Test without network access or credentials
- Edge cases: Inject unusual data to test error handling
- Performance testing: Test with large datasets without rate limiting
- Regression testing: Ensure changes don't break existing behavior
- Demo environments: Show functionality without real integrations
Creating Sample Inputs
Sample inputs are YAML/JSON structures that override the entry step output:
# Sample input for user sync strategy
users:
- id: "user123"
email: "john.doe@example.com"
first_name: "John"
last_name: "Doe"
department: "Engineering"
active: true
- id: "user456"
email: "jane.smith@example.com"
first_name: "Jane"
last_name: "Smith"
department: "Sales"
active: false
groups:
- id: "group789"
name: "Developers"
member_ids: ["user123"]
Sample Inputs from Production
- Run a test with real credentials
- Download debug events (YAML)
- Extract the
inventory_finishevent'snext_inventory_inputfield - Save as sample input for future tests
- Sanitize sensitive data (emails, IDs, etc.) before sharing
β οΈ Sanitize Production Data
Sample inputs created from production data may contain PII, credentials, or sensitive information.
Always sanitize data before committing to version control or sharing with team members. Use tools
like sed or custom scripts to replace sensitive values with realistic fake data.
π Performance Analysis
The Performance tab provides a Gantt chart visualization showing the execution timeline of all steps, connector calls, and database operations. Master this tool to identify and eliminate bottlenecks.
Reading the Performance Gantt Chart
The Gantt chart displays concurrent execution across multiple dimensions:
- X-axis (time): Relative milliseconds from inventory start. Zoom and pan to focus on specific time ranges.
- Y-axis (steps): Each row represents a step. Grouped by step name with instance numbers for repeated steps.
- Bar length: Duration of operation. Longer bars indicate potential bottlenecks.
- Bar color: Operation type (connector call, transformation, database operation, etc.)
- Gaps: Idle time waiting for dependencies or database commits
Identifying Bottlenecks
Connector Call Bottlenecks
Connector calls to external APIs are often the slowest operations. Look for:
- Long bars: Individual calls taking >1 second indicate slow API responses
- Sequential patterns: Calls happening one after another instead of parallel
- Repeated calls: Same connector called many times (pagination or lack of batching)
- Timeouts: Bars that end abruptly at the timeout threshold
Solutions:
- Enable parallel execution by removing unnecessary dependencies
- Implement batching to reduce number of API calls
- Add pagination to handle large datasets efficiently
- Use circuit breakers to fail fast on slow/unavailable services
- Cache frequently accessed data between runs
Transformation Bottlenecks
Schema mappings and template expressions can become CPU-intensive with large datasets:
- Long transformation bars: Complex expressions or large item counts
- Repeated evaluations: Same expression evaluated many times unnecessarily
- Type conversion overhead: Excessive parsing or serialization
Solutions:
- Simplify complex expressions by breaking into smaller pieces
- Move repeated calculations to reusable functions or step outputs
- Use
disable_changesstorage config for high-volume entity types you don't need to store - Optimize schema mappings to avoid redundant field transformations
Database Operation Bottlenecks
Database commits happen at step completion and can block progress:
- Long commit bars: Writing many entities or changes in single transaction
- Lock contention: Multiple steps trying to commit simultaneously
- Index overhead: Slow inserts due to missing or excessive indexes
Solutions:
- Batch entity production across fewer steps instead of many small steps
- Use
disable_changesfor entity types where you only need final state - Review database indexes for Entity and StepRunChange tables
- Consider
enable_entities_transformed_dataonly for types you need to query
Parallel Step Execution Patterns
Effective use of parallelism is key to fast inventory runs. The Gantt chart makes parallel execution visible:
Optimal Parallel Pattern
Suboptimal Sequential Pattern
Optimization Strategies
| Strategy | When to Use | Expected Improvement |
|---|---|---|
| Parallelize Independent Calls | Multiple connectors that don't depend on each other | 50-80% reduction in total time |
| Batch API Requests | Many individual calls to same endpoint | 80-95% reduction in connector call count |
| Implement Pagination | Large datasets causing timeouts | Eliminates timeouts, consistent performance |
| Disable Unnecessary Storage | High-volume entities you don't query later | 30-60% reduction in commit time |
| Cache Entry Data | Slow entry step that rarely changes | Eliminates entry step duration on subsequent runs |
| Circuit Breakers | Occasional slow/failing external services | Prevents cascading delays, fail-fast behavior |
π‘ Performance Baseline
Before optimizing, establish a baseline by running the same test multiple times and averaging the results. Network variability can cause 20-30% variance in connector call times. Document baseline metrics and compare after each optimization to measure actual impact.
Reading Performance Metrics
The Steps tab provides detailed timing breakdown for each step:
| Metric | Description | What to Look For |
|---|---|---|
| actions_duration_ms | Time spent executing actions (connector calls, transformations) | High values indicate slow connectors or complex expressions |
| idle_duration_ms | Time waiting between actions complete and commit start (queueing in the committer) | High values suggest committer backpressure |
| commit_duration_ms | Time writing entities and changes to database (excludes kafka_wait_ms and file_wait_ms) |
High values indicate database bottlenecks or excessive change volume |
| kafka_wait_ms / file_wait_ms | Time blocked waiting for Kafka / file destinations to flush diffs | High values indicate slow downstream consumers or disk pressure |
| total_duration_ms | Total time from step_start to step_finish | Overall step latency; not a strict sum of the others (commit excludes wait_ms) |
Interpreting Timing Patterns
- High actions, low idle: Step is CPU/API bound, optimize the work itself
- Low actions, high idle: Step is waiting on dependencies, review step order
- High commit relative to actions: Database bottleneck, reduce change volume
- Similar duration across repeated steps: Consistent performance, good baseline
- Increasing duration in repeated steps: Memory leak or resource exhaustion
π οΈ Development Workflow
A systematic workflow for developing, testing, and debugging DataCollector strategies. The DC project ships several test layers; this section covers the patterns you'll actually use, the debug API protocol behind the UI's Test button, and a few engine details that turn into footguns if you don't know about them.
Hot-reload during development
DC polls the configured plugin directories for file changes. Custom strategies (Python files in the
custom-strategies path) and connectors hot-reload β the polling interval is configurable
(plugins.reload_interval_seconds in config.yaml). The DC-UI Test button
runs against whatever the latest reload picked up.
What hot-reloads:
- Custom strategy and connector Python files
- Schema mapping YAML files
- System and system-type YAML files
What requires a restart:
- DC core code changes (
src/...) - Database migrations
- Environment variables
- The DC
config.yamlitself
End-to-end test pattern (tests_e2e/)
The canonical e2e harness is python -m src.main --execute <system>: DC runs
headless, executes a single inventory for the named system, writes monitoring events and diffs to
files, and exits. Tests assert against those files.
# Run a single inventory headless
python -m src.main --execute test_36_universal_basic --config tests_e2e/config/config.yaml
# Output ends up in (configured via monitoring.file and diff.file in config.yaml):
# tests_e2e/output/monitoring/<run_id>.yaml
# tests_e2e/output/diffs/<system_name>/<destination>.yaml
The pattern in pytest:
# tests_e2e/test_e2e_NN_yourtest.py
def test_my_strategy(run_inventory, read_monitoring_events, read_diffs):
events = run_inventory("my_test_system") # invokes --execute, returns events
assert any(e["event_type"] == "step_finish" and e["outcome"] == "success" for e in events)
diffs = read_diffs("my_test_system", "eid")
assert len(diffs) == expected_count
The conftest.py in tests_e2e/ provides the fixtures. Real PostgreSQL via
docker-compose. No mocked connectors β connectors talk to test stub services
(echo, flatfile) configured in the test system YAML.
ChangesProcessor integration tests (tests_integration/)
For tests that need to exercise the changes-processing pipeline directly (without going through a full inventory), the integration test pattern uses three DC config knobs:
kafka.mode = DIVERT_TO_FILEβ replace Kafka destinations with file destinationsempowerid.mode = DIVERT_TO_FILEβ replace EmpowerID destinations with file destinationsmirror_all_stores_to_file.enabled = Trueβ file shadow of every store
Tests construct a list of ChangeDispatchEvent objects, call
ChangesProcessor.process_changes() directly with a real WritableEntitiesRepository,
then read the resulting diff files. Diff file output is the contract β that's what the consumer
sees.
Sample inventory inputs for fast iteration
Strategies provide named sample inputs via get_sample_inventory_inputs() for the UI's
"Sample Input" picker. Use them during development to avoid waiting for real API state to converge.
@staticmethod
async def get_sample_inventory_inputs(base_engine: BaseEngine) -> List[SampleInventoryInput]:
now = await base_engine.now()
one_hour_ago = now - timedelta(hours=1)
return [
SampleInventoryInput(
stream="users",
title="Force Delta",
input={
"force_delta": True,
"started_at": one_hour_ago,
"full_completed_at": one_hour_ago,
},
description_markdown="Force a **delta** sync starting from one hour ago.",
),
]
Each sample is tagged with its stream so the UI offers it for the right stream's trigger flow.
Step statistics patterns
engine.get_step_statistics() queries real-time step state during step execution.
Three patterns dominate:
Wait for prerequisite
stats = await engine.get_step_statistics(name="fetch_data")
finished = sum(c for s, c in stats.running_states if s == RunningState.FINISHED)
if finished == 0:
return StepExecutionConfig(delay=True) # not yet ready
Check for failures before purging
failures = await engine.get_step_statistics(
outcomes=[Outcome.EXPECTED_FAILURE, Outcome.FATAL_FAILURE],
)
if failures.count > 0:
engine.finish_step(Outcome.EXPECTED_FAILURE, "Purge canceled due to failures")
return
# safe to purge
This is the canonical pattern for _purge-style cleanup steps β never delete entities if any prior step failed.
Track progress
success = await engine.get_step_statistics(outcome=Outcome.SUCCESS)
engine.log_message("progress", f"Completed {success.count} sub-steps")
Eager dispatch β the footguns
engine.dispatch_step(name, immediate=True) starts the dispatched step before the
parent commits. Useful for latency-sensitive fan-out, but it has hard rules. From
strategies-development/080-strategy-ground-rules.md:
- The child must not declare a step-name dependency on the parent β the dependency check would block the child until the parent commits, silently degrading
immediate=Trueto normal timing. - The child must not depend on the parent's entity changes being in the DB.
- The child must not depend on NII written by the parent (cross-stream NII writes happen during commit).
- The child must not rely on step statistics reflecting the parent's outcome.
- If the parent fails, eagerly-dispatched children get the
DISCARDEDoutcome β connector calls already happened, but entity processing and DB commit don't run. Side effects from the connector call cannot be undone. immediate=Truecannot be combined withunique_keyβ raisesValueError. Unique-key merge logic requires durable Step rows that don't exist during eager dispatch's transient phase.
Unique-key behavior β MERGE vs IDEMPOTENCY
get_unique_key_logic() picks the dedup behavior per step name. Two modes; their
late-dispatch semantics differ:
MERGE (default) | IDEMPOTENCY | |
|---|---|---|
| Multiple dispatches | Parameters merged (intersection check; FATAL_FAILURE on overlap) | Parameters must be deeply equal (FATAL_FAILURE if not) |
| Duplicates | SUCCESS_REDUCED | SUCCESS_REDUCED |
| Late dispatch (after the group already executed) | FATAL_FAILURE | SUCCESS_REDUCED |
Strategies that fan out and re-merge across long-running scenarios usually want IDEMPOTENCY β late dispatches are common.
Visibility-rule debugging
When entities go invisible (because an AttributeCheck matched, or a referenced entity
in a ForeignKeyCheck is no longer alive), the Diffs tab shows ChangeDecisionEvents
with explicit decision tags:
"BecameInvisible"β direct VβI transition, DELETED diff produced"BecameVisible"β direct IβV transition, CREATED diff produced"CascadeBecameInvisible"β cascaded VβI along the FK DAG"CascadeBecameVisible"β cascaded IβV
If you expect a cascade and don't see it, check that your ForeignKeyCheck rule's relation_key_number matches the relation{N}_key1 the strategy actually dispatches.
Throttle-group debugging
The Performance tab shows step durations on a Gantt chart; combined with the Steps tab, you can see:
- Slot occupancy β how many steps are running concurrently in a group at each point
- Backoff delays β gaps between failed steps and their retries (visible as idle stretches in the Gantt chart)
- Backoff vs delay_until β backoff is per-slot and computed from
RetryBackoff;delay_untilis per-instance and explicit. The Gantt chart distinguishes them only by the explicit-delay marker on the step row.
JSON trimming for large events
Monitoring events are serialized to Kafka under two configurable thresholds β a per-event total
(default ~256KB, matching Kafka's typical message limit) and a per-item cap (default ~100KB) that
triggers field-level trimming before the total ever blows. DC's JsonTrimmer walks the
event tree depth-first: oversized strings get truncation markers, oversized arrays get sampled, and
large dict values get recursively trimmed. File destinations are unaffected β they receive the full
untrimmed event.
If you're debugging via Kafka and seeing truncation markers, switch to file output for full fidelity, or compare against a debug-API streaming run (which captures events to memory without going through Kafka).
Before you build a connector β the universal questionnaire
Building a connector against a new source API is a 79-question exercise documented in
strategies-development/060-universal-questionnaire.md. It covers token lifecycle,
pagination semantics, ID stability, deletion lifecycle, consistency model, throttling, schema
evolution, and edge cases. Work through it before writing any connector code β most connector
bugs are unanswered questions in disguise.
Strategy and connector ground rules
The headlines from strategies-development/070-connector-ground-rules.md and 080-strategy-ground-rules.md β read these before merging strategy or connector code:
| Connector | Strategy |
|---|---|
| JSON-safe responses (no datetime / bytes / Decimal) | Single Python file, no shared base classes between strategies |
Shared state protected by threading.Lock (not asyncio) |
No class-level or static state β steps run on different pods |
| Each call may run on a different thread / event loop | Step input β€ 1KB serialized β coordinates, not the work itself |
| Self-managed cleanup (no framework teardown hook) | One step β one connector call; pagination = next page as new step |
| Fail fast β never silently skip; failed inventory beats silent incomplete | |
resolve_next_inventory_input_conflict() must be pure data merge, <10s |
π Where to go for full reference
strategies-development/060-universal-questionnaire.mdβ source-API evaluation checkliststrategies-development/070-connector-ground-rules.mdβ connector authoring rulesstrategies-development/080-strategy-ground-rules.mdβ strategy authoring rulesstrategies-development/100-throttle-groups.mdβ full throttle-group spec including jitter math and multi-group ANDstrategies-development/030-inventory-flow-and-state.mdβ outcomes, dependencies, unique-key
π Troubleshooting Guide
Trigger validation: system_type_name not permitted
When passing system_type_yaml in a debug request to override the system type's YAML
inline, omit system_type_name. The two are mutually exclusive β DC validates that
you specify either a name to load from disk, or an inline YAML, not both.
"Resource not found" errors that you expect to fail gracefully
If the source API returning 404 is a normal operational state (e.g., looking up an entity that
might not exist), use finish_step(Outcome.EXPECTED_FAILURE, ...) on
NOT_FOUND_ERROR. The step's changes will still persist (unlike
FATAL_FAILURE), and it won't trigger retry.
Step timing on the Performance tab looks wrong
Each step timing has four components: actions_duration_ms (work),
idle_duration_ms (waiting), commit_duration_ms (database write), and
total_duration_ms (overall). High idle without high actions usually points at
dependency contention β the step is waiting for prerequisites. High commit without high actions
points at heavy storage config (consider trimming
enable_changes_transformed_data).
Cascade decisions don't appear in Diffs tab
Visibility cascades produce "CascadeBecameInvisible" /
"CascadeBecameVisible" ChangeDecisionEvents. If you expect a cascade and don't see
one, check that:
- The
ForeignKeyCheckrule'srelation_key_numbermatches the relation key the strategy actually populates on the dependent entity. - The dependent entity actually has the relation key populated β DC can't cascade from a NULL relation key.
- The
store_nameon the rule matches the store you're inspecting in the Diffs tab.
Throttle group not throttling
If steps don't appear to respect a throttle group's max_parallelism:
- Check that
get_step_execution_configreturnsthrottle_groups=["..."]for the steps you expect to be in the group, with the same group name asget_throttle_group_config. - Throttle group membership must be stable across attempts β if a step's group set varies between attempts, slot accounting breaks.
- Steps with
throttle_groups=[]fall back to a hardcoded 1000 cap, not the group cap.
Authentication failures (401 / 403)
See system-admins/06-authentication-and-authorization.md for the full troubleshooting
guide. The shape: 401 = bad token or no token; 403 = good token, missing
scope or PermissionGrant. 401 actor_arn_required specifically means a trusted-service
token arrived without an X-Actor-ARN header β the calling service forgot to forward
the user identity.
Inventory won't start
- Stream disabled? Auto-scheduling skips disabled streams. Triggered runs still work β but check the
streams.statuscolumn in the DB if a scheduled run isn't firing. - Stream
GONE? A stream that disappeared from the strategy'sget_streams()result is marked GONE; triggered runs are rejected. - Lease still held? If a previous instance crashed without releasing the lease, the new instance waits up to 60s before stealing. Check
leases.heartbeat_atif a stream sits idle. - Existing inventory in DB (writable mode)? Writable inventories can't be triggered if a non-completed inventory exists for that stream. Use
EXISTING_READONLY_INVENTORYto inspect, or cleanup-ongoing-inventory to abandon.
Replay produces no diffs
Replay only re-publishes entities whose attribute_transformed_data is stored. If the
profile's store doesn't have enable_entities_transformed_data for the entity types
being replayed, replay fails fast with a readiness error before publishing anything. Enable storage
for the relevant entity types in get_step_storage_config and run a normal inventory
first to populate.
π Debug Modal Details
"View Details" buttons in the Logs and Steps tabs open a unified DetailsModal.
Logs Tab
Eye icon in action column opens DetailsModal with: datetime, message, logger_name, labels (JSON viewer).
Steps Tab
Eye icon opens DetailsModal with: datetime, event_type, message, labels, parameters, next_inventory_input. Both columns in visibility chooser.
Pattern
Uses reusable DetailsModal from common components (same as Analytics LogViewer). Declarative Modal controlled via state, not imperative Modal.error().
π Plugin Documentation UI
Documentation and Validation tabs for strategies and connectors.
Documentation Tab
- PluginDocumentation component displays markdown for each plugin class in the file
- Uses
markedfor markdown-to-HTML - Fetches via listConnectorPlugins / listStrategyPlugins, filters by source + module_path
Validation Tab
- PluginValidation shows success or error alerts per class
- Red badge when any class has error
- Shows traceback for validation failures
Connector Doc Buttons in Universal Designer
Documentation buttons at: Settings Tab connector dropdown, Health Check card title, ConnectorCallDrawer Parameters card. Modal z-index 5100 for drawer overlay.
π Glossary
| Term | Definition |
|---|---|
| Debug Path | String identifying expression location for event correlation (e.g., produce/fetch_users/account/key1). Must match backend template_evaluation labels.status. |
| step_id β call_name map | Built from step_add events; correlates step_start and change_dispatch to connector call names for badge counts. |
| entity_confirmed_exact | change_dispatch status indicating confirm action. Maps to changeType 'confirm'. |
| another_step_run | step_add labels.status indicating follow-up dispatch (not entry point). Used for Follow-up Calls badge. |
| DETACHED_READONLY_INVENTORY | Test mode: in-memory, no DB writes. Optional use_db_entities for referencing existing entities. |
| apply_schema_mapping | Debug operation: re-apply schema to raw items. Used for Reliability Check and Evaluate in SchemaDebugDrawer. |
| reconstructValue | Converts editor content + type to stored value. Strictβno fallbacks. Throws on invalid JSON/YAML. |
| normalizeYamlValue | Removed (was corrupting string literals). Value used directly. |
| UniversalDebuggingTab | Shared component for Debugging tab across Universal, non-Universal, System edit. Export from universal/index.ts. |
| ConnectorCallsDebugPanel | Reusable panel showing connector calls grid, schema badges, row selection, detail panel. Used in Schema and future Universal designer. |
βοΈ Testing Checklist
Verification steps from implementation docs.
Debugging Tab
- Universal strategy debugging tab works
- Non-Universal strategy (e.g., Sapias) has same Debugging tab UI
- System edit page has Debugging tab
- Auto-clear on save works for all
- Test/Stop/Clear buttons work
- Empty state shows BugOutlined + Test button
- Templates tab "Debug All" shows evaluations (not "Click Test...")
- Templates tab individual "Debug" buttons open ExpressionEditorDrawer
Expression Editor
- String literal "start_index: 50100" stays as string after save/reopen
- Invalid YAML shows validation error
- Typing in Literal Value doesn't transform on every keystroke
- Radio clicks change type and stay selected
- Can switch from object to jinja
- Sandbox OFF: editor readonly, Evaluate disabled
- Sandbox ON: editor editable, Evaluate enabled, changes don't persist
- Sandbox resets when selecting different row
Schema Debug
- Cell selection highlights row and column
- Reliability check shows β/β per cell
- Evaluate shows new values with strikethrough on originals
- Add Field creates new field; column appears after Evaluate
- Columns chooser Show All / Minimal works
Download
- Download as JSON exports with inventory_run_id
- Download as YAML exports correctly
- Button disabled when no events
β‘ Quick Reference
API Endpoints
| Endpoint | Purpose |
|---|---|
POST /api/datacoll.../debug/strategy/stream | Start debug SSE stream |
POST /api/datacoll.../debug/strategy | apply_schema_mapping |
GET .../systems?schema_mapping=X | List systems by schema |
GET .../monitoring/schema | Monitoring schema |
GET .../strategies/{name}/sample-inputs | Sample inventory inputs |
Canonical Docs
| Doc | Topic |
|---|---|
system-admins/02-dc-ui.md | DC-UI feature catalog |
system-admins/04-replay.md | Replay mechanics |
system-admins/05-execution-modes.md | Five trigger modes, lifecycle, persistence |
system-admins/06-authentication-and-authorization.md | Auth modes, trusted services, troubleshooting |
strategies-development/030-inventory-flow-and-state.md | Step outcomes, persistence rules, dependencies |
strategies-development/060-universal-questionnaire.md | Source-API evaluation checklist (79 questions) |
strategies-development/070-connector-ground-rules.md | Connector authoring rules |
strategies-development/080-strategy-ground-rules.md | Strategy authoring rules |
strategies-development/085-entity-lookup-and-dispatched-changes.md | Lookup methods, range fan-out pattern |
strategies-development/090-visibility-rules.md | Visibility rules, cascade behavior |
strategies-development/100-throttle-groups.md | Throttle groups, jitter, multi-group AND |
File Locations
| Component | Path |
|---|---|
| ExpressionEditorDrawer | src/components/V3Config/common/ExpressionEditorDrawer.tsx |
| DebugEventsContext | src/contexts/DebugEventsContext.tsx |
| ExpressionInput | src/components/V3Config/common/ExpressionInput.tsx |
| UniversalDebuggingTab | universal/UniversalStrategyDesigner.tsx (exported) |
| DebugResultsPanel | universal/components/DebugResultsPanel.tsx |
| ConnectorCallsDebugPanel | common/ConnectorCallsDebugPanel.tsx |
| SchemaDebugDrawer | common/SchemaDebugDrawer.tsx |
| TestConfigModal | common/TestConfigModal.tsx |
| GlobalThemeOverrides (badge CSS) | src/styles/GlobalThemeOverrides.tsx |
π©Ί HealthCheckResultsModal
The HealthCheckResultsModal renders the result of two API calls β testSystem (run from a system or system-type edit page) and testCredentialPointer (run from a credential edit page or the Inventories table's per-row Test action). It's the standard surface for "is this thing healthy right now?", separate from the time-series HealthCheckMiniGraph on the Inventories page.
Per-result rows
The modal lists one row per health check the test produced. Each row shows:
- Status icon β green check on success, red cross on failure
- Connector badge β which connector ran the check
- Credential pointer (for credential tests) or system (for system tests)
- Duration β milliseconds, useful for spotting slow auth flows
- Error message β primary line surfaces immediately on failed rows; full traceback / response is one click away
Per-row detail modal
Clicking a row opens a nested modal with the full HealthCheckDetails result: the params the connector was called with, the response body, any error_data (status code, type, hints), and the raw response when it differs from the parsed view. This is the same shape that ends up in the Admin Events health_check category β the modal is a real-time view; Admin Events is the historical view.
Where it appears
- System / system-type edit pages β Test Health buttons in the header and Connection tab
- Credential edit pages β Test Credential button
- Inventories table β per-row Test action (calls
testSystemfor the row's system)
All entry points gate through withAuth using the appropriate domain (dc-ui:system manage / dc-ui:credential manage). Failed health checks also fan out to Admin Events, where the same per-row detail is reachable via the Failed Health Checks filter preset.
β Debugging FAQ
Why does the Connector Calls tab badge show 4 when I expected 1334?
The tab badge shows actual API calls (step_start count), not expression evaluations. 4 means 4 connector call executions; 1334 would be the total template_evaluation count if that was what was being shown.
Why is Entity Production badge 0 when I have produce actions?
Entity Production shows change_dispatch count scoped to the current call. If the call hasn't dispatched any changes (e.g., empty result, condition not met), it stays 0. Verify step_idβcall correlation; check that produce actions ran.
When should I use Sandbox mode?
When viewing expressions from Templates tabβyou can't save anyway. Enable Sandbox to try different expression syntax, switch types, and hit Evaluate to see new values. Changes don't persist. Resets when you select another row.
Why do columns in Schema Debug grid not update after Add Field?
Grid columns come from server data only. Add Field updates local editedExpressions. Click Evaluate to send modified mappings to server; server returns new data; grid then includes the new column.
What's the difference between Evaluate and Check Reliability?
Evaluate: Sends your edited expressions to server, re-applies transformation, returns new values. Shows strikethrough when different from original. Reliability Check: Re-applies original mappings to raw data, compares with original transformed. Identifies serialization/roundtrip issues.
How do I share a debug session with my team?
Use the Download dropdown (JSON or YAML). Filename includes inventory_run_id. Share the file; teammates can inspect events offline. No live replayβstatic export.
β¨ Summary
Debugging DataCollector spans two layers: the UI layer (the Debugging tab on
every system / system-type / schema designer, the "Follow the Rabbit" badge trail, the
ExpressionEditorDrawer with sandbox mode, the SchemaDebugDrawer with cell selection and
reliability checks, the DebugResultsPanel with all the per-event sub-tabs, JSON/YAML download)
and the engine layer (the /debug/strategy protocol with four
Universal-strategy operations and SSE streaming, the e2e test harness running
--execute headless, the integration-test pattern with file-diverted destinations,
get_step_statistics patterns, eager-dispatch limits, throttle-group debugging via
the Performance Gantt, visibility-cascade decisions in the Diffs tab). Use this page as the
debugging entry point; use day 26 for the underlying concepts and day 27 for the full UI feature
catalog.