Silgi

Analytics

Built-in monitoring dashboard with session tracking, deep error tracing, AI-ready markdown export, and custom span tracking.

Built-in analytics for Silgi. No Prometheus, no Grafana, no external infrastructure. One flag gives you a live dashboard, full error context, session tracking, and one-click markdown export for AI debugging.

Quick start

Pass an analytics options object with a required auth token to serve() or handler():

s.serve(appRouter, {
  : 3000,
  : {
    : process.env.ANALYTICS_TOKEN,
  },
})

Open http://localhost:3000/api/analytics in your browser. The dashboard will prompt for the token on first visit.

What it tracks

MetricHowOverhead
Request count per procedureAtomic counter~0
Error log with full contextInput, headers, stack trace, spans~0
Latency (avg, p50, p95, p99)Ring buffer + performance.now()~2ns
Requests/secCount / elapsed time~0
Time-series (sparkline)1-second windows~0
Custom spans (DB, API calls)trace() helper~0 when unused
HTTP request groupingRequest accumulator with IDs~0
Session trackingCookie-based (_sid)~0
Response headersCaptured after Response is built~0

By default, data lives in memory and is also flushed to storage periodically. When you configure a data mount (Redis, filesystem, etc.), analytics entries survive restarts. Entries older than retentionDays are automatically pruned on each flush.

Dashboard

The dashboard is a self-contained single-file app served at /api/analytics. It auto-refreshes every 2 seconds and groups the most important signals into overview, request, session, and error workflows.

Overview tab:

  • Health summary banner with busiest, noisiest, and slowest procedure callouts
  • Total requests, req/s, error rate, and average latency stat cards
  • Traffic console with peak windows, focus-mode panels for traffic, latency, and failures, plus a sortable per-procedure breakdown

Requests tab:

  • Recent traced requests with procedure, status, and latency filters plus sortable columns
  • Clickable session ID badges to navigate to the session detail
  • Per-request detail pages with span waterfall timing, captured input/output payloads, request and response headers, timing breakdown by category (db, cache, http, etc.), and session link
  • Export menu for Markdown, Timing, Markdown URL, cURL, and JSON
  • Double-click any request row to jump straight to the full detail page

Sessions tab:

  • All active sessions with request count, error count, total/average latency, and procedures called
  • Search and sortable columns
  • Session detail page with:
    • Stat strip (requests, errors, wall clock time, CPU time, avg latency, slowest/fastest request)
    • Procedure journey — visual breadcrumb trail showing the sequence of procedures called across the session
    • Session flow Gantt chart — timeline visualization of when each request happened relative to session start
    • Clickable request timeline with inline span waterfall and input/output preview
    • Per-request mini timing bars showing db/cache/http breakdown at a glance
    • Method and status code breakdown
    • Time by category chart with tooltips
    • Per-procedure stats cards (call count, total/avg duration, spans, errors)
    • Copy for AI (Markdown) and Copy JSON for the entire session

Errors tab:

  • Filterable error log with procedure, severity, and trace-presence controls
  • Detail pages with full input, headers, stack trace, and traced spans for each error
  • Clickable request ID badge to navigate to the HTTP request that caused the error
  • Export menu for Markdown, Markdown URL, cURL, and JSON
  • Double-click any error row to jump straight to the full detail page

Only API traffic is recorded. Analytics ignores dashboard assets and unrelated browser requests such as favicon.ico.

Request IDs

Every response includes an x-request-id header with a unique Snowflake-style ID. The ID is a 13-character Base36 string that is lexicographically time-sorted and collision-resistant across processes.

x-request-id: 1a2b3c4d5e6f7

Request IDs link HTTP requests to errors in the dashboard — clicking an error's request badge navigates to the full request detail.

Session tracking

Analytics automatically tracks user sessions via a _sid cookie. Sessions group multiple HTTP requests from the same browser or client, letting you see the full user journey.

  • Cookie: _sidHttpOnly, SameSite=Lax, 1-year expiry
  • ID format: Same Snowflake format as request IDs — unique and time-sorted
  • No server-side session state: Session data is derived from request entries client-side

Sessions appear in the dashboard's Sessions tab and as clickable badges in request detail pages.

Tracing DB queries and API calls

When analytics is enabled, every request context gets a trace() method. Use it to measure any async operation inside your procedures:

const  = s.$resolve(async ({  }) => {
  const  = await .trace('db.users.findMany', () => db.users.findMany())
  const  = await .trace('db.users.count', () => db.users.count())
  return { ,  }
})

Each traced operation records its name, duration, start offset, and error status. Spans appear in the request detail page with a waterfall timeline, and in the error detail panel when a request fails.

Span options

The trace() method accepts an options object as the third argument:

await ctx.trace('db.users.findMany', () => db.users.findMany(), {
  : 'db',
  : 'SELECT * FROM users WHERE active = true',
  : { : 'active' },
  : () => ({ : .length }),
})
OptionTypeDescription
kindSpanKindCategory for color-coding in the dashboard (see below)
detailstringExtra info shown in the expanded span view (SQL query, URL, etc.)
inputunknownInput data captured for this span (shown in expanded detail)
outputunknown | (result) => unknownOutput data — a value or a function that receives the result
procedure{ input?, output? }Set procedure-level input/output (recorded on the HTTP request)

The output option can be a function to derive the captured value from the trace result, avoiding capturing the full response:

// Capture just the count, not the full user array
await ctx.trace('db.users.findMany', () => db.users.findMany(), {
  : { : 10 },
  : () => ({ : .length }),
})

Procedure-level capture

Use procedure to record input/output at the HTTP request level (visible in the request detail page), not just on the individual span:

const  = s.$input(z.object({ : z.number() })).$resolve(async ({ ,  }) => {
  return .trace('db.users.findById', () => db.users.findById(.id), {
    : { : .id },
    : () => ({ : .name }),
    : {
      ,
      : () => ,
    },
  })
})

Span kinds

Spans are automatically categorized by their name prefix. You can override this with the kind option.

KindAuto-detected when name containsDashboard color
dbdb., sql, prisma, drizzle, query, mongoPurple
httphttp., fetch, api.Blue
cachecache., redis, memcacheEmerald
queuequeue, publish, nats, kafkaAmber
emailemail, smtp, sesOrange
aiai, llm, openai, geminiCyan
customAnything elseGray

Standalone trace() helper

If you prefer an explicit import over the context method, use the standalone trace() function. It works whether analytics is enabled or not — when disabled, it calls the function directly with zero overhead:

import {  } from 'silgi/analytics'

const  = s.$resolve(async ({  }) => {
  const  = await (, 'db.users.findMany', () => db.users.findMany())
  const  = await (, 'api.weather', () => (weatherUrl))
  return { ,  }
})

This is useful when you want tracing code that compiles cleanly regardless of whether the handler has analytics turned on.

Copy for AI

Both errors and requests support one-click copy for AI debugging:

  • Errors: Full context with procedure path, error code, input, headers, stack trace, traced spans, and total duration
  • Requests: HTTP metadata, all procedure calls with input/output, span waterfall with timing breakdown
  • Timing: Focused performance view with just the span timeline and timing categories
  • Sessions: Full session summary with all requests and their procedures

The markdown format is optimized for pasting into Claude, ChatGPT, or any AI assistant. Each export ends with analysis prompts for performance optimization.

The export menu also includes:

  • Markdown URL — a direct link to the server-generated markdown endpoint
  • cURL — a ready-to-run command that fetches the markdown with Accept: text/markdown

For example:

curl -L 'http://localhost:3000/api/analytics/requests/<request-id>/md' \
  -H 'accept: text/markdown'

Protecting the dashboard

The auth option is required. The analytics dashboard exposes request bodies, headers, and stack traces, so authentication must be configured before the dashboard is mounted. Omitting auth throws a startup error.

Token auth

Pass a secret string. The dashboard will prompt for the token on first visit and store it in the browser's session storage.

s.serve(appRouter, {
  : {
    : process.env.ANALYTICS_TOKEN,
  },
})

The token is checked against the Authorization: Bearer <token> header or the silgi-auth cookie (set automatically by the login page).

Custom auth

Pass a function for full control — check cookies, IP allowlists, OAuth tokens, or anything else:

s.serve(appRouter, {
  : {
    : () => {
      const  = .headers.get('cookie') ?? ''
      return .includes('admin_session=valid')
    },
  },
})

The function receives the raw Request and returns boolean or Promise<boolean>. Return true to allow access, false to block with a 401.

auth is mandatory. Silgi will throw at startup if auth is not provided. The dashboard exposes request payloads, headers, error stacks, and internal procedure names.

Persistent storage

Analytics automatically persists request and error entries to storage. By default, storage uses an in-memory driver — data is lost on restart. Mount a persistent driver on the data prefix to keep analytics across restarts:

import  from 'unstorage/drivers/fs'

const  = silgi({
  : () => ({}),
  : {
    : ({ : '.data' }),
  },
})

.serve(appRouter, {
  : {
    : ..,
    : 90,
  },
})

Entries older than retentionDays (default: 30) are pruned on each flush. Aggregate counters (total requests, total errors) are also persisted and restored on startup.

Any unstorage driver works — Redis, S3, Cloudflare KV, etc. See the Storage docs for the full driver list.

Configuration

Pass an options object instead of true to customize buffer sizes, history, and retention:

s.serve(appRouter, {
  : {
    : 'my-secret-token',
    : 2048,
    : 300,
    : 90,
    : ['api/health', 'api/metrics'],
  },
})
OptionTypeDefaultDescription
authstring | (req: Request) => boolean(required)Token string or custom auth function
bufferSizenumber1024Latency samples to keep per procedure (ring buffer)
historySecondsnumber120Time-series sparkline history in seconds
retentionDaysnumber30Days to keep request and error entries in storage
flushIntervalnumber5000Interval in milliseconds between storage flushes
ignorePathsstring[][]Path prefixes to exclude from tracking

Latency samples use fixed-size ring buffers — memory usage stays constant regardless of traffic volume. Request and error entries are retained for retentionDays and pruned automatically on each storage flush.

With handler()

Works the same way when using s.handler() instead of s.serve():

const  = s.handler(appRouter, {
  : { : process.env.ANALYTICS_TOKEN },
})

Bun.serve({ : 3000, :  })
// Dashboard at /api/analytics

With framework adapters

All fetch-based adapters (SvelteKit, Next.js, Astro, Remix, SolidStart) support analytics and scalar options:

// SvelteKit — src/routes/api/[...path]/+server.ts
import {  } from 'silgi/sveltekit'

const  = (appRouter, {
  : () => ({ : .locals.user }),
  : {
    : ..,
  },
})

export const  = 
export const  = 
// Next.js — app/api/[...path]/route.ts
import {  } from 'silgi/nextjs'

const  = (appRouter, {
  : () => ({ : .(.) }),
  : { : .. },
})

export {  as ,  as  }

The dashboard is available at /api/analytics regardless of which adapter you use.

Ignoring paths

Noisy endpoints like health checks or metrics can be completely excluded from tracking via config:

s.serve(appRouter, {
  : {
    : ['api/health', 'api/metrics'],
  },
})

Requests matching these prefixes are never recorded — zero overhead, no storage, no dashboard entries. This is server-side only and cannot be changed at runtime.

Hiding paths

You can also hide paths from the dashboard without stopping server-side tracking. This is useful when you want to keep the data but reduce noise in the UI.

Hidden paths are managed at runtime via the API and persisted to storage across restarts.

MethodEndpointBodyDescription
GET/api/analytics/hiddenList hidden paths
POST/api/analytics/hidden{ "path": "api/..." }Hide a path
DELETE/api/analytics/hidden{ "path": "api/..." }Unhide a path
# Hide health checks from dashboard
curl -X POST http://localhost:3000/api/analytics/hidden \
  -H 'content-type: application/json' \
  -d '{"path": "api/health"}'

# List hidden paths
curl http://localhost:3000/api/analytics/hidden

In the dashboard, right-click any request row and select Hide path to filter it from the view. The data is still recorded and accessible via the JSON API without the hidden filter.

JSON API

The dashboard reads from JSON endpoints. You can also query them directly for custom integrations, alerting, or external dashboards:

EndpointDescription
/api/analytics/statsProcedure metrics, latency percentiles, time-series
/api/analytics/errorsFull error log with input, headers, stack trace, spans
/api/analytics/errors/:idOne error entry as JSON
/api/analytics/requestsRecent requests with trace spans and session IDs
/api/analytics/requests/:idOne request entry as JSON
/api/analytics/requests/:requestIdOne request entry by request ID as JSON
/api/analytics/hiddenManage hidden paths (GET/POST/DELETE)

All endpoints return JSON with cache-control: no-cache.

Pagination

The /api/analytics/requests, /api/analytics/errors, and /api/analytics/tasks endpoints support pagination via query parameters:

ParameterDefaultMaxDescription
page1Page number
limit50200Items per page

Response shape:

{
  "data": [...],
  "page": 1,
  "limit": 50,
  "total": 342,
  "totalPages": 7
}
# Get page 2 with 20 items
curl 'http://localhost:3000/api/analytics/requests?page=2&limit=20'

Markdown export endpoints

Server-generated markdown is available over HTTP, so bots, LLM tools, Slack jobs, and shell scripts can fetch the same export shown in the dashboard:

EndpointDescription
/api/analytics/errors/mdEntire error log as markdown
/api/analytics/errors/:id/mdOne error as markdown
/api/analytics/requests/:id/mdOne request as markdown
/api/analytics/requests/:requestId/mdOne request by request ID

These endpoints return Content-Type: text/markdown; charset=utf-8.

Stats response shape

interface AnalyticsSnapshot {
  : number // seconds since start
  : number
  : number
  : number // percentage (0-100)
  : number
  : number // milliseconds
  : <
    string,
    {
      : number
      : number
      : number
      : { : number; : number; : number; : number }
      : string | null
      : number | null
    }
  >
  : <{ : number; : number; : number }>
}

Error response shape

interface ErrorEntry {
  : number
  : string // links to the HTTP request that caused this error
  : number
  : string
  : string
  : string // e.g. 'BAD_REQUEST', 'NOT_FOUND', 'INTERNAL_SERVER_ERROR'
  : number
  : string
  : unknown
  : <string, string>
  : number
  : []
}

Request response shape

interface RequestEntry {
  : number
  : string // unique ID (also in x-request-id response header)
  : string // persistent session ID (from cookie)
  : number
  : number
  : string
  : string
  : string
  : string
  : <string, string>
  : <string, string>
  : string
  : number
  : ProcedureCall[]
  : boolean
}

interface ProcedureCall {
  : string
  : number
  : number
  : unknown
  : unknown
  : TraceSpan[]
  ?: string
}

interface TraceSpan {
  : string
  : 'db' | 'http' | 'cache' | 'queue' | 'email' | 'ai' | 'custom'
  : number
  ?: number
  ?: string
  ?: unknown
  ?: unknown
  ?: string
}

Server-side markdown export

You can also generate the markdown programmatically for Slack notifications, log files, or custom alerting:

import { , ,  } from 'silgi/analytics'

// Error context for AI debugging
const  = (errorEntry)

// Full request with all procedures and spans
const  = (requestEntry)

// Entire session with all requests
const  = (sessionRequests, sessionId)

Privacy behavior

Sensitive HTTP headers are always redacted, regardless of environment:

  • authorization, cookie, set-cookie, x-api-key, x-auth-token, and proxy-authorization are stored as [REDACTED] in all analytics entries, JSON exports, and markdown exports
  • This applies in development, staging, and production — there is no environment-dependent behavior

Request and response bodies (input, output) are captured as-is. Avoid recording sensitive fields by not returning them from procedures or by filtering them before returning.

Built-in analytics supports persistent storage for single-service use. For multi-service correlation and alerting, use OpenTelemetry with your preferred backend.

What's next?

On this page