Hono-based API server for error ingestion. Receives errors from client applications and stores them in ClickHouse for analysis.
# From the project root
docker compose up -dpnpm --filter @error-ingestor/server db:setup# Development (with hot reload)
pnpm --filter @error-ingestor/server dev
# Production
pnpm --filter @error-ingestor/server build
pnpm --filter @error-ingestor/server startServer runs at http://localhost:3000.
Create a .env file or set these environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
CLICKHOUSE_HOST |
http://localhost:8123 |
ClickHouse HTTP endpoint |
CLICKHOUSE_USER |
default |
ClickHouse username |
CLICKHOUSE_PASSWORD |
(empty) | ClickHouse password |
CLICKHOUSE_DATABASE |
error_ingestor |
Database name |
Full health check including dependencies.
curl http://localhost:3000/healthResponse:
{
"status": "healthy",
"timestamp": "2024-01-15T10:30:00.000Z",
"services": {
"clickhouse": "up"
}
}Liveness probe (always returns 200 if server is running).
Readiness probe (503 if ClickHouse is unavailable).
All /api/* routes require the X-API-Key header.
Batch ingest error events.
Headers:
Content-Type: application/json
X-API-Key: your-api-key
Request Body:
{
"events": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "network/request-failed",
"message": "Failed to fetch user data",
"stackTrace": "Error: Failed to fetch...",
"appId": "com.example.app",
"appVersion": "1.0.0",
"platform": "ios",
"platformVersion": "17.0",
"userId": "user-123",
"timestamp": "2024-01-15T10:30:00.000Z",
"metadata": {
"endpoint": "/api/users",
"statusCode": 500
},
"tags": {
"severity": "high"
}
}
]
}Response (200):
{
"success": true,
"accepted": 1
}Response (400 - Invalid app ID):
{
"success": false,
"error": "Some events do not match authenticated app. Expected appId: com.example.app"
}Test API key validity.
curl -H "X-API-Key: ei_test_key_12345" http://localhost:3000/api/v1/ingest/testResponse:
{
"success": true,
"appId": "test-app",
"appName": "Test Application"
}These endpoints power the dashboard UI.
Query error events with filters.
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
appId |
string | Yes | Application ID |
startTime |
ISO datetime | No | Start of time range (default: 24h ago) |
endTime |
ISO datetime | No | End of time range (default: now) |
code |
string | No | Filter by error code |
userId |
string | No | Filter by user ID |
limit |
number | No | Results per page (default: 100, max: 1000) |
offset |
number | No | Pagination offset |
curl "http://localhost:3000/dashboard/api/errors?appId=test-app&limit=10"Response:
{
"success": true,
"errors": [
{
"id": "...",
"code": "network/request-failed",
"message": "...",
"timestamp": "2024-01-15T10:30:00.000Z",
...
}
],
"pagination": {
"limit": 10,
"offset": 0,
"hasMore": true
}
}Get error trends over time.
Query Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
appId |
string | Yes | Application ID |
startTime |
ISO datetime | No | Start of time range (default: 7 days ago) |
endTime |
ISO datetime | No | End of time range (default: now) |
granularity |
hour | day |
No | Time bucket size (default: hour) |
curl "http://localhost:3000/dashboard/api/trends?appId=test-app&granularity=day"Response:
{
"success": true,
"trends": [
{
"time": "2024-01-14T00:00:00.000Z",
"count": 42,
"uniqueCodes": 5,
"affectedUsers": 12
},
...
]
}Get all error codes for an app with counts.
curl "http://localhost:3000/dashboard/api/codes?appId=test-app"Response:
{
"success": true,
"codes": [
{ "code": "network/request-failed", "count": 156 },
{ "code": "auth/session-expired", "count": 89 },
...
]
}List registered applications.
curl http://localhost:3000/dashboard/api/appsResponse:
{
"success": true,
"apps": [
{
"id": "test-app",
"name": "Test Application",
"createdAt": "2024-01-01T00:00:00.000Z"
}
]
}The server uses API key authentication for the ingestion endpoint.
Test API Key (Development):
ei_test_key_12345
Using the API Key:
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: ei_test_key_12345" \
-d '{"events": [...]}' \
http://localhost:3000/api/v1/ingestIn production, implement the registerApp function in src/middleware/auth.ts:
import { registerApp } from './middleware/auth';
// Register a new app and get its API key
const { apiKey } = registerApp('com.example.app', 'My Application');
console.log('API Key:', apiKey); // ei_abc123...The server uses ClickHouse with the following schema:
| Column | Type | Description |
|---|---|---|
id |
UUID | Unique event ID |
code |
LowCardinality(String) | Error code |
message |
String | Error message |
stack_trace |
String | Stack trace |
app_id |
LowCardinality(String) | Application ID |
app_version |
LowCardinality(String) | App version |
platform |
LowCardinality(String) | ios/android/web |
platform_version |
LowCardinality(String) | Platform version |
user_id |
Nullable(String) | User ID |
timestamp |
DateTime64(3) | Event timestamp |
metadata |
String (JSON) | Additional data |
tags |
Map(String, String) | Tags/labels |
Data is partitioned by month and has a 90-day TTL.
FROM node:20-slim
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
CMD ["node", "dist/index.js"]The server is built with Hono, which supports multiple runtimes. Export the app for your platform:
// For Vercel Edge
export const runtime = 'edge';
export default app;
// For Cloudflare Workers
export default {
fetch: app.fetch,
};For production:
- Set up a ClickHouse cluster (or use ClickHouse Cloud)
- Configure environment variables
- Implement proper API key storage (database instead of in-memory)
- Add rate limiting
- Enable HTTPS
# Run in development mode with hot reload
pnpm dev
# Build for production
pnpm build
# Type check
pnpm typecheck
# Run database setup
pnpm db:setup