@owlmetry/cli 0.1.6 → 0.1.8
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/LICENSE +21 -0
- package/dist/index.cjs +90 -78
- package/dist/skills/owlmetry-cli/SKILL.md +43 -18
- package/dist/skills/owlmetry-node/SKILL.md +38 -14
- package/dist/skills/owlmetry-swift/SKILL.md +63 -31
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adapted Hub LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
CHANGED
|
@@ -1169,7 +1169,7 @@ var require_command = __commonJS({
|
|
|
1169
1169
|
var EventEmitter = require("events").EventEmitter;
|
|
1170
1170
|
var childProcess = require("child_process");
|
|
1171
1171
|
var path = require("path");
|
|
1172
|
-
var
|
|
1172
|
+
var fs2 = require("fs");
|
|
1173
1173
|
var process3 = require("process");
|
|
1174
1174
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
1175
1175
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -2150,7 +2150,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2150
2150
|
* @param {string} subcommandName
|
|
2151
2151
|
*/
|
|
2152
2152
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
2153
|
-
if (
|
|
2153
|
+
if (fs2.existsSync(executableFile)) return;
|
|
2154
2154
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
2155
2155
|
const executableMissing = `'${executableFile}' does not exist
|
|
2156
2156
|
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
@@ -2169,10 +2169,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2169
2169
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2170
2170
|
function findFile(baseDir, baseName) {
|
|
2171
2171
|
const localBin = path.resolve(baseDir, baseName);
|
|
2172
|
-
if (
|
|
2172
|
+
if (fs2.existsSync(localBin)) return localBin;
|
|
2173
2173
|
if (sourceExt.includes(path.extname(baseName))) return void 0;
|
|
2174
2174
|
const foundExt = sourceExt.find(
|
|
2175
|
-
(ext) =>
|
|
2175
|
+
(ext) => fs2.existsSync(`${localBin}${ext}`)
|
|
2176
2176
|
);
|
|
2177
2177
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
2178
2178
|
return void 0;
|
|
@@ -2184,7 +2184,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2184
2184
|
if (this._scriptPath) {
|
|
2185
2185
|
let resolvedScriptPath;
|
|
2186
2186
|
try {
|
|
2187
|
-
resolvedScriptPath =
|
|
2187
|
+
resolvedScriptPath = fs2.realpathSync(this._scriptPath);
|
|
2188
2188
|
} catch {
|
|
2189
2189
|
resolvedScriptPath = this._scriptPath;
|
|
2190
2190
|
}
|
|
@@ -6360,11 +6360,11 @@ var METRIC_PHASES = ["start", "complete", "fail", "cancel", "record"];
|
|
|
6360
6360
|
init_cjs_shims();
|
|
6361
6361
|
|
|
6362
6362
|
// src/commands/apps.ts
|
|
6363
|
-
var appsCommand = new Command("apps").description("List apps").option("--project <id>", "Filter by project ID").action(async (opts, cmd) => {
|
|
6363
|
+
var appsCommand = new Command("apps").description("List apps").enablePositionalOptions().option("--project-id <id>", "Filter by project ID").action(async (opts, cmd) => {
|
|
6364
6364
|
const { client, globals } = createClient(cmd);
|
|
6365
6365
|
let apps = await client.listApps();
|
|
6366
|
-
if (opts.
|
|
6367
|
-
apps = apps.filter((a) => a.project_id === opts.
|
|
6366
|
+
if (opts.projectId) {
|
|
6367
|
+
apps = apps.filter((a) => a.project_id === opts.projectId);
|
|
6368
6368
|
}
|
|
6369
6369
|
output(globals.format, apps, () => formatAppsTable(apps));
|
|
6370
6370
|
});
|
|
@@ -6454,6 +6454,7 @@ function formatMetricEventsLog(events, slug) {
|
|
|
6454
6454
|
|
|
6455
6455
|
// src/utils/parse.ts
|
|
6456
6456
|
init_cjs_shims();
|
|
6457
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
6457
6458
|
function parsePositiveInt(value, flagName) {
|
|
6458
6459
|
const n = parseInt(value, 10);
|
|
6459
6460
|
if (Number.isNaN(n) || n <= 0) {
|
|
@@ -6461,6 +6462,34 @@ function parsePositiveInt(value, flagName) {
|
|
|
6461
6462
|
}
|
|
6462
6463
|
return n;
|
|
6463
6464
|
}
|
|
6465
|
+
function resolveJsonArray(inline, filePath, opts) {
|
|
6466
|
+
if (inline && filePath) {
|
|
6467
|
+
return "Error: --steps and --steps-file are mutually exclusive";
|
|
6468
|
+
}
|
|
6469
|
+
if (!inline && !filePath) {
|
|
6470
|
+
return opts.required ? "Error: either --steps or --steps-file is required" : "";
|
|
6471
|
+
}
|
|
6472
|
+
let json;
|
|
6473
|
+
if (filePath) {
|
|
6474
|
+
try {
|
|
6475
|
+
json = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
|
6476
|
+
} catch (err) {
|
|
6477
|
+
return `Error reading --steps-file: ${err.message}`;
|
|
6478
|
+
}
|
|
6479
|
+
} else {
|
|
6480
|
+
json = inline;
|
|
6481
|
+
}
|
|
6482
|
+
let parsed;
|
|
6483
|
+
try {
|
|
6484
|
+
parsed = JSON.parse(json);
|
|
6485
|
+
} catch {
|
|
6486
|
+
return "Error: steps must be valid JSON";
|
|
6487
|
+
}
|
|
6488
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
6489
|
+
return "Error: steps must be a non-empty JSON array";
|
|
6490
|
+
}
|
|
6491
|
+
return parsed;
|
|
6492
|
+
}
|
|
6464
6493
|
|
|
6465
6494
|
// src/utils/time.ts
|
|
6466
6495
|
init_cjs_shims();
|
|
@@ -6500,9 +6529,9 @@ ${source_default.dim(`More results available. Use --cursor ${result.cursor}`)}`;
|
|
|
6500
6529
|
}
|
|
6501
6530
|
|
|
6502
6531
|
// src/commands/events.ts
|
|
6503
|
-
var eventsCommand = new Command("events").description("Query events").option("--project <id>", "Filter by project ID").option("--app <id>", "Filter by app ID").option("--since <time>", "Start time (e.g. 1h, 30m, 7d, or ISO 8601)").option("--until <time>", "End time").addOption(
|
|
6532
|
+
var eventsCommand = new Command("events").description("Query events").option("--project-id <id>", "Filter by project ID").option("--app-id <id>", "Filter by app ID").option("--since <time>", "Start time (e.g. 1h, 30m, 7d, or ISO 8601)").option("--until <time>", "End time").addOption(
|
|
6504
6533
|
new Option("--level <level>", "Filter by log level").choices(LOG_LEVELS)
|
|
6505
|
-
).option("--user <id>", "Filter by user ID").option("--session <id>", "Filter by session ID").option("--screen <name>", "Filter by screen name").addOption(
|
|
6534
|
+
).option("--user-id <id>", "Filter by user ID").option("--session-id <id>", "Filter by session ID").option("--screen-name <name>", "Filter by screen name").addOption(
|
|
6506
6535
|
new Option("--limit <n>", "Max events to return").argParser((v) => parsePositiveInt(v, "--limit"))
|
|
6507
6536
|
).option("--cursor <cursor>", "Pagination cursor").addOption(
|
|
6508
6537
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
@@ -6511,14 +6540,14 @@ var eventsCommand = new Command("events").description("Query events").option("--
|
|
|
6511
6540
|
const since = opts.since ? parseTimeInput(opts.since) : !opts.until ? parseTimeInput("24h") : void 0;
|
|
6512
6541
|
const until = opts.until ? parseTimeInput(opts.until) : void 0;
|
|
6513
6542
|
const result = await client.queryEvents({
|
|
6514
|
-
project_id: opts.
|
|
6515
|
-
app_id: opts.
|
|
6543
|
+
project_id: opts.projectId,
|
|
6544
|
+
app_id: opts.appId,
|
|
6516
6545
|
since,
|
|
6517
6546
|
until,
|
|
6518
6547
|
level: opts.level,
|
|
6519
|
-
user_id: opts.
|
|
6520
|
-
session_id: opts.
|
|
6521
|
-
screen_name: opts.
|
|
6548
|
+
user_id: opts.userId,
|
|
6549
|
+
session_id: opts.sessionId,
|
|
6550
|
+
screen_name: opts.screenName,
|
|
6522
6551
|
limit: opts.limit,
|
|
6523
6552
|
cursor: opts.cursor,
|
|
6524
6553
|
data_mode: opts.dataMode
|
|
@@ -6753,14 +6782,14 @@ function formatQueryResult(result) {
|
|
|
6753
6782
|
}
|
|
6754
6783
|
return lines.join("\n");
|
|
6755
6784
|
}
|
|
6756
|
-
var metricsCommand = new Command("metrics").description("List metric definitions").requiredOption("--project <id>", "Project ID").action(async (opts, cmd) => {
|
|
6785
|
+
var metricsCommand = new Command("metrics").description("List metric definitions").enablePositionalOptions().requiredOption("--project-id <id>", "Project ID").action(async (opts, cmd) => {
|
|
6757
6786
|
const { client, globals } = createClient(cmd);
|
|
6758
|
-
const metrics = await client.listMetrics(opts.
|
|
6787
|
+
const metrics = await client.listMetrics(opts.projectId);
|
|
6759
6788
|
output(globals.format, metrics, () => formatMetricsTable(metrics));
|
|
6760
6789
|
});
|
|
6761
|
-
metricsCommand.command("events <slug>").description("Query raw metric events for a metric").requiredOption("--project <id>", "Project ID").addOption(
|
|
6790
|
+
metricsCommand.command("events <slug>").description("Query raw metric events for a metric").requiredOption("--project-id <id>", "Project ID").addOption(
|
|
6762
6791
|
new Option("--phase <phase>", "Filter by phase").choices(METRIC_PHASES)
|
|
6763
|
-
).option("--tracking-id <id>", "Filter by tracking ID").option("--user <id>", "Filter by user ID").option("--since <time>", "Start time (e.g. 1h, 30m, 7d, or ISO 8601)").option("--until <time>", "End time").addOption(
|
|
6792
|
+
).option("--tracking-id <id>", "Filter by tracking ID").option("--user-id <id>", "Filter by user ID").option("--since <time>", "Start time (e.g. 1h, 30m, 7d, or ISO 8601)").option("--until <time>", "End time").addOption(
|
|
6764
6793
|
new Option("--limit <n>", "Max events to return").argParser((v) => parsePositiveInt(v, "--limit"))
|
|
6765
6794
|
).option("--cursor <cursor>", "Pagination cursor").option("--environment <env>", "Filter by environment (ios, ipados, macos, android, web, backend)").addOption(
|
|
6766
6795
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
@@ -6768,10 +6797,10 @@ metricsCommand.command("events <slug>").description("Query raw metric events for
|
|
|
6768
6797
|
const { client, globals } = createClient(cmd);
|
|
6769
6798
|
const since = opts.since ? parseTimeInput(opts.since) : !opts.until ? parseTimeInput("24h") : void 0;
|
|
6770
6799
|
const until = opts.until ? parseTimeInput(opts.until) : void 0;
|
|
6771
|
-
const result = await client.queryMetricEvents(slug, opts.
|
|
6800
|
+
const result = await client.queryMetricEvents(slug, opts.projectId, {
|
|
6772
6801
|
phase: opts.phase,
|
|
6773
6802
|
tracking_id: opts.trackingId,
|
|
6774
|
-
user_id: opts.
|
|
6803
|
+
user_id: opts.userId,
|
|
6775
6804
|
environment: opts.environment,
|
|
6776
6805
|
since,
|
|
6777
6806
|
until,
|
|
@@ -6787,12 +6816,12 @@ metricsCommand.command("events <slug>").description("Query raw metric events for
|
|
|
6787
6816
|
() => formatMetricEventsLog(result.events, slug) + hint
|
|
6788
6817
|
);
|
|
6789
6818
|
});
|
|
6790
|
-
metricsCommand.command("view <slug>").description("View metric definition details").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6819
|
+
metricsCommand.command("view <slug>").description("View metric definition details").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6791
6820
|
const { client, globals } = createClient(cmd);
|
|
6792
|
-
const metric = await client.getMetric(slug, opts.
|
|
6821
|
+
const metric = await client.getMetric(slug, opts.projectId);
|
|
6793
6822
|
output(globals.format, metric, () => formatMetricDetail(metric));
|
|
6794
6823
|
});
|
|
6795
|
-
metricsCommand.command("create").description("Create a new metric definition").requiredOption("--project <id>", "Project ID").requiredOption("--name <name>", "Metric name").requiredOption("--slug <slug>", "Metric slug").option("--description <desc>", "Description").option("--docs <markdown>", "Documentation (markdown)").option("--lifecycle", "Mark as lifecycle metric (has start/complete/fail phases)").action(async (opts, cmd) => {
|
|
6824
|
+
metricsCommand.command("create").description("Create a new metric definition").requiredOption("--project-id <id>", "Project ID").requiredOption("--name <name>", "Metric name").requiredOption("--slug <slug>", "Metric slug").option("--description <desc>", "Description").option("--docs <markdown>", "Documentation (markdown)").option("--lifecycle", "Mark as lifecycle metric (has start/complete/fail phases)").action(async (opts, cmd) => {
|
|
6796
6825
|
const slugError = validateMetricSlug(opts.slug);
|
|
6797
6826
|
if (slugError) {
|
|
6798
6827
|
console.error(source_default.red(`Error: ${slugError}`));
|
|
@@ -6800,7 +6829,7 @@ metricsCommand.command("create").description("Create a new metric definition").r
|
|
|
6800
6829
|
return;
|
|
6801
6830
|
}
|
|
6802
6831
|
const { client, globals } = createClient(cmd);
|
|
6803
|
-
const metric = await client.createMetric(opts.
|
|
6832
|
+
const metric = await client.createMetric(opts.projectId, {
|
|
6804
6833
|
name: opts.name,
|
|
6805
6834
|
slug: opts.slug,
|
|
6806
6835
|
description: opts.description,
|
|
@@ -6809,36 +6838,36 @@ metricsCommand.command("create").description("Create a new metric definition").r
|
|
|
6809
6838
|
});
|
|
6810
6839
|
output(globals.format, metric, () => formatMetricDetail(metric));
|
|
6811
6840
|
});
|
|
6812
|
-
metricsCommand.command("query <slug>").description("Query metric aggregation").requiredOption("--project <id>", "Project ID").option("--since <date>", "Start date (ISO)").option("--until <date>", "End date (ISO)").option("--app <id>", "Filter by app ID").option("--app-version <version>", "Filter by app version").option("--device-model <model>", "Filter by device model").option("--os-version <version>", "Filter by OS version").option("--user <id>", "Filter by user ID").option("--environment <env>", "Filter by environment (ios, ipados, macos, android, web, backend)").option("--group-by <field>", "Group by: app_id, app_version, device_model, os_version, environment, time:hour, time:day, time:week").addOption(
|
|
6841
|
+
metricsCommand.command("query <slug>").description("Query metric aggregation").requiredOption("--project-id <id>", "Project ID").option("--since <date>", "Start date (ISO)").option("--until <date>", "End date (ISO)").option("--app-id <id>", "Filter by app ID").option("--app-version <version>", "Filter by app version").option("--device-model <model>", "Filter by device model").option("--os-version <version>", "Filter by OS version").option("--user-id <id>", "Filter by user ID").option("--environment <env>", "Filter by environment (ios, ipados, macos, android, web, backend)").option("--group-by <field>", "Group by: app_id, app_version, device_model, os_version, environment, time:hour, time:day, time:week").addOption(
|
|
6813
6842
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
6814
6843
|
).action(async (slug, opts, cmd) => {
|
|
6815
6844
|
const { client, globals } = createClient(cmd);
|
|
6816
|
-
const result = await client.queryMetric(slug, opts.
|
|
6845
|
+
const result = await client.queryMetric(slug, opts.projectId, {
|
|
6817
6846
|
since: opts.since,
|
|
6818
6847
|
until: opts.until,
|
|
6819
|
-
app_id: opts.
|
|
6848
|
+
app_id: opts.appId,
|
|
6820
6849
|
app_version: opts.appVersion,
|
|
6821
6850
|
device_model: opts.deviceModel,
|
|
6822
6851
|
os_version: opts.osVersion,
|
|
6823
|
-
user_id: opts.
|
|
6852
|
+
user_id: opts.userId,
|
|
6824
6853
|
environment: opts.environment,
|
|
6825
6854
|
data_mode: opts.dataMode,
|
|
6826
6855
|
group_by: opts.groupBy
|
|
6827
6856
|
});
|
|
6828
6857
|
output(globals.format, result, () => formatQueryResult(result));
|
|
6829
6858
|
});
|
|
6830
|
-
metricsCommand.command("update <slug>").description("Update a metric definition").requiredOption("--project <id>", "Project ID").option("--name <name>", "New name").option("--description <desc>", "New description").option("--status <status>", "active or paused").action(async (slug, opts, cmd) => {
|
|
6859
|
+
metricsCommand.command("update <slug>").description("Update a metric definition").requiredOption("--project-id <id>", "Project ID").option("--name <name>", "New name").option("--description <desc>", "New description").option("--status <status>", "active or paused").action(async (slug, opts, cmd) => {
|
|
6831
6860
|
const { client, globals } = createClient(cmd);
|
|
6832
|
-
const metric = await client.updateMetric(slug, opts.
|
|
6861
|
+
const metric = await client.updateMetric(slug, opts.projectId, {
|
|
6833
6862
|
name: opts.name,
|
|
6834
6863
|
description: opts.description,
|
|
6835
6864
|
status: opts.status
|
|
6836
6865
|
});
|
|
6837
6866
|
output(globals.format, metric, () => formatMetricDetail(metric));
|
|
6838
6867
|
});
|
|
6839
|
-
metricsCommand.command("delete <slug>").description("Delete a metric definition").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6868
|
+
metricsCommand.command("delete <slug>").description("Delete a metric definition").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6840
6869
|
const { client, globals } = createClient(cmd);
|
|
6841
|
-
await client.deleteMetric(slug, opts.
|
|
6870
|
+
await client.deleteMetric(slug, opts.projectId);
|
|
6842
6871
|
console.log(source_default.green(`Metric "${slug}" deleted.`));
|
|
6843
6872
|
});
|
|
6844
6873
|
|
|
@@ -6924,82 +6953,65 @@ function formatQueryResult2(result) {
|
|
|
6924
6953
|
}
|
|
6925
6954
|
return lines.join("\n");
|
|
6926
6955
|
}
|
|
6927
|
-
var funnelsCommand = new Command("funnels").description("List funnel definitions").
|
|
6928
|
-
if (!opts.project) {
|
|
6929
|
-
console.error(source_default.red("Error: --project is required"));
|
|
6930
|
-
process.exitCode = 1;
|
|
6931
|
-
return;
|
|
6932
|
-
}
|
|
6956
|
+
var funnelsCommand = new Command("funnels").description("List funnel definitions").enablePositionalOptions().requiredOption("--project-id <id>", "Project ID").action(async (opts, cmd) => {
|
|
6933
6957
|
const { client, globals } = createClient(cmd);
|
|
6934
|
-
const result = await client.listFunnels(opts.
|
|
6958
|
+
const result = await client.listFunnels(opts.projectId);
|
|
6935
6959
|
output(globals.format, result.funnels, () => formatFunnelsTable(result.funnels));
|
|
6936
6960
|
});
|
|
6937
|
-
funnelsCommand.command("view <slug>").description("View funnel definition details").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6961
|
+
funnelsCommand.command("view <slug>").description("View funnel definition details").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6938
6962
|
const { client, globals } = createClient(cmd);
|
|
6939
|
-
const funnel = await client.getFunnel(slug, opts.
|
|
6963
|
+
const funnel = await client.getFunnel(slug, opts.projectId);
|
|
6940
6964
|
output(globals.format, funnel, () => formatFunnelDetail(funnel));
|
|
6941
6965
|
});
|
|
6942
|
-
funnelsCommand.command("create").description("Create a new funnel definition").requiredOption("--project <id>", "Project ID").requiredOption("--name <name>", "Funnel name").requiredOption("--slug <slug>", "Funnel slug").option("--description <desc>", "Description").
|
|
6966
|
+
funnelsCommand.command("create").description("Create a new funnel definition").requiredOption("--project-id <id>", "Project ID").requiredOption("--name <name>", "Funnel name").requiredOption("--slug <slug>", "Funnel slug").option("--description <desc>", "Description").option("--steps <json>", "Steps as JSON array").option("--steps-file <path>", "Read steps from a JSON file").action(async (opts, cmd) => {
|
|
6943
6967
|
const slugError = validateFunnelSlug(opts.slug);
|
|
6944
6968
|
if (slugError) {
|
|
6945
6969
|
console.error(source_default.red(`Error: ${slugError}`));
|
|
6946
6970
|
process.exitCode = 1;
|
|
6947
6971
|
return;
|
|
6948
6972
|
}
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
} catch {
|
|
6953
|
-
console.error(source_default.red("Error: --steps must be valid JSON"));
|
|
6954
|
-
process.exitCode = 1;
|
|
6955
|
-
return;
|
|
6956
|
-
}
|
|
6957
|
-
if (!Array.isArray(steps) || steps.length === 0) {
|
|
6958
|
-
console.error(source_default.red("Error: --steps must be a non-empty JSON array"));
|
|
6973
|
+
const stepsResult = resolveJsonArray(opts.steps, opts.stepsFile, { required: true });
|
|
6974
|
+
if (typeof stepsResult === "string") {
|
|
6975
|
+
console.error(source_default.red(stepsResult));
|
|
6959
6976
|
process.exitCode = 1;
|
|
6960
6977
|
return;
|
|
6961
6978
|
}
|
|
6962
6979
|
const { client, globals } = createClient(cmd);
|
|
6963
|
-
const funnel = await client.createFunnel(opts.
|
|
6980
|
+
const funnel = await client.createFunnel(opts.projectId, {
|
|
6964
6981
|
name: opts.name,
|
|
6965
6982
|
slug: opts.slug,
|
|
6966
6983
|
description: opts.description,
|
|
6967
|
-
steps
|
|
6984
|
+
steps: stepsResult
|
|
6968
6985
|
});
|
|
6969
6986
|
output(globals.format, funnel, () => formatFunnelDetail(funnel));
|
|
6970
6987
|
});
|
|
6971
|
-
funnelsCommand.command("update <slug>").description("Update a funnel definition").requiredOption("--project <id>", "Project ID").option("--name <name>", "New name").option("--description <desc>", "New description").option("--steps <json>", "New steps as JSON array").action(async (slug, opts, cmd) => {
|
|
6988
|
+
funnelsCommand.command("update <slug>").description("Update a funnel definition").requiredOption("--project-id <id>", "Project ID").option("--name <name>", "New name").option("--description <desc>", "New description").option("--steps <json>", "New steps as JSON array").option("--steps-file <path>", "Read steps from a JSON file").action(async (slug, opts, cmd) => {
|
|
6972
6989
|
const body = {};
|
|
6973
6990
|
if (opts.name !== void 0) body.name = opts.name;
|
|
6974
6991
|
if (opts.description !== void 0) body.description = opts.description;
|
|
6975
|
-
if (opts.steps
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
console.error(source_default.red("Error: --steps must be valid JSON"));
|
|
6980
|
-
process.exitCode = 1;
|
|
6981
|
-
return;
|
|
6982
|
-
}
|
|
6983
|
-
if (!Array.isArray(body.steps) || body.steps.length === 0) {
|
|
6984
|
-
console.error(source_default.red("Error: --steps must be a non-empty JSON array"));
|
|
6992
|
+
if (opts.steps || opts.stepsFile) {
|
|
6993
|
+
const stepsResult = resolveJsonArray(opts.steps, opts.stepsFile, { required: false });
|
|
6994
|
+
if (typeof stepsResult === "string") {
|
|
6995
|
+
console.error(source_default.red(stepsResult));
|
|
6985
6996
|
process.exitCode = 1;
|
|
6986
6997
|
return;
|
|
6987
6998
|
}
|
|
6999
|
+
body.steps = stepsResult;
|
|
6988
7000
|
}
|
|
6989
7001
|
const { client, globals } = createClient(cmd);
|
|
6990
|
-
const funnel = await client.updateFunnel(slug, opts.
|
|
7002
|
+
const funnel = await client.updateFunnel(slug, opts.projectId, body);
|
|
6991
7003
|
output(globals.format, funnel, () => formatFunnelDetail(funnel));
|
|
6992
7004
|
});
|
|
6993
|
-
funnelsCommand.command("delete <slug>").description("Delete a funnel definition").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
7005
|
+
funnelsCommand.command("delete <slug>").description("Delete a funnel definition").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6994
7006
|
const { client, globals } = createClient(cmd);
|
|
6995
|
-
await client.deleteFunnel(slug, opts.
|
|
7007
|
+
await client.deleteFunnel(slug, opts.projectId);
|
|
6996
7008
|
console.log(source_default.green(`Funnel "${slug}" deleted.`));
|
|
6997
7009
|
});
|
|
6998
|
-
funnelsCommand.command("query <slug>").description("Query funnel analytics").requiredOption("--project <id>", "Project ID").option("--since <date>", "Start date (ISO)").option("--until <date>", "End date (ISO)").option("--open", "Make this an open funnel. In an open funnel, users don't have to complete a previous step in order to be included in a subsequent step.").option("--app-version <version>", "Filter by app version").option("--environment <env>", "Filter by environment (ios, ipados, macos, android, web, backend)").option("--experiment <name:variant>", "Filter by experiment (format: name:variant)").option("--group-by <field>", "Group by: environment, app_version, or experiment:<name>").addOption(
|
|
7010
|
+
funnelsCommand.command("query <slug>").description("Query funnel analytics").requiredOption("--project-id <id>", "Project ID").option("--since <date>", "Start date (ISO)").option("--until <date>", "End date (ISO)").option("--open", "Make this an open funnel. In an open funnel, users don't have to complete a previous step in order to be included in a subsequent step.").option("--app-version <version>", "Filter by app version").option("--environment <env>", "Filter by environment (ios, ipados, macos, android, web, backend)").option("--experiment <name:variant>", "Filter by experiment (format: name:variant)").option("--group-by <field>", "Group by: environment, app_version, or experiment:<name>").addOption(
|
|
6999
7011
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
7000
7012
|
).action(async (slug, opts, cmd) => {
|
|
7001
7013
|
const { client, globals } = createClient(cmd);
|
|
7002
|
-
const result = await client.queryFunnel(slug, opts.
|
|
7014
|
+
const result = await client.queryFunnel(slug, opts.projectId, {
|
|
7003
7015
|
since: opts.since,
|
|
7004
7016
|
until: opts.until,
|
|
7005
7017
|
mode: opts.open ? "open" : "closed",
|
|
@@ -7015,7 +7027,7 @@ funnelsCommand.command("query <slug>").description("Query funnel analytics").req
|
|
|
7015
7027
|
// src/commands/audit-logs.ts
|
|
7016
7028
|
init_cjs_shims();
|
|
7017
7029
|
var auditLogCommand = new Command("audit-log").description("View audit logs");
|
|
7018
|
-
auditLogCommand.command("list").description("List audit log entries").requiredOption("--team <id>", "Team ID").option("--resource-type <type>", "Filter by resource type").option("--resource-id <id>", "Filter by resource ID").option("--actor <id>", "Filter by actor ID").addOption(
|
|
7030
|
+
auditLogCommand.command("list").description("List audit log entries").requiredOption("--team-id <id>", "Team ID").option("--resource-type <type>", "Filter by resource type").option("--resource-id <id>", "Filter by resource ID").option("--actor-id <id>", "Filter by actor ID").addOption(
|
|
7019
7031
|
new Option("--action <action>", "Filter by action").choices(["create", "update", "delete"])
|
|
7020
7032
|
).option("--since <time>", "Start time (e.g. 1h, 30m, 7d, or ISO 8601)").option("--until <time>", "End time").addOption(
|
|
7021
7033
|
new Option("--limit <n>", "Max entries to return").argParser((v) => parsePositiveInt(v, "--limit"))
|
|
@@ -7023,10 +7035,10 @@ auditLogCommand.command("list").description("List audit log entries").requiredOp
|
|
|
7023
7035
|
const { client, globals } = createClient(cmd);
|
|
7024
7036
|
const since = opts.since ? parseTimeInput(opts.since) : void 0;
|
|
7025
7037
|
const until = opts.until ? parseTimeInput(opts.until) : void 0;
|
|
7026
|
-
const result = await client.queryAuditLogs(opts.
|
|
7038
|
+
const result = await client.queryAuditLogs(opts.teamId, {
|
|
7027
7039
|
resource_type: opts.resourceType,
|
|
7028
7040
|
resource_id: opts.resourceId,
|
|
7029
|
-
actor_id: opts.
|
|
7041
|
+
actor_id: opts.actorId,
|
|
7030
7042
|
action: opts.action,
|
|
7031
7043
|
since,
|
|
7032
7044
|
until,
|
|
@@ -7053,7 +7065,7 @@ auditLogCommand.command("list").description("List audit log entries").requiredOp
|
|
|
7053
7065
|
|
|
7054
7066
|
// src/commands/skills.ts
|
|
7055
7067
|
init_cjs_shims();
|
|
7056
|
-
var
|
|
7068
|
+
var import_node_fs3 = require("fs");
|
|
7057
7069
|
var import_node_path2 = require("path");
|
|
7058
7070
|
var import_node_url = require("url");
|
|
7059
7071
|
var SKILLS = [
|
|
@@ -7065,7 +7077,7 @@ var skillsCommand = new Command("skills").description("Show paths to AI skill fi
|
|
|
7065
7077
|
const __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
|
|
7066
7078
|
const __dirname = (0, import_node_path2.dirname)(__filename2);
|
|
7067
7079
|
const skillsDir = (0, import_node_path2.resolve)(__dirname, "skills");
|
|
7068
|
-
if (!(0,
|
|
7080
|
+
if (!(0, import_node_fs3.existsSync)(skillsDir)) {
|
|
7069
7081
|
console.error(
|
|
7070
7082
|
source_default.red("Skills directory not found. This may indicate a broken installation.")
|
|
7071
7083
|
);
|
|
@@ -7076,7 +7088,7 @@ var skillsCommand = new Command("skills").description("Show paths to AI skill fi
|
|
|
7076
7088
|
for (const skill of SKILLS) {
|
|
7077
7089
|
const skillPath = (0, import_node_path2.join)(skillsDir, skill.dir, "SKILL.md");
|
|
7078
7090
|
const label = skill.label.padEnd(maxLabelLen);
|
|
7079
|
-
if ((0,
|
|
7091
|
+
if ((0, import_node_fs3.existsSync)(skillPath)) {
|
|
7080
7092
|
console.log(` ${source_default.cyan(label)} ${skillPath}`);
|
|
7081
7093
|
} else {
|
|
7082
7094
|
console.log(` ${source_default.cyan(label)} ${source_default.dim("(not found)")}`);
|
|
@@ -7183,7 +7195,7 @@ var switchCommand = new Command("switch").description("Switch active team profil
|
|
|
7183
7195
|
});
|
|
7184
7196
|
|
|
7185
7197
|
// src/index.ts
|
|
7186
|
-
var program2 = new Command().name("owlmetry").version("0.1.
|
|
7198
|
+
var program2 = new Command().name("owlmetry").version("0.1.8").description("OwlMetry CLI \u2014 query metrics and manage your apps from the terminal").addOption(
|
|
7187
7199
|
new Option("--format <format>", "Output format").choices(["table", "json", "log"]).default("table")
|
|
7188
7200
|
).option("--endpoint <url>", "OwlMetry API server URL").option("--api-key <key>", "API key").option("--ingest-endpoint <url>", "OwlMetry ingest endpoint URL (for SDKs; defaults to API endpoint for self-hosted)").option("--team <name-or-id>", "Use a specific team profile for this command");
|
|
7189
7201
|
program2.addCommand(authCommand);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: owlmetry-cli
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.8
|
|
4
4
|
description: >-
|
|
5
5
|
Install the OwlMetry CLI, sign up, and manage projects, apps, metrics,
|
|
6
6
|
funnels, and events. Use when adding OwlMetry to a project, querying
|
|
7
7
|
analytics, or when another OwlMetry skill needs CLI setup as a prerequisite.
|
|
8
|
+
IMPORTANT: You MUST load this skill before running ANY `owlmetry` CLI
|
|
9
|
+
command. The CLI has non-obvious subcommand syntax and flags — do not guess.
|
|
8
10
|
allowed-tools: Bash
|
|
9
11
|
---
|
|
10
12
|
|
|
@@ -14,11 +16,13 @@ OwlMetry is a self-hosted analytics platform for mobile and backend apps. It cap
|
|
|
14
16
|
|
|
15
17
|
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
18
|
|
|
19
|
+
> **Do not guess CLI syntax.** The `owlmetry` command has specific subcommand patterns (e.g., `owlmetry apps` not `owlmetry apps list`, flags like `--format json` not `--message`). Always reference the command documentation in this skill file before running a command. If you haven't loaded this skill yet, load it first.
|
|
20
|
+
|
|
17
21
|
## Version Check
|
|
18
22
|
|
|
19
23
|
Run these checks silently. Only inform the user if updates are available.
|
|
20
24
|
|
|
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 `
|
|
25
|
+
1. **Skill version**: `curl -sf https://raw.githubusercontent.com/Jasonvdb/owlmetry/main/skills/owlmetry-cli/SKILL.md | head -5` — compare the remote `version:` field to this file's `version:` frontmatter above. If the remote version is newer, ask the user if they want to update the local skill file.
|
|
22
26
|
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
27
|
|
|
24
28
|
If everything is current or the remote is unreachable, continue silently.
|
|
@@ -131,7 +135,7 @@ An app represents a single deployable target. The `client_key` returned on creat
|
|
|
131
135
|
|
|
132
136
|
```bash
|
|
133
137
|
owlmetry apps --format json # List all
|
|
134
|
-
owlmetry apps --project <id> --format json
|
|
138
|
+
owlmetry apps --project-id <id> --format json # List by project
|
|
135
139
|
owlmetry apps view <id> --format json # View details
|
|
136
140
|
owlmetry apps create --project-id <id> --name <name> --platform <platform> [--bundle-id <id>] --format json
|
|
137
141
|
owlmetry apps update <id> --name <new-name> --format json
|
|
@@ -151,11 +155,11 @@ Metrics are project-scoped definitions that tell OwlMetry what structured data t
|
|
|
151
155
|
The metric definition must exist on the server **before** the SDK emits events for that slug, otherwise the server will reject the events.
|
|
152
156
|
|
|
153
157
|
```bash
|
|
154
|
-
owlmetry metrics --project <id> --format json
|
|
155
|
-
owlmetry metrics view <slug> --project <id> --format json
|
|
156
|
-
owlmetry metrics create --project <id> --name <name> --slug <slug> [--lifecycle] [--description <desc>] --format json
|
|
157
|
-
owlmetry metrics update <slug> --project <id> [--name <name>] [--status active|paused] --format json
|
|
158
|
-
owlmetry metrics delete <slug> --project <id>
|
|
158
|
+
owlmetry metrics --project-id <id> --format json # List all
|
|
159
|
+
owlmetry metrics view <slug> --project-id <id> --format json # View details
|
|
160
|
+
owlmetry metrics create --project-id <id> --name <name> --slug <slug> [--lifecycle] [--description <desc>] --format json
|
|
161
|
+
owlmetry metrics update <slug> --project-id <id> [--name <name>] [--status active|paused] --format json
|
|
162
|
+
owlmetry metrics delete <slug> --project-id <id>
|
|
159
163
|
```
|
|
160
164
|
|
|
161
165
|
Slugs: lowercase letters, numbers, hyphens only (`/^[a-z0-9-]+$/`).
|
|
@@ -173,13 +177,34 @@ Funnels support two analysis modes:
|
|
|
173
177
|
Maximum 20 steps per funnel.
|
|
174
178
|
|
|
175
179
|
```bash
|
|
176
|
-
owlmetry funnels --project <id> --format json
|
|
177
|
-
owlmetry funnels view <slug> --project <id> --format json
|
|
178
|
-
owlmetry funnels
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
owlmetry funnels --project-id <id> --format json # List all
|
|
181
|
+
owlmetry funnels view <slug> --project-id <id> --format json # View details
|
|
182
|
+
owlmetry funnels delete <slug> --project-id <id>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Creating funnels** — use `--steps-file` to avoid shell quoting issues with JSON:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# 1. Write steps to a JSON file
|
|
189
|
+
cat > /tmp/funnel-steps.json << 'EOF'
|
|
190
|
+
[
|
|
191
|
+
{"name": "Step Name", "event_filter": {"message": "track:step-name"}},
|
|
192
|
+
{"name": "Next Step", "event_filter": {"message": "track:next-step"}}
|
|
193
|
+
]
|
|
194
|
+
EOF
|
|
195
|
+
|
|
196
|
+
# 2. Create the funnel referencing the file
|
|
197
|
+
owlmetry funnels create --project-id <id> --name <name> --slug <slug> \
|
|
198
|
+
--steps-file /tmp/funnel-steps.json [--description <desc>] --format json
|
|
181
199
|
```
|
|
182
200
|
|
|
201
|
+
**Updating funnel steps** — same pattern:
|
|
202
|
+
```bash
|
|
203
|
+
owlmetry funnels update <slug> --project-id <id> --steps-file /tmp/updated-steps.json --format json
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Inline `--steps '<json>'` also works but is error-prone in shell environments due to JSON quoting. Prefer `--steps-file`.
|
|
207
|
+
|
|
183
208
|
Steps JSON format: `[{"name":"Step Name","event_filter":{"message":"track:step-name"}}]`
|
|
184
209
|
|
|
185
210
|
## Querying
|
|
@@ -189,7 +214,7 @@ Steps JSON format: `[{"name":"Step Name","event_filter":{"message":"track:step-n
|
|
|
189
214
|
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.
|
|
190
215
|
|
|
191
216
|
```bash
|
|
192
|
-
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
|
|
217
|
+
owlmetry events [--project-id <id>] [--app-id <id>] [--since <time>] [--until <time>] [--level info|debug|warn|error] [--user-id <id>] [--session-id <id>] [--screen-name <name>] [--limit <n>] [--cursor <cursor>] [--data-mode production|development|all] --format json
|
|
193
218
|
owlmetry events view <id> --format json
|
|
194
219
|
```
|
|
195
220
|
|
|
@@ -221,8 +246,8 @@ There are two ways to look at metric data:
|
|
|
221
246
|
- **`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.
|
|
222
247
|
|
|
223
248
|
```bash
|
|
224
|
-
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
|
|
225
|
-
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
|
|
249
|
+
owlmetry metrics events <slug> --project-id <id> [--phase start|complete|fail|cancel|record] [--tracking-id <id>] [--user-id <id>] [--since <time>] [--until <time>] [--environment <env>] [--data-mode <mode>] --format json
|
|
250
|
+
owlmetry metrics query <slug> --project-id <id> [--since <date>] [--until <date>] [--app-id <id>] [--app-version <v>] [--environment <env>] [--user-id <id>] [--group-by app_id|app_version|device_model|os_version|environment|time:hour|time:day|time:week] [--data-mode <mode>] --format json
|
|
226
251
|
```
|
|
227
252
|
|
|
228
253
|
### Funnel Analytics
|
|
@@ -230,7 +255,7 @@ owlmetry metrics query <slug> --project <id> [--since <date>] [--until <date>] [
|
|
|
230
255
|
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.
|
|
231
256
|
|
|
232
257
|
```bash
|
|
233
|
-
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
|
|
258
|
+
owlmetry funnels query <slug> --project-id <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
|
|
234
259
|
```
|
|
235
260
|
|
|
236
261
|
`--open` = open funnel mode (steps evaluated independently, not sequentially).
|
|
@@ -240,7 +265,7 @@ owlmetry funnels query <slug> --project <id> [--since <date>] [--until <date>] [
|
|
|
240
265
|
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).
|
|
241
266
|
|
|
242
267
|
```bash
|
|
243
|
-
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
|
|
268
|
+
owlmetry audit-log list --team-id <id> [--resource-type <type>] [--resource-id <id>] [--actor-id <id>] [--action create|update|delete] [--since <time>] [--until <time>] [--limit <n>] --format json
|
|
244
269
|
```
|
|
245
270
|
|
|
246
271
|
## Key Notes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: owlmetry-node
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.8
|
|
4
4
|
description: >-
|
|
5
5
|
Integrate the OwlMetry Node.js SDK into a backend service for server-side
|
|
6
6
|
analytics, event tracking, metrics, funnels, and A/B experiments. Use when
|
|
@@ -18,7 +18,7 @@ The SDK has zero runtime dependencies. All calls are non-blocking — events are
|
|
|
18
18
|
|
|
19
19
|
Run these checks silently. Only inform the user if updates are available.
|
|
20
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 `
|
|
21
|
+
1. **Skill version**: `curl -sf https://raw.githubusercontent.com/Jasonvdb/owlmetry/main/skills/owlmetry-node/SKILL.md | head -5` — compare the remote `version:` field to this file's `version:` frontmatter above. If the remote version is newer, ask the user if they want to update.
|
|
22
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
23
|
|
|
24
24
|
## Prerequisite
|
|
@@ -27,6 +27,8 @@ You need an **ingest endpoint** and a **client key** (`owl_client_...`) for a ba
|
|
|
27
27
|
|
|
28
28
|
If the user doesn't have these yet, follow the `/owlmetry-cli` skill first — it handles sign-up, project creation, and app creation. The ingest endpoint is saved to `~/.owlmetry/config.json` (`ingest_endpoint` field) and the client key is returned when creating an app.
|
|
29
29
|
|
|
30
|
+
> **Any time you need to run an `owlmetry` CLI command** (querying events, creating metrics/funnels, listing apps, etc.), **load the `/owlmetry-cli` skill first**. Do not guess CLI syntax — it has non-obvious subcommand patterns and flags.
|
|
31
|
+
|
|
30
32
|
## Install
|
|
31
33
|
|
|
32
34
|
```bash
|
|
@@ -62,13 +64,24 @@ Owl.configure({
|
|
|
62
64
|
**Serverless (Firebase Cloud Functions, AWS Lambda, Vercel):** After adding `Owl.configure()`, also wrap your exported handler functions with `Owl.wrapHandler()` to guarantee events are flushed before the runtime freezes. This is essential boilerplate for serverless — without it, buffered events are lost:
|
|
63
65
|
|
|
64
66
|
```typescript
|
|
65
|
-
//
|
|
66
|
-
export const
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
67
|
+
// AWS Lambda / generic serverless:
|
|
68
|
+
export const handler = Owl.wrapHandler(async (event, context) => { ... });
|
|
69
|
+
|
|
70
|
+
// Firebase Cloud Functions v2:
|
|
71
|
+
// IMPORTANT: Explicitly type the request parameter — TypeScript cannot infer
|
|
72
|
+
// the CallableRequest type through wrapHandler's generics.
|
|
73
|
+
import { onCall, type CallableRequest } from 'firebase-functions/v2/https';
|
|
74
|
+
|
|
75
|
+
export const myFunction = onCall(
|
|
76
|
+
Owl.wrapHandler(async (request: CallableRequest) => {
|
|
77
|
+
const { data, auth } = request; // works — TypeScript knows the type
|
|
78
|
+
// ...
|
|
79
|
+
})
|
|
80
|
+
);
|
|
70
81
|
```
|
|
71
82
|
|
|
83
|
+
**TypeScript note:** `wrapHandler()` uses generic rest parameters (`<TArgs extends unknown[]>`), which means TypeScript sometimes infers handler parameters as `unknown` when the outer function (like Firebase's `onCall`) expects a specific callback type. If you see type errors like `Property 'data' does not exist on type 'unknown'`, explicitly annotate the handler's parameters (e.g., `request: CallableRequest`, `event: APIGatewayEvent`).
|
|
84
|
+
|
|
72
85
|
## Next Steps — Codebase Instrumentation
|
|
73
86
|
|
|
74
87
|
Once `Owl.configure()` is in place and the project builds successfully, **you MUST stop here and ask the user** which area they'd like to instrument first — even if the user's original prompt asked you to "instrument the app." Do not proceed with any code changes until the user chooses. Present these three options:
|
|
@@ -97,16 +110,21 @@ Events are the core data unit. Use the four log levels to capture different kind
|
|
|
97
110
|
|
|
98
111
|
- **`info`** — normal operations: server started, request handled, job completed, user action processed.
|
|
99
112
|
- **`debug`** — verbose detail for development: cache lookups, query plans, config loading, intermediate state.
|
|
100
|
-
- **`warn`** —
|
|
101
|
-
- **`error`** —
|
|
113
|
+
- **`warn`** — something didn't go as expected but the process can continue: failed validation, precondition checks that fail, slow queries, rate limits approaching, fallback paths, deprecated API usage, missing optional config.
|
|
114
|
+
- **`error`** — a caught exception or hard failure inside a `try`/`catch`: database connection errors, external API timeouts, unhandled rejections, file system errors. Reserve for actual thrown errors, not for anticipated validation outcomes.
|
|
102
115
|
|
|
103
116
|
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.
|
|
104
117
|
|
|
105
118
|
```typescript
|
|
106
119
|
Owl.info('Server started', { port: 4000 });
|
|
107
120
|
Owl.debug('Cache miss', { key: 'user:123' });
|
|
108
|
-
Owl.warn('
|
|
109
|
-
|
|
121
|
+
Owl.warn('Invalid request payload', { field: 'email', reason: 'missing' });
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await db.connect();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
Owl.error('Database connection failed', { host: 'db.example.com', error: String(err) });
|
|
127
|
+
}
|
|
110
128
|
```
|
|
111
129
|
|
|
112
130
|
All methods: `Owl.info/debug/warn/error(message: string, attrs?: Record<string, unknown>)`.
|
|
@@ -118,7 +136,12 @@ Source module (file:line) is auto-captured from the call stack.
|
|
|
118
136
|
Owl.info('Request handled', { method: 'POST', path: '/api/orders', status: 201 });
|
|
119
137
|
Owl.info('Background job completed', { job: 'send-emails', processed: 150 });
|
|
120
138
|
Owl.warn('Rate limit approaching', { current: 95, limit: 100, client_id: 'abc' });
|
|
121
|
-
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await stripe.charges.create(params);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
Owl.error('Stripe charge failed', { endpoint: '/charges', error: String(err) });
|
|
144
|
+
}
|
|
122
145
|
```
|
|
123
146
|
|
|
124
147
|
## Per-Request User Scoping
|
|
@@ -183,7 +206,7 @@ op.cancel({ reason: 'timeout' });
|
|
|
183
206
|
|
|
184
207
|
`duration_ms` and `tracking_id` (UUID) are auto-added. Create the metric definition first:
|
|
185
208
|
```bash
|
|
186
|
-
owlmetry metrics create --project <id> --name "Database Query" --slug database-query --lifecycle --format json
|
|
209
|
+
owlmetry metrics create --project-id <id> --name "Database Query" --slug database-query --lifecycle --format json
|
|
187
210
|
```
|
|
188
211
|
|
|
189
212
|
### Single-shot measurements
|
|
@@ -339,7 +362,8 @@ When instrumenting a backend service, follow this priority:
|
|
|
339
362
|
**Always instrument (events — no CLI setup needed):**
|
|
340
363
|
- Server startup and shutdown (`info`)
|
|
341
364
|
- Request handling: key route hits, responses sent (`info` with method/path/status)
|
|
342
|
-
-
|
|
365
|
+
- Caught exceptions: catch blocks, unhandled rejections, external API failures (`error`)
|
|
366
|
+
- Validation failures and pre-checks: bad input, missing optional config, rate limits (`warn`)
|
|
343
367
|
- Authentication events: login, logout, token refresh (`info`)
|
|
344
368
|
- Core business actions: order placed, payment processed, email sent (`info`)
|
|
345
369
|
- Background jobs: started, completed, failed (`info`/`error`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: owlmetry-swift
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.8
|
|
4
4
|
description: >-
|
|
5
5
|
Integrate the OwlMetry Swift SDK into an iOS or macOS app for analytics,
|
|
6
6
|
event tracking, metrics, funnels, and A/B experiments. Use when
|
|
@@ -18,7 +18,7 @@ The SDK is a static `Owl` enum with no external dependencies. All calls are non-
|
|
|
18
18
|
|
|
19
19
|
Run these checks silently. Only inform the user if updates are available.
|
|
20
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 `
|
|
21
|
+
1. **Skill version**: `curl -sf https://raw.githubusercontent.com/Jasonvdb/owlmetry/main/skills/owlmetry-swift/SKILL.md | head -5` — compare the remote `version:` field to this file's `version:` frontmatter above. If the remote version is newer, ask the user if they want to update.
|
|
22
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
23
|
|
|
24
24
|
## Prerequisite
|
|
@@ -27,9 +27,16 @@ You need an **ingest endpoint** and a **client key** (`owl_client_...`) for an A
|
|
|
27
27
|
|
|
28
28
|
If the user doesn't have these yet, follow the `/owlmetry-cli` skill first — it handles sign-up, project creation, and app creation. The ingest endpoint is saved to `~/.owlmetry/config.json` (`ingest_endpoint` field) and the client key is returned when creating an app.
|
|
29
29
|
|
|
30
|
+
> **Any time you need to run an `owlmetry` CLI command** (querying events, creating metrics/funnels, listing apps, etc.), **load the `/owlmetry-cli` skill first**. Do not guess CLI syntax — it has non-obvious subcommand patterns and flags.
|
|
31
|
+
|
|
30
32
|
## Add Swift Package
|
|
31
33
|
|
|
32
|
-
**
|
|
34
|
+
**Minimum platforms:** iOS 16.0, macOS 13.0. Zero external dependencies.
|
|
35
|
+
|
|
36
|
+
### Option A — Package.swift projects
|
|
37
|
+
|
|
38
|
+
If the project has a `Package.swift`, add the dependency there:
|
|
39
|
+
|
|
33
40
|
```swift
|
|
34
41
|
dependencies: [
|
|
35
42
|
.package(url: "https://github.com/Jasonvdb/owlmetry.git", branch: "main")
|
|
@@ -42,29 +49,31 @@ Add to your target:
|
|
|
42
49
|
])
|
|
43
50
|
```
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
Then run `swift package resolve` to fetch the dependency.
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
### Option B — Xcode projects (.xcodeproj)
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
For `.xcodeproj`-based projects with no `Package.swift`, add the OwlMetry Swift package by editing `<Project>.xcodeproj/project.pbxproj` directly to add a remote Swift package reference for `https://github.com/Jasonvdb/owlmetry.git` (branch: `main`, product: `OwlMetry`). Do not ask the user to add it manually in Xcode.
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
### Option C — Ask the user (last resort)
|
|
52
59
|
|
|
53
|
-
If
|
|
60
|
+
If pbxproj editing fails or the project structure is too complex, ask the user to add the package in Xcode:
|
|
54
61
|
|
|
55
|
-
1.
|
|
56
|
-
2.
|
|
57
|
-
3.
|
|
58
|
-
4.
|
|
59
|
-
5. Scroll to "Frameworks, Libraries, and Embedded Content"
|
|
60
|
-
6. Click the **+** button
|
|
61
|
-
7. Click "Add Other…" > "Add Package Dependency…"
|
|
62
|
-
8. Enter the URL: `https://github.com/Jasonvdb/owlmetry.git`
|
|
63
|
-
9. Set "Dependency Rule" to **Branch** → `main`
|
|
64
|
-
10. Click "Add Package"
|
|
65
|
-
11. Select the **OwlMetry** library and click "Add Package"
|
|
62
|
+
1. File > Add Package Dependencies
|
|
63
|
+
2. Enter URL: `https://github.com/Jasonvdb/owlmetry.git`
|
|
64
|
+
3. Set rule to **Branch** > `main`
|
|
65
|
+
4. Add **OwlMetry** to the app target
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
## Verify Package Integration
|
|
68
|
+
|
|
69
|
+
After adding the package, resolve dependencies and build:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
xcodebuild -resolvePackageDependencies -project <path>.xcodeproj -quiet
|
|
73
|
+
xcodebuild -project <path>.xcodeproj -scheme <SchemeName> -destination 'platform=iOS Simulator,name=iPhone 16' build -quiet
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If the build succeeds, proceed with configuration. The "No such module 'OwlMetry'" warning in editors (SourceKit) is expected and resolves during a real `xcodebuild`.
|
|
68
77
|
|
|
69
78
|
## Configure
|
|
70
79
|
|
|
@@ -187,16 +196,26 @@ Events are the core unit of data in OwlMetry. Use the four log levels to capture
|
|
|
187
196
|
|
|
188
197
|
- **`info`** — normal operations worth recording: screen views, user actions, feature usage, successful completions. This is your default level.
|
|
189
198
|
- **`debug`** — verbose detail useful only during development: cache hits, state transitions, intermediate values. These are filtered out in production data mode.
|
|
190
|
-
- **`warn`** — something
|
|
191
|
-
- **`error`** —
|
|
199
|
+
- **`warn`** — something didn't go as expected but the app can continue: failed validation, precondition checks that fail, slow responses, fallback paths taken, deprecated API usage, missing optional data.
|
|
200
|
+
- **`error`** — a caught exception or hard failure inside a `do`/`catch` block: network errors, JSON decode failures, file I/O errors, keychain access failures. Reserve for actual thrown errors, not for anticipated validation outcomes.
|
|
192
201
|
|
|
193
202
|
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.
|
|
194
203
|
|
|
195
204
|
```swift
|
|
205
|
+
// In a screen context — pass screenName to tie the event to the screen
|
|
196
206
|
Owl.info("User opened settings", screenName: "SettingsView")
|
|
197
207
|
Owl.debug("Cache hit", screenName: "HomeView", customAttributes: ["key": "user_prefs"])
|
|
198
|
-
Owl.warn("
|
|
199
|
-
|
|
208
|
+
Owl.warn("Invalid email format", screenName: "SignUpView", customAttributes: ["input": email])
|
|
209
|
+
|
|
210
|
+
do {
|
|
211
|
+
let profile = try await api.loadProfile(id: userId)
|
|
212
|
+
} catch {
|
|
213
|
+
Owl.error("Failed to load profile", screenName: "ProfileView", customAttributes: ["error": "\(error)"])
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Outside a screen context — omit screenName entirely
|
|
217
|
+
Owl.info("Background sync completed", customAttributes: ["items": "\(count)"])
|
|
218
|
+
Owl.error("Keychain write failed", customAttributes: ["error": "\(error)"])
|
|
200
219
|
```
|
|
201
220
|
|
|
202
221
|
All logging methods share the same signature:
|
|
@@ -204,6 +223,8 @@ All logging methods share the same signature:
|
|
|
204
223
|
Owl.info(_ message: String, screenName: String? = nil, customAttributes: [String: String]? = nil)
|
|
205
224
|
```
|
|
206
225
|
|
|
226
|
+
**`screenName` is optional.** Only pass it when the event originates from a specific screen in the UI (e.g., a button tap handler inside a view). **Do NOT pass `screenName`** when logging from utility functions, services, managers, network layers, background tasks, or anywhere that isn't directly tied to a visible screen. Passing a fabricated or guessed screen name is worse than omitting it — it pollutes screen-level analytics.
|
|
227
|
+
|
|
207
228
|
Source file, function, and line are auto-captured.
|
|
208
229
|
|
|
209
230
|
**Avoid logging PII** (emails, phone numbers, passwords) or high-frequency events (every frame, every scroll position). Focus on actions and outcomes.
|
|
@@ -251,9 +272,18 @@ Owl.track("first-post")
|
|
|
251
272
|
|
|
252
273
|
Each `track()` call emits an info-level event with message `"track:<stepName>"`. Define matching funnel definitions via `/owlmetry-cli`:
|
|
253
274
|
```bash
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
# Write steps to a JSON file (avoids shell quoting issues)
|
|
276
|
+
cat > /tmp/funnel-steps.json << 'EOF'
|
|
277
|
+
[
|
|
278
|
+
{"name": "Welcome", "event_filter": {"message": "track:welcome-screen"}},
|
|
279
|
+
{"name": "Account", "event_filter": {"message": "track:create-account"}},
|
|
280
|
+
{"name": "Profile", "event_filter": {"message": "track:complete-profile"}},
|
|
281
|
+
{"name": "First Post", "event_filter": {"message": "track:first-post"}}
|
|
282
|
+
]
|
|
283
|
+
EOF
|
|
284
|
+
|
|
285
|
+
owlmetry funnels create --project-id <id> --name "Onboarding" --slug onboarding \
|
|
286
|
+
--steps-file /tmp/funnel-steps.json --format json
|
|
257
287
|
```
|
|
258
288
|
|
|
259
289
|
## Structured Metrics
|
|
@@ -283,7 +313,7 @@ op.cancel(attributes: ["reason": "user_cancelled"])
|
|
|
283
313
|
|
|
284
314
|
`duration_ms` and `tracking_id` (UUID) are auto-added. Create the metric definition first:
|
|
285
315
|
```bash
|
|
286
|
-
owlmetry metrics create --project <id> --name "Photo Upload" --slug photo-upload --lifecycle --format json
|
|
316
|
+
owlmetry metrics create --project-id <id> --name "Photo Upload" --slug photo-upload --lifecycle --format json
|
|
287
317
|
```
|
|
288
318
|
|
|
289
319
|
### Single-shot measurements
|
|
@@ -328,7 +358,8 @@ When instrumenting a new app, follow this priority:
|
|
|
328
358
|
- Screen views (`.owlScreen("ScreenName")` on every distinct screen)
|
|
329
359
|
- App launch / cold start (`info` in `init()` or `didFinishLaunching`)
|
|
330
360
|
- Authentication events (login, logout, signup)
|
|
331
|
-
-
|
|
361
|
+
- Caught exceptions (`error` in `catch` blocks, error handlers)
|
|
362
|
+
- Validation failures and pre-checks (`warn` for bad input, missing optional data, fallback paths)
|
|
332
363
|
- Core business actions (purchase, share, create, delete)
|
|
333
364
|
|
|
334
365
|
**Instrument when relevant (metrics — requires CLI `owlmetry metrics create` first):**
|
|
@@ -341,8 +372,9 @@ When instrumenting a new app, follow this priority:
|
|
|
341
372
|
|
|
342
373
|
**Where to place calls:**
|
|
343
374
|
- Screen views: `.owlScreen("Name")` on the outermost view of each screen (SwiftUI), `viewDidAppear` in UIKit
|
|
344
|
-
- User actions: button action handlers, gesture callbacks
|
|
345
|
-
- Errors: `catch` blocks, `Result.failure` handlers
|
|
375
|
+
- User actions: button action handlers, gesture callbacks — pass `screenName` since you know which screen the user is on
|
|
376
|
+
- Errors: `catch` blocks, `Result.failure` handlers — pass `screenName` only if the error is caught inside a view; omit it if caught in a service, manager, or utility
|
|
377
|
+
- Services, utilities, background tasks: log freely but **never pass `screenName`** — these are not screen-bound
|
|
346
378
|
- Metrics: wrap the async operation between `startOperation()` and `complete()`/`fail()`
|
|
347
379
|
|
|
348
380
|
**What NOT to instrument:**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmetry/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "OwlMetry CLI — manage projects, apps, metrics, funnels, and events from the terminal. Includes AI skill files for agent-assisted development.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|