@owlmetry/cli 0.1.7 → 0.1.9
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 +93 -78
- package/dist/skills/owlmetry-cli/SKILL.md +39 -18
- package/dist/skills/owlmetry-node/SKILL.md +2 -2
- package/dist/skills/owlmetry-swift/SKILL.md +53 -6
- 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,12 @@ 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("
|
|
6363
|
+
var appsCommand = new Command("apps").description("Manage apps");
|
|
6364
|
+
appsCommand.command("list").description("List apps").option("--project-id <id>", "Filter by project ID").action(async (opts, cmd) => {
|
|
6364
6365
|
const { client, globals } = createClient(cmd);
|
|
6365
6366
|
let apps = await client.listApps();
|
|
6366
|
-
if (opts.
|
|
6367
|
-
apps = apps.filter((a) => a.project_id === opts.
|
|
6367
|
+
if (opts.projectId) {
|
|
6368
|
+
apps = apps.filter((a) => a.project_id === opts.projectId);
|
|
6368
6369
|
}
|
|
6369
6370
|
output(globals.format, apps, () => formatAppsTable(apps));
|
|
6370
6371
|
});
|
|
@@ -6454,6 +6455,7 @@ function formatMetricEventsLog(events, slug) {
|
|
|
6454
6455
|
|
|
6455
6456
|
// src/utils/parse.ts
|
|
6456
6457
|
init_cjs_shims();
|
|
6458
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
6457
6459
|
function parsePositiveInt(value, flagName) {
|
|
6458
6460
|
const n = parseInt(value, 10);
|
|
6459
6461
|
if (Number.isNaN(n) || n <= 0) {
|
|
@@ -6461,6 +6463,34 @@ function parsePositiveInt(value, flagName) {
|
|
|
6461
6463
|
}
|
|
6462
6464
|
return n;
|
|
6463
6465
|
}
|
|
6466
|
+
function resolveJsonArray(inline, filePath, opts) {
|
|
6467
|
+
if (inline && filePath) {
|
|
6468
|
+
return "Error: --steps and --steps-file are mutually exclusive";
|
|
6469
|
+
}
|
|
6470
|
+
if (!inline && !filePath) {
|
|
6471
|
+
return opts.required ? "Error: either --steps or --steps-file is required" : "";
|
|
6472
|
+
}
|
|
6473
|
+
let json;
|
|
6474
|
+
if (filePath) {
|
|
6475
|
+
try {
|
|
6476
|
+
json = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
|
6477
|
+
} catch (err) {
|
|
6478
|
+
return `Error reading --steps-file: ${err.message}`;
|
|
6479
|
+
}
|
|
6480
|
+
} else {
|
|
6481
|
+
json = inline;
|
|
6482
|
+
}
|
|
6483
|
+
let parsed;
|
|
6484
|
+
try {
|
|
6485
|
+
parsed = JSON.parse(json);
|
|
6486
|
+
} catch {
|
|
6487
|
+
return "Error: steps must be valid JSON";
|
|
6488
|
+
}
|
|
6489
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
6490
|
+
return "Error: steps must be a non-empty JSON array";
|
|
6491
|
+
}
|
|
6492
|
+
return parsed;
|
|
6493
|
+
}
|
|
6464
6494
|
|
|
6465
6495
|
// src/utils/time.ts
|
|
6466
6496
|
init_cjs_shims();
|
|
@@ -6500,9 +6530,9 @@ ${source_default.dim(`More results available. Use --cursor ${result.cursor}`)}`;
|
|
|
6500
6530
|
}
|
|
6501
6531
|
|
|
6502
6532
|
// 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(
|
|
6533
|
+
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
6534
|
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(
|
|
6535
|
+
).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
6536
|
new Option("--limit <n>", "Max events to return").argParser((v) => parsePositiveInt(v, "--limit"))
|
|
6507
6537
|
).option("--cursor <cursor>", "Pagination cursor").addOption(
|
|
6508
6538
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
@@ -6511,14 +6541,14 @@ var eventsCommand = new Command("events").description("Query events").option("--
|
|
|
6511
6541
|
const since = opts.since ? parseTimeInput(opts.since) : !opts.until ? parseTimeInput("24h") : void 0;
|
|
6512
6542
|
const until = opts.until ? parseTimeInput(opts.until) : void 0;
|
|
6513
6543
|
const result = await client.queryEvents({
|
|
6514
|
-
project_id: opts.
|
|
6515
|
-
app_id: opts.
|
|
6544
|
+
project_id: opts.projectId,
|
|
6545
|
+
app_id: opts.appId,
|
|
6516
6546
|
since,
|
|
6517
6547
|
until,
|
|
6518
6548
|
level: opts.level,
|
|
6519
|
-
user_id: opts.
|
|
6520
|
-
session_id: opts.
|
|
6521
|
-
screen_name: opts.
|
|
6549
|
+
user_id: opts.userId,
|
|
6550
|
+
session_id: opts.sessionId,
|
|
6551
|
+
screen_name: opts.screenName,
|
|
6522
6552
|
limit: opts.limit,
|
|
6523
6553
|
cursor: opts.cursor,
|
|
6524
6554
|
data_mode: opts.dataMode
|
|
@@ -6753,14 +6783,15 @@ function formatQueryResult(result) {
|
|
|
6753
6783
|
}
|
|
6754
6784
|
return lines.join("\n");
|
|
6755
6785
|
}
|
|
6756
|
-
var metricsCommand = new Command("metrics").description("
|
|
6786
|
+
var metricsCommand = new Command("metrics").description("Manage metric definitions");
|
|
6787
|
+
metricsCommand.command("list").description("List metric definitions").requiredOption("--project-id <id>", "Project ID").action(async (opts, cmd) => {
|
|
6757
6788
|
const { client, globals } = createClient(cmd);
|
|
6758
|
-
const metrics = await client.listMetrics(opts.
|
|
6789
|
+
const metrics = await client.listMetrics(opts.projectId);
|
|
6759
6790
|
output(globals.format, metrics, () => formatMetricsTable(metrics));
|
|
6760
6791
|
});
|
|
6761
|
-
metricsCommand.command("events <slug>").description("Query raw metric events for a metric").requiredOption("--project <id>", "Project ID").addOption(
|
|
6792
|
+
metricsCommand.command("events <slug>").description("Query raw metric events for a metric").requiredOption("--project-id <id>", "Project ID").addOption(
|
|
6762
6793
|
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(
|
|
6794
|
+
).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
6795
|
new Option("--limit <n>", "Max events to return").argParser((v) => parsePositiveInt(v, "--limit"))
|
|
6765
6796
|
).option("--cursor <cursor>", "Pagination cursor").option("--environment <env>", "Filter by environment (ios, ipados, macos, android, web, backend)").addOption(
|
|
6766
6797
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
@@ -6768,10 +6799,10 @@ metricsCommand.command("events <slug>").description("Query raw metric events for
|
|
|
6768
6799
|
const { client, globals } = createClient(cmd);
|
|
6769
6800
|
const since = opts.since ? parseTimeInput(opts.since) : !opts.until ? parseTimeInput("24h") : void 0;
|
|
6770
6801
|
const until = opts.until ? parseTimeInput(opts.until) : void 0;
|
|
6771
|
-
const result = await client.queryMetricEvents(slug, opts.
|
|
6802
|
+
const result = await client.queryMetricEvents(slug, opts.projectId, {
|
|
6772
6803
|
phase: opts.phase,
|
|
6773
6804
|
tracking_id: opts.trackingId,
|
|
6774
|
-
user_id: opts.
|
|
6805
|
+
user_id: opts.userId,
|
|
6775
6806
|
environment: opts.environment,
|
|
6776
6807
|
since,
|
|
6777
6808
|
until,
|
|
@@ -6787,12 +6818,12 @@ metricsCommand.command("events <slug>").description("Query raw metric events for
|
|
|
6787
6818
|
() => formatMetricEventsLog(result.events, slug) + hint
|
|
6788
6819
|
);
|
|
6789
6820
|
});
|
|
6790
|
-
metricsCommand.command("view <slug>").description("View metric definition details").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6821
|
+
metricsCommand.command("view <slug>").description("View metric definition details").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6791
6822
|
const { client, globals } = createClient(cmd);
|
|
6792
|
-
const metric = await client.getMetric(slug, opts.
|
|
6823
|
+
const metric = await client.getMetric(slug, opts.projectId);
|
|
6793
6824
|
output(globals.format, metric, () => formatMetricDetail(metric));
|
|
6794
6825
|
});
|
|
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) => {
|
|
6826
|
+
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
6827
|
const slugError = validateMetricSlug(opts.slug);
|
|
6797
6828
|
if (slugError) {
|
|
6798
6829
|
console.error(source_default.red(`Error: ${slugError}`));
|
|
@@ -6800,7 +6831,7 @@ metricsCommand.command("create").description("Create a new metric definition").r
|
|
|
6800
6831
|
return;
|
|
6801
6832
|
}
|
|
6802
6833
|
const { client, globals } = createClient(cmd);
|
|
6803
|
-
const metric = await client.createMetric(opts.
|
|
6834
|
+
const metric = await client.createMetric(opts.projectId, {
|
|
6804
6835
|
name: opts.name,
|
|
6805
6836
|
slug: opts.slug,
|
|
6806
6837
|
description: opts.description,
|
|
@@ -6809,36 +6840,36 @@ metricsCommand.command("create").description("Create a new metric definition").r
|
|
|
6809
6840
|
});
|
|
6810
6841
|
output(globals.format, metric, () => formatMetricDetail(metric));
|
|
6811
6842
|
});
|
|
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(
|
|
6843
|
+
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
6844
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
6814
6845
|
).action(async (slug, opts, cmd) => {
|
|
6815
6846
|
const { client, globals } = createClient(cmd);
|
|
6816
|
-
const result = await client.queryMetric(slug, opts.
|
|
6847
|
+
const result = await client.queryMetric(slug, opts.projectId, {
|
|
6817
6848
|
since: opts.since,
|
|
6818
6849
|
until: opts.until,
|
|
6819
|
-
app_id: opts.
|
|
6850
|
+
app_id: opts.appId,
|
|
6820
6851
|
app_version: opts.appVersion,
|
|
6821
6852
|
device_model: opts.deviceModel,
|
|
6822
6853
|
os_version: opts.osVersion,
|
|
6823
|
-
user_id: opts.
|
|
6854
|
+
user_id: opts.userId,
|
|
6824
6855
|
environment: opts.environment,
|
|
6825
6856
|
data_mode: opts.dataMode,
|
|
6826
6857
|
group_by: opts.groupBy
|
|
6827
6858
|
});
|
|
6828
6859
|
output(globals.format, result, () => formatQueryResult(result));
|
|
6829
6860
|
});
|
|
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) => {
|
|
6861
|
+
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
6862
|
const { client, globals } = createClient(cmd);
|
|
6832
|
-
const metric = await client.updateMetric(slug, opts.
|
|
6863
|
+
const metric = await client.updateMetric(slug, opts.projectId, {
|
|
6833
6864
|
name: opts.name,
|
|
6834
6865
|
description: opts.description,
|
|
6835
6866
|
status: opts.status
|
|
6836
6867
|
});
|
|
6837
6868
|
output(globals.format, metric, () => formatMetricDetail(metric));
|
|
6838
6869
|
});
|
|
6839
|
-
metricsCommand.command("delete <slug>").description("Delete a metric definition").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6870
|
+
metricsCommand.command("delete <slug>").description("Delete a metric definition").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6840
6871
|
const { client, globals } = createClient(cmd);
|
|
6841
|
-
await client.deleteMetric(slug, opts.
|
|
6872
|
+
await client.deleteMetric(slug, opts.projectId);
|
|
6842
6873
|
console.log(source_default.green(`Metric "${slug}" deleted.`));
|
|
6843
6874
|
});
|
|
6844
6875
|
|
|
@@ -6924,82 +6955,66 @@ function formatQueryResult2(result) {
|
|
|
6924
6955
|
}
|
|
6925
6956
|
return lines.join("\n");
|
|
6926
6957
|
}
|
|
6927
|
-
var funnelsCommand = new Command("funnels").description("
|
|
6928
|
-
|
|
6929
|
-
console.error(source_default.red("Error: --project is required"));
|
|
6930
|
-
process.exitCode = 1;
|
|
6931
|
-
return;
|
|
6932
|
-
}
|
|
6958
|
+
var funnelsCommand = new Command("funnels").description("Manage funnel definitions");
|
|
6959
|
+
funnelsCommand.command("list").description("List funnel definitions").requiredOption("--project-id <id>", "Project ID").action(async (opts, cmd) => {
|
|
6933
6960
|
const { client, globals } = createClient(cmd);
|
|
6934
|
-
const result = await client.listFunnels(opts.
|
|
6961
|
+
const result = await client.listFunnels(opts.projectId);
|
|
6935
6962
|
output(globals.format, result.funnels, () => formatFunnelsTable(result.funnels));
|
|
6936
6963
|
});
|
|
6937
|
-
funnelsCommand.command("view <slug>").description("View funnel definition details").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6964
|
+
funnelsCommand.command("view <slug>").description("View funnel definition details").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6938
6965
|
const { client, globals } = createClient(cmd);
|
|
6939
|
-
const funnel = await client.getFunnel(slug, opts.
|
|
6966
|
+
const funnel = await client.getFunnel(slug, opts.projectId);
|
|
6940
6967
|
output(globals.format, funnel, () => formatFunnelDetail(funnel));
|
|
6941
6968
|
});
|
|
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").
|
|
6969
|
+
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
6970
|
const slugError = validateFunnelSlug(opts.slug);
|
|
6944
6971
|
if (slugError) {
|
|
6945
6972
|
console.error(source_default.red(`Error: ${slugError}`));
|
|
6946
6973
|
process.exitCode = 1;
|
|
6947
6974
|
return;
|
|
6948
6975
|
}
|
|
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"));
|
|
6976
|
+
const stepsResult = resolveJsonArray(opts.steps, opts.stepsFile, { required: true });
|
|
6977
|
+
if (typeof stepsResult === "string") {
|
|
6978
|
+
console.error(source_default.red(stepsResult));
|
|
6959
6979
|
process.exitCode = 1;
|
|
6960
6980
|
return;
|
|
6961
6981
|
}
|
|
6962
6982
|
const { client, globals } = createClient(cmd);
|
|
6963
|
-
const funnel = await client.createFunnel(opts.
|
|
6983
|
+
const funnel = await client.createFunnel(opts.projectId, {
|
|
6964
6984
|
name: opts.name,
|
|
6965
6985
|
slug: opts.slug,
|
|
6966
6986
|
description: opts.description,
|
|
6967
|
-
steps
|
|
6987
|
+
steps: stepsResult
|
|
6968
6988
|
});
|
|
6969
6989
|
output(globals.format, funnel, () => formatFunnelDetail(funnel));
|
|
6970
6990
|
});
|
|
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) => {
|
|
6991
|
+
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
6992
|
const body = {};
|
|
6973
6993
|
if (opts.name !== void 0) body.name = opts.name;
|
|
6974
6994
|
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"));
|
|
6995
|
+
if (opts.steps || opts.stepsFile) {
|
|
6996
|
+
const stepsResult = resolveJsonArray(opts.steps, opts.stepsFile, { required: false });
|
|
6997
|
+
if (typeof stepsResult === "string") {
|
|
6998
|
+
console.error(source_default.red(stepsResult));
|
|
6985
6999
|
process.exitCode = 1;
|
|
6986
7000
|
return;
|
|
6987
7001
|
}
|
|
7002
|
+
body.steps = stepsResult;
|
|
6988
7003
|
}
|
|
6989
7004
|
const { client, globals } = createClient(cmd);
|
|
6990
|
-
const funnel = await client.updateFunnel(slug, opts.
|
|
7005
|
+
const funnel = await client.updateFunnel(slug, opts.projectId, body);
|
|
6991
7006
|
output(globals.format, funnel, () => formatFunnelDetail(funnel));
|
|
6992
7007
|
});
|
|
6993
|
-
funnelsCommand.command("delete <slug>").description("Delete a funnel definition").requiredOption("--project <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
7008
|
+
funnelsCommand.command("delete <slug>").description("Delete a funnel definition").requiredOption("--project-id <id>", "Project ID").action(async (slug, opts, cmd) => {
|
|
6994
7009
|
const { client, globals } = createClient(cmd);
|
|
6995
|
-
await client.deleteFunnel(slug, opts.
|
|
7010
|
+
await client.deleteFunnel(slug, opts.projectId);
|
|
6996
7011
|
console.log(source_default.green(`Funnel "${slug}" deleted.`));
|
|
6997
7012
|
});
|
|
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(
|
|
7013
|
+
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
7014
|
new Option("--data-mode <mode>", "Data mode: production, development, or all").choices(["production", "development", "all"]).default("production")
|
|
7000
7015
|
).action(async (slug, opts, cmd) => {
|
|
7001
7016
|
const { client, globals } = createClient(cmd);
|
|
7002
|
-
const result = await client.queryFunnel(slug, opts.
|
|
7017
|
+
const result = await client.queryFunnel(slug, opts.projectId, {
|
|
7003
7018
|
since: opts.since,
|
|
7004
7019
|
until: opts.until,
|
|
7005
7020
|
mode: opts.open ? "open" : "closed",
|
|
@@ -7015,7 +7030,7 @@ funnelsCommand.command("query <slug>").description("Query funnel analytics").req
|
|
|
7015
7030
|
// src/commands/audit-logs.ts
|
|
7016
7031
|
init_cjs_shims();
|
|
7017
7032
|
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(
|
|
7033
|
+
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
7034
|
new Option("--action <action>", "Filter by action").choices(["create", "update", "delete"])
|
|
7020
7035
|
).option("--since <time>", "Start time (e.g. 1h, 30m, 7d, or ISO 8601)").option("--until <time>", "End time").addOption(
|
|
7021
7036
|
new Option("--limit <n>", "Max entries to return").argParser((v) => parsePositiveInt(v, "--limit"))
|
|
@@ -7023,10 +7038,10 @@ auditLogCommand.command("list").description("List audit log entries").requiredOp
|
|
|
7023
7038
|
const { client, globals } = createClient(cmd);
|
|
7024
7039
|
const since = opts.since ? parseTimeInput(opts.since) : void 0;
|
|
7025
7040
|
const until = opts.until ? parseTimeInput(opts.until) : void 0;
|
|
7026
|
-
const result = await client.queryAuditLogs(opts.
|
|
7041
|
+
const result = await client.queryAuditLogs(opts.teamId, {
|
|
7027
7042
|
resource_type: opts.resourceType,
|
|
7028
7043
|
resource_id: opts.resourceId,
|
|
7029
|
-
actor_id: opts.
|
|
7044
|
+
actor_id: opts.actorId,
|
|
7030
7045
|
action: opts.action,
|
|
7031
7046
|
since,
|
|
7032
7047
|
until,
|
|
@@ -7053,7 +7068,7 @@ auditLogCommand.command("list").description("List audit log entries").requiredOp
|
|
|
7053
7068
|
|
|
7054
7069
|
// src/commands/skills.ts
|
|
7055
7070
|
init_cjs_shims();
|
|
7056
|
-
var
|
|
7071
|
+
var import_node_fs3 = require("fs");
|
|
7057
7072
|
var import_node_path2 = require("path");
|
|
7058
7073
|
var import_node_url = require("url");
|
|
7059
7074
|
var SKILLS = [
|
|
@@ -7065,7 +7080,7 @@ var skillsCommand = new Command("skills").description("Show paths to AI skill fi
|
|
|
7065
7080
|
const __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
|
|
7066
7081
|
const __dirname = (0, import_node_path2.dirname)(__filename2);
|
|
7067
7082
|
const skillsDir = (0, import_node_path2.resolve)(__dirname, "skills");
|
|
7068
|
-
if (!(0,
|
|
7083
|
+
if (!(0, import_node_fs3.existsSync)(skillsDir)) {
|
|
7069
7084
|
console.error(
|
|
7070
7085
|
source_default.red("Skills directory not found. This may indicate a broken installation.")
|
|
7071
7086
|
);
|
|
@@ -7076,7 +7091,7 @@ var skillsCommand = new Command("skills").description("Show paths to AI skill fi
|
|
|
7076
7091
|
for (const skill of SKILLS) {
|
|
7077
7092
|
const skillPath = (0, import_node_path2.join)(skillsDir, skill.dir, "SKILL.md");
|
|
7078
7093
|
const label = skill.label.padEnd(maxLabelLen);
|
|
7079
|
-
if ((0,
|
|
7094
|
+
if ((0, import_node_fs3.existsSync)(skillPath)) {
|
|
7080
7095
|
console.log(` ${source_default.cyan(label)} ${skillPath}`);
|
|
7081
7096
|
} else {
|
|
7082
7097
|
console.log(` ${source_default.cyan(label)} ${source_default.dim("(not found)")}`);
|
|
@@ -7183,7 +7198,7 @@ var switchCommand = new Command("switch").description("Switch active team profil
|
|
|
7183
7198
|
});
|
|
7184
7199
|
|
|
7185
7200
|
// src/index.ts
|
|
7186
|
-
var program2 = new Command().name("owlmetry").version("0.1.
|
|
7201
|
+
var program2 = new Command().name("owlmetry").version("0.1.9").description("OwlMetry CLI \u2014 query metrics and manage your apps from the terminal").addOption(
|
|
7187
7202
|
new Option("--format <format>", "Output format").choices(["table", "json", "log"]).default("table")
|
|
7188
7203
|
).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
7204
|
program2.addCommand(authCommand);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: owlmetry-cli
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.9
|
|
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
|
|
@@ -134,8 +134,8 @@ owlmetry projects update <id> --name <new-name> --format json
|
|
|
134
134
|
An app represents a single deployable target. The `client_key` returned on creation is what SDKs use for event ingestion. The `bundle_id` is **immutable after creation** — to change it, delete and recreate the app. Backend apps have no bundle_id.
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
owlmetry apps --format json
|
|
138
|
-
owlmetry apps --project <id> --format json
|
|
137
|
+
owlmetry apps list --format json # List all
|
|
138
|
+
owlmetry apps list --project-id <id> --format json # List by project
|
|
139
139
|
owlmetry apps view <id> --format json # View details
|
|
140
140
|
owlmetry apps create --project-id <id> --name <name> --platform <platform> [--bundle-id <id>] --format json
|
|
141
141
|
owlmetry apps update <id> --name <new-name> --format json
|
|
@@ -155,11 +155,11 @@ Metrics are project-scoped definitions that tell OwlMetry what structured data t
|
|
|
155
155
|
The metric definition must exist on the server **before** the SDK emits events for that slug, otherwise the server will reject the events.
|
|
156
156
|
|
|
157
157
|
```bash
|
|
158
|
-
owlmetry metrics --project <id> --format json
|
|
159
|
-
owlmetry metrics view <slug> --project <id> --format json
|
|
160
|
-
owlmetry metrics create --project <id> --name <name> --slug <slug> [--lifecycle] [--description <desc>] --format json
|
|
161
|
-
owlmetry metrics update <slug> --project <id> [--name <name>] [--status active|paused] --format json
|
|
162
|
-
owlmetry metrics delete <slug> --project <id>
|
|
158
|
+
owlmetry metrics list --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>
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
Slugs: lowercase letters, numbers, hyphens only (`/^[a-z0-9-]+$/`).
|
|
@@ -177,13 +177,34 @@ Funnels support two analysis modes:
|
|
|
177
177
|
Maximum 20 steps per funnel.
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
owlmetry funnels --project <id> --format json
|
|
181
|
-
owlmetry funnels view <slug> --project <id> --format json
|
|
182
|
-
owlmetry funnels
|
|
183
|
-
owlmetry funnels update <slug> --project <id> [--name <name>] [--steps '<json>'] --format json
|
|
184
|
-
owlmetry funnels delete <slug> --project <id>
|
|
180
|
+
owlmetry funnels list --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>
|
|
185
183
|
```
|
|
186
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
|
|
199
|
+
```
|
|
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
|
+
|
|
187
208
|
Steps JSON format: `[{"name":"Step Name","event_filter":{"message":"track:step-name"}}]`
|
|
188
209
|
|
|
189
210
|
## Querying
|
|
@@ -193,7 +214,7 @@ Steps JSON format: `[{"name":"Step Name","event_filter":{"message":"track:step-n
|
|
|
193
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.
|
|
194
215
|
|
|
195
216
|
```bash
|
|
196
|
-
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
|
|
197
218
|
owlmetry events view <id> --format json
|
|
198
219
|
```
|
|
199
220
|
|
|
@@ -225,8 +246,8 @@ There are two ways to look at metric data:
|
|
|
225
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.
|
|
226
247
|
|
|
227
248
|
```bash
|
|
228
|
-
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
|
|
229
|
-
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
|
|
230
251
|
```
|
|
231
252
|
|
|
232
253
|
### Funnel Analytics
|
|
@@ -234,7 +255,7 @@ owlmetry metrics query <slug> --project <id> [--since <date>] [--until <date>] [
|
|
|
234
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.
|
|
235
256
|
|
|
236
257
|
```bash
|
|
237
|
-
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
|
|
238
259
|
```
|
|
239
260
|
|
|
240
261
|
`--open` = open funnel mode (steps evaluated independently, not sequentially).
|
|
@@ -244,7 +265,7 @@ owlmetry funnels query <slug> --project <id> [--since <date>] [--until <date>] [
|
|
|
244
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).
|
|
245
266
|
|
|
246
267
|
```bash
|
|
247
|
-
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
|
|
248
269
|
```
|
|
249
270
|
|
|
250
271
|
## Key Notes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: owlmetry-node
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.9
|
|
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
|
|
@@ -206,7 +206,7 @@ op.cancel({ reason: 'timeout' });
|
|
|
206
206
|
|
|
207
207
|
`duration_ms` and `tracking_id` (UUID) are auto-added. Create the metric definition first:
|
|
208
208
|
```bash
|
|
209
|
-
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
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
### Single-shot measurements
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: owlmetry-swift
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.9
|
|
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
|
|
@@ -272,9 +272,18 @@ Owl.track("first-post")
|
|
|
272
272
|
|
|
273
273
|
Each `track()` call emits an info-level event with message `"track:<stepName>"`. Define matching funnel definitions via `/owlmetry-cli`:
|
|
274
274
|
```bash
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
278
287
|
```
|
|
279
288
|
|
|
280
289
|
## Structured Metrics
|
|
@@ -302,9 +311,47 @@ op.fail(error: "timeout", attributes: ["retry_count": "3"])
|
|
|
302
311
|
op.cancel(attributes: ["reason": "user_cancelled"])
|
|
303
312
|
```
|
|
304
313
|
|
|
305
|
-
`duration_ms` and `tracking_id` (UUID) are auto-added.
|
|
314
|
+
`duration_ms` and `tracking_id` (UUID) are auto-added.
|
|
315
|
+
|
|
316
|
+
**Rules for lifecycle operations:**
|
|
317
|
+
|
|
318
|
+
- **Every `startOperation()` must end** with exactly one `.complete()`, `.fail()`, or `.cancel()`. An operation that starts but never ends creates orphaned metric data with no duration.
|
|
319
|
+
- **`.complete()`** — the operation succeeded and produced its intended result.
|
|
320
|
+
- **`.fail(error:)`** — the operation attempted work but encountered an error.
|
|
321
|
+
- **`.cancel()`** — the operation was intentionally stopped before completion (user cancelled, view disappeared, became irrelevant).
|
|
322
|
+
- **Don't start for no-ops** — if the operation is skipped entirely (cache hit, dedup, precondition not met), don't call `startOperation()` at all. Only start when actual work begins.
|
|
323
|
+
- **Don't track duration manually** — `duration_ms` is auto-calculated from start to complete/fail/cancel. Never pass a manual duration attribute.
|
|
324
|
+
- **Long-lived operations** — if the operation outlives the scope where it was started (e.g., recording that spans a view lifecycle), store the `OwlOperation` handle as a property. Cancel it on cleanup (`.onDisappear`, `deinit`) if it hasn't ended yet:
|
|
325
|
+
|
|
326
|
+
```swift
|
|
327
|
+
// Store handle for operations that span a lifecycle
|
|
328
|
+
@State private var recordingOp: OwlOperation?
|
|
329
|
+
|
|
330
|
+
func startRecording() {
|
|
331
|
+
recordingOp = Owl.startOperation("video-recording")
|
|
332
|
+
// ... begin recording
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func stopRecording(url: URL) {
|
|
336
|
+
recordingOp?.complete(attributes: ["format": "mp4"])
|
|
337
|
+
recordingOp = nil
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
func onError(_ error: Error) {
|
|
341
|
+
recordingOp?.fail(error: error.localizedDescription)
|
|
342
|
+
recordingOp = nil
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Safety net: cancel if view disappears mid-operation
|
|
346
|
+
.onDisappear {
|
|
347
|
+
recordingOp?.cancel()
|
|
348
|
+
recordingOp = nil
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Create the metric definition first:
|
|
306
353
|
```bash
|
|
307
|
-
owlmetry metrics create --project <id> --name "Photo Upload" --slug photo-upload --lifecycle --format json
|
|
354
|
+
owlmetry metrics create --project-id <id> --name "Photo Upload" --slug photo-upload --lifecycle --format json
|
|
308
355
|
```
|
|
309
356
|
|
|
310
357
|
### Single-shot measurements
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmetry/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|