A small Express 5 app that exercises most of Better Stack: logs, traces, metrics, error tracking, uptime monitors, heartbeats, incidents and browser analytics. Every page and endpoint exists to produce one specific signal, so you can click a button and find the result in the matching Better Stack dashboard.
It is deployed to Vercel from a container image (Containerfile.vercel).
- How the signals flow
- Pages and endpoints
- Better Stack features
- Configuration
- Run locally
- Deploy to Vercel
- Vercel log drain
- Better Stack MCP with fx
- Project layout
| Signal | Produced by | Sent to Better Stack via |
|---|---|---|
| Logs | log() in server.js |
@logtail/node client to BETTERSTACK_INGEST_HOST |
| Traces | OpenTelemetry (otel.js) |
OTLP/HTTP to https://<ingest host>/v1/traces |
| Metrics (OTLP) | OpenTelemetry meter | OTLP/HTTP to https://<ingest host>/v1/metrics, every 15 s |
| Metrics (Prometheus) | prom-client |
Scraped from /metrics |
| Errors | @sentry/node |
Sentry protocol to the Better Stack error tracking DSN |
| Uptime | Better Stack monitors | Probe /health, /fail, /slow from outside |
| Heartbeat | /cron |
HTTP GET to HEARTBEAT_URL |
| Browser analytics | Script tag on / |
betterstack.net/b.js |
otel.js is preloaded with node --import ./otel.js, so the HTTP and Express instrumentation patch the libraries before server.js imports them. Log lines written inside a request carry the active trace_id, which lets you jump from a log to its trace.
If a variable is missing, that signal is skipped and the app still runs.
The home page (/) is "Mission Control": a space-themed page with one button per endpoint below. Clicking a button calls the endpoint with fetch and prints the status and the first 600 characters of the body.
| Path | Button | Behaviour |
|---|---|---|
/health |
Health ping | Returns { ok: true, uptime }. Target for the uptime monitor. |
/cron |
Heartbeat | GETs HEARTBEAT_URL and logs the result. Skips with a message if the variable is unset. |
/slow |
Slow orbit | Responds after 3 to 5 seconds. Use it to trigger a response time alert. |
/fail |
Engine failure | Logs an error and returns HTTP 500. Use it to trigger an incident. |
| Path | Button | Behaviour |
|---|---|---|
/log |
Flight log | Sends one info, one warn and one error log with structured fields. |
/work |
Orbit maneuver | Runs nested spans db.query then external.api (20 to 250 ms of simulated work), records a duration histogram and logs the total. |
/metrics |
Telemetry feed | Prometheus text format, see Prometheus scraping. |
| Path | Button | Behaviour |
|---|---|---|
/boom |
Meteor strike | Throws an unhandled error. The Express error handler marks the active span as failed, records the exception and logs the stack. |
/sentry/error |
Shield breach | Catches a JSON parse error and reports it with captureException. |
/sentry/message |
Signal flare | captureMessage at warning level with tags. |
/sentry/context |
Crew report | Reports an exception with user, tags, a custom order context and two breadcrumbs. |
/sentry/crash |
Solar flare | Throws an unhandled error that the Sentry Express handler reports. |
Structured logs go through the @logtail/node client and also to stdout. Search by trace_id, route or any field passed to log(). Try /log for mixed levels, /work for a log tied to a trace, and /fail for an error log.
/work produces a trace with the HTTP and Express spans from auto-instrumentation plus the custom db.query and external.api child spans. /boom produces a trace whose span is marked as an error with the exception attached. The service name is betterstack-demo.
Two paths:
- OTLP:
demo.clicks(counter, per route),demo.work.duration(histogram, ms),demo.in_flightanddemo.heap_used(gauges). Pushed every 15 seconds. - Prometheus: see below.
GET /metrics serves Node process metrics plus demo_http_requests_total{route,status} and demo_http_request_duration_seconds{route}.
Scrape target after deploy: https://<your-app>.vercel.app/metrics (locally http://localhost:18080/metrics). Point a Prometheus scrape job or Better Stack's collector at it (Prometheus docs).
The endpoint is public. Put it behind auth if you keep the app deployed. On Vercel each function instance reports its own counters, so values reset on cold start.
Better Stack error tracking accepts the Sentry protocol, so the app uses @sentry/node with a Better Stack DSN. otel.js calls Sentry.init with skipOpenTelemetrySetup: true because OpenTelemetry is already configured by the same file. The environment tag comes from VERCEL_ENV and falls back to local.
Suggested setup in Better Stack:
| Monitor | URL | Purpose |
|---|---|---|
| Healthy | https://<your-app>.vercel.app/health |
Stays green. |
| Failing | https://<your-app>.vercel.app/fail |
Always 500, so it opens an incident. |
| Slow | https://<your-app>.vercel.app/slow |
Set a response time threshold below 3 s to see a degraded alert. |
A monitor that fails (/fail) opens an incident and notifies whoever is on the escalation policy. Acknowledge and resolve it in Better Stack, or ask the MCP agent (see fx).
Create a heartbeat in Better Stack and set its URL as HEARTBEAT_URL. Each click on /cron sends a ping. Stop clicking and the heartbeat goes overdue, which opens an incident. The /cron path is also what you would point a Vercel cron job at.
The / page loads the Better Stack browser script (betterstack.net/b.js) and calls betterstack('init', { environment: 'production' }). The site token is hardcoded in server.js, so change it to your own before reusing the app.
Copy .env.example to .env.
| Variable | Used by | Description |
|---|---|---|
BETTERSTACK_SOURCE_TOKEN |
Logs, traces, metrics | Source token. Sent as a bearer token to the OTLP endpoints. |
BETTERSTACK_INGEST_HOST |
Logs, traces, metrics | Ingest host of your source, without https://. |
HEARTBEAT_URL |
/cron |
Heartbeat URL from Better Stack. |
SENTRY_DSN |
Error tracking | DSN of the Better Stack error tracking application. |
PORT |
Server | Defaults to 3000. The container sets it to 80. |
cp .env.example .env # fill in values
docker build -f Containerfile.vercel -t bs-demo .
docker run --rm -p 18080:80 --env-file .env bs-demo
Then open http://localhost:18080.
Without Docker: npm ci && npm start (needs Node 22, listens on port 3000, reads variables from your shell, not from .env).
vercel link
vercel env add BETTERSTACK_SOURCE_TOKEN
vercel env add BETTERSTACK_INGEST_HOST
vercel env add HEARTBEAT_URL
vercel env add SENTRY_DSN
vercel --prod
The app ships its own logs directly. A log drain adds what the app can't see: build logs, edge and request logs, and function runtime output.
- Option A, automatic: in Vercel open Marketplace, Better Stack, Add integration, and pick this project. Needs a Vercel Pro or Enterprise plan.
- Option B, manual: in Better Stack create an HTTP source. In Vercel go to Team Settings, Drains, and add a custom drain with format NDJSON and header
Authorization: Bearer <source token>.
Docs: https://betterstack.com/docs/logs/vercel/
The container's stdout is also captured by Vercel, so with a drain enabled the same log line can arrive twice (once via the drain, once via @logtail/node). Filter by source or turn one path off.
Vercel can also forward OpenTelemetry traces through Trace Drains (OTLP/HTTP). We did not find Better Stack documenting that as a supported target, so this app exports traces itself from otel.js.
fx is Vercel's small open-source coding agent CLI (macOS and Linux). With Better Stack's hosted MCP server it can query logs, traces, incidents and monitors while you debug this app.
Install (the installer is not signature-checked, so read it first):
curl -fsSL https://fx.sh/setup.sh | bash
fx doctor
Authenticate through AI Gateway:
npx vercel@latest ai-gateway setup --agent fx # or: fx login
Connect Better Stack (OAuth, no token on disk):
fx mcp add --transport http betterstack https://mcp.betterstack.com
fx mcp login betterstack
fx rejects a literal Authorization header in ~/.fx/mcp.json. For an API token, use bearer_token_env (the name of an env var) instead. The exact JSON shape is not documented in the sources we found, so check fx mcp --help.
The Better Stack server supports X-MCP-Tools-Only and X-MCP-Tools-Except headers to limit which tools the agent can call. Prefer read-only tools for a demo.
Try: "Show errors from the last hour and the trace for /boom", or "Is the /health monitor up? Any open incidents?"
Keep ~/.fx/mcp.json out of git.
| File | Role |
|---|---|
server.js |
Express app: home page, all endpoints, logging, metrics, Sentry handlers. |
otel.js |
Preloaded setup for OpenTelemetry traces and metrics, and Sentry.init. |
Containerfile.vercel |
Node 22 Alpine image, runs npm start on port 80. |
.env.example |
List of supported environment variables. |