primitive-admin 1.0.37 → 1.0.39
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/README.md +46 -7
- package/dist/bin/primitive.js +2 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/analytics.js +464 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/databases.js +202 -29
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/init.js +290 -118
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +481 -20
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/secrets.js +108 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/workflows.js +14 -4
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.js +84 -12
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/init-config.js +87 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,49 +1,220 @@
|
|
|
1
1
|
import { ApiClient } from "../lib/api-client.js";
|
|
2
2
|
import { resolveAppId } from "../lib/config.js";
|
|
3
|
-
import { error, info,
|
|
3
|
+
import { error, info, formatTable, formatId, formatDate, formatNumber, formatPercent, formatDuration, json, heading, keyValue, dim, } from "../lib/output.js";
|
|
4
|
+
/** Format a value that is already a percentage (e.g. 33.3 → "33.3%") */
|
|
5
|
+
function formatPctRaw(value) {
|
|
6
|
+
if (value === undefined || value === null)
|
|
7
|
+
return "-";
|
|
8
|
+
return `${value.toFixed(1)}%`;
|
|
9
|
+
}
|
|
4
10
|
export function registerAnalyticsCommands(program) {
|
|
5
11
|
const analytics = program
|
|
6
12
|
.command("analytics")
|
|
7
13
|
.description("View usage analytics, user activity, and integration metrics")
|
|
8
14
|
.addHelpText("after", `
|
|
15
|
+
Commands:
|
|
16
|
+
overview DAU / WAU / MAU and growth accounting
|
|
17
|
+
daily-active Daily active users time series
|
|
18
|
+
rolling-active Rolling active users (28 points)
|
|
19
|
+
cohort-retention Weekly cohort retention matrix
|
|
20
|
+
top-users Most active users
|
|
21
|
+
user-search Search users by email or ULID
|
|
22
|
+
user-detail Detailed activity for a single user
|
|
23
|
+
user-snapshot Latest context snapshot for a user
|
|
24
|
+
events Paginated event feed
|
|
25
|
+
events-grouped Events grouped by a dimension
|
|
26
|
+
integrations Integration usage metrics
|
|
27
|
+
workflows Top workflows by runs
|
|
28
|
+
prompts Top prompts by executions
|
|
29
|
+
|
|
9
30
|
Examples:
|
|
10
31
|
$ primitive analytics overview
|
|
11
|
-
$ primitive analytics
|
|
12
|
-
$ primitive analytics
|
|
32
|
+
$ primitive analytics daily-active --window-days 14
|
|
33
|
+
$ primitive analytics top-users --limit 20
|
|
34
|
+
$ primitive analytics user-search --query user@example.com
|
|
35
|
+
$ primitive analytics user-detail <user-ulid>
|
|
36
|
+
$ primitive analytics events --window-days 7 --page 0
|
|
37
|
+
$ primitive analytics events-grouped --group-by feature
|
|
38
|
+
$ primitive analytics cohort-retention
|
|
39
|
+
$ primitive analytics workflows --limit 5
|
|
40
|
+
$ primitive analytics prompts --window-days 14
|
|
13
41
|
`);
|
|
14
|
-
// Overview
|
|
42
|
+
// ── Overview (DAU / WAU / MAU / Growth) ──────────────────────────
|
|
15
43
|
analytics
|
|
16
44
|
.command("overview")
|
|
17
|
-
.description("
|
|
45
|
+
.description("Show DAU, WAU, MAU, and growth accounting")
|
|
18
46
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
19
47
|
.option("--app <app-id>", "App ID")
|
|
20
|
-
.option("--window-days <n>", "
|
|
48
|
+
.option("--window-days <n>", "Growth window in days", "28")
|
|
21
49
|
.option("--json", "Output as JSON")
|
|
22
50
|
.action(async (appId, options) => {
|
|
23
51
|
const resolvedAppId = resolveAppId(appId, options);
|
|
24
52
|
const client = new ApiClient();
|
|
25
53
|
try {
|
|
26
|
-
const
|
|
54
|
+
const [dau, wau, mau, growth] = await Promise.all([
|
|
55
|
+
client.getAnalyticsOverviewDau(resolvedAppId),
|
|
56
|
+
client.getAnalyticsOverviewWau(resolvedAppId),
|
|
57
|
+
client.getAnalyticsOverviewMau(resolvedAppId),
|
|
58
|
+
client.getAnalyticsOverviewGrowth(resolvedAppId, {
|
|
59
|
+
windowDays: parseInt(options.windowDays),
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
if (options.json) {
|
|
63
|
+
json({ dau, wau, mau, growth });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
heading("Active Users");
|
|
67
|
+
console.log();
|
|
68
|
+
const formatDelta = (d) => d > 0 ? `+${(d * 100).toFixed(1)}%` : d === 0 ? "0%" : `${(d * 100).toFixed(1)}%`;
|
|
69
|
+
console.log(formatTable([
|
|
70
|
+
{ metric: "DAU", value: dau.value, previous: dau.previous, delta: formatDelta(dau.deltaPct) },
|
|
71
|
+
{ metric: "WAU", value: wau.value, previous: wau.previous, delta: formatDelta(wau.deltaPct) },
|
|
72
|
+
{ metric: "MAU", value: mau.value, previous: mau.previous, delta: formatDelta(mau.deltaPct) },
|
|
73
|
+
], [
|
|
74
|
+
{ header: "METRIC", key: "metric" },
|
|
75
|
+
{ header: "CURRENT", key: "value", align: "right", format: formatNumber },
|
|
76
|
+
{ header: "PREVIOUS", key: "previous", align: "right", format: formatNumber },
|
|
77
|
+
{ header: "CHANGE", key: "delta", align: "right" },
|
|
78
|
+
]));
|
|
79
|
+
console.log();
|
|
80
|
+
heading(`Growth Accounting (${growth.window_days} days)`);
|
|
81
|
+
console.log();
|
|
82
|
+
keyValue("Current active", String(growth.current_active));
|
|
83
|
+
keyValue("Previous active", String(growth.previous_active));
|
|
84
|
+
keyValue("New users", String(growth.new_users));
|
|
85
|
+
keyValue("Retained", String(growth.retained_users));
|
|
86
|
+
keyValue("Reactivated", String(growth.reactivated_users));
|
|
87
|
+
keyValue("Churned", String(growth.churned_users));
|
|
88
|
+
keyValue("Delta", formatDelta(growth.deltaPct));
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
error(err.message);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// ── Daily Active ─────────────────────────────────────────────────
|
|
96
|
+
analytics
|
|
97
|
+
.command("daily-active")
|
|
98
|
+
.description("Daily active users time series")
|
|
99
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
100
|
+
.option("--app <app-id>", "App ID")
|
|
101
|
+
.option("--window-days <n>", "Time window in days (7-90)", "28")
|
|
102
|
+
.option("--json", "Output as JSON")
|
|
103
|
+
.action(async (appId, options) => {
|
|
104
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
105
|
+
const client = new ApiClient();
|
|
106
|
+
try {
|
|
107
|
+
const result = await client.getAnalyticsDailyActive(resolvedAppId, {
|
|
27
108
|
windowDays: parseInt(options.windowDays),
|
|
28
109
|
});
|
|
29
110
|
if (options.json) {
|
|
30
111
|
json(result);
|
|
31
112
|
return;
|
|
32
113
|
}
|
|
33
|
-
heading(`
|
|
114
|
+
heading(`Daily Active Users (${result.window_days} days)`);
|
|
34
115
|
console.log();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
116
|
+
if (!result.rows || result.rows.length === 0) {
|
|
117
|
+
info("No data found.");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log(formatTable(result.rows, [
|
|
121
|
+
{ header: "DATE", key: "day_label" },
|
|
122
|
+
{ header: "ACTIVE USERS", key: "active_users", align: "right", format: formatNumber },
|
|
123
|
+
]));
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
error(err.message);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// ── Rolling Active ───────────────────────────────────────────────
|
|
131
|
+
analytics
|
|
132
|
+
.command("rolling-active")
|
|
133
|
+
.description("Rolling active users (28 data points)")
|
|
134
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
135
|
+
.option("--app <app-id>", "App ID")
|
|
136
|
+
.option("--window-days <n>", "Rolling window size in days (1-28)", "7")
|
|
137
|
+
.option("--json", "Output as JSON")
|
|
138
|
+
.action(async (appId, options) => {
|
|
139
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
140
|
+
const client = new ApiClient();
|
|
141
|
+
try {
|
|
142
|
+
const result = await client.getAnalyticsRollingActive(resolvedAppId, {
|
|
143
|
+
windowDays: parseInt(options.windowDays),
|
|
144
|
+
});
|
|
145
|
+
if (options.json) {
|
|
146
|
+
json(result);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
heading(`Rolling Active Users (${result.window_days}-day window)`);
|
|
150
|
+
console.log();
|
|
151
|
+
if (!result.rows || result.rows.length === 0) {
|
|
152
|
+
info("No data found.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log(formatTable(result.rows, [
|
|
156
|
+
{ header: "DATE", key: "day_label" },
|
|
157
|
+
{ header: "ACTIVE USERS", key: "active_users", align: "right", format: formatNumber },
|
|
158
|
+
]));
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
error(err.message);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// ── Cohort Retention ─────────────────────────────────────────────
|
|
166
|
+
analytics
|
|
167
|
+
.command("cohort-retention")
|
|
168
|
+
.description("Weekly cohort retention matrix")
|
|
169
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
170
|
+
.option("--app <app-id>", "App ID")
|
|
171
|
+
.option("--json", "Output as JSON")
|
|
172
|
+
.action(async (appId, options) => {
|
|
173
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
174
|
+
const client = new ApiClient();
|
|
175
|
+
try {
|
|
176
|
+
const result = await client.getAnalyticsCohortRetention(resolvedAppId);
|
|
177
|
+
if (options.json) {
|
|
178
|
+
json(result);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
heading("Cohort Retention");
|
|
182
|
+
console.log();
|
|
183
|
+
if (!result.rows || result.rows.length === 0) {
|
|
184
|
+
info("No cohort data found.");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Build week headers
|
|
188
|
+
const weekHeaders = result.weeks.map((w) => `W${w}`);
|
|
189
|
+
// Build table rows with retention percentages
|
|
190
|
+
const tableRows = result.rows.map((row) => {
|
|
191
|
+
const entry = {
|
|
192
|
+
cohort: row.signup_week_label,
|
|
193
|
+
size: row.cohort_size,
|
|
194
|
+
};
|
|
195
|
+
row.retention.forEach((r, i) => {
|
|
196
|
+
entry[`w${result.weeks[i]}`] =
|
|
197
|
+
r !== null ? `${r.toFixed(0)}%` : "-";
|
|
198
|
+
});
|
|
199
|
+
return entry;
|
|
200
|
+
});
|
|
201
|
+
const columns = [
|
|
202
|
+
{ header: "COHORT", key: "cohort" },
|
|
203
|
+
{ header: "SIZE", key: "size", align: "right", format: formatNumber },
|
|
204
|
+
...weekHeaders.map((h, i) => ({
|
|
205
|
+
header: h,
|
|
206
|
+
key: `w${result.weeks[i]}`,
|
|
207
|
+
align: "right",
|
|
208
|
+
})),
|
|
209
|
+
];
|
|
210
|
+
console.log(formatTable(tableRows, columns));
|
|
211
|
+
// Print averages row
|
|
212
|
+
if (result.averages) {
|
|
213
|
+
const avgStr = result.averages
|
|
214
|
+
.map((a) => a !== null ? `${a.toFixed(0)}%` : "-")
|
|
215
|
+
.join(" ");
|
|
216
|
+
console.log();
|
|
217
|
+
dim(`Averages: ${avgStr}`);
|
|
47
218
|
}
|
|
48
219
|
}
|
|
49
220
|
catch (err) {
|
|
@@ -51,7 +222,7 @@ Examples:
|
|
|
51
222
|
process.exit(1);
|
|
52
223
|
}
|
|
53
224
|
});
|
|
54
|
-
// Top
|
|
225
|
+
// ── Top Users ────────────────────────────────────────────────────
|
|
55
226
|
analytics
|
|
56
227
|
.command("top-users")
|
|
57
228
|
.description("Get top users by activity")
|
|
@@ -80,6 +251,7 @@ Examples:
|
|
|
80
251
|
}
|
|
81
252
|
console.log(formatTable(result.results, [
|
|
82
253
|
{ header: "USER", key: "userUlid", format: formatId },
|
|
254
|
+
{ header: "EMAIL", key: "email" },
|
|
83
255
|
{ header: "EVENTS", key: "eventCount", align: "right" },
|
|
84
256
|
{ header: "FIRST SEEN", key: "firstSeen", format: formatDate },
|
|
85
257
|
{ header: "LAST SEEN", key: "lastSeen", format: formatDate },
|
|
@@ -90,53 +262,93 @@ Examples:
|
|
|
90
262
|
process.exit(1);
|
|
91
263
|
}
|
|
92
264
|
});
|
|
93
|
-
// User
|
|
265
|
+
// ── User Search ──────────────────────────────────────────────────
|
|
94
266
|
analytics
|
|
95
|
-
.command("user")
|
|
96
|
-
.description("
|
|
267
|
+
.command("user-search")
|
|
268
|
+
.description("Search users by email or ULID")
|
|
97
269
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
98
|
-
.argument("<user-ulid>", "User ULID")
|
|
99
270
|
.option("--app <app-id>", "App ID")
|
|
100
|
-
.
|
|
271
|
+
.requiredOption("--query <q>", "Email or ULID to search for")
|
|
272
|
+
.option("--limit <n>", "Max results", "25")
|
|
101
273
|
.option("--json", "Output as JSON")
|
|
102
|
-
.action(async (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
274
|
+
.action(async (appId, options) => {
|
|
275
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
276
|
+
const client = new ApiClient();
|
|
277
|
+
try {
|
|
278
|
+
const result = await client.getAnalyticsUserSearch(resolvedAppId, {
|
|
279
|
+
q: options.query,
|
|
280
|
+
limit: parseInt(options.limit),
|
|
281
|
+
});
|
|
282
|
+
if (options.json) {
|
|
283
|
+
json(result);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
heading(`User Search: "${result.query}"`);
|
|
287
|
+
console.log();
|
|
288
|
+
if (!result.results || result.results.length === 0) {
|
|
289
|
+
info("No users found.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
console.log(formatTable(result.results, [
|
|
293
|
+
{ header: "USER", key: "userUlid", format: formatId },
|
|
294
|
+
{ header: "EMAIL", key: "email" },
|
|
295
|
+
{ header: "NAME", key: "name" },
|
|
296
|
+
{ header: "EVENTS", key: "eventCount", align: "right" },
|
|
297
|
+
{ header: "LAST SEEN", key: "lastSeen", format: formatDate },
|
|
298
|
+
]));
|
|
108
299
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
300
|
+
catch (err) {
|
|
301
|
+
error(err.message);
|
|
302
|
+
process.exit(1);
|
|
112
303
|
}
|
|
304
|
+
});
|
|
305
|
+
// ── User Detail ──────────────────────────────────────────────────
|
|
306
|
+
analytics
|
|
307
|
+
.command("user-detail")
|
|
308
|
+
.description("Detailed activity for a single user")
|
|
309
|
+
.argument("<user-ulid>", "User ULID")
|
|
310
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
311
|
+
.option("--app <app-id>", "App ID")
|
|
312
|
+
.option("--json", "Output as JSON")
|
|
313
|
+
.action(async (userUlid, appId, options) => {
|
|
314
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
113
315
|
const client = new ApiClient();
|
|
114
316
|
try {
|
|
115
|
-
const result = await client.
|
|
116
|
-
windowDays: parseInt(options.windowDays),
|
|
117
|
-
});
|
|
317
|
+
const result = await client.getAnalyticsUserDetail(resolvedAppId, userUlid);
|
|
118
318
|
if (options.json) {
|
|
119
319
|
json(result);
|
|
120
320
|
return;
|
|
121
321
|
}
|
|
122
|
-
heading(
|
|
123
|
-
keyValue("User", userUlid);
|
|
322
|
+
heading("User Detail");
|
|
124
323
|
console.log();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
324
|
+
keyValue("ULID", result.user.user_ulid);
|
|
325
|
+
if (result.user.email)
|
|
326
|
+
keyValue("Email", result.user.email);
|
|
327
|
+
if (result.user.name)
|
|
328
|
+
keyValue("Name", result.user.name);
|
|
329
|
+
console.log();
|
|
330
|
+
keyValue("First seen", result.stats.first_seen ? formatDate(result.stats.first_seen) : "-");
|
|
331
|
+
keyValue("Last active", result.stats.last_active ? formatDate(result.stats.last_active) : "-");
|
|
332
|
+
keyValue("Total events", String(result.stats.total_events));
|
|
333
|
+
keyValue("Days active", String(result.stats.days_active));
|
|
334
|
+
if (result.events_by_action?.length > 0) {
|
|
335
|
+
console.log();
|
|
336
|
+
heading("Events by Action");
|
|
337
|
+
console.log();
|
|
338
|
+
console.log(formatTable(result.events_by_action, [
|
|
128
339
|
{ header: "ACTION", key: "action" },
|
|
129
|
-
{ header: "
|
|
130
|
-
{ header: "
|
|
131
|
-
{ header: "EVENTS", key: "events", align: "right" },
|
|
340
|
+
{ header: "COUNT", key: "event_count", align: "right", format: formatNumber },
|
|
341
|
+
{ header: "LAST OCCURRED", key: "last_occurred", format: formatDate },
|
|
132
342
|
]));
|
|
133
343
|
}
|
|
134
|
-
if (result.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
console.log(
|
|
138
|
-
|
|
139
|
-
{ header: "
|
|
344
|
+
if (result.events_by_feature?.length > 0) {
|
|
345
|
+
console.log();
|
|
346
|
+
heading("Events by Feature");
|
|
347
|
+
console.log();
|
|
348
|
+
console.log(formatTable(result.events_by_feature, [
|
|
349
|
+
{ header: "FEATURE", key: "feature" },
|
|
350
|
+
{ header: "COUNT", key: "event_count", align: "right", format: formatNumber },
|
|
351
|
+
{ header: "PCT", key: "pct", align: "right", format: formatPctRaw },
|
|
140
352
|
]));
|
|
141
353
|
}
|
|
142
354
|
}
|
|
@@ -145,7 +357,123 @@ Examples:
|
|
|
145
357
|
process.exit(1);
|
|
146
358
|
}
|
|
147
359
|
});
|
|
148
|
-
//
|
|
360
|
+
// ── User Snapshot ────────────────────────────────────────────────
|
|
361
|
+
analytics
|
|
362
|
+
.command("user-snapshot")
|
|
363
|
+
.description("Latest context snapshot for a user")
|
|
364
|
+
.argument("<user-ulid>", "User ULID")
|
|
365
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
366
|
+
.option("--app <app-id>", "App ID")
|
|
367
|
+
.option("--json", "Output as JSON")
|
|
368
|
+
.action(async (userUlid, appId, options) => {
|
|
369
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
370
|
+
const client = new ApiClient();
|
|
371
|
+
try {
|
|
372
|
+
const result = await client.getAnalyticsUserSnapshot(resolvedAppId, userUlid);
|
|
373
|
+
if (options.json) {
|
|
374
|
+
json(result);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
heading("User Snapshot");
|
|
378
|
+
console.log();
|
|
379
|
+
if (!result.snapshot) {
|
|
380
|
+
info("No snapshot found for this user.");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
keyValue("Timestamp", formatDate(result.snapshot.timestamp));
|
|
384
|
+
console.log();
|
|
385
|
+
console.log(JSON.stringify(result.snapshot.values, null, 2));
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
error(err.message);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
// ── Events Feed ──────────────────────────────────────────────────
|
|
393
|
+
analytics
|
|
394
|
+
.command("events")
|
|
395
|
+
.description("Paginated event feed")
|
|
396
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
397
|
+
.option("--app <app-id>", "App ID")
|
|
398
|
+
.option("--window-days <n>", "Time window in days", "7")
|
|
399
|
+
.option("--page <n>", "Page number (0-based)", "0")
|
|
400
|
+
.option("--json", "Output as JSON")
|
|
401
|
+
.action(async (appId, options) => {
|
|
402
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
403
|
+
const client = new ApiClient();
|
|
404
|
+
try {
|
|
405
|
+
const result = await client.getAnalyticsEvents(resolvedAppId, {
|
|
406
|
+
windowDays: parseInt(options.windowDays),
|
|
407
|
+
page: parseInt(options.page),
|
|
408
|
+
});
|
|
409
|
+
if (options.json) {
|
|
410
|
+
json(result);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
heading(`Events (page ${result.page}, ${result.total_rows} total)`);
|
|
414
|
+
console.log();
|
|
415
|
+
if (!result.rows || result.rows.length === 0) {
|
|
416
|
+
info("No events found.");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
console.log(formatTable(result.rows, [
|
|
420
|
+
{ header: "TIME", key: "timestamp", format: formatDate },
|
|
421
|
+
{ header: "USER", key: "user_ulid", format: formatId },
|
|
422
|
+
{ header: "ACTION", key: "action" },
|
|
423
|
+
{ header: "FEATURE", key: "feature" },
|
|
424
|
+
{ header: "ROUTE", key: "route" },
|
|
425
|
+
{ header: "COUNTRY", key: "country" },
|
|
426
|
+
]));
|
|
427
|
+
const totalPages = Math.ceil(result.total_rows / result.page_size);
|
|
428
|
+
if (result.page + 1 < totalPages) {
|
|
429
|
+
console.log();
|
|
430
|
+
dim(`Page ${result.page + 1} of ${totalPages}. Use --page ${result.page + 1} for the next page.`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
error(err.message);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
// ── Events Grouped ───────────────────────────────────────────────
|
|
439
|
+
analytics
|
|
440
|
+
.command("events-grouped")
|
|
441
|
+
.description("Events grouped by a dimension")
|
|
442
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
443
|
+
.option("--app <app-id>", "App ID")
|
|
444
|
+
.option("--group-by <dim>", "Dimension: action, feature, route, country, deviceType, plan, day", "action")
|
|
445
|
+
.option("--window-days <n>", "Time window in days", "7")
|
|
446
|
+
.option("--json", "Output as JSON")
|
|
447
|
+
.action(async (appId, options) => {
|
|
448
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
449
|
+
const client = new ApiClient();
|
|
450
|
+
try {
|
|
451
|
+
const result = await client.getAnalyticsEventsGrouped(resolvedAppId, {
|
|
452
|
+
groupBy: options.groupBy,
|
|
453
|
+
windowDays: parseInt(options.windowDays),
|
|
454
|
+
});
|
|
455
|
+
if (options.json) {
|
|
456
|
+
json(result);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
heading(`Events by ${result.group_by}`);
|
|
460
|
+
console.log();
|
|
461
|
+
if (!result.rows || result.rows.length === 0) {
|
|
462
|
+
info("No events found.");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
console.log(formatTable(result.rows, [
|
|
466
|
+
{ header: result.group_by.toUpperCase(), key: "group_value" },
|
|
467
|
+
{ header: "EVENTS", key: "events", align: "right", format: formatNumber },
|
|
468
|
+
{ header: "UNIQUE USERS", key: "unique_users", align: "right", format: formatNumber },
|
|
469
|
+
]));
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
error(err.message);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
// ── Integration Metrics ──────────────────────────────────────────
|
|
149
477
|
analytics
|
|
150
478
|
.command("integrations")
|
|
151
479
|
.description("Get integration usage metrics")
|
|
@@ -174,8 +502,89 @@ Examples:
|
|
|
174
502
|
{ header: "INTEGRATION", key: "integrationKey" },
|
|
175
503
|
{ header: "CALLS", key: "invocations", align: "right", format: formatNumber },
|
|
176
504
|
{ header: "ERROR RATE", key: "errorRate", align: "right", format: formatPercent },
|
|
177
|
-
{ header: "
|
|
178
|
-
{ header: "P95
|
|
505
|
+
{ header: "MEDIAN", key: "medianDurationMs", align: "right", format: formatDuration },
|
|
506
|
+
{ header: "P95", key: "p95DurationMs", align: "right", format: formatDuration },
|
|
507
|
+
]));
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
error(err.message);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
// ── Top Workflows ────────────────────────────────────────────────
|
|
515
|
+
analytics
|
|
516
|
+
.command("workflows")
|
|
517
|
+
.description("Top workflows by runs")
|
|
518
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
519
|
+
.option("--app <app-id>", "App ID")
|
|
520
|
+
.option("--window-days <n>", "Time window in days", "30")
|
|
521
|
+
.option("--limit <n>", "Number of workflows to show", "10")
|
|
522
|
+
.option("--json", "Output as JSON")
|
|
523
|
+
.action(async (appId, options) => {
|
|
524
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
525
|
+
const client = new ApiClient();
|
|
526
|
+
try {
|
|
527
|
+
const result = await client.getAnalyticsTopWorkflows(resolvedAppId, {
|
|
528
|
+
windowDays: parseInt(options.windowDays),
|
|
529
|
+
limit: parseInt(options.limit),
|
|
530
|
+
});
|
|
531
|
+
if (options.json) {
|
|
532
|
+
json(result);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
heading(`Top Workflows (${result.windowDays} days)`);
|
|
536
|
+
console.log();
|
|
537
|
+
if (!result.workflows || result.workflows.length === 0) {
|
|
538
|
+
info("No workflow data found.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
console.log(formatTable(result.workflows, [
|
|
542
|
+
{ header: "WORKFLOW", key: "workflowKey" },
|
|
543
|
+
{ header: "RUNS", key: "runs", align: "right", format: formatNumber },
|
|
544
|
+
{ header: "SUCCESS", key: "successRate", align: "right", format: formatPercent },
|
|
545
|
+
{ header: "MEDIAN", key: "medianDurationMs", align: "right", format: formatDuration },
|
|
546
|
+
{ header: "P95", key: "p95", align: "right", format: formatDuration },
|
|
547
|
+
{ header: "TOKENS", key: "totalTokens", align: "right", format: formatNumber },
|
|
548
|
+
]));
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
error(err.message);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
// ── Top Prompts ──────────────────────────────────────────────────
|
|
556
|
+
analytics
|
|
557
|
+
.command("prompts")
|
|
558
|
+
.description("Top prompts by executions")
|
|
559
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
560
|
+
.option("--app <app-id>", "App ID")
|
|
561
|
+
.option("--window-days <n>", "Time window in days", "30")
|
|
562
|
+
.option("--limit <n>", "Number of prompts to show", "10")
|
|
563
|
+
.option("--json", "Output as JSON")
|
|
564
|
+
.action(async (appId, options) => {
|
|
565
|
+
const resolvedAppId = resolveAppId(appId, options);
|
|
566
|
+
const client = new ApiClient();
|
|
567
|
+
try {
|
|
568
|
+
const result = await client.getAnalyticsTopPrompts(resolvedAppId, {
|
|
569
|
+
windowDays: parseInt(options.windowDays),
|
|
570
|
+
limit: parseInt(options.limit),
|
|
571
|
+
});
|
|
572
|
+
if (options.json) {
|
|
573
|
+
json(result);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
heading(`Top Prompts (${result.windowDays} days)`);
|
|
577
|
+
console.log();
|
|
578
|
+
if (!result.prompts || result.prompts.length === 0) {
|
|
579
|
+
info("No prompt data found.");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
console.log(formatTable(result.prompts, [
|
|
583
|
+
{ header: "PROMPT", key: "promptKey" },
|
|
584
|
+
{ header: "RUNS", key: "executions", align: "right", format: formatNumber },
|
|
585
|
+
{ header: "MEDIAN", key: "medianDurationMs", align: "right", format: formatDuration },
|
|
586
|
+
{ header: "P95", key: "p95DurationMs", align: "right", format: formatDuration },
|
|
587
|
+
{ header: "TOKENS", key: "totalTokens", align: "right", format: formatNumber },
|
|
179
588
|
]));
|
|
180
589
|
}
|
|
181
590
|
catch (err) {
|