@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,269 @@
1
+ ---
2
+ name: owlmetry-swift
3
+ version: 0.1.0
4
+ description: >-
5
+ Integrate the OwlMetry Swift SDK into an iOS or macOS app for analytics,
6
+ event tracking, metrics, funnels, and A/B experiments. Use when
7
+ instrumenting a Swift or SwiftUI 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 Swift SDK captures events from iOS, iPadOS, and macOS apps and delivers them to the OwlMetry server. It handles buffering, gzip compression, offline queuing, session management, and network monitoring automatically — you just call logging methods and the SDK takes care of delivery.
14
+
15
+ The SDK is a static `Owl` enum with no external dependencies. All calls are non-blocking (events are buffered and flushed in batches). A single `configure()` call initialises everything.
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-swift/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**: Read `Package.resolved` for the current resolved revision, then compare against `curl -sf https://api.github.com/repos/Jasonvdb/owlmetry/releases/latest | jq -r .tag_name`. If newer, inform the user.
23
+
24
+ ## Prerequisite
25
+
26
+ You need an OwlMetry endpoint URL and a `client_key` (starts with `owl_client_...`) for an app with `platform: apple`.
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 apple --bundle-id <bundle-id>`
32
+ 4. Note the `client_key` from the app creation response
33
+
34
+ ## Add Swift Package
35
+
36
+ **Swift Package Manager (Package.swift):**
37
+ ```swift
38
+ dependencies: [
39
+ .package(url: "https://github.com/Jasonvdb/owlmetry.git", branch: "main")
40
+ ]
41
+ ```
42
+ Add to your target:
43
+ ```swift
44
+ .target(name: "YourApp", dependencies: [
45
+ .product(name: "OwlMetry", package: "owlmetry")
46
+ ])
47
+ ```
48
+
49
+ **Xcode:** File > Add Package Dependencies > enter `https://github.com/Jasonvdb/owlmetry.git`, select branch `main`, add `OwlMetry` to your target.
50
+
51
+ **Minimum platforms:** iOS 16.0, macOS 13.0. Zero external dependencies.
52
+
53
+ ## Configure
54
+
55
+ Configuration must happen once, before any other `Owl` calls — typically in your `@main` App init or AppDelegate `didFinishLaunching`. Each `configure()` call generates a fresh `session_id` (UUID) that groups all subsequent events together. This means each app launch gets its own session, making it easy to see everything a user did in a single sitting.
56
+
57
+ ```swift
58
+ import OwlMetry
59
+
60
+ @main
61
+ struct MyApp: App {
62
+ init() {
63
+ do {
64
+ try Owl.configure(
65
+ endpoint: "https://ingest.owlmetry.com",
66
+ apiKey: "owl_client_..."
67
+ )
68
+ } catch {
69
+ print("OwlMetry configuration failed: \(error)")
70
+ }
71
+ }
72
+ // ...
73
+ }
74
+ ```
75
+
76
+ **Parameters:**
77
+ - `endpoint: String` — server URL (required)
78
+ - `apiKey: String` — client key, must start with `owl_client_` (required)
79
+ - `flushOnBackground: Bool` — auto-flush when app backgrounds (default: `true`)
80
+ - `compressionEnabled: Bool` — gzip request bodies (default: `true`)
81
+
82
+ Auto-detects: bundle ID, debug mode (`#if DEBUG`). Auto-generates: session ID (fresh each launch).
83
+
84
+ ## Log Events
85
+
86
+ Events are the core unit of data in OwlMetry. Use the four log levels to capture different kinds of information:
87
+
88
+ - **`info`** — normal operations worth recording: screen views, user actions, feature usage, successful completions. This is your default level.
89
+ - **`debug`** — verbose detail useful only during development: cache hits, state transitions, intermediate values. These are filtered out in production data mode.
90
+ - **`warn`** — something unexpected that the app recovered from: slow responses, fallback paths taken, retries needed.
91
+ - **`error`** — something failed: network errors, parse failures, missing data, caught exceptions.
92
+
93
+ Choose **message strings** that are specific and searchable. Prefer `"Failed to load profile image"` over `"error"`. Use `screenName` to tie events to where they happened in the UI. Use `customAttributes` for structured data you'll want to filter or search on later.
94
+
95
+ ```swift
96
+ Owl.info("User opened settings", screenName: "SettingsView")
97
+ Owl.debug("Cache hit", screenName: "HomeView", customAttributes: ["key": "user_prefs"])
98
+ Owl.warn("Slow network response", customAttributes: ["latency_ms": "1200"])
99
+ Owl.error("Failed to load profile", screenName: "ProfileView")
100
+ ```
101
+
102
+ All logging methods share the same signature:
103
+ ```swift
104
+ Owl.info(_ message: String, screenName: String? = nil, customAttributes: [String: String]? = nil)
105
+ ```
106
+
107
+ Source file, function, and line are auto-captured.
108
+
109
+ **Avoid logging PII** (emails, phone numbers, passwords) or high-frequency events (every frame, every scroll position). Focus on actions and outcomes.
110
+
111
+ ## User Identity
112
+
113
+ Identity connects events to real users. Before `setUser()` is called, all events are tagged with an anonymous ID (`owl_anon_...`). After login, calling `setUser()` does two things:
114
+
115
+ 1. Tags all future events with the real user ID.
116
+ 2. Retroactively claims all previous anonymous events for that user (server-side), so you get a complete history.
117
+
118
+ Call `setUser()` right after successful authentication. Call `clearUser()` on logout to revert to anonymous tracking.
119
+
120
+ ```swift
121
+ // After login — claims all previous anonymous events
122
+ Owl.setUser("user_123")
123
+
124
+ // On logout — reverts to anonymous tracking
125
+ Owl.clearUser()
126
+
127
+ // On logout with fresh anonymous ID
128
+ Owl.clearUser(newAnonymousId: true)
129
+ ```
130
+
131
+ **Important:** The SDK automatically flushes buffered events before claiming identity.
132
+
133
+ ## Funnel Tracking
134
+
135
+ Funnels measure how users progress through a multi-step flow (onboarding, checkout, activation) and where they drop off. The system has three parts:
136
+
137
+ 1. **Define** the funnel server-side (via CLI or API) with ordered steps and event filters.
138
+ 2. **Track** steps client-side with `Owl.track("step-name")` — each call emits an event with message `"track:step-name"`.
139
+ 3. **Query** analytics to see conversion rates and drop-off between steps.
140
+
141
+ Choose step names that match the `event_filter` in your funnel definition. For example, if the step filter is `{"message": "track:welcome-screen"}`, then call `Owl.track("welcome-screen")`.
142
+
143
+ Use `attributes` when you need to segment funnel analytics later (e.g., by signup method or referral source).
144
+
145
+ ```swift
146
+ Owl.track("welcome-screen")
147
+ Owl.track("create-account", attributes: ["method": "email"])
148
+ Owl.track("complete-profile")
149
+ Owl.track("first-post")
150
+ ```
151
+
152
+ Each `track()` call emits an info-level event with message `"track:<stepName>"`. Define matching funnel definitions via `/owlmetry-cli`:
153
+ ```bash
154
+ owlmetry funnels create --project <id> --name "Onboarding" --slug onboarding \
155
+ --steps '[{"name":"Welcome","event_filter":{"message":"track:welcome-screen"}},{"name":"Account","event_filter":{"message":"track:create-account"}},{"name":"Profile","event_filter":{"message":"track:complete-profile"}},{"name":"First Post","event_filter":{"message":"track:first-post"}}]' \
156
+ --format json
157
+ ```
158
+
159
+ ## Structured Metrics
160
+
161
+ Use structured metrics instead of plain log events when you want aggregated statistics (averages, percentiles, error rates) rather than just a list of individual events. Metrics give you `p50`, `p95`, `p99` latencies, success/failure rates, and trend data over time.
162
+
163
+ **Decision: lifecycle vs single-shot:**
164
+ - **Lifecycle** — when you're measuring something with a duration (start → end). Examples: image upload, API call, video encoding, onboarding flow. The SDK auto-tracks `duration_ms`.
165
+ - **Single-shot** — when you're recording a point-in-time value. Examples: app cold-start time, memory usage, items in cart at checkout.
166
+
167
+ The metric definition must exist on the server **before** the SDK emits events for that slug. Create it via CLI first.
168
+
169
+ ### Lifecycle operations (start → complete/fail/cancel)
170
+
171
+ ```swift
172
+ let op = Owl.startOperation("photo-upload", attributes: ["format": "heic"])
173
+
174
+ // On success:
175
+ op.complete(attributes: ["size_bytes": "524288"])
176
+
177
+ // On failure:
178
+ op.fail(error: "timeout", attributes: ["retry_count": "3"])
179
+
180
+ // On cancellation:
181
+ op.cancel(attributes: ["reason": "user_cancelled"])
182
+ ```
183
+
184
+ `duration_ms` and `tracking_id` (UUID) are auto-added. Create the metric definition first:
185
+ ```bash
186
+ owlmetry metrics create --project <id> --name "Photo Upload" --slug photo-upload --lifecycle --format json
187
+ ```
188
+
189
+ ### Single-shot measurements
190
+
191
+ ```swift
192
+ Owl.recordMetric("app-cold-start", attributes: ["screen": "home"])
193
+ ```
194
+
195
+ **Slug rules:** lowercase letters, numbers, hyphens only. Invalid slugs are auto-corrected with a console warning.
196
+
197
+ ## A/B Experiments
198
+
199
+ OwlMetry provides lightweight client-side A/B testing. The flow is:
200
+
201
+ 1. **Assign a variant**: `getVariant("experiment-name", options: ["control", "variant-a"])` randomly picks a variant on first call.
202
+ 2. **Render conditionally**: use the returned variant string to show different UI.
203
+ 3. **Events are auto-tagged**: all subsequent events include the experiment assignment in their `experiments` field.
204
+ 4. **Analyse**: query funnel or metric data segmented by variant to compare performance.
205
+
206
+ `getVariant()` persists the assignment in Keychain, so the same user always sees the same variant across launches. Use `setExperiment()` to force a specific variant (e.g., from a server-side feature flag system). Use `clearExperiments()` to reset all assignments (e.g., for testing).
207
+
208
+ ```swift
209
+ // Random assignment on first call, persisted to Keychain thereafter
210
+ let variant = Owl.getVariant("checkout-redesign", options: ["control", "variant-a", "variant-b"])
211
+
212
+ // Force-set a variant (e.g., from server config)
213
+ Owl.setExperiment("checkout-redesign", variant: "variant-a")
214
+
215
+ // Clear all assignments
216
+ Owl.clearExperiments()
217
+ ```
218
+
219
+ - Assignments persist in Keychain (`com.owlmetry.experiments`).
220
+ - All events automatically include an `experiments` field with current assignments.
221
+ - Query funnel analytics segmented by variant via CLI: `owlmetry funnels query <slug> --project <id> --group-by experiment:checkout-redesign`
222
+
223
+ ## Instrumentation Strategy
224
+
225
+ When instrumenting a new app, follow this priority:
226
+
227
+ **Always instrument:**
228
+ - App launch / cold start (`info` in `init()` or `didFinishLaunching`)
229
+ - Key screen views (`info` with `screenName` in `onAppear`)
230
+ - Authentication events (login, logout, signup)
231
+ - Errors and failures (`error` in `catch` blocks, error handlers)
232
+ - Core business actions (purchase, share, create, delete)
233
+
234
+ **Instrument when relevant:**
235
+ - Funnel steps for multi-step flows you want to optimize
236
+ - Lifecycle metrics for operations where duration matters (uploads, loads, syncs)
237
+ - A/B experiments when testing alternative UI or flows
238
+
239
+ **Where to place calls:**
240
+ - Screen views: `.onAppear` modifiers in SwiftUI, `viewDidAppear` in UIKit
241
+ - User actions: button action handlers, gesture callbacks
242
+ - Errors: `catch` blocks, `Result.failure` handlers
243
+ - Metrics: wrap the async operation between `startOperation()` and `complete()`/`fail()`
244
+
245
+ **What NOT to instrument:**
246
+ - PII (emails, phone numbers, passwords, tokens)
247
+ - Every UI interaction (every tap, every scroll)
248
+ - High-frequency timer events
249
+ - Sensitive business data (prices, payment details)
250
+
251
+ ## Lifecycle
252
+
253
+ ```swift
254
+ // In your app's termination handler or ScenePhase .background
255
+ await Owl.shutdown()
256
+ ```
257
+
258
+ `flushOnBackground: true` (default) handles most cases automatically. Call `shutdown()` explicitly only if you need to guarantee delivery at a specific point.
259
+
260
+ ## Auto-Captured Data
261
+
262
+ Every event automatically includes:
263
+ - `session_id` — fresh UUID per `configure()` call
264
+ - Device model, OS version, locale
265
+ - `app_version`, `build_number` (from bundle)
266
+ - `is_dev` — `true` in DEBUG builds
267
+ - `_connection` — network type (wifi, cellular, ethernet, offline) via `NWPathMonitor`
268
+ - `experiments` — current A/B experiment assignments
269
+ - `environment` — specific runtime (ios, ipados, macos)
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@owlmetry/cli",
3
+ "version": "0.1.0",
4
+ "description": "OwlMetry CLI — manage projects, apps, metrics, funnels, and events from the terminal. Includes AI skill files for agent-assisted development.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Jasonvdb/owlmetry.git",
10
+ "directory": "apps/cli"
11
+ },
12
+ "homepage": "https://owlmetry.com",
13
+ "bugs": "https://github.com/nicktechnolog/owlmetry/issues",
14
+ "keywords": [
15
+ "owlmetry",
16
+ "analytics",
17
+ "metrics",
18
+ "funnels",
19
+ "mobile",
20
+ "observability",
21
+ "cli",
22
+ "ai-skills"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "bin": {
28
+ "owlmetry": "dist/index.cjs"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "build:tsc": "tsc",
36
+ "dev": "tsx src/index.ts",
37
+ "test": "vitest run",
38
+ "prepublishOnly": "pnpm run build"
39
+ },
40
+ "devDependencies": {
41
+ "@owlmetry/shared": "workspace:*",
42
+ "@types/node": "^25.4.0",
43
+ "chalk": "^5.4.1",
44
+ "cli-table3": "^0.6.5",
45
+ "commander": "^13.1.0",
46
+ "tsup": "^8.5.1",
47
+ "tsx": "^4.19.0",
48
+ "typescript": "^5.7.0",
49
+ "vitest": "^4.0.18"
50
+ }
51
+ }