@supalytics/cli 0.4.1 → 0.4.3
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/package.json +1 -1
- package/src/api.ts +259 -36
- package/src/commands/annotations.ts +26 -8
- package/src/commands/completions.ts +20 -9
- package/src/commands/countries.ts +2 -2
- package/src/commands/events.ts +14 -4
- package/src/commands/funnels.ts +485 -0
- package/src/commands/pages.ts +2 -2
- package/src/commands/query.ts +2 -2
- package/src/commands/referrers.ts +2 -2
- package/src/commands/sites.ts +231 -227
- package/src/commands/stats.ts +4 -4
- package/src/commands/trend.ts +2 -2
- package/src/index.ts +47 -79
- package/src/ui.ts +18 -2
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { Command } from "commander"
|
|
3
|
+
import {
|
|
4
|
+
listFunnels,
|
|
5
|
+
getFunnel,
|
|
6
|
+
createFunnel,
|
|
7
|
+
updateFunnel,
|
|
8
|
+
analyzeFunnel,
|
|
9
|
+
formatNumber,
|
|
10
|
+
formatPercent,
|
|
11
|
+
type FunnelStepInput,
|
|
12
|
+
type FunnelAnalysisRow,
|
|
13
|
+
} from "../api"
|
|
14
|
+
import { getDefaultSite } from "../config"
|
|
15
|
+
import { parsePeriod, sparkBar } from "../ui"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Merge subcommand options with parent command options.
|
|
19
|
+
* Commander doesn't propagate options like -s/--site and --json
|
|
20
|
+
* from parent to subcommands, so we check both.
|
|
21
|
+
*/
|
|
22
|
+
function mergedOpts(cmd: Command): Record<string, unknown> {
|
|
23
|
+
const parentOpts = cmd.parent?.opts() || {}
|
|
24
|
+
const ownOpts = cmd.opts()
|
|
25
|
+
// Own opts take priority, but fall back to parent for unset values
|
|
26
|
+
return { ...parentOpts, ...ownOpts }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function resolveSiteOption(cmd: Command): Promise<string | undefined> {
|
|
30
|
+
const opts = mergedOpts(cmd)
|
|
31
|
+
return (opts.site as string) || (await getDefaultSite())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const funnelsExamples = `
|
|
35
|
+
Examples:
|
|
36
|
+
# List all funnels
|
|
37
|
+
supalytics funnels
|
|
38
|
+
|
|
39
|
+
# View a funnel with conversion analysis
|
|
40
|
+
supalytics funnels view <id>
|
|
41
|
+
|
|
42
|
+
# View with custom date range
|
|
43
|
+
supalytics funnels view <id> -p 90d
|
|
44
|
+
|
|
45
|
+
# Create a funnel
|
|
46
|
+
supalytics funnels create "Signup Flow" --step "page:/pricing" --step "event:signup_clicked" --step "purchase"
|
|
47
|
+
|
|
48
|
+
# Create with page matching
|
|
49
|
+
supalytics funnels create "Blog to Signup" --step "page:starts_with:/blog" --step "page:/signup" --step "event:account_created"
|
|
50
|
+
|
|
51
|
+
# Create an ordered funnel
|
|
52
|
+
supalytics funnels create "Checkout" --mode ordered --step "page:/cart" --step "page:/checkout" --step "purchase"
|
|
53
|
+
|
|
54
|
+
# Update funnel name
|
|
55
|
+
supalytics funnels update <id> --name "New Name"
|
|
56
|
+
|
|
57
|
+
# Update funnel steps
|
|
58
|
+
supalytics funnels update <id> --step "page:/pricing" --step "purchase"`
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a step definition string into a FunnelStepInput.
|
|
62
|
+
*
|
|
63
|
+
* Formats:
|
|
64
|
+
* page:/pricing → page, exact, /pricing
|
|
65
|
+
* page:starts_with:/blog → page, starts_with, /blog
|
|
66
|
+
* page:contains:pricing → page, contains, pricing
|
|
67
|
+
* page:regex:^/blog/.* → page, regex, ^/blog/.*
|
|
68
|
+
* event:signup_clicked → event, match_value=signup_clicked
|
|
69
|
+
* event:signup:plan:pro → event, match_value=signup, property_key=plan, property_value=pro
|
|
70
|
+
* purchase → purchase type
|
|
71
|
+
* trial → trial type
|
|
72
|
+
*/
|
|
73
|
+
function parseStep(definition: string): FunnelStepInput {
|
|
74
|
+
const parts = definition.split(":")
|
|
75
|
+
|
|
76
|
+
// Simple types: "purchase" or "trial"
|
|
77
|
+
if (parts.length === 1) {
|
|
78
|
+
const type = parts[0].toLowerCase()
|
|
79
|
+
if (type === "purchase" || type === "trial") {
|
|
80
|
+
return { name: type === "purchase" ? "Purchase" : "Trial start", type }
|
|
81
|
+
}
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Invalid step: "${definition}". Use "page:<path>", "event:<name>", "purchase", or "trial"`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const type = parts[0].toLowerCase()
|
|
88
|
+
|
|
89
|
+
if (type === "page") {
|
|
90
|
+
// Check for explicit match type: page:starts_with:/blog
|
|
91
|
+
const matchTypes = ["exact", "starts_with", "contains", "regex"]
|
|
92
|
+
if (parts.length >= 3 && matchTypes.includes(parts[1])) {
|
|
93
|
+
const matchType = parts[1]
|
|
94
|
+
const matchValue = parts.slice(2).join(":")
|
|
95
|
+
return {
|
|
96
|
+
name: `Visit ${matchValue}`,
|
|
97
|
+
type: "page",
|
|
98
|
+
match_type: matchType,
|
|
99
|
+
match_value: matchValue,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Default: page:/pricing → exact match
|
|
103
|
+
const matchValue = parts.slice(1).join(":")
|
|
104
|
+
return {
|
|
105
|
+
name: `Visit ${matchValue}`,
|
|
106
|
+
type: "page",
|
|
107
|
+
match_type: "exact",
|
|
108
|
+
match_value: matchValue,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (type === "event") {
|
|
113
|
+
const eventName = parts[1]
|
|
114
|
+
// event:signup:plan:pro → event with property filter
|
|
115
|
+
if (parts.length >= 4) {
|
|
116
|
+
return {
|
|
117
|
+
name: eventName,
|
|
118
|
+
type: "event",
|
|
119
|
+
match_value: eventName,
|
|
120
|
+
property_key: parts[2],
|
|
121
|
+
property_value: parts.slice(3).join(":"),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
name: eventName,
|
|
126
|
+
type: "event",
|
|
127
|
+
match_value: eventName,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Invalid step type "${type}". Use "page", "event", "purchase", or "trial"`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get a human-readable label for a step
|
|
138
|
+
*/
|
|
139
|
+
function stepLabel(step: { type: string; match_type?: string | null; match_value?: string | null; name?: string }): string {
|
|
140
|
+
if (step.type === "purchase") return "Purchase"
|
|
141
|
+
if (step.type === "trial") return "Trial start"
|
|
142
|
+
if (step.type === "page") {
|
|
143
|
+
const prefix = step.match_type && step.match_type !== "exact" ? `${step.match_type}: ` : ""
|
|
144
|
+
return `${prefix}${step.match_value || step.name || "page"}`
|
|
145
|
+
}
|
|
146
|
+
if (step.type === "event") {
|
|
147
|
+
return step.match_value || step.name || "event"
|
|
148
|
+
}
|
|
149
|
+
return step.name || step.type
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Render funnel analysis results
|
|
154
|
+
*/
|
|
155
|
+
function renderAnalysis(rows: FunnelAnalysisRow[], funnel: { name: string; description?: string | null; mode: string; steps: Array<{ name: string; type: string; match_type?: string | null; match_value?: string | null }> }) {
|
|
156
|
+
console.log()
|
|
157
|
+
console.log(chalk.bold(funnel.name))
|
|
158
|
+
if (funnel.description) {
|
|
159
|
+
console.log(chalk.dim(` ${funnel.description}`))
|
|
160
|
+
}
|
|
161
|
+
console.log(chalk.dim(` Mode: ${funnel.mode}`))
|
|
162
|
+
console.log()
|
|
163
|
+
|
|
164
|
+
if (rows.length === 0) {
|
|
165
|
+
console.log(chalk.dim(" No data for the selected period"))
|
|
166
|
+
console.log()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const maxVisitors = Math.max(...rows.map((r) => r.visitors))
|
|
171
|
+
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
// ClickHouse may return "ps.<col>" instead of "<col>" for joined columns
|
|
174
|
+
const rawRow = row as Record<string, unknown>
|
|
175
|
+
const stepOrder = row.step_order ?? (rawRow["ps.step_order"] as number)
|
|
176
|
+
const stepType = row.step_type ?? (rawRow["ps.step_type"] as string)
|
|
177
|
+
const matchType = row.match_type ?? (rawRow["ps.match_type"] as string | null)
|
|
178
|
+
const matchValue = row.match_value ?? (rawRow["ps.match_value"] as string | null)
|
|
179
|
+
const stepNum = `Step ${stepOrder}:`
|
|
180
|
+
const label = stepLabel({
|
|
181
|
+
type: stepType,
|
|
182
|
+
match_type: matchType,
|
|
183
|
+
match_value: matchValue,
|
|
184
|
+
name: funnel.steps[stepOrder - 1]?.name,
|
|
185
|
+
})
|
|
186
|
+
const visitors = formatNumber(row.visitors)
|
|
187
|
+
const bar = sparkBar(row.visitors, maxVisitors, 16)
|
|
188
|
+
const pct = formatPercent(row.conversion_rate_from_start)
|
|
189
|
+
|
|
190
|
+
let dropoff = ""
|
|
191
|
+
if (stepOrder > 1) {
|
|
192
|
+
const drop = row.conversion_rate_from_prev - 100
|
|
193
|
+
dropoff = chalk.red(` (${drop >= 0 ? "+" : ""}${drop.toFixed(1)}%)`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(
|
|
197
|
+
` ${chalk.dim(stepNum.padEnd(8))} ${label.padEnd(30)} ${visitors.padStart(10)} visitors ${bar} ${pct.padStart(6)}${dropoff}`
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Overall conversion
|
|
202
|
+
if (rows.length >= 2) {
|
|
203
|
+
const first = rows[0]
|
|
204
|
+
const last = rows[rows.length - 1]
|
|
205
|
+
const overall = first.visitors > 0 ? (last.visitors / first.visitors) * 100 : 0
|
|
206
|
+
console.log()
|
|
207
|
+
console.log(
|
|
208
|
+
chalk.bold(` Overall conversion: ${formatPercent(overall)}`) +
|
|
209
|
+
chalk.dim(` (${formatNumber(first.visitors)} → ${formatNumber(last.visitors)})`)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
console.log()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const funnelsCommand = new Command("funnels")
|
|
216
|
+
.description("Manage and analyze conversion funnels")
|
|
217
|
+
.addHelpText("after", funnelsExamples)
|
|
218
|
+
.option("-s, --site <site>", "Site to query")
|
|
219
|
+
.option("--json", "Output as JSON")
|
|
220
|
+
.action(async (options) => {
|
|
221
|
+
const site = options.site || (await getDefaultSite())
|
|
222
|
+
|
|
223
|
+
if (!site) {
|
|
224
|
+
console.error(
|
|
225
|
+
chalk.red(
|
|
226
|
+
"Error: No site specified. Use --site or set a default with `supalytics login --site`"
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
process.exit(1)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const response = await listFunnels(site)
|
|
234
|
+
|
|
235
|
+
if (options.json) {
|
|
236
|
+
console.log(JSON.stringify(response, null, 2))
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log()
|
|
241
|
+
console.log(chalk.bold("Funnels"))
|
|
242
|
+
console.log()
|
|
243
|
+
|
|
244
|
+
if (response.data.length === 0) {
|
|
245
|
+
console.log(chalk.dim(" No funnels found"))
|
|
246
|
+
console.log()
|
|
247
|
+
console.log(
|
|
248
|
+
chalk.dim(
|
|
249
|
+
' Create one with: supalytics funnels create "Name" --step "page:/pricing" --step "purchase"'
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
console.log()
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const funnel of response.data) {
|
|
257
|
+
const stepCount = funnel.steps?.length || 0
|
|
258
|
+
const created = new Date(funnel.created_at).toISOString().split("T")[0]
|
|
259
|
+
console.log(
|
|
260
|
+
` ${chalk.cyan(funnel.name.padEnd(30))} ${String(stepCount).padStart(2)} steps ${funnel.mode.padEnd(10)} ${chalk.dim(created)}`
|
|
261
|
+
)
|
|
262
|
+
if (funnel.description) {
|
|
263
|
+
console.log(` ${chalk.dim(funnel.description)}`)
|
|
264
|
+
}
|
|
265
|
+
console.log(chalk.dim(` id: ${funnel.id}`))
|
|
266
|
+
console.log()
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`))
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Subcommand: view
|
|
275
|
+
funnelsCommand
|
|
276
|
+
.command("view <id>")
|
|
277
|
+
.description("View funnel details and conversion analysis")
|
|
278
|
+
.option("-s, --site <site>", "Site to query")
|
|
279
|
+
.option("-p, --period <period>", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
|
|
280
|
+
.option("--start <date>", "Start date (YYYY-MM-DD)")
|
|
281
|
+
.option("--end <date>", "End date (YYYY-MM-DD)")
|
|
282
|
+
.option("--json", "Output as JSON")
|
|
283
|
+
.action(async (id, _options, cmd) => {
|
|
284
|
+
const options = mergedOpts(cmd)
|
|
285
|
+
const site = await resolveSiteOption(cmd)
|
|
286
|
+
|
|
287
|
+
if (!site) {
|
|
288
|
+
console.error(
|
|
289
|
+
chalk.red(
|
|
290
|
+
"Error: No site specified. Use --site or set a default with `supalytics login --site`"
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
process.exit(1)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const dateRange =
|
|
298
|
+
options.start && options.end
|
|
299
|
+
? ([options.start, options.end] as [string, string])
|
|
300
|
+
: parsePeriod(options.period as string)
|
|
301
|
+
|
|
302
|
+
// Fetch funnel definition and run analysis in parallel
|
|
303
|
+
const [funnelResponse, analysisResponse] = await Promise.all([
|
|
304
|
+
getFunnel(site, id),
|
|
305
|
+
analyzeFunnel(site, id, dateRange),
|
|
306
|
+
])
|
|
307
|
+
|
|
308
|
+
if (options.json) {
|
|
309
|
+
console.log(
|
|
310
|
+
JSON.stringify(
|
|
311
|
+
{
|
|
312
|
+
funnel: funnelResponse.data,
|
|
313
|
+
analysis: analysisResponse.data,
|
|
314
|
+
meta: analysisResponse.meta,
|
|
315
|
+
},
|
|
316
|
+
null,
|
|
317
|
+
2
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const [startDate, endDate] = analysisResponse.meta.date_range
|
|
324
|
+
console.log(chalk.dim(` ${startDate} → ${endDate} (${analysisResponse.meta.query_ms}ms)`))
|
|
325
|
+
|
|
326
|
+
renderAnalysis(analysisResponse.data, funnelResponse.data)
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`))
|
|
329
|
+
process.exit(1)
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// Subcommand: create
|
|
334
|
+
funnelsCommand
|
|
335
|
+
.command("create <name>")
|
|
336
|
+
.description("Create a new funnel")
|
|
337
|
+
.option("-s, --site <site>", "Site to query")
|
|
338
|
+
.option("--step <definition>", "Step definition (repeatable)", (val: string, prev: string[]) => prev.concat(val), [] as string[])
|
|
339
|
+
.option("--mode <mode>", "Funnel mode: ordered or unordered", "unordered")
|
|
340
|
+
.option("-d, --description <text>", "Optional description")
|
|
341
|
+
.option("--json", "Output as JSON")
|
|
342
|
+
.action(async (name, _options, cmd) => {
|
|
343
|
+
const options = mergedOpts(cmd)
|
|
344
|
+
const site = await resolveSiteOption(cmd)
|
|
345
|
+
|
|
346
|
+
if (!site) {
|
|
347
|
+
console.error(
|
|
348
|
+
chalk.red(
|
|
349
|
+
"Error: No site specified. Use --site or set a default with `supalytics login --site`"
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
process.exit(1)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const steps = options.step as string[] | undefined
|
|
356
|
+
if (!steps || steps.length === 0) {
|
|
357
|
+
console.error(chalk.red("Error: At least one --step is required"))
|
|
358
|
+
console.error(
|
|
359
|
+
chalk.dim(' Example: --step "page:/pricing" --step "event:signup" --step "purchase"')
|
|
360
|
+
)
|
|
361
|
+
process.exit(1)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!["ordered", "unordered"].includes(options.mode as string)) {
|
|
365
|
+
console.error(chalk.red("Error: --mode must be 'ordered' or 'unordered'"))
|
|
366
|
+
process.exit(1)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const parsedSteps: FunnelStepInput[] = steps.map((s: string) => parseStep(s))
|
|
371
|
+
|
|
372
|
+
const response = await createFunnel(site, {
|
|
373
|
+
name,
|
|
374
|
+
description: options.description as string | undefined,
|
|
375
|
+
mode: options.mode as string | undefined,
|
|
376
|
+
steps: parsedSteps,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
if (options.json) {
|
|
380
|
+
console.log(JSON.stringify(response, null, 2))
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
console.log()
|
|
385
|
+
console.log(chalk.green("Funnel created"))
|
|
386
|
+
console.log()
|
|
387
|
+
console.log(` ${chalk.bold(response.data.name)}`)
|
|
388
|
+
if (response.data.description) {
|
|
389
|
+
console.log(` ${chalk.dim(response.data.description)}`)
|
|
390
|
+
}
|
|
391
|
+
console.log(chalk.dim(` Mode: ${response.data.mode}`))
|
|
392
|
+
console.log()
|
|
393
|
+
|
|
394
|
+
for (const step of response.data.steps) {
|
|
395
|
+
console.log(
|
|
396
|
+
` ${chalk.dim(`Step ${step.step_order}:`)} ${stepLabel(step)}`
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
console.log()
|
|
400
|
+
console.log(chalk.dim(` id: ${response.data.id}`))
|
|
401
|
+
console.log()
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`))
|
|
404
|
+
process.exit(1)
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// Subcommand: update
|
|
409
|
+
funnelsCommand
|
|
410
|
+
.command("update <id>")
|
|
411
|
+
.description("Update an existing funnel")
|
|
412
|
+
.option("-s, --site <site>", "Site to query")
|
|
413
|
+
.option("--name <name>", "New funnel name")
|
|
414
|
+
.option("-d, --description <text>", "New description")
|
|
415
|
+
.option("--mode <mode>", "Funnel mode: ordered or unordered")
|
|
416
|
+
.option("--step <definition>", "Replace steps (repeatable)", (val: string, prev: string[]) => prev.concat(val), [] as string[])
|
|
417
|
+
.option("--json", "Output as JSON")
|
|
418
|
+
.action(async (id, _options, cmd) => {
|
|
419
|
+
const options = mergedOpts(cmd)
|
|
420
|
+
const site = await resolveSiteOption(cmd)
|
|
421
|
+
|
|
422
|
+
if (!site) {
|
|
423
|
+
console.error(
|
|
424
|
+
chalk.red(
|
|
425
|
+
"Error: No site specified. Use --site or set a default with `supalytics login --site`"
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
process.exit(1)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (options.mode && !["ordered", "unordered"].includes(options.mode as string)) {
|
|
432
|
+
console.error(chalk.red("Error: --mode must be 'ordered' or 'unordered'"))
|
|
433
|
+
process.exit(1)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const updateData: {
|
|
437
|
+
name?: string
|
|
438
|
+
description?: string
|
|
439
|
+
mode?: string
|
|
440
|
+
steps?: FunnelStepInput[]
|
|
441
|
+
} = {}
|
|
442
|
+
|
|
443
|
+
if (options.name) updateData.name = options.name as string
|
|
444
|
+
if (options.description) updateData.description = options.description as string
|
|
445
|
+
if (options.mode) updateData.mode = options.mode as string
|
|
446
|
+
const updateSteps = options.step as string[] | undefined
|
|
447
|
+
if (updateSteps && updateSteps.length > 0) {
|
|
448
|
+
updateData.steps = updateSteps.map((s: string) => parseStep(s))
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (Object.keys(updateData).length === 0) {
|
|
452
|
+
console.error(chalk.red("Error: No updates specified"))
|
|
453
|
+
console.error(chalk.dim(" Use --name, --description, --mode, or --step"))
|
|
454
|
+
process.exit(1)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const response = await updateFunnel(site, id, updateData)
|
|
459
|
+
|
|
460
|
+
if (options.json) {
|
|
461
|
+
console.log(JSON.stringify(response, null, 2))
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log()
|
|
466
|
+
console.log(chalk.green("Funnel updated"))
|
|
467
|
+
console.log()
|
|
468
|
+
console.log(` ${chalk.bold(response.data.name)}`)
|
|
469
|
+
if (response.data.description) {
|
|
470
|
+
console.log(` ${chalk.dim(response.data.description)}`)
|
|
471
|
+
}
|
|
472
|
+
console.log(chalk.dim(` Mode: ${response.data.mode}`))
|
|
473
|
+
console.log()
|
|
474
|
+
|
|
475
|
+
for (const step of response.data.steps) {
|
|
476
|
+
console.log(
|
|
477
|
+
` ${chalk.dim(`Step ${step.step_order}:`)} ${stepLabel(step)}`
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
console.log()
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`))
|
|
483
|
+
process.exit(1)
|
|
484
|
+
}
|
|
485
|
+
})
|
package/src/commands/pages.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { query, formatNumber } from "../api";
|
|
4
4
|
import { getDefaultSite } from "../config";
|
|
5
|
-
import { sparkBar, truncate, formatRevenue } from "../ui";
|
|
5
|
+
import { sparkBar, truncate, formatRevenue, parsePeriod } from "../ui";
|
|
6
6
|
|
|
7
7
|
export const pagesCommand = new Command("pages")
|
|
8
8
|
.description("Top pages by visitors")
|
|
@@ -25,7 +25,7 @@ export const pagesCommand = new Command("pages")
|
|
|
25
25
|
|
|
26
26
|
const dateRange = options.start && options.end
|
|
27
27
|
? [options.start, options.end] as [string, string]
|
|
28
|
-
: options.period;
|
|
28
|
+
: parsePeriod(options.period);
|
|
29
29
|
|
|
30
30
|
const filters = options.filter
|
|
31
31
|
? options.filter.map((f: string) => {
|
package/src/commands/query.ts
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from "chalk"
|
|
|
2
2
|
import { Command } from "commander"
|
|
3
3
|
import { formatDuration, formatNumber, formatPercent, query } from "../api"
|
|
4
4
|
import { getDefaultSite } from "../config"
|
|
5
|
-
import { type TableColumn, table } from "../ui"
|
|
5
|
+
import { type TableColumn, table, parsePeriod } from "../ui"
|
|
6
6
|
|
|
7
7
|
const queryExamples = `
|
|
8
8
|
Examples:
|
|
@@ -75,7 +75,7 @@ export const queryCommand = new Command("query")
|
|
|
75
75
|
const dateRange =
|
|
76
76
|
options.start && options.end
|
|
77
77
|
? ([options.start, options.end] as [string, string])
|
|
78
|
-
: options.period
|
|
78
|
+
: parsePeriod(options.period)
|
|
79
79
|
|
|
80
80
|
// Parse metrics
|
|
81
81
|
const metrics = options.metrics.split(",").map((m: string) => m.trim())
|
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { query, formatNumber } from "../api";
|
|
4
4
|
import { getDefaultSite } from "../config";
|
|
5
|
-
import { sparkBar, truncate, formatRevenue } from "../ui";
|
|
5
|
+
import { sparkBar, truncate, formatRevenue, parsePeriod } from "../ui";
|
|
6
6
|
|
|
7
7
|
export const referrersCommand = new Command("referrers")
|
|
8
8
|
.description("Top referrers")
|
|
@@ -25,7 +25,7 @@ export const referrersCommand = new Command("referrers")
|
|
|
25
25
|
|
|
26
26
|
const dateRange = options.start && options.end
|
|
27
27
|
? [options.start, options.end] as [string, string]
|
|
28
|
-
: options.period;
|
|
28
|
+
: parsePeriod(options.period);
|
|
29
29
|
|
|
30
30
|
const filters = options.filter
|
|
31
31
|
? options.filter.map((f: string) => {
|