@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.
- package/dist/index.cjs +7004 -0
- package/dist/skills/owlmetry-cli/SKILL.md +216 -0
- package/dist/skills/owlmetry-node/SKILL.md +315 -0
- package/dist/skills/owlmetry-swift/SKILL.md +269 -0
- package/package.json +51 -0
|
@@ -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
|
+
}
|