@supalytics/cli 0.3.8 → 0.4.1
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/commands/annotations.ts +128 -116
- package/src/commands/events.ts +184 -157
- package/src/commands/journeys.ts +442 -414
- package/src/commands/logout.ts +49 -13
- package/src/commands/query.ts +181 -158
- package/src/commands/update.ts +68 -52
package/src/commands/logout.ts
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { Command } from "commander"
|
|
3
|
+
import { clearAuth, getAuth } from "../config"
|
|
4
|
+
|
|
5
|
+
const WEB_BASE = process.env.SUPALYTICS_WEB_URL || "https://www.supalytics.co"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Revoke the PAT on the server
|
|
9
|
+
* Returns true if successful, false if failed (but we still proceed with local cleanup)
|
|
10
|
+
*/
|
|
11
|
+
async function revokeToken(accessToken: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(`${WEB_BASE}/api/cli/auth/revoke`, {
|
|
14
|
+
method: "DELETE",
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${accessToken}`,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Treat 200, 401, and 404 as success (token revoked or already invalid)
|
|
21
|
+
return response.ok || response.status === 401 || response.status === 404
|
|
22
|
+
} catch {
|
|
23
|
+
// Network errors shouldn't prevent local logout
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
5
27
|
|
|
6
28
|
export const logoutCommand = new Command("logout")
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
.description("Log out and remove all stored credentials")
|
|
30
|
+
.action(async () => {
|
|
31
|
+
const auth = await getAuth()
|
|
32
|
+
|
|
33
|
+
if (!auth?.accessToken) {
|
|
34
|
+
console.log(chalk.dim("Already logged out"))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try to revoke token on server (don't block on failure)
|
|
39
|
+
const revoked = await revokeToken(auth.accessToken)
|
|
40
|
+
|
|
41
|
+
// Clear local config regardless of server result
|
|
42
|
+
await clearAuth()
|
|
43
|
+
|
|
44
|
+
if (revoked) {
|
|
45
|
+
console.log(chalk.green("Logged out"))
|
|
46
|
+
} else {
|
|
47
|
+
// Local logout succeeded but server revocation failed
|
|
48
|
+
console.log(chalk.green("Logged out"))
|
|
49
|
+
console.log(chalk.dim("Note: Could not reach server to revoke token"))
|
|
50
|
+
}
|
|
51
|
+
})
|
package/src/commands/query.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { getDefaultSite } from "../config"
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const queryDescription = `Flexible query with custom metrics and dimensions.
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { Command } from "commander"
|
|
3
|
+
import { formatDuration, formatNumber, formatPercent, query } from "../api"
|
|
4
|
+
import { getDefaultSite } from "../config"
|
|
5
|
+
import { type TableColumn, table } from "../ui"
|
|
8
6
|
|
|
7
|
+
const queryExamples = `
|
|
9
8
|
Examples:
|
|
10
9
|
# Top pages with revenue
|
|
11
10
|
supalytics query -d page -m visitors,revenue
|
|
@@ -32,156 +31,180 @@ Examples:
|
|
|
32
31
|
supalytics query -d country -f "event:is:signup"
|
|
33
32
|
|
|
34
33
|
# Pages visited by premium users
|
|
35
|
-
supalytics query -d page -f "event_property:is:plan:premium"
|
|
34
|
+
supalytics query -d page -f "event_property:is:plan:premium"`
|
|
36
35
|
|
|
37
36
|
export const queryCommand = new Command("query")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
37
|
+
.description("Flexible analytics query with custom metrics and dimensions")
|
|
38
|
+
.addHelpText("after", queryExamples)
|
|
39
|
+
.option("-s, --site <site>", "Site to query")
|
|
40
|
+
.option(
|
|
41
|
+
"-m, --metrics <metrics>",
|
|
42
|
+
"Metrics: visitors, bounce_rate, avg_session_duration, revenue, conversions",
|
|
43
|
+
"visitors,revenue"
|
|
44
|
+
)
|
|
45
|
+
.option(
|
|
46
|
+
"-d, --dimensions <dimensions>",
|
|
47
|
+
"Dimensions: page, referrer, country, region, city, browser, os, device, date, hour, event, utm_* (max 2, event cannot combine)"
|
|
48
|
+
)
|
|
49
|
+
.option(
|
|
50
|
+
"-f, --filter <filters...>",
|
|
51
|
+
"Filters: field:operator:value (e.g., 'page:contains:/blog', 'event:is:signup', 'event_property:is:plan:premium')"
|
|
52
|
+
)
|
|
53
|
+
.option("--sort <sort>", "Sort by field:order (e.g., 'revenue:desc', 'visitors:asc')")
|
|
54
|
+
.option("--timezone <tz>", "Timezone for date grouping (e.g., 'America/New_York')", "UTC")
|
|
55
|
+
.option("-p, --period <period>", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
|
|
56
|
+
.option("--start <date>", "Start date (YYYY-MM-DD)")
|
|
57
|
+
.option("--end <date>", "End date (YYYY-MM-DD)")
|
|
58
|
+
.option("-l, --limit <number>", "Number of results (1-1000)", "10")
|
|
59
|
+
.option("--offset <number>", "Skip results for pagination", "0")
|
|
60
|
+
.option("--no-revenue", "Exclude revenue metrics")
|
|
61
|
+
.option("--json", "Output as JSON")
|
|
62
|
+
.option("-t, --test", "Test mode: query localhost data instead of production")
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
const site = options.site || (await getDefaultSite())
|
|
65
|
+
|
|
66
|
+
if (!site) {
|
|
67
|
+
console.error(
|
|
68
|
+
chalk.red(
|
|
69
|
+
"Error: No site specified. Use --site or set a default with `supalytics login --site`"
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const dateRange =
|
|
76
|
+
options.start && options.end
|
|
77
|
+
? ([options.start, options.end] as [string, string])
|
|
78
|
+
: options.period
|
|
79
|
+
|
|
80
|
+
// Parse metrics
|
|
81
|
+
const metrics = options.metrics.split(",").map((m: string) => m.trim())
|
|
82
|
+
|
|
83
|
+
// Parse dimensions
|
|
84
|
+
const dimensions = options.dimensions
|
|
85
|
+
? options.dimensions.split(",").map((d: string) => d.trim())
|
|
86
|
+
: undefined
|
|
87
|
+
|
|
88
|
+
// Parse filters - NEW FORMAT: field:operator:value
|
|
89
|
+
const filters = options.filter
|
|
90
|
+
? options.filter.map((f: string) => {
|
|
91
|
+
const parts = f.split(":")
|
|
92
|
+
if (parts.length < 3) {
|
|
93
|
+
console.error(chalk.red(`Invalid filter format: ${f}. Use 'field:operator:value'`))
|
|
94
|
+
process.exit(1)
|
|
95
|
+
}
|
|
96
|
+
const [field, operator, ...valueParts] = parts
|
|
97
|
+
return [field, operator, valueParts.join(":")] as [string, string, string]
|
|
98
|
+
})
|
|
99
|
+
: undefined
|
|
100
|
+
|
|
101
|
+
// Parse sort
|
|
102
|
+
const sort = options.sort
|
|
103
|
+
? (() => {
|
|
104
|
+
const [field, order] = options.sort.split(":")
|
|
105
|
+
if (!field || !order || (order !== "asc" && order !== "desc")) {
|
|
106
|
+
console.error(
|
|
107
|
+
chalk.red(
|
|
108
|
+
`Invalid sort format: ${options.sort}. Use 'field:order' where order is 'asc' or 'desc'`
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
process.exit(1)
|
|
112
|
+
}
|
|
113
|
+
return { field, order: order as "asc" | "desc" }
|
|
114
|
+
})()
|
|
115
|
+
: undefined
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const response = await query(site, {
|
|
119
|
+
metrics,
|
|
120
|
+
dimensions,
|
|
121
|
+
filters,
|
|
122
|
+
sort,
|
|
123
|
+
timezone: options.timezone,
|
|
124
|
+
date_range: dateRange,
|
|
125
|
+
limit: parseInt(options.limit),
|
|
126
|
+
offset: parseInt(options.offset),
|
|
127
|
+
include_revenue: options.revenue !== false,
|
|
128
|
+
is_dev: options.test || false,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
if (options.json) {
|
|
132
|
+
console.log(JSON.stringify(response, null, 2))
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const [startDate, endDate] = response.meta.date_range
|
|
137
|
+
|
|
138
|
+
console.log()
|
|
139
|
+
console.log(chalk.bold("Query Results"), chalk.dim(`${startDate} → ${endDate}`))
|
|
140
|
+
console.log(chalk.dim(` ${response.data.length} rows in ${response.meta.query_ms}ms`))
|
|
141
|
+
if (response.pagination.has_more) {
|
|
142
|
+
console.log(
|
|
143
|
+
chalk.dim(
|
|
144
|
+
` More results available (offset: ${response.pagination.offset + response.pagination.limit})`
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
console.log()
|
|
149
|
+
|
|
150
|
+
if (response.data.length === 0) {
|
|
151
|
+
console.log(chalk.dim(" No data"))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Build columns from data
|
|
156
|
+
const columns: TableColumn[] = []
|
|
157
|
+
|
|
158
|
+
// Add dimension columns
|
|
159
|
+
if (response.data[0]?.dimensions) {
|
|
160
|
+
for (const key of Object.keys(response.data[0].dimensions)) {
|
|
161
|
+
columns.push({
|
|
162
|
+
key: `dim_${key}`,
|
|
163
|
+
label: key.toUpperCase(),
|
|
164
|
+
align: "left",
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Add metric columns
|
|
170
|
+
for (const key of Object.keys(response.data[0]?.metrics || {})) {
|
|
171
|
+
columns.push({
|
|
172
|
+
key: `met_${key}`,
|
|
173
|
+
label: key.toUpperCase().replaceAll("_", " "),
|
|
174
|
+
align: "right",
|
|
175
|
+
format: (val) => {
|
|
176
|
+
if (val === null || val === undefined) return "-"
|
|
177
|
+
const v = val as number
|
|
178
|
+
if (key === "bounce_rate" || key === "conversion_rate") {
|
|
179
|
+
return formatPercent(v)
|
|
180
|
+
} else if (key === "avg_session_duration") {
|
|
181
|
+
return formatDuration(v)
|
|
182
|
+
} else if (key === "revenue") {
|
|
183
|
+
return chalk.green("$" + (v / 100).toFixed(2))
|
|
184
|
+
}
|
|
185
|
+
return formatNumber(v)
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Transform data for table
|
|
191
|
+
const tableData = response.data.map((r) => {
|
|
192
|
+
const row: Record<string, unknown> = {}
|
|
193
|
+
if (r.dimensions) {
|
|
194
|
+
for (const [k, v] of Object.entries(r.dimensions)) {
|
|
195
|
+
row[`dim_${k}`] = v || "(none)"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const [k, v] of Object.entries(r.metrics)) {
|
|
199
|
+
row[`met_${k}`] = v
|
|
200
|
+
}
|
|
201
|
+
return row
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
console.log(table(tableData, columns))
|
|
205
|
+
console.log()
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`))
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
})
|
package/src/commands/update.ts
CHANGED
|
@@ -1,54 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import chalk from "chalk"
|
|
3
|
-
import {
|
|
1
|
+
import { $ } from "bun"
|
|
2
|
+
import chalk from "chalk"
|
|
3
|
+
import { Command } from "commander"
|
|
4
4
|
|
|
5
5
|
export const updateCommand = new Command("update")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
6
|
+
.description("Update Supalytics CLI to the latest version")
|
|
7
|
+
.action(async () => {
|
|
8
|
+
// Read current version
|
|
9
|
+
const pkg = await Bun.file(new URL("../../package.json", import.meta.url)).json()
|
|
10
|
+
console.log(chalk.dim(`Current version: ${pkg.version}`))
|
|
11
|
+
console.log(chalk.dim("Checking for updates..."))
|
|
12
|
+
console.log()
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Check latest version from npm
|
|
16
|
+
const response = await fetch("https://registry.npmjs.org/@supalytics/cli/latest")
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new Error("Failed to check npm registry")
|
|
19
|
+
}
|
|
20
|
+
const data = await response.json()
|
|
21
|
+
const latestVersion = data.version
|
|
22
|
+
|
|
23
|
+
if (latestVersion === pkg.version) {
|
|
24
|
+
console.log(chalk.green("✓ Already on the latest version"))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(chalk.cyan(`New version available: ${latestVersion}`))
|
|
29
|
+
console.log()
|
|
30
|
+
console.log(chalk.dim("Updating..."))
|
|
31
|
+
|
|
32
|
+
// Try to update using the appropriate package manager
|
|
33
|
+
let updated = false
|
|
34
|
+
|
|
35
|
+
// Try bun first (bun add -g for global packages)
|
|
36
|
+
try {
|
|
37
|
+
await $`bun add -g @supalytics/cli@latest`.quiet()
|
|
38
|
+
updated = true
|
|
39
|
+
} catch {
|
|
40
|
+
// bun not available or failed, try npm
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fall back to npm if bun didn't work
|
|
44
|
+
if (!updated) {
|
|
45
|
+
try {
|
|
46
|
+
await $`npm install -g @supalytics/cli@latest`.quiet()
|
|
47
|
+
updated = true
|
|
48
|
+
} catch {
|
|
49
|
+
// npm also failed
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!updated) {
|
|
54
|
+
console.log(chalk.yellow("Automatic update failed."))
|
|
55
|
+
console.log()
|
|
56
|
+
console.log("Please update manually:")
|
|
57
|
+
console.log(chalk.cyan(" bun add -g @supalytics/cli@latest"))
|
|
58
|
+
console.log(" or")
|
|
59
|
+
console.log(chalk.cyan(" npm install -g @supalytics/cli@latest"))
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(chalk.green(`✓ Updated to ${latestVersion}`))
|
|
64
|
+
console.log()
|
|
65
|
+
console.log(chalk.dim("Run `supalytics --version` to verify"))
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`))
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
})
|