Skip to main content
Named views group related filters and aggregates into focused sets. Each view appears as a card at /dashboard and has its own route at /dashboard/[viewName].

Create a view

app.dashboard.view('performance', { label: 'Performance Metrics' })
  .filter('turn-count', ({ stats }) => stats.turnCount, { label: 'Turn Count' })
  .filter('tool-calls', ({ stats }) => stats.toolCallCount, { label: 'Tool Calls' });

Options

OptionTypeDefaultDescription
labelstringview nameHuman-readable label shown on the view card
cachedOnlybooleanfalseOnly show sessions with complete cached results

Chaining filters and aggregates

The view builder returns itself, so you can chain .filter() and .aggregate() calls:
app.dashboard.view('quality', { label: 'Quality Checks' })
  .filter('has-errors', ({ entries }) =>
    entries.some(e =>
      Array.isArray(e.message?.content) &&
      e.message.content.some(b => b.type === 'tool_use' && b.is_error)
    ),
    { label: 'Has Errors' }
  )
  .filter('primary-model', ({ stats }) => stats.models[0] || 'unknown',
    { label: 'Primary Model' }
  )
  .aggregate('session-metrics', {
    collect: ({ stats }) => ({
      turnCount: stats.turnCount,
      toolCalls: stats.toolCallCount,
    }),
    reduce: (collected) => {
      const n = collected.length || 1;
      let turns = 0, tools = 0;
      for (const s of collected) {
        turns += typeof s.values.turnCount === 'number' ? s.values.turnCount : 0;
        tools += typeof s.values.toolCalls === 'number' ? s.values.toolCalls : 0;
      }
      return [
        { Metric: 'Avg Turns', Value: +(turns / n).toFixed(1) },
        { Metric: 'Avg Tool Calls', Value: +(tools / n).toFixed(1) },
      ];
    },
  });

Routing

URLBehavior
/dashboardIf named views exist, shows a card grid of all views. If only default filters are registered, shows them directly.
/dashboard/[viewName]Specific view with its filters, sessions table, and aggregates.

cachedOnly views

When cachedOnly: true, only sessions with complete cache entries appear in the view. Uncached sessions are skipped before any filter or log parsing runs - no eval execution, no log reads. This makes cachedOnly views much faster for large session sets. Sessions appear as the background queue processes them. Filter functions in cachedOnly views receive ctx.evalResults - a record of cached eval results. Use this to build per-eval score filters without re-running evals:
app.dashboard.view('eval-results', { cachedOnly: true, label: 'Eval Score Filters' })
  .filter('quality-score',
    (ctx) => ctx.evalResults?.['quality']?.score ?? 0,
    { label: 'Quality Score' }
  )
  .filter('all-passing', (ctx) => {
    if (!ctx.evalResults) return false;
    return Object.values(ctx.evalResults).every(r => r.pass);
  }, { label: 'All Evals Passing' });

Default view (backward-compatible)

app.dashboard.filter() called directly (without a view) registers filters on the implicit "default" view. Default filters appear below the view cards on /dashboard.
// Still works - goes to the default view
app.dashboard.filter('model', ({ stats }) => stats.models[0] || 'unknown',
  { label: 'Model' }
);