# {{capProjectName}} {{capAppName}} — observability + testing

The production-readiness slice in one backend: **Prometheus metrics**, **Sentry
error tracking**, **distributed tracing**, and a **`@voltro/testing` unit test**
— so a real service is observable AND covered from day one.

## What it demonstrates

- **Prometheus** ([`@voltro/plugin-prometheus`](https://voltro.cloud/docs/observability/metrics)) —
  `GET /metrics` in the text exposition format. The framework already records
  `voltro_rpc_*` / `voltro_http_*` / subscription / cache metrics; `notes.create`
  adds a **custom counter** (`app_notes_created_total`) that shows up automatically.
- **Sentry** ([`@voltro/plugin-sentry`](https://voltro.cloud/docs/observability/errors)) —
  rpc / REST / workflow / render errors reported correlated to the active
  `traceId`. **Inert without `SENTRY_DSN`**, so it ships wired — set the env to turn it on.
- **Tracing** — on by default in `voltro dev` (DevTools Traces panel + `traceId`
  on every log line). Set `OTEL_EXPORTER_OTLP_ENDPOINT` to ship to Jaeger / Tempo / Honeycomb.
- **Testing** ([`@voltro/testing`](https://voltro.cloud/docs/testing)) — a unit
  test that runs the `notes.create` handler with `makeTestContext` + `mockStore`
  (no DB, no server) and asserts the **real** tenant scoping.

## Run it

```bash
voltro dev .
```

### Metrics

```bash
# Create a note (custom counter increments)
curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
  -H 'content-type: application/json' \
  -d '{"tag":"notes.create","input":{"title":"Hello","body":"World"}}'

# Scrape Prometheus — your custom counter + the framework's rpc metrics
curl -s http://localhost:4000/metrics | grep -E 'app_notes_created_total|voltro_rpc_requests_total'
# app_notes_created_total 1
# voltro_rpc_requests_total{tag="notes.create",status="ok"} 1
```

Point Prometheus / Grafana Agent at `/metrics`; gate it with
`prometheusPlugin({ token })` / `PROMETHEUS_TOKEN` for a public deploy.

### Errors → Sentry

Set `SENTRY_DSN` (api) — then any handler throw, REST 5xx, workflow failure, or
React render/loader error is captured, tagged with the request's `traceId`. For
the full browser→backend waterfall, add the `sentry` field to a paired web app's
`app.config.ts` (see the docs).

### A custom metric

```ts
// mutations/notes.create.mutation.server.ts
import { Effect, Metric } from 'effect'
import { counter } from '@voltro/runtime'

const notesCreated = counter('app_notes_created_total', 'Notes created.')
Effect.runSync(Metric.increment(notesCreated))   // shows up in /metrics + the dashboard
```

## Test it

The template ships a unit test (`tests/notes.create.test.ts`) and the dev deps
(`@voltro/testing`, `vitest`). After `pnpm install`:

```bash
voltro test               # vitest against this app
```

```ts
const ctx = makeTestContext({
  subject: { type: 'user', id: 'user_1', tenantId: 'acme' },
  store:   mockStore({ notes: [] }),
})
const row = await createNote({ title: 'Hello', body: 'World' }, ctx)
expect(row.tenantId).toBe('acme')   // tenant() stamped it — the REAL store, no DB
```

`ctx.store` is the same mixin-wrapped store production uses, so tenant
auto-scoping, soft-delete filtering, and audit auto-fill all behave identically —
in milliseconds, with no docker. `ctx.withTenant('t2', …)` re-scopes the same
data to prove cross-tenant isolation. See `@voltro/testing` for `MockClock` /
`MockEmail` / `mockAi` / `makeWorkflowRunner` / `runDialectParity`.

## Going to production

| Want… | Do |
|---|---|
| Metrics survive a public deploy | `prometheusPlugin({ token })` + scrape over TLS |
| Errors in Sentry | set `SENTRY_DSN`; add `sentryPlugin({ traces: true })` for performance traces |
| Spans in your tracer | `OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318` |
| Vendor-native (Datadog) | add `@voltro/plugin-datadog` — agentless metrics + correlated logs/traces |
| CI gate | run `voltro test` in CI; add `runDialectParity` for hand-written SQL |

## Anti-patterns

- **Re-creating the counter per call.** Declare `counter(...)` at MODULE level —
  a per-call `counter()` resets the series. (The runtime dedupes by name, but
  module-level is the clear intent.)
- **Assuming Sentry needs code to wire each primitive.** The plugin subscribes
  to the framework's server-error bus once — rpc, REST, workflows, schedules,
  subscribers, webhooks, and startups are all covered. You just set the DSN.
- **Reaching for a real DB in unit tests.** `makeTestContext` + `mockStore` give
  the real store behaviour in-memory. Save a live database for `voltro e2e`.
