@owlmetry/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ ---
2
+ name: owlmetry-cli
3
+ version: 0.1.0
4
+ description: >-
5
+ Install the OwlMetry CLI, sign up, and manage projects, apps, metrics,
6
+ funnels, and events. Use when adding OwlMetry to a project, querying
7
+ analytics, or when another OwlMetry skill needs CLI setup as a prerequisite.
8
+ allowed-tools: Bash
9
+ ---
10
+
11
+ ## What is OwlMetry?
12
+
13
+ OwlMetry is a self-hosted analytics platform for mobile and backend apps. It captures events, structured metrics, and funnel conversions from client SDKs (Swift, Node.js), stores them in a partitioned PostgreSQL database, and exposes query and management APIs.
14
+
15
+ The **CLI** is the management and query tool — it does not ingest events. You use it to create projects and apps, define metrics and funnels, query event data, and view analytics. SDKs handle event ingestion using client keys (`owl_client_...`). The CLI uses agent keys (`owl_agent_...`) for reading data and managing resources.
16
+
17
+ ## Version Check
18
+
19
+ Run these checks silently. Only inform the user if updates are available.
20
+
21
+ 1. **Skill version**: `curl -sf https://raw.githubusercontent.com/Jasonvdb/owlmetry/main/skills/owlmetry-cli/SKILL.md | head -5` — compare the `version:` field to `0.1.0`. If newer, ask the user if they want to update the local skill file.
22
+ 2. **CLI version** (if installed): compare `owlmetry --version` to `npm view @owlmetry/cli version 2>/dev/null`. If a newer version exists, offer `npm install -g @owlmetry/cli@latest`.
23
+
24
+ If everything is current or the remote is unreachable, continue silently.
25
+
26
+ ## Setup
27
+
28
+ **Prerequisites:** Node.js 20+
29
+
30
+ **Install:**
31
+ ```bash
32
+ npm install -g @owlmetry/cli
33
+ ```
34
+
35
+ **Sign up / log in:**
36
+ ```bash
37
+ owlmetry auth send-code --email <user-email>
38
+ ```
39
+ Ask the user for the 6-digit verification code that arrives by email, then:
40
+ ```bash
41
+ owlmetry auth verify --email <user-email> --code <code> --format json
42
+ ```
43
+
44
+ - New users are auto-provisioned with a team, project, and backend app.
45
+ - The response includes an agent API key (`owl_agent_...`), team ID, project ID, and app details.
46
+ - Config is saved to `~/.owlmetry/config.json`.
47
+ - Default endpoint: `https://api.owlmetry.com`
48
+
49
+ If the user already has a config file (`~/.owlmetry/config.json`), they're already set up — skip auth.
50
+
51
+ **Manual setup** (if the user already has an API key):
52
+ ```bash
53
+ owlmetry setup --endpoint <url> --api-key <key>
54
+ ```
55
+
56
+ ## Resource Hierarchy
57
+
58
+ OwlMetry organises resources in a `Team → Project → Apps` hierarchy:
59
+
60
+ - **Team** — the top-level account. Users belong to one or more teams. All resources (projects, apps, keys) are team-scoped.
61
+ - **Project** — groups related apps under one product (e.g., "MyApp" project). Metrics and funnels are defined at the project level so they span all apps in the project.
62
+ - **App** — represents a single deployable artifact. Each app has a `platform` (`apple`, `android`, `web`, `backend`) and, for non-backend platforms, a `bundle_id`. Creating an app auto-generates a `client_key` for SDK use.
63
+
64
+ Projects group apps cross-platform: an iOS app and its backend API can share the same project, enabling unified funnel and metric analysis across both.
65
+
66
+ ## Resource Management
67
+
68
+ ### Projects
69
+
70
+ Projects group apps by product and scope metrics and funnels. Create one project per product (e.g., "Acme" with iOS + backend apps under it).
71
+
72
+ ```bash
73
+ owlmetry projects --format json # List all
74
+ owlmetry projects view <id> --format json # View details + apps
75
+ owlmetry projects create --team-id <id> --name <name> --slug <slug> --format json
76
+ owlmetry projects update <id> --name <new-name> --format json
77
+ ```
78
+
79
+ ### Apps
80
+
81
+ An app represents a single deployable target. The `client_key` returned on creation is what SDKs use for event ingestion. The `bundle_id` is **immutable after creation** — to change it, delete and recreate the app. Backend apps have no bundle_id.
82
+
83
+ ```bash
84
+ owlmetry apps --format json # List all
85
+ owlmetry apps --project <id> --format json # List by project
86
+ owlmetry apps view <id> --format json # View details
87
+ owlmetry apps create --project-id <id> --name <name> --platform <platform> [--bundle-id <id>] --format json
88
+ owlmetry apps update <id> --name <new-name> --format json
89
+ ```
90
+
91
+ - **Platforms:** `apple`, `android`, `web`, `backend`
92
+ - `--bundle-id` is required for apple/android/web, omitted for backend
93
+ - The create response includes `client_key` — this is the SDK API key
94
+
95
+ ### Metric Definitions
96
+
97
+ Metrics are project-scoped definitions that tell OwlMetry what structured data to expect from SDKs. There are two kinds:
98
+
99
+ - **Lifecycle metrics** (`--lifecycle`): track operations with a start → complete/fail/cancel flow. Use for things with duration — API calls, uploads, database queries. The SDK auto-tracks `duration_ms`.
100
+ - **Single-shot metrics** (no `--lifecycle`): record a point-in-time measurement. Use for snapshots — cache hit rates, queue depth, cold start time.
101
+
102
+ The metric definition must exist on the server **before** the SDK emits events for that slug, otherwise the server will reject the events.
103
+
104
+ ```bash
105
+ owlmetry metrics --project <id> --format json # List all
106
+ owlmetry metrics view <slug> --project <id> --format json # View details
107
+ owlmetry metrics create --project <id> --name <name> --slug <slug> [--lifecycle] [--description <desc>] --format json
108
+ owlmetry metrics update <slug> --project <id> [--name <name>] [--status active|paused] --format json
109
+ owlmetry metrics delete <slug> --project <id>
110
+ ```
111
+
112
+ Slugs: lowercase letters, numbers, hyphens only (`/^[a-z0-9-]+$/`).
113
+
114
+ ### Funnel Definitions
115
+
116
+ Funnels measure how users progress through a multi-step flow and where they drop off. Each funnel has an ordered list of steps, and each step has an `event_filter` that matches incoming events (by `message` and/or `screen_name`).
117
+
118
+ SDKs emit funnel events via `track("step-name")`, which generates events with message `"track:step-name"`. The `event_filter` in each step must match these messages exactly.
119
+
120
+ Funnels support two analysis modes:
121
+ - **Closed mode** (default): sequential — a user must complete steps in order (step 2 only counts if step 1 was completed first). Use for linear flows like onboarding or checkout.
122
+ - **Open mode** (`--open` on query): independent — each step is evaluated separately. Use when steps can happen in any order.
123
+
124
+ Maximum 20 steps per funnel.
125
+
126
+ ```bash
127
+ owlmetry funnels --project <id> --format json # List all
128
+ owlmetry funnels view <slug> --project <id> --format json # View details
129
+ owlmetry funnels create --project <id> --name <name> --slug <slug> --steps '<json>' [--description <desc>] --format json
130
+ owlmetry funnels update <slug> --project <id> [--name <name>] [--steps '<json>'] --format json
131
+ owlmetry funnels delete <slug> --project <id>
132
+ ```
133
+
134
+ Steps JSON format: `[{"name":"Step Name","event_filter":{"message":"track:step-name"}}]`
135
+
136
+ ## Querying
137
+
138
+ ### Events
139
+
140
+ Events are the raw log records emitted by SDKs — every `Owl.info()`, `Owl.error()`, `Owl.track()`, etc. Query events when debugging specific issues, investigating user behavior, or reviewing what happened in a time window.
141
+
142
+ ```bash
143
+ owlmetry events [--project <id>] [--app <id>] [--since <time>] [--until <time>] [--level info|debug|warn|error] [--user <id>] [--session <id>] [--screen <name>] [--limit <n>] [--cursor <cursor>] [--data-mode production|development|all] --format json
144
+ owlmetry events view <id> --format json
145
+ ```
146
+
147
+ Defaults to last 24 hours if no `--since`/`--until` specified.
148
+
149
+ ### Investigate (contextual events)
150
+
151
+ Investigate works like a flight recorder — given a single event ID, it returns the surrounding events from the same app within a time window. Use this when you have a specific error or anomaly and want to see what happened immediately before and after it.
152
+
153
+ ```bash
154
+ owlmetry investigate <eventId> [--window <minutes>] --format json
155
+ ```
156
+
157
+ Shows events surrounding a target event. Default window: 5 minutes.
158
+
159
+ ### Users
160
+
161
+ ```bash
162
+ owlmetry users <app-id> [--anonymous] [--real] [--search <query>] [--limit <n>] --format json
163
+ ```
164
+
165
+ `--anonymous` and `--real` are mutually exclusive.
166
+
167
+ ### Metric Events & Aggregation
168
+
169
+ There are two ways to look at metric data:
170
+
171
+ - **`metrics events`** — raw metric event records, useful for debugging individual operations (e.g., "why did this specific upload fail?"). Shows each start/complete/fail/cancel/record event individually.
172
+ - **`metrics query`** — aggregated statistics (count, avg/p50/p95/p99 duration, error rate), useful for spotting trends and regressions. Supports grouping by app, version, environment, device, or time bucket.
173
+
174
+ ```bash
175
+ owlmetry metrics events <slug> --project <id> [--phase start|complete|fail|cancel|record] [--tracking-id <id>] [--user <id>] [--since <time>] [--until <time>] [--environment <env>] [--data-mode <mode>] --format json
176
+ owlmetry metrics query <slug> --project <id> [--since <date>] [--until <date>] [--app <id>] [--app-version <v>] [--environment <env>] [--user <id>] [--group-by app_id|app_version|device_model|os_version|environment|time:hour|time:day|time:week] [--data-mode <mode>] --format json
177
+ ```
178
+
179
+ ### Funnel Analytics
180
+
181
+ Funnel queries return conversion rates and drop-off between steps. The output shows how many users entered each step and what percentage continued to the next. Use `--group-by` to segment results and compare conversion across environments, app versions, or A/B experiment variants.
182
+
183
+ ```bash
184
+ owlmetry funnels query <slug> --project <id> [--since <date>] [--until <date>] [--open] [--app-version <v>] [--environment <env>] [--experiment <name:variant>] [--group-by environment|app_version|experiment:<name>] [--data-mode <mode>] --format json
185
+ ```
186
+
187
+ `--open` = open funnel mode (steps evaluated independently, not sequentially).
188
+
189
+ ### Audit Logs
190
+
191
+ Audit logs record who performed what action on which resource — creating an app, revoking an API key, changing a team member's role, etc. Query them when investigating configuration changes or tracking administrative activity. Requires `audit_logs:read` permission on the agent key (included in default agent key permissions).
192
+
193
+ ```bash
194
+ owlmetry audit-log list --team <id> [--resource-type <type>] [--resource-id <id>] [--actor <id>] [--action create|update|delete] [--since <time>] [--until <time>] [--limit <n>] --format json
195
+ ```
196
+
197
+ ## Key Notes
198
+
199
+ - Always use `--format json` when parsing output programmatically.
200
+ - **Global flags** available on all commands: `--endpoint <url>`, `--api-key <key>`, `--format <format>`
201
+ - **Agent keys** (`owl_agent_...`) are for CLI queries. **Client keys** (`owl_client_...`) are for SDK event ingestion.
202
+ - **Time format:** relative (`1h`, `30m`, `7d`) or ISO 8601 (`2026-03-20T00:00:00Z`). Relative times go backwards from now — `1h` means "the last hour", `7d` means "the last 7 days".
203
+ - **Data mode:** `production` (default), `debug`, or `all` — filters events by their debug flag. SDKs auto-detect debug mode (DEBUG builds on iOS, `NODE_ENV !== "production"` on Node). Use `debug` mode during development to see test events; use `production` (the default) for real analytics.
204
+ - Ask the user for their email address; the verification code arrives by email.
205
+
206
+ ## Typical Workflow
207
+
208
+ A typical end-to-end flow for adding OwlMetry to a new project:
209
+
210
+ 1. **Sign up**: `owlmetry auth send-code` → verify code → auto-provisioned with team, project, and backend app
211
+ 2. **Create apps**: `owlmetry apps create --platform apple --bundle-id com.example.myapp` (and/or android, web, backend)
212
+ 3. **Note the client key**: from the app creation response — pass this to the SDK
213
+ 4. **Instrument the app**: Add the Swift or Node SDK, configure with the endpoint and client key, add logging calls
214
+ 5. **Define metrics**: `owlmetry metrics create --slug photo-upload --lifecycle` for operations you want to track with duration
215
+ 6. **Define funnels**: `owlmetry funnels create --slug onboarding --steps '[...]'` for multi-step flows you want to measure conversion on
216
+ 7. **Query data**: Use `owlmetry events`, `owlmetry metrics query`, and `owlmetry funnels query` to analyze behavior
@@ -0,0 +1,315 @@
1
+ ---
2
+ name: owlmetry-node
3
+ version: 0.1.0
4
+ description: >-
5
+ Integrate the OwlMetry Node.js SDK into a backend service for server-side
6
+ analytics, event tracking, metrics, funnels, and A/B experiments. Use when
7
+ instrumenting a Node.js, Express, Fastify, or serverless project with OwlMetry.
8
+ allowed-tools: Read, Bash, Grep, Glob
9
+ ---
10
+
11
+ ## What is OwlMetry?
12
+
13
+ OwlMetry is a self-hosted analytics platform. The Node.js SDK captures events from backend services — API servers, job workers, serverless functions — and delivers them to the OwlMetry server. It buffers events in memory and flushes them in batches to avoid blocking your application's request path.
14
+
15
+ The SDK has zero runtime dependencies. All calls are non-blocking — events are buffered internally and sent in the background. A single `Owl.configure()` call initialises everything including a background flush timer.
16
+
17
+ ## Version Check
18
+
19
+ Run these checks silently. Only inform the user if updates are available.
20
+
21
+ 1. **Skill version**: `curl -sf https://raw.githubusercontent.com/Jasonvdb/owlmetry/main/skills/owlmetry-node/SKILL.md | head -5` — compare the `version:` field to `0.1.0`. If newer, ask the user if they want to update.
22
+ 2. **SDK version**: `npm ls @owlmetry/node --json 2>/dev/null` for current version, `npm view @owlmetry/node version 2>/dev/null` for latest. If newer, offer `npm install @owlmetry/node@latest`.
23
+
24
+ ## Prerequisite
25
+
26
+ You need an OwlMetry endpoint URL and a `client_key` (starts with `owl_client_...`) for an app with `platform: backend`.
27
+
28
+ If the user doesn't have these yet, invoke `/owlmetry-cli` first to:
29
+ 1. Sign up or log in
30
+ 2. Create a project (if needed)
31
+ 3. Create an app with `--platform backend` (no `--bundle-id` needed)
32
+ 4. Note the `client_key` from the app creation response
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm install @owlmetry/node
38
+ ```
39
+
40
+ Zero runtime dependencies. Node.js 20+. ESM only.
41
+
42
+ ## Configure
43
+
44
+ ```typescript
45
+ import { Owl } from '@owlmetry/node';
46
+
47
+ Owl.configure({
48
+ endpoint: 'https://ingest.owlmetry.com',
49
+ apiKey: 'owl_client_...',
50
+ serviceName: 'my-api', // optional, default: "unknown"
51
+ appVersion: '1.2.0', // optional
52
+ isDev: false, // optional, default: process.env.NODE_ENV !== "production"
53
+ flushIntervalMs: 5000, // optional, default: 5000
54
+ flushThreshold: 20, // optional, default: 20
55
+ maxBufferSize: 10000, // optional, default: 10000
56
+ });
57
+ ```
58
+
59
+ - `apiKey` must start with `owl_client_`
60
+ - `isDev` defaults to `true` when `NODE_ENV !== "production"`
61
+ - Generates a fresh `sessionId` (UUID) on each `configure()` call
62
+ - Registers a `beforeExit` handler to auto-flush on graceful shutdown
63
+
64
+ ## Buffering and Delivery
65
+
66
+ The SDK buffers events in memory and sends them to the server in batches. Understanding how buffering works helps you avoid lost events:
67
+
68
+ - **Buffer size** (`maxBufferSize: 10000`): maximum events held in memory. If the buffer fills up, new events are dropped. Increase this if your service produces bursts of events.
69
+ - **Flush interval** (`flushIntervalMs: 5000`): a background timer fires every 5 seconds and sends all buffered events. The timer uses `.unref()` so it won't keep the process alive.
70
+ - **Flush threshold** (`flushThreshold: 20`): if the buffer reaches this many events before the timer fires, an immediate flush is triggered.
71
+ - **HTTP timeout**: each flush request has a 10-second timeout to prevent hanging on slow or unreachable servers. Failed flushes are silently dropped (events are not retried).
72
+ - **Graceful exit**: a `beforeExit` handler auto-flushes remaining events on normal process termination. For SIGTERM/SIGINT, you must call `Owl.shutdown()` explicitly (see Graceful Shutdown).
73
+
74
+ The key implication: if your process crashes or is force-killed, any events still in the buffer are lost. For critical routes, use `wrapHandler()` to flush after each request.
75
+
76
+ ## Log Events
77
+
78
+ Events are the core data unit. Use the four log levels to capture different kinds of backend activity:
79
+
80
+ - **`info`** — normal operations: server started, request handled, job completed, user action processed.
81
+ - **`debug`** — verbose detail for development: cache lookups, query plans, config loading, intermediate state.
82
+ - **`warn`** — recoverable problems: slow queries, rate limits approaching, fallback paths, deprecated API usage.
83
+ - **`error`** — failures: database connection errors, external API failures, unhandled rejections, missing resources.
84
+
85
+ Choose **message strings** that are specific and searchable. Prefer `"Payment processing failed"` over `"error occurred"`. Use attributes for structured data you'll filter on later.
86
+
87
+ ```typescript
88
+ Owl.info('Server started', { port: 4000 });
89
+ Owl.debug('Cache miss', { key: 'user:123' });
90
+ Owl.warn('Slow query', { duration_ms: 2500, table: 'events' });
91
+ Owl.error('Database connection failed', { host: 'db.example.com' });
92
+ ```
93
+
94
+ All methods: `Owl.info/debug/warn/error(message: string, attrs?: Record<string, unknown>)`.
95
+
96
+ Source module (file:line) is auto-captured from the call stack.
97
+
98
+ **Backend-specific examples:**
99
+ ```typescript
100
+ Owl.info('Request handled', { method: 'POST', path: '/api/orders', status: 201 });
101
+ Owl.info('Background job completed', { job: 'send-emails', processed: 150 });
102
+ Owl.warn('Rate limit approaching', { current: 95, limit: 100, client_id: 'abc' });
103
+ Owl.error('External API timeout', { service: 'stripe', endpoint: '/charges', timeout_ms: 10000 });
104
+ ```
105
+
106
+ ## Per-Request User Scoping
107
+
108
+ In a multi-user backend, many requests are processed concurrently. Without scoping, you'd need to pass a user ID to every log call. `Owl.withUser()` creates a scoped instance that automatically tags all events with the user ID — create one per request in your auth middleware.
109
+
110
+ Use scoped instances whenever you know who the user is. Use the global `Owl` for system-level events (startup, shutdown, background jobs without a user context).
111
+
112
+ ```typescript
113
+ const owl = Owl.withUser('user_42');
114
+ owl.info('User logged in');
115
+ owl.warn('Rate limit approaching', { requests: 95 });
116
+ owl.error('Payment failed', { reason: 'insufficient_funds' });
117
+ ```
118
+
119
+ `ScopedOwl` has the same logging methods as `Owl` (`info`, `debug`, `warn`, `error`, `track`, `startOperation`, `recordMetric`).
120
+
121
+ ## Funnel Tracking
122
+
123
+ Backend funnels track server-side conversion flows — signup pipelines, payment processing, onboarding sequences, data processing stages. Unlike client-side funnels where users interact with UI, backend funnels typically track state transitions in response to API calls or background jobs.
124
+
125
+ **User ID is critical for backend funnels.** Funnel analytics group by user to calculate conversion — without a user ID, events can't be attributed. Always use a scoped instance (`Owl.withUser()`) or ensure user identity is set.
126
+
127
+ ```typescript
128
+ Owl.track('signup-started');
129
+ Owl.track('email-verified', { method: 'link' });
130
+ Owl.track('profile-completed');
131
+
132
+ // With user scoping:
133
+ const owl = Owl.withUser(userId);
134
+ owl.track('checkout-completed', { item_count: '3' });
135
+ ```
136
+
137
+ Each `track()` call emits an info-level event with message `"track:<stepName>"`. Define matching funnels via `/owlmetry-cli`.
138
+
139
+ **Note:** `track()` attributes must be `Record<string, string>` (string values only).
140
+
141
+ ## Structured Metrics
142
+
143
+ Use structured metrics when you want aggregated performance data (averages, percentiles, error rates) rather than individual event logs. Metrics give you trend data and alerting signals; events give you individual records for debugging.
144
+
145
+ **Decision: lifecycle vs single-shot:**
146
+ - **Lifecycle** — when measuring something with a duration (start → end). Backend examples: HTTP request handling, database queries, external API calls, file processing, queue job execution.
147
+ - **Single-shot** — when recording a point-in-time measurement. Backend examples: queue depth, cache hit rate, active connections, deployment markers.
148
+
149
+ The metric definition must exist on the server **before** the SDK emits events for that slug. Create it via CLI first.
150
+
151
+ ### Lifecycle operations (start -> complete/fail/cancel)
152
+
153
+ ```typescript
154
+ const op = Owl.startOperation('database-query', { table: 'users' });
155
+ try {
156
+ const result = await db.query('SELECT * FROM users');
157
+ op.complete({ rows: result.length });
158
+ } catch (err) {
159
+ op.fail(String(err), { table: 'users' });
160
+ }
161
+
162
+ // Or cancel:
163
+ op.cancel({ reason: 'timeout' });
164
+ ```
165
+
166
+ `duration_ms` and `tracking_id` (UUID) are auto-added. Create the metric definition first:
167
+ ```bash
168
+ owlmetry metrics create --project <id> --name "Database Query" --slug database-query --lifecycle --format json
169
+ ```
170
+
171
+ ### Single-shot measurements
172
+
173
+ ```typescript
174
+ Owl.recordMetric('cache-hit-rate', { rate: '0.95', cache: 'redis' });
175
+ ```
176
+
177
+ Works with scoped instances too: `owl.startOperation(...)`, `owl.recordMetric(...)`.
178
+
179
+ **Slug rules:** lowercase letters, numbers, hyphens only. Invalid slugs are auto-corrected with a warning.
180
+
181
+ ## A/B Experiments
182
+
183
+ Backend experiments let you vary server-side behavior (algorithm versions, feature flags, response formats) and measure the impact through metrics and funnels.
184
+
185
+ How it works end-to-end:
186
+ 1. **Assign a variant**: `getVariant("pricing-algo", ["control", "v2"])` randomly picks on first call.
187
+ 2. **Branch your logic**: use the returned variant string to execute different code paths.
188
+ 3. **Events are auto-tagged**: all subsequent events include the experiment assignment.
189
+ 4. **Analyse**: query metrics or funnels segmented by variant to compare outcomes.
190
+
191
+ Assignments persist to `~/.owlmetry/experiments.json` on disk, so they survive restarts. **Serverless caveat**: in serverless environments (Lambda, Cloud Functions), each cold start reads from the file — but if the filesystem is ephemeral, assignments won't persist across invocations. Use `setExperiment()` with a server-side assignment for stable bucketing in serverless.
192
+
193
+ ```typescript
194
+ // Random assignment on first call, persisted to ~/.owlmetry/experiments.json
195
+ const variant = Owl.getVariant('checkout-redesign', ['control', 'variant-a', 'variant-b']);
196
+
197
+ // Force-set (e.g., from server config)
198
+ Owl.setExperiment('checkout-redesign', 'variant-a');
199
+
200
+ // Clear all
201
+ Owl.clearExperiments();
202
+ ```
203
+
204
+ All events automatically include an `experiments` field with current assignments.
205
+
206
+ ## Serverless Support
207
+
208
+ Serverless environments (AWS Lambda, Vercel Functions, Cloud Functions) freeze the runtime after each invocation. Events still in the buffer are lost when the runtime freezes — the flush timer and `beforeExit` handler may never fire.
209
+
210
+ `wrapHandler()` solves this by calling `Owl.flush()` in a `finally` block after every invocation, guaranteeing delivery before the runtime freezes.
211
+
212
+ **When to use `wrapHandler()`:**
213
+ - **Serverless functions**: always wrap — this is the only reliable way to flush.
214
+ - **Long-running servers, critical routes**: wrap routes where losing events is unacceptable (payment processing, user signup). The 5-second flush timer handles the rest.
215
+ - **Long-running servers, general routes**: not needed — the background timer and shutdown handler cover normal operation.
216
+
217
+ ```typescript
218
+ // AWS Lambda
219
+ export const handler = Owl.wrapHandler(async (event, context) => {
220
+ Owl.info('Lambda invoked', { functionName: context.functionName });
221
+ // ... handle request ...
222
+ return { statusCode: 200, body: JSON.stringify({ ok: true }) };
223
+ });
224
+
225
+ // Express route
226
+ app.post('/api/checkout', Owl.wrapHandler(async (req, res) => {
227
+ const owl = Owl.withUser(req.user?.id);
228
+ const op = owl.startOperation('checkout');
229
+ // ... process ...
230
+ op.complete();
231
+ res.json({ ok: true });
232
+ }));
233
+ ```
234
+
235
+ `wrapHandler` calls `Owl.flush()` in a `finally` block, ensuring events are sent even if the handler throws.
236
+
237
+ ## Graceful Shutdown
238
+
239
+ Without explicit shutdown, events buffered since the last flush (up to 5 seconds worth) are lost when the process exits via SIGTERM or SIGINT. The `beforeExit` handler only covers graceful exits (no pending work, no signal termination).
240
+
241
+ Always add a shutdown handler for production services:
242
+
243
+ ```typescript
244
+ process.on('SIGTERM', async () => {
245
+ await Owl.shutdown();
246
+ process.exit(0);
247
+ });
248
+ ```
249
+
250
+ - `shutdown()` stops the flush timer, flushes remaining events, and clears state.
251
+ - The `beforeExit` handler auto-flushes on graceful process exit even without explicit shutdown.
252
+
253
+ ## Integration Patterns
254
+
255
+ These patterns show how to wire OwlMetry into common Node.js frameworks. The key ideas:
256
+ - **Create a scoped logger in auth middleware** so all route handlers get user-attributed events automatically.
257
+ - **Use `wrapHandler()` on critical routes** where losing events is unacceptable.
258
+ - **Call `shutdown()` in the framework's close hook** to flush on process termination.
259
+
260
+ ### Express middleware
261
+
262
+ ```typescript
263
+ import express from 'express';
264
+ import { Owl } from '@owlmetry/node';
265
+
266
+ const app = express();
267
+
268
+ // Scoped logging middleware
269
+ app.use((req, res, next) => {
270
+ req.owl = req.user?.id ? Owl.withUser(req.user.id) : Owl;
271
+ next();
272
+ });
273
+
274
+ app.post('/api/order', Owl.wrapHandler(async (req, res) => {
275
+ const op = req.owl.startOperation('create-order', { items: req.body.items?.length });
276
+ try {
277
+ const order = await createOrder(req.body);
278
+ op.complete({ order_id: order.id });
279
+ res.json(order);
280
+ } catch (err) {
281
+ op.fail(String(err));
282
+ res.status(500).json({ error: 'Order failed' });
283
+ }
284
+ }));
285
+
286
+ process.on('SIGTERM', async () => {
287
+ await Owl.shutdown();
288
+ process.exit(0);
289
+ });
290
+ ```
291
+
292
+ ### Fastify hooks
293
+
294
+ ```typescript
295
+ import Fastify from 'fastify';
296
+ import { Owl } from '@owlmetry/node';
297
+
298
+ const fastify = Fastify();
299
+
300
+ fastify.addHook('onRequest', (request, reply, done) => {
301
+ request.owl = request.user?.id ? Owl.withUser(request.user.id) : Owl;
302
+ done();
303
+ });
304
+
305
+ fastify.addHook('onClose', async () => {
306
+ await Owl.shutdown();
307
+ });
308
+
309
+ fastify.post('/api/process', async (request, reply) => {
310
+ request.owl.info('Processing request', { path: request.url });
311
+ // ... handle request ...
312
+ await Owl.flush();
313
+ return { ok: true };
314
+ });
315
+ ```