@agent-analytics/cli 0.1.9 → 0.1.11
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/bin/cli.mjs +343 -286
- package/lib/api.mjs +37 -25
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
* npx @agent-analytics/cli init <name> — Alias for create
|
|
19
19
|
* npx @agent-analytics/cli delete <id> — Delete a project
|
|
20
20
|
* npx @agent-analytics/cli revoke-key — Revoke and regenerate API key
|
|
21
|
+
* npx @agent-analytics/cli experiments list <project> — List experiments
|
|
22
|
+
* npx @agent-analytics/cli experiments create <p> ... — Create experiment
|
|
23
|
+
* npx @agent-analytics/cli experiments get <id> — Get experiment with results
|
|
24
|
+
* npx @agent-analytics/cli experiments pause <id> — Pause experiment
|
|
25
|
+
* npx @agent-analytics/cli experiments resume <id> — Resume experiment
|
|
26
|
+
* npx @agent-analytics/cli experiments complete <id> — Complete experiment
|
|
27
|
+
* npx @agent-analytics/cli experiments delete <id> — Delete experiment
|
|
21
28
|
* npx @agent-analytics/cli delete-account — Delete your account (opens dashboard)
|
|
22
29
|
* npx @agent-analytics/cli whoami — Show current account
|
|
23
30
|
*/
|
|
@@ -47,6 +54,22 @@ function requireKey() {
|
|
|
47
54
|
return new AgentAnalyticsAPI(key, getBaseUrl());
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
function withApi(fn) {
|
|
58
|
+
return async (...args) => {
|
|
59
|
+
const api = requireKey();
|
|
60
|
+
try {
|
|
61
|
+
return await fn(api, ...args);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
error(err.message);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ifEmpty(arr, label) {
|
|
69
|
+
if (!arr || arr.length === 0) { log(` No ${label} found.`); return true; }
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
// ==================== COMMANDS ====================
|
|
51
74
|
|
|
52
75
|
async function cmdLogin(token) {
|
|
@@ -82,343 +105,257 @@ async function cmdLogin(token) {
|
|
|
82
105
|
}
|
|
83
106
|
}
|
|
84
107
|
|
|
85
|
-
async
|
|
108
|
+
const cmdCreate = withApi(async (api, name, domain) => {
|
|
86
109
|
if (!name) error('Usage: npx @agent-analytics/cli create <project-name> --domain https://mysite.com');
|
|
87
110
|
if (!domain) error('Usage: npx @agent-analytics/cli create <project-name> --domain https://mysite.com\n\nThe domain is required so we can restrict tracking to your site.');
|
|
88
111
|
|
|
89
|
-
const api = requireKey();
|
|
90
|
-
|
|
91
112
|
heading(`Creating project: ${name}`);
|
|
92
113
|
|
|
93
|
-
|
|
94
|
-
const data = await api.createProject(name, domain);
|
|
95
|
-
|
|
96
|
-
success(data.existing
|
|
97
|
-
? `Found existing project for ${BOLD}${domain}${RESET}!\n`
|
|
98
|
-
: `Project created for ${BOLD}${domain}${RESET}!\n`);
|
|
114
|
+
const data = await api.createProject(name, domain);
|
|
99
115
|
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
success(data.existing
|
|
117
|
+
? `Found existing project for ${BOLD}${domain}${RESET}!\n`
|
|
118
|
+
: `Project created for ${BOLD}${domain}${RESET}!\n`);
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
heading('1. Add this snippet to your site:');
|
|
121
|
+
log(`${CYAN}${data.snippet}${RESET}\n`);
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
heading('2. Your agent queries stats with:');
|
|
124
|
+
log(`${CYAN}${data.api_example}${RESET}\n`);
|
|
108
125
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
126
|
+
heading('Project token (for the snippet):');
|
|
127
|
+
log(`${YELLOW}${data.project_token}${RESET}\n`);
|
|
128
|
+
});
|
|
113
129
|
|
|
114
|
-
|
|
115
|
-
const
|
|
130
|
+
const cmdProjects = withApi(async (api) => {
|
|
131
|
+
const data = await api.listProjects();
|
|
132
|
+
const projects = data.projects;
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
if (!projects || projects.length === 0) {
|
|
135
|
+
log('No projects yet. Create one:');
|
|
136
|
+
log(` ${CYAN}npx @agent-analytics/cli create my-site${RESET}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
120
139
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
log(` ${CYAN}npx @agent-analytics/cli create my-site${RESET}`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
140
|
+
heading(`Your Projects (${projects.length})`);
|
|
141
|
+
log('');
|
|
126
142
|
|
|
127
|
-
|
|
143
|
+
for (const p of projects) {
|
|
144
|
+
const created = new Date(p.created_at).toLocaleDateString();
|
|
145
|
+
log(` ${BOLD}${p.name}${RESET} ${DIM}created ${created}${RESET}`);
|
|
146
|
+
log(` ${DIM}token:${RESET} ${p.project_token}`);
|
|
147
|
+
log(` ${DIM}origins:${RESET} ${p.allowed_origins || '*'}`);
|
|
128
148
|
log('');
|
|
129
|
-
|
|
130
|
-
for (const p of projects) {
|
|
131
|
-
const created = new Date(p.created_at).toLocaleDateString();
|
|
132
|
-
log(` ${BOLD}${p.name}${RESET} ${DIM}created ${created}${RESET}`);
|
|
133
|
-
log(` ${DIM}token:${RESET} ${p.project_token}`);
|
|
134
|
-
log(` ${DIM}origins:${RESET} ${p.allowed_origins || '*'}`);
|
|
135
|
-
log('');
|
|
136
|
-
}
|
|
137
|
-
} catch (err) {
|
|
138
|
-
error(`Failed to list projects: ${err.message}`);
|
|
139
149
|
}
|
|
140
|
-
}
|
|
150
|
+
});
|
|
141
151
|
|
|
142
|
-
async
|
|
152
|
+
const cmdStats = withApi(async (api, project, days = 7) => {
|
|
143
153
|
if (!project) error('Usage: npx @agent-analytics/cli stats <project-name> [--days N]');
|
|
144
154
|
|
|
145
|
-
const
|
|
155
|
+
const result = await api.getStats(project, days, { returnHeaders: true });
|
|
156
|
+
const data = result.data;
|
|
157
|
+
const headers = result.headers;
|
|
146
158
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const data = result.data;
|
|
150
|
-
const headers = result.headers;
|
|
151
|
-
|
|
152
|
-
heading(`Stats: ${project} (last ${days} days)`);
|
|
153
|
-
log('');
|
|
154
|
-
|
|
155
|
-
if (data.totals) {
|
|
156
|
-
log(` ${BOLD}Total events:${RESET} ${data.totals.total_events || 0}`);
|
|
157
|
-
log(` ${BOLD}Unique users:${RESET} ${data.totals.unique_users || 0}`);
|
|
158
|
-
}
|
|
159
|
+
heading(`Stats: ${project} (last ${days} days)`);
|
|
160
|
+
log('');
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
log(` ${e.event} ${DIM}→${RESET} ${BOLD}${e.count}${RESET} ${DIM}(${e.unique_users} users)${RESET}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
162
|
+
if (data.totals) {
|
|
163
|
+
log(` ${BOLD}Total events:${RESET} ${data.totals.total_events || 0}`);
|
|
164
|
+
log(` ${BOLD}Unique users:${RESET} ${data.totals.unique_users || 0}`);
|
|
165
|
+
}
|
|
167
166
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
log(` ${d.date} ${GREEN}${bar}${RESET} ${d.total_events} events`);
|
|
174
|
-
}
|
|
167
|
+
if (data.events && data.events.length > 0) {
|
|
168
|
+
log('');
|
|
169
|
+
heading('Events:');
|
|
170
|
+
for (const e of data.events) {
|
|
171
|
+
log(` ${e.event} ${DIM}→${RESET} ${BOLD}${e.count}${RESET} ${DIM}(${e.unique_users} users)${RESET}`);
|
|
175
172
|
}
|
|
173
|
+
}
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
const pct = headers['x-monthly-usage-percent'];
|
|
184
|
-
log('');
|
|
185
|
-
if (monthlyLimit && pct) {
|
|
186
|
-
const capDollars = (parseInt(monthlyLimit, 10) / 1000) * 2;
|
|
187
|
-
log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)}) — ${pct}% of $${capDollars.toFixed(2)} cap`);
|
|
188
|
-
} else {
|
|
189
|
-
log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)})`);
|
|
190
|
-
}
|
|
175
|
+
if (data.daily && data.daily.length > 0) {
|
|
176
|
+
log('');
|
|
177
|
+
heading('Daily:');
|
|
178
|
+
for (const d of data.daily) {
|
|
179
|
+
const bar = '█'.repeat(Math.min(Math.ceil(d.total_events / 5), 40));
|
|
180
|
+
log(` ${d.date} ${GREEN}${bar}${RESET} ${d.total_events} events`);
|
|
191
181
|
}
|
|
182
|
+
}
|
|
192
183
|
|
|
184
|
+
// Monthly usage summary from response headers
|
|
185
|
+
const monthlyUsage = headers['x-monthly-usage'];
|
|
186
|
+
if (monthlyUsage) {
|
|
187
|
+
const events = parseInt(monthlyUsage, 10);
|
|
188
|
+
const bill = (events / 1000) * 2;
|
|
189
|
+
const monthlyLimit = headers['x-monthly-limit'];
|
|
190
|
+
const pct = headers['x-monthly-usage-percent'];
|
|
193
191
|
log('');
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
if (monthlyLimit && pct) {
|
|
193
|
+
const capDollars = (parseInt(monthlyLimit, 10) / 1000) * 2;
|
|
194
|
+
log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)}) — ${pct}% of $${capDollars.toFixed(2)} cap`);
|
|
195
|
+
} else {
|
|
196
|
+
log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)})`);
|
|
197
|
+
}
|
|
196
198
|
}
|
|
197
|
-
}
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
200
|
+
log('');
|
|
201
|
+
});
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
const cmdEvents = withApi(async (api, project, opts = {}) => {
|
|
204
|
+
if (!project) error('Usage: npx @agent-analytics/cli events <project-name> [--days N] [--limit N]');
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
const data = await api.getEvents(project, opts);
|
|
206
|
+
const data = await api.getEvents(project, opts);
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
heading(`Events: ${project}`);
|
|
209
|
+
log('');
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
log(' No events yet.');
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
211
|
+
if (ifEmpty(data.events, 'events')) return;
|
|
214
212
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
213
|
+
for (const e of data.events) {
|
|
214
|
+
const time = new Date(e.timestamp).toLocaleString();
|
|
215
|
+
log(` ${DIM}${time}${RESET} ${BOLD}${e.event}${RESET} ${DIM}${e.user_id || ''}${RESET}`);
|
|
216
|
+
if (e.properties) {
|
|
217
|
+
log(` ${DIM}${JSON.stringify(e.properties)}${RESET}`);
|
|
221
218
|
}
|
|
222
|
-
log('');
|
|
223
|
-
} catch (err) {
|
|
224
|
-
error(`Failed to get events: ${err.message}`);
|
|
225
219
|
}
|
|
226
|
-
|
|
220
|
+
log('');
|
|
221
|
+
});
|
|
227
222
|
|
|
228
|
-
async
|
|
223
|
+
const cmdPropertiesReceived = withApi(async (api, project, opts = {}) => {
|
|
229
224
|
if (!project) error('Usage: npx @agent-analytics/cli properties-received <project-name> [--since DATE] [--sample N]');
|
|
230
225
|
|
|
231
|
-
const
|
|
226
|
+
const data = await api.getPropertiesReceived(project, opts);
|
|
232
227
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
heading(`Received Properties: ${project}`);
|
|
237
|
-
log('');
|
|
228
|
+
heading(`Received Properties: ${project}`);
|
|
229
|
+
log('');
|
|
238
230
|
|
|
239
|
-
|
|
240
|
-
log(' No properties found.');
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
231
|
+
if (ifEmpty(data.properties, 'properties')) return;
|
|
243
232
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
233
|
+
// Group by event for display
|
|
234
|
+
const byEvent = {};
|
|
235
|
+
for (const p of data.properties) {
|
|
236
|
+
if (!byEvent[p.event]) byEvent[p.event] = [];
|
|
237
|
+
byEvent[p.event].push(p.key);
|
|
238
|
+
}
|
|
250
239
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
240
|
+
for (const [event, keys] of Object.entries(byEvent)) {
|
|
241
|
+
log(` ${BOLD}${event}${RESET}`);
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
log(` ${CYAN}${key}${RESET}`);
|
|
256
244
|
}
|
|
257
|
-
|
|
258
|
-
log(`\n${DIM}Sampled from last ${data.sample_size} events${RESET}`);
|
|
259
|
-
log('');
|
|
260
|
-
} catch (err) {
|
|
261
|
-
error(`Failed to get properties: ${err.message}`);
|
|
262
245
|
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function cmdInsights(project, period = '7d') {
|
|
266
|
-
if (!project) error('Usage: npx @agent-analytics/cli insights <project-name> [--period 7d]');
|
|
267
246
|
|
|
268
|
-
|
|
247
|
+
log(`\n${DIM}Sampled from last ${data.sample_size} events${RESET}`);
|
|
248
|
+
log('');
|
|
249
|
+
});
|
|
269
250
|
|
|
270
|
-
|
|
271
|
-
|
|
251
|
+
const cmdInsights = withApi(async (api, project, period = '7d') => {
|
|
252
|
+
if (!project) error('Usage: npx @agent-analytics/cli insights <project-name> [--period 7d]');
|
|
272
253
|
|
|
273
|
-
|
|
274
|
-
log('');
|
|
254
|
+
const data = await api.getInsights(project, { period });
|
|
275
255
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const label = key.replace(/_/g, ' ');
|
|
279
|
-
const arrow = metric.change > 0 ? `${GREEN}↑` : metric.change < 0 ? `${RED}↓` : `${DIM}—`;
|
|
280
|
-
const pct = metric.change_pct !== null ? ` (${metric.change_pct > 0 ? '+' : ''}${metric.change_pct}%)` : '';
|
|
281
|
-
log(` ${BOLD}${label}:${RESET} ${metric.current} ${arrow}${pct}${RESET} ${DIM}was ${metric.previous}${RESET}`);
|
|
282
|
-
}
|
|
256
|
+
heading(`Insights: ${project} (${period} vs previous)`);
|
|
257
|
+
log('');
|
|
283
258
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
259
|
+
const m = data.metrics;
|
|
260
|
+
for (const [key, metric] of Object.entries(m)) {
|
|
261
|
+
const label = key.replace(/_/g, ' ');
|
|
262
|
+
const arrow = metric.change > 0 ? `${GREEN}↑` : metric.change < 0 ? `${RED}↓` : `${DIM}—`;
|
|
263
|
+
const pct = metric.change_pct !== null ? ` (${metric.change_pct > 0 ? '+' : ''}${metric.change_pct}%)` : '';
|
|
264
|
+
log(` ${BOLD}${label}:${RESET} ${metric.current} ${arrow}${pct}${RESET} ${DIM}was ${metric.previous}${RESET}`);
|
|
289
265
|
}
|
|
290
|
-
}
|
|
291
266
|
|
|
292
|
-
|
|
293
|
-
|
|
267
|
+
log('');
|
|
268
|
+
log(` ${BOLD}Trend:${RESET} ${data.trend}`);
|
|
269
|
+
log('');
|
|
270
|
+
});
|
|
294
271
|
|
|
295
|
-
|
|
272
|
+
const cmdBreakdown = withApi(async (api, project, property, opts = {}) => {
|
|
273
|
+
if (!project || !property) error('Usage: npx @agent-analytics/cli breakdown <project-name> --property <key> [--event page_view] [--limit 20]');
|
|
296
274
|
|
|
297
|
-
|
|
298
|
-
const data = await api.getBreakdown(project, { property, ...opts });
|
|
275
|
+
const data = await api.getBreakdown(project, { property, ...opts });
|
|
299
276
|
|
|
300
|
-
|
|
301
|
-
|
|
277
|
+
heading(`Breakdown: ${project} — ${property}${data.event ? ` (${data.event})` : ''}`);
|
|
278
|
+
log('');
|
|
302
279
|
|
|
303
|
-
|
|
304
|
-
log(' No data found.');
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
280
|
+
if (ifEmpty(data.values, 'data')) return;
|
|
307
281
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
log(`\n${DIM}${data.total_with_property} of ${data.total_events} events have this property${RESET}`);
|
|
312
|
-
log('');
|
|
313
|
-
} catch (err) {
|
|
314
|
-
error(`Failed to get breakdown: ${err.message}`);
|
|
282
|
+
for (const v of data.values) {
|
|
283
|
+
log(` ${BOLD}${v.value}${RESET} ${v.count} events ${DIM}(${v.unique_users} users)${RESET}`);
|
|
315
284
|
}
|
|
316
|
-
}
|
|
285
|
+
log(`\n${DIM}${data.total_with_property} of ${data.total_events} events have this property${RESET}`);
|
|
286
|
+
log('');
|
|
287
|
+
});
|
|
317
288
|
|
|
318
|
-
async
|
|
289
|
+
const cmdPages = withApi(async (api, project, type = 'entry', opts = {}) => {
|
|
319
290
|
if (!project) error('Usage: npx @agent-analytics/cli pages <project-name> [--type entry|exit|both] [--limit 20]');
|
|
320
291
|
|
|
321
|
-
const
|
|
292
|
+
const data = await api.getPages(project, { type, ...opts });
|
|
322
293
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
heading(`Pages: ${project} (${type})`);
|
|
327
|
-
log('');
|
|
294
|
+
heading(`Pages: ${project} (${type})`);
|
|
295
|
+
log('');
|
|
328
296
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
log(' No page data found.');
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
297
|
+
const pages = data.entry_pages || data.exit_pages || [];
|
|
298
|
+
if (ifEmpty(pages, 'page data')) return;
|
|
334
299
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
300
|
+
for (const p of pages) {
|
|
301
|
+
const bounceStr = `${Math.round(p.bounce_rate * 100)}% bounce`;
|
|
302
|
+
const durStr = `${Math.round(p.avg_duration / 1000)}s avg`;
|
|
303
|
+
log(` ${BOLD}${p.page}${RESET} ${p.sessions} sessions ${DIM}${bounceStr} ${durStr} ${p.avg_events} events/session${RESET}`);
|
|
304
|
+
}
|
|
340
305
|
|
|
341
|
-
|
|
342
|
-
log('');
|
|
343
|
-
heading('Exit pages:');
|
|
344
|
-
for (const p of data.exit_pages) {
|
|
345
|
-
log(` ${BOLD}${p.page}${RESET} ${p.sessions} sessions`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
306
|
+
if (data.exit_pages && data.entry_pages) {
|
|
348
307
|
log('');
|
|
349
|
-
|
|
350
|
-
|
|
308
|
+
heading('Exit pages:');
|
|
309
|
+
for (const p of data.exit_pages) {
|
|
310
|
+
log(` ${BOLD}${p.page}${RESET} ${p.sessions} sessions`);
|
|
311
|
+
}
|
|
351
312
|
}
|
|
352
|
-
|
|
313
|
+
log('');
|
|
314
|
+
});
|
|
353
315
|
|
|
354
|
-
async
|
|
316
|
+
const cmdSessionsDist = withApi(async (api, project) => {
|
|
355
317
|
if (!project) error('Usage: npx @agent-analytics/cli sessions-dist <project-name>');
|
|
356
318
|
|
|
357
|
-
const
|
|
319
|
+
const data = await api.getSessionDistribution(project);
|
|
358
320
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
heading(`Session Distribution: ${project}`);
|
|
363
|
-
log('');
|
|
321
|
+
heading(`Session Distribution: ${project}`);
|
|
322
|
+
log('');
|
|
364
323
|
|
|
365
|
-
|
|
366
|
-
log(' No session data found.');
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
324
|
+
if (ifEmpty(data.distribution, 'session data')) return;
|
|
369
325
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
log('');
|
|
376
|
-
log(` ${BOLD}Median:${RESET} ${data.median_bucket} ${BOLD}Engaged:${RESET} ${data.engaged_pct}% (sessions ≥30s)`);
|
|
377
|
-
log('');
|
|
378
|
-
} catch (err) {
|
|
379
|
-
error(`Failed to get session distribution: ${err.message}`);
|
|
326
|
+
for (const b of data.distribution) {
|
|
327
|
+
const bar = '█'.repeat(Math.min(Math.ceil(b.pct / 2), 40));
|
|
328
|
+
log(` ${b.bucket.padEnd(7)} ${GREEN}${bar}${RESET} ${b.sessions} (${b.pct}%)`);
|
|
380
329
|
}
|
|
381
|
-
}
|
|
382
330
|
|
|
383
|
-
|
|
384
|
-
|
|
331
|
+
log('');
|
|
332
|
+
log(` ${BOLD}Median:${RESET} ${data.median_bucket} ${BOLD}Engaged:${RESET} ${data.engaged_pct}% (sessions ≥30s)`);
|
|
333
|
+
log('');
|
|
334
|
+
});
|
|
385
335
|
|
|
386
|
-
|
|
336
|
+
const cmdHeatmap = withApi(async (api, project) => {
|
|
337
|
+
if (!project) error('Usage: npx @agent-analytics/cli heatmap <project-name>');
|
|
387
338
|
|
|
388
|
-
|
|
389
|
-
const data = await api.getHeatmap(project);
|
|
339
|
+
const data = await api.getHeatmap(project);
|
|
390
340
|
|
|
391
|
-
|
|
392
|
-
|
|
341
|
+
heading(`Heatmap: ${project}`);
|
|
342
|
+
log('');
|
|
393
343
|
|
|
394
|
-
|
|
395
|
-
log(' No heatmap data found.');
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
344
|
+
if (ifEmpty(data.heatmap, 'heatmap data')) return;
|
|
398
345
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
log(` ${BOLD}Busiest day:${RESET} ${data.busiest_day}`);
|
|
403
|
-
log(` ${BOLD}Busiest hour:${RESET} ${data.busiest_hour}:00`);
|
|
404
|
-
log('');
|
|
405
|
-
} catch (err) {
|
|
406
|
-
error(`Failed to get heatmap: ${err.message}`);
|
|
346
|
+
if (data.peak) {
|
|
347
|
+
log(` ${BOLD}Peak:${RESET} ${data.peak.day_name} at ${data.peak.hour}:00 (${data.peak.events} events, ${data.peak.users} users)`);
|
|
407
348
|
}
|
|
408
|
-
}
|
|
349
|
+
log(` ${BOLD}Busiest day:${RESET} ${data.busiest_day}`);
|
|
350
|
+
log(` ${BOLD}Busiest hour:${RESET} ${data.busiest_hour}:00`);
|
|
351
|
+
log('');
|
|
352
|
+
});
|
|
409
353
|
|
|
410
|
-
async
|
|
354
|
+
const cmdDelete = withApi(async (api, id) => {
|
|
411
355
|
if (!id) error('Usage: npx @agent-analytics/cli delete <project-id>');
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
await api.deleteProject(id);
|
|
417
|
-
success(`Project ${id} deleted`);
|
|
418
|
-
} catch (err) {
|
|
419
|
-
error(`Failed to delete project: ${err.message}`);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
356
|
+
await api.deleteProject(id);
|
|
357
|
+
success(`Project ${id} deleted`);
|
|
358
|
+
});
|
|
422
359
|
|
|
423
360
|
function cmdDeleteAccount() {
|
|
424
361
|
heading('Delete Account');
|
|
@@ -428,42 +365,146 @@ function cmdDeleteAccount() {
|
|
|
428
365
|
log('');
|
|
429
366
|
}
|
|
430
367
|
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
368
|
+
const cmdRevokeKey = withApi(async (api) => {
|
|
369
|
+
const data = await api.revokeKey();
|
|
370
|
+
setApiKey(data.api_key);
|
|
371
|
+
|
|
372
|
+
warn('Old API key revoked');
|
|
373
|
+
success('New API key generated and saved\n');
|
|
374
|
+
heading('New API key:');
|
|
375
|
+
log(`${YELLOW}${data.api_key}${RESET}`);
|
|
376
|
+
log(`${DIM}Saved to ~/.config/agent-analytics/config.json${RESET}\n`);
|
|
377
|
+
warn('Update your agent with this new key!');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const cmdWhoami = withApi(async (api) => {
|
|
381
|
+
const data = await api.getAccount();
|
|
382
|
+
heading('Account');
|
|
383
|
+
log(` ${BOLD}Email:${RESET} ${data.email}`);
|
|
384
|
+
log(` ${BOLD}GitHub:${RESET} ${data.github_login || 'N/A'}`);
|
|
385
|
+
log(` ${BOLD}Tier:${RESET} ${data.tier}`);
|
|
386
|
+
log(` ${BOLD}Projects:${RESET} ${data.projects_count}/${data.projects_limit}`);
|
|
387
|
+
if (data.tier === 'pro' && data.monthly_spend_cap_dollars != null) {
|
|
388
|
+
log(` ${BOLD}Spend cap:${RESET} $${data.monthly_spend_cap_dollars.toFixed(2)}/month`);
|
|
446
389
|
}
|
|
447
|
-
|
|
390
|
+
log('');
|
|
391
|
+
});
|
|
448
392
|
|
|
449
|
-
|
|
450
|
-
const api = requireKey();
|
|
393
|
+
// ==================== EXPERIMENTS ====================
|
|
451
394
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
395
|
+
const cmdExperiments = withApi(async (api, sub, ...rest) => {
|
|
396
|
+
if (!sub) error('Usage: npx @agent-analytics/cli experiments <list|create|get|pause|resume|complete|delete> ...');
|
|
397
|
+
|
|
398
|
+
switch (sub) {
|
|
399
|
+
case 'list': {
|
|
400
|
+
const project = rest[0];
|
|
401
|
+
if (!project) error('Usage: npx @agent-analytics/cli experiments list <project>');
|
|
402
|
+
const data = await api.listExperiments(project);
|
|
403
|
+
heading(`Experiments: ${project}`);
|
|
404
|
+
log('');
|
|
405
|
+
if (ifEmpty(data.experiments, 'experiments')) return;
|
|
406
|
+
for (const e of data.experiments) {
|
|
407
|
+
const status = e.status === 'active' ? `${GREEN}active${RESET}` : e.status === 'paused' ? `${YELLOW}paused${RESET}` : `${DIM}completed${RESET}`;
|
|
408
|
+
log(` ${BOLD}${e.name}${RESET} ${DIM}${e.id}${RESET} ${status} ${DIM}goal: ${e.goal_event}${RESET}`);
|
|
409
|
+
const variants = e.variants.map(v => `${v.key}(${v.weight}%)`).join(', ');
|
|
410
|
+
log(` variants: ${variants}`);
|
|
411
|
+
if (e.winner) log(` ${GREEN}winner: ${e.winner}${RESET}`);
|
|
412
|
+
}
|
|
413
|
+
log('');
|
|
414
|
+
break;
|
|
461
415
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
416
|
+
case 'create': {
|
|
417
|
+
const project = rest[0];
|
|
418
|
+
if (!project) error('Usage: npx @agent-analytics/cli experiments create <project> --name <name> --variants control,new_cta --goal <event> [--weights 60,40]');
|
|
419
|
+
const name = getArg('--name');
|
|
420
|
+
const variantsStr = getArg('--variants');
|
|
421
|
+
const goal = getArg('--goal');
|
|
422
|
+
if (!name || !variantsStr || !goal) error('Required: --name, --variants, --goal');
|
|
423
|
+
const variants = variantsStr.split(',').map(v => v.trim());
|
|
424
|
+
const weightsStr = getArg('--weights');
|
|
425
|
+
const weights = weightsStr ? weightsStr.split(',').map(w => parseInt(w.trim(), 10)) : undefined;
|
|
426
|
+
const data = await api.createExperiment(project, { name, variants, goal_event: goal, weights });
|
|
427
|
+
success(`Experiment created: ${BOLD}${data.name}${RESET} (${data.id})`);
|
|
428
|
+
log(` ${DIM}variants:${RESET} ${data.variants.map(v => `${v.key}(${v.weight}%)`).join(', ')}`);
|
|
429
|
+
log(` ${DIM}goal:${RESET} ${data.goal_event}`);
|
|
430
|
+
log('');
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case 'get': {
|
|
434
|
+
const id = rest[0];
|
|
435
|
+
if (!id) error('Usage: npx @agent-analytics/cli experiments get <id>');
|
|
436
|
+
const data = await api.getExperiment(id);
|
|
437
|
+
const status = data.status === 'active' ? `${GREEN}active${RESET}` : data.status === 'paused' ? `${YELLOW}paused${RESET}` : `${DIM}completed${RESET}`;
|
|
438
|
+
heading(`Experiment: ${data.name}`);
|
|
439
|
+
log(` ${DIM}id:${RESET} ${data.id} status: ${status} ${DIM}goal: ${data.goal_event}${RESET}`);
|
|
440
|
+
if (data.winner) log(` ${GREEN}winner: ${data.winner}${RESET}`);
|
|
441
|
+
log('');
|
|
442
|
+
if (data.results) {
|
|
443
|
+
heading('Results:');
|
|
444
|
+
for (const v of data.results.variants) {
|
|
445
|
+
const rate = (v.conversion_rate * 100).toFixed(1);
|
|
446
|
+
log(` ${BOLD}${v.key}${RESET} ${v.unique_users} users ${v.conversions} conversions ${CYAN}${rate}%${RESET}`);
|
|
447
|
+
}
|
|
448
|
+
log('');
|
|
449
|
+
if (data.results.probability_best) {
|
|
450
|
+
heading('Probability best:');
|
|
451
|
+
for (const [k, v] of Object.entries(data.results.probability_best)) {
|
|
452
|
+
const pct = (v * 100).toFixed(1);
|
|
453
|
+
log(` ${BOLD}${k}:${RESET} ${pct}%`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (data.results.lift) {
|
|
457
|
+
heading('Lift:');
|
|
458
|
+
for (const [k, v] of Object.entries(data.results.lift)) {
|
|
459
|
+
const pct = (v * 100).toFixed(1);
|
|
460
|
+
const arrow = v > 0 ? `${GREEN}+${pct}%` : v < 0 ? `${RED}${pct}%` : `${DIM}0%`;
|
|
461
|
+
log(` ${BOLD}${k}:${RESET} ${arrow}${RESET}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
log('');
|
|
465
|
+
log(` ${BOLD}Sufficient data:${RESET} ${data.results.sufficient_data ? `${GREEN}yes` : `${YELLOW}no`}${RESET}`);
|
|
466
|
+
if (data.results.recommendation) {
|
|
467
|
+
log(` ${BOLD}Recommendation:${RESET} ${data.results.recommendation}`);
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
log(` ${DIM}No results available yet (need exposure + conversion events)${RESET}`);
|
|
471
|
+
}
|
|
472
|
+
log('');
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
case 'pause': {
|
|
476
|
+
const id = rest[0];
|
|
477
|
+
if (!id) error('Usage: npx @agent-analytics/cli experiments pause <id>');
|
|
478
|
+
await api.updateExperiment(id, { status: 'paused' });
|
|
479
|
+
success(`Experiment ${id} paused`);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 'resume': {
|
|
483
|
+
const id = rest[0];
|
|
484
|
+
if (!id) error('Usage: npx @agent-analytics/cli experiments resume <id>');
|
|
485
|
+
await api.updateExperiment(id, { status: 'active' });
|
|
486
|
+
success(`Experiment ${id} resumed`);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case 'complete': {
|
|
490
|
+
const id = rest[0];
|
|
491
|
+
if (!id) error('Usage: npx @agent-analytics/cli experiments complete <id> [--winner <variant>]');
|
|
492
|
+
const winner = getArg('--winner');
|
|
493
|
+
await api.updateExperiment(id, { status: 'completed', winner });
|
|
494
|
+
success(`Experiment ${id} completed${winner ? ` — winner: ${winner}` : ''}`);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case 'delete': {
|
|
498
|
+
const id = rest[0];
|
|
499
|
+
if (!id) error('Usage: npx @agent-analytics/cli experiments delete <id>');
|
|
500
|
+
await api.deleteExperiment(id);
|
|
501
|
+
success(`Experiment ${id} deleted`);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
default:
|
|
505
|
+
error(`Unknown experiments subcommand: ${sub}. Use: list, create, get, pause, resume, complete, delete`);
|
|
465
506
|
}
|
|
466
|
-
}
|
|
507
|
+
});
|
|
467
508
|
|
|
468
509
|
function showHelp() {
|
|
469
510
|
log(`
|
|
@@ -486,6 +527,14 @@ ${BOLD}COMMANDS${RESET}
|
|
|
486
527
|
${CYAN}pages${RESET} <name> Entry/exit page performance
|
|
487
528
|
${CYAN}sessions-dist${RESET} <name> Session duration distribution
|
|
488
529
|
${CYAN}heatmap${RESET} <name> Peak hours & busiest days
|
|
530
|
+
${CYAN}experiments${RESET} <sub> A/B testing (pro only)
|
|
531
|
+
${DIM}list <project>${RESET} List experiments
|
|
532
|
+
${DIM}create <project>${RESET} Create experiment
|
|
533
|
+
${DIM}get <id>${RESET} Get experiment with results
|
|
534
|
+
${DIM}pause <id>${RESET} Pause experiment
|
|
535
|
+
${DIM}resume <id>${RESET} Resume experiment
|
|
536
|
+
${DIM}complete <id>${RESET} Complete experiment
|
|
537
|
+
${DIM}delete <id>${RESET} Delete experiment
|
|
489
538
|
${CYAN}whoami${RESET} Show current account
|
|
490
539
|
${CYAN}revoke-key${RESET} Revoke and regenerate API key
|
|
491
540
|
${CYAN}delete-account${RESET} Delete your account (opens dashboard)
|
|
@@ -500,6 +549,11 @@ ${BOLD}OPTIONS${RESET}
|
|
|
500
549
|
--property <key> Property key for breakdown (required)
|
|
501
550
|
--event <name> Filter by event name (breakdown only)
|
|
502
551
|
--type <T> Page type: entry, exit, both (default: entry)
|
|
552
|
+
--name <name> Experiment name (experiments create)
|
|
553
|
+
--variants <a,b> Comma-separated variant keys (experiments create)
|
|
554
|
+
--goal <event> Goal event name (experiments create)
|
|
555
|
+
--weights <50,50> Comma-separated weights (experiments create, optional)
|
|
556
|
+
--winner <variant> Winning variant (experiments complete, optional)
|
|
503
557
|
|
|
504
558
|
${BOLD}ENVIRONMENT${RESET}
|
|
505
559
|
AGENT_ANALYTICS_API_KEY API key (overrides config file)
|
|
@@ -581,6 +635,9 @@ try {
|
|
|
581
635
|
case 'heatmap':
|
|
582
636
|
await cmdHeatmap(args[1]);
|
|
583
637
|
break;
|
|
638
|
+
case 'experiments':
|
|
639
|
+
await cmdExperiments(args[1], args[2]);
|
|
640
|
+
break;
|
|
584
641
|
case 'delete':
|
|
585
642
|
await cmdDelete(args[1]);
|
|
586
643
|
break;
|
package/lib/api.mjs
CHANGED
|
@@ -11,6 +11,13 @@ export class AgentAnalyticsAPI {
|
|
|
11
11
|
this.baseUrl = baseUrl;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
_qs(params) {
|
|
15
|
+
return Object.entries(params)
|
|
16
|
+
.filter(([, v]) => v != null)
|
|
17
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
18
|
+
.join('&');
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
async request(method, path, body, { returnHeaders = false } = {}) {
|
|
15
22
|
const opts = {
|
|
16
23
|
method,
|
|
@@ -68,55 +75,60 @@ export class AgentAnalyticsAPI {
|
|
|
68
75
|
|
|
69
76
|
// Stats
|
|
70
77
|
async getStats(project, days = 7, { returnHeaders = false } = {}) {
|
|
71
|
-
return this.request('GET', `/stats
|
|
78
|
+
return this.request('GET', `/stats?${this._qs({ project, days })}`, undefined, { returnHeaders });
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
async getEvents(project, { event, days = 7, limit = 100 } = {}) {
|
|
75
|
-
|
|
76
|
-
if (event) qs += `&event=${encodeURIComponent(event)}`;
|
|
77
|
-
return this.request('GET', `/events?${qs}`);
|
|
82
|
+
return this.request('GET', `/events?${this._qs({ project, days, limit, event })}`);
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
async getProperties(project, days = 30) {
|
|
81
|
-
return this.request('GET', `/properties
|
|
86
|
+
return this.request('GET', `/properties?${this._qs({ project, days })}`);
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
async getPropertiesReceived(project, { since, sample } = {}) {
|
|
85
|
-
|
|
86
|
-
if (since) qs += `&since=${encodeURIComponent(since)}`;
|
|
87
|
-
if (sample) qs += `&sample=${sample}`;
|
|
88
|
-
return this.request('GET', `/properties/received?${qs}`);
|
|
90
|
+
return this.request('GET', `/properties/received?${this._qs({ project, since, sample })}`);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
// Analytics
|
|
92
94
|
async getBreakdown(project, { property, event, since, limit = 20 } = {}) {
|
|
93
|
-
|
|
94
|
-
if (event) qs += `&event=${encodeURIComponent(event)}`;
|
|
95
|
-
if (since) qs += `&since=${encodeURIComponent(since)}`;
|
|
96
|
-
if (limit) qs += `&limit=${limit}`;
|
|
97
|
-
return this.request('GET', `/breakdown?${qs}`);
|
|
95
|
+
return this.request('GET', `/breakdown?${this._qs({ project, property, event, since, limit })}`);
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
async getInsights(project, { period = '7d' } = {}) {
|
|
101
|
-
return this.request('GET', `/insights
|
|
99
|
+
return this.request('GET', `/insights?${this._qs({ project, period })}`);
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
async getPages(project, { type = 'entry', since, limit = 20 } = {}) {
|
|
105
|
-
|
|
106
|
-
if (since) qs += `&since=${encodeURIComponent(since)}`;
|
|
107
|
-
if (limit) qs += `&limit=${limit}`;
|
|
108
|
-
return this.request('GET', `/pages?${qs}`);
|
|
103
|
+
return this.request('GET', `/pages?${this._qs({ project, type, since, limit })}`);
|
|
109
104
|
}
|
|
110
105
|
|
|
111
106
|
async getSessionDistribution(project, { since } = {}) {
|
|
112
|
-
|
|
113
|
-
if (since) qs += `&since=${encodeURIComponent(since)}`;
|
|
114
|
-
return this.request('GET', `/sessions/distribution?${qs}`);
|
|
107
|
+
return this.request('GET', `/sessions/distribution?${this._qs({ project, since })}`);
|
|
115
108
|
}
|
|
116
109
|
|
|
117
110
|
async getHeatmap(project, { since } = {}) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
return this.request('GET', `/heatmap?${this._qs({ project, since })}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Experiments
|
|
115
|
+
async createExperiment(project, { name, variants, goal_event, weights }) {
|
|
116
|
+
return this.request('POST', '/experiments', { project, name, variants, goal_event, weights });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async listExperiments(project) {
|
|
120
|
+
return this.request('GET', `/experiments?${this._qs({ project })}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getExperiment(id) {
|
|
124
|
+
return this.request('GET', `/experiments/${id}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async updateExperiment(id, { status, winner }) {
|
|
128
|
+
return this.request('PATCH', `/experiments/${id}`, { status, winner });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async deleteExperiment(id) {
|
|
132
|
+
return this.request('DELETE', `/experiments/${id}`);
|
|
121
133
|
}
|
|
122
134
|
}
|