@supalytics/cli 0.3.8 → 0.4.0
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/journeys.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { Command } from "commander"
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../api"
|
|
10
|
-
import { getDefaultSite } from "../config"
|
|
4
|
+
formatRevenue,
|
|
5
|
+
getJourneyStats,
|
|
6
|
+
getJourneyTimeline,
|
|
7
|
+
listJourneys,
|
|
8
|
+
type VisitorTimelineAction,
|
|
9
|
+
} from "../api"
|
|
10
|
+
import { getDefaultSite } from "../config"
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "../ui"
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
12
|
+
formatDate,
|
|
13
|
+
formatSessionDuration,
|
|
14
|
+
formatTime,
|
|
15
|
+
formatTimelineDate,
|
|
16
|
+
parsePeriod,
|
|
17
|
+
type TableColumn,
|
|
18
|
+
table,
|
|
19
|
+
} from "../ui"
|
|
20
|
+
|
|
21
|
+
const journeysExamples = `
|
|
23
22
|
Examples:
|
|
24
23
|
# List all visitors (last 30 days)
|
|
25
24
|
supalytics journeys
|
|
@@ -40,203 +39,216 @@ Examples:
|
|
|
40
39
|
supalytics journeys abc123xyz
|
|
41
40
|
|
|
42
41
|
# JSON output
|
|
43
|
-
supalytics journeys --json
|
|
42
|
+
supalytics journeys --json`
|
|
44
43
|
|
|
45
44
|
export const journeysCommand = new Command("journeys")
|
|
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
|
-
|
|
45
|
+
.description("View visitor journeys and individual visitor details")
|
|
46
|
+
.addHelpText("after", journeysExamples)
|
|
47
|
+
.argument("[visitor-id]", "Visitor ID to view details")
|
|
48
|
+
.option("-s, --site <site>", "Site to query")
|
|
49
|
+
.option("-p, --period <period>", "Time period: 7d, 14d, 30d, 90d, 12mo, all", "30d")
|
|
50
|
+
.option("--start <date>", "Start date (YYYY-MM-DD)")
|
|
51
|
+
.option("--end <date>", "End date (YYYY-MM-DD)")
|
|
52
|
+
.option("-f, --filter <filters...>", "Page-level filters (field:operator:value)")
|
|
53
|
+
.option("--min-pageviews <n>", "Minimum pageviews", parseInt)
|
|
54
|
+
.option("--max-pageviews <n>", "Maximum pageviews", parseInt)
|
|
55
|
+
.option("--min-sessions <n>", "Minimum sessions", parseInt)
|
|
56
|
+
.option("--max-sessions <n>", "Maximum sessions", parseInt)
|
|
57
|
+
.option("--min-revenue <cents>", "Minimum revenue (cents)", parseInt)
|
|
58
|
+
.option("--max-revenue <cents>", "Maximum revenue (cents)", parseInt)
|
|
59
|
+
.option("--customers", "Only visitors with revenue")
|
|
60
|
+
.option("--first-purchase", "Only first-time purchases")
|
|
61
|
+
.option(
|
|
62
|
+
"--sort-by <field>",
|
|
63
|
+
"Sort by: last_seen, first_seen, pageviews, sessions, revenue, latest_revenue",
|
|
64
|
+
"last_seen"
|
|
65
|
+
)
|
|
66
|
+
.option("--sort-order <order>", "Sort order: asc, desc", "desc")
|
|
67
|
+
.option("-l, --limit <n>", "Results per page (max 100)", "50")
|
|
68
|
+
.option("--offset <n>", "Pagination offset", "0")
|
|
69
|
+
.option("--json", "Output as JSON")
|
|
70
|
+
.option("-t, --test", "Test mode: query localhost data")
|
|
71
|
+
.action(async (visitorId, options) => {
|
|
72
|
+
const site = options.site || (await getDefaultSite())
|
|
73
|
+
|
|
74
|
+
if (!site) {
|
|
75
|
+
console.error(
|
|
76
|
+
chalk.red(
|
|
77
|
+
"Error: No site specified. Use --site or set a default with `supalytics login --site`"
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dateRange =
|
|
84
|
+
options.start && options.end
|
|
85
|
+
? ([options.start, options.end] as [string, string])
|
|
86
|
+
: parsePeriod(options.period)
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// If visitor ID provided, show visitor details
|
|
90
|
+
if (visitorId) {
|
|
91
|
+
await showVisitorDetails(site, visitorId, dateRange, options)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Otherwise, list all visitors
|
|
96
|
+
await listVisitors(site, dateRange, options)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`))
|
|
99
|
+
process.exit(1)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
93
102
|
|
|
94
103
|
// ============================================================================
|
|
95
104
|
// LIST VISITORS
|
|
96
105
|
// ============================================================================
|
|
97
106
|
|
|
98
107
|
async function listVisitors(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
site: string,
|
|
109
|
+
dateRange: string | [string, string],
|
|
110
|
+
options: Record<string, unknown>
|
|
102
111
|
) {
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
112
|
+
// Parse filters
|
|
113
|
+
const filters = options.filter
|
|
114
|
+
? (options.filter as string[]).map((f: string) => {
|
|
115
|
+
const parts = f.split(":")
|
|
116
|
+
if (parts.length < 3) {
|
|
117
|
+
console.error(chalk.red(`Invalid filter format: ${f}. Use 'field:operator:value'`))
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
const [field, operator, ...valueParts] = parts
|
|
121
|
+
return [field, operator, valueParts.join(":")] as [string, string, string]
|
|
122
|
+
})
|
|
123
|
+
: undefined
|
|
124
|
+
|
|
125
|
+
const response = await listJourneys(site, {
|
|
126
|
+
date_range: dateRange,
|
|
127
|
+
filters,
|
|
128
|
+
pageviews_gt: options.minPageviews as number | undefined,
|
|
129
|
+
pageviews_lt: options.maxPageviews as number | undefined,
|
|
130
|
+
sessions_gt: options.minSessions as number | undefined,
|
|
131
|
+
sessions_lt: options.maxSessions as number | undefined,
|
|
132
|
+
revenue_gt: options.minRevenue as number | undefined,
|
|
133
|
+
revenue_lt: options.maxRevenue as number | undefined,
|
|
134
|
+
first_purchase: options.firstPurchase as boolean | undefined,
|
|
135
|
+
has_revenue: options.customers as boolean | undefined,
|
|
136
|
+
sort_by: options.sortBy as string,
|
|
137
|
+
sort_order: options.sortOrder as "asc" | "desc",
|
|
138
|
+
limit: parseInt(options.limit as string),
|
|
139
|
+
offset: parseInt(options.offset as string),
|
|
140
|
+
is_dev: (options.test as boolean) || false,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
if (options.json) {
|
|
144
|
+
console.log(JSON.stringify(response, null, 2))
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const [startDate, endDate] = response.meta.date_range
|
|
149
|
+
|
|
150
|
+
console.log()
|
|
151
|
+
console.log(
|
|
152
|
+
chalk.bold("VISITOR JOURNEYS"),
|
|
153
|
+
chalk.dim(`${startDate.split(" ")[0]} → ${endDate.split(" ")[0]}`)
|
|
154
|
+
)
|
|
155
|
+
console.log()
|
|
156
|
+
|
|
157
|
+
if (response.data.length === 0) {
|
|
158
|
+
console.log(chalk.dim(" No visitors found"))
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const columns: TableColumn[] = [
|
|
163
|
+
{
|
|
164
|
+
key: "visitor_id",
|
|
165
|
+
label: "Visitor ID",
|
|
166
|
+
align: "left",
|
|
167
|
+
width: 36,
|
|
168
|
+
format: (v) => chalk.cyan(String(v)),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
key: "country",
|
|
172
|
+
label: "Country",
|
|
173
|
+
align: "left",
|
|
174
|
+
width: 7,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: "device",
|
|
178
|
+
label: "Device",
|
|
179
|
+
align: "left",
|
|
180
|
+
width: 8,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: "first_referrer",
|
|
184
|
+
label: "Source",
|
|
185
|
+
align: "left",
|
|
186
|
+
width: 18,
|
|
187
|
+
format: (v) => {
|
|
188
|
+
const source = String(v || "Direct")
|
|
189
|
+
return source.length > 18 ? source.slice(0, 15) + "..." : source
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
key: "entry_page",
|
|
194
|
+
label: "Entry Page",
|
|
195
|
+
align: "left",
|
|
196
|
+
width: 20,
|
|
197
|
+
format: (v) => {
|
|
198
|
+
const page = String(v || "/")
|
|
199
|
+
return page.length > 20 ? page.slice(0, 17) + "..." : page
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
key: "pageview_count",
|
|
204
|
+
label: "Views",
|
|
205
|
+
align: "right",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
key: "session_count",
|
|
209
|
+
label: "Sessions",
|
|
210
|
+
align: "right",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
key: "total_revenue_cents",
|
|
214
|
+
label: "Revenue",
|
|
215
|
+
align: "right",
|
|
216
|
+
format: (v) => {
|
|
217
|
+
const cents = Number(v)
|
|
218
|
+
return cents > 0 ? formatRevenue(cents) : chalk.dim("$0")
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: "first_seen",
|
|
223
|
+
label: "First Seen",
|
|
224
|
+
align: "left",
|
|
225
|
+
width: 18,
|
|
226
|
+
format: (v) => formatDate(String(v)),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: "last_seen",
|
|
230
|
+
label: "Last Seen",
|
|
231
|
+
align: "left",
|
|
232
|
+
width: 18,
|
|
233
|
+
format: (v) => formatDate(String(v)),
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
console.log(table(response.data, columns, { showHeader: true }))
|
|
238
|
+
console.log()
|
|
239
|
+
|
|
240
|
+
// Pagination info
|
|
241
|
+
const { total, has_more, limit, offset } = response.pagination
|
|
242
|
+
if (total !== undefined) {
|
|
243
|
+
console.log(chalk.dim(` ${response.data.length} visitors shown (${total} total)`))
|
|
244
|
+
} else {
|
|
245
|
+
console.log(chalk.dim(` ${response.data.length} visitors shown`))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (has_more) {
|
|
249
|
+
console.log(chalk.dim(` Use --offset ${offset + limit} for next page`))
|
|
250
|
+
}
|
|
251
|
+
console.log()
|
|
240
252
|
}
|
|
241
253
|
|
|
242
254
|
// ============================================================================
|
|
@@ -244,93 +256,107 @@ async function listVisitors(
|
|
|
244
256
|
// ============================================================================
|
|
245
257
|
|
|
246
258
|
async function showVisitorDetails(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
259
|
+
site: string,
|
|
260
|
+
visitorId: string,
|
|
261
|
+
dateRange: string | [string, string],
|
|
262
|
+
options: Record<string, unknown>
|
|
251
263
|
) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
264
|
+
// Fetch both stats and timeline
|
|
265
|
+
const [statsResponse, timelineResponse] = await Promise.all([
|
|
266
|
+
getJourneyStats(site, visitorId, (options.test as boolean) || false),
|
|
267
|
+
getJourneyTimeline(site, {
|
|
268
|
+
visitor_id: visitorId,
|
|
269
|
+
date_range: dateRange,
|
|
270
|
+
include_auto_events: true,
|
|
271
|
+
limit: parseInt(options.limit as string),
|
|
272
|
+
offset: parseInt(options.offset as string),
|
|
273
|
+
is_dev: (options.test as boolean) || false,
|
|
274
|
+
}),
|
|
275
|
+
])
|
|
276
|
+
|
|
277
|
+
if (options.json) {
|
|
278
|
+
console.log(
|
|
279
|
+
JSON.stringify(
|
|
280
|
+
{
|
|
281
|
+
stats: statsResponse,
|
|
282
|
+
timeline: timelineResponse,
|
|
283
|
+
},
|
|
284
|
+
null,
|
|
285
|
+
2
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const stats = statsResponse.data
|
|
292
|
+
const timeline = timelineResponse.data
|
|
293
|
+
|
|
294
|
+
console.log()
|
|
295
|
+
console.log(chalk.bold(`VISITOR: ${chalk.cyan(visitorId)}`))
|
|
296
|
+
console.log()
|
|
297
|
+
|
|
298
|
+
// Stats section
|
|
299
|
+
console.log(chalk.bold("STATS"))
|
|
300
|
+
console.log(` First Seen: ${formatDate(stats.first_seen)}`)
|
|
301
|
+
console.log(` Last Seen: ${formatDate(stats.last_seen)}`)
|
|
302
|
+
console.log(` Total Sessions: ${stats.total_sessions}`)
|
|
303
|
+
console.log(` Total Pageviews: ${stats.total_pageviews}`)
|
|
304
|
+
console.log(` Total Events: ${stats.total_events}`)
|
|
305
|
+
console.log(
|
|
306
|
+
` Total Revenue: ${stats.total_revenue_cents > 0 ? formatRevenue(stats.total_revenue_cents) : chalk.dim("$0")}`
|
|
307
|
+
)
|
|
308
|
+
console.log(` Avg Scroll Depth: ${stats.avg_scroll_depth.toFixed(0)}%`)
|
|
309
|
+
console.log(` Avg Session: ${formatSessionDuration(stats.avg_session_duration_seconds)}`)
|
|
310
|
+
console.log()
|
|
311
|
+
console.log(
|
|
312
|
+
` Location: ${[stats.city, stats.region, stats.country].filter(Boolean).join(", ")}`
|
|
313
|
+
)
|
|
314
|
+
console.log(` Device: ${stats.device} • ${stats.browser} • ${stats.os}`)
|
|
315
|
+
console.log(` Source: ${stats.source}`)
|
|
316
|
+
console.log(` Entry Page: ${stats.entry_page}`)
|
|
317
|
+
if (stats.utm_campaign) {
|
|
318
|
+
console.log(` UTM Campaign: ${stats.utm_campaign}`)
|
|
319
|
+
}
|
|
320
|
+
console.log()
|
|
321
|
+
|
|
322
|
+
// Timeline section
|
|
323
|
+
console.log(chalk.bold("TIMELINE"))
|
|
324
|
+
console.log()
|
|
325
|
+
|
|
326
|
+
if (timeline.length === 0) {
|
|
327
|
+
console.log(chalk.dim(" No timeline data"))
|
|
328
|
+
console.log()
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Group timeline by date and session
|
|
333
|
+
const grouped = groupTimeline(timeline)
|
|
334
|
+
|
|
335
|
+
for (const dateGroup of grouped) {
|
|
336
|
+
console.log(chalk.dim(` ▼ ${dateGroup.date}`))
|
|
337
|
+
|
|
338
|
+
for (const session of dateGroup.sessions) {
|
|
339
|
+
if (session.isGrouped) {
|
|
340
|
+
console.log(chalk.dim(` Session ${session.sessionNumber}`))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const action of session.actions) {
|
|
344
|
+
console.log(formatTimelineAction(action))
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Pagination info
|
|
352
|
+
if (timelineResponse.pagination.has_more) {
|
|
353
|
+
console.log(
|
|
354
|
+
chalk.dim(
|
|
355
|
+
` ${timeline.length} items shown • Use --offset ${timelineResponse.pagination.offset + timelineResponse.pagination.limit} for more`
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
console.log()
|
|
359
|
+
}
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
// ============================================================================
|
|
@@ -338,133 +364,135 @@ async function showVisitorDetails(
|
|
|
338
364
|
// ============================================================================
|
|
339
365
|
|
|
340
366
|
interface TimelineSession {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
367
|
+
sessionNumber: number
|
|
368
|
+
sessionId: string
|
|
369
|
+
isGrouped: boolean
|
|
370
|
+
actions: VisitorTimelineAction[]
|
|
345
371
|
}
|
|
346
372
|
|
|
347
373
|
interface TimelineDateGroup {
|
|
348
|
-
|
|
349
|
-
|
|
374
|
+
date: string
|
|
375
|
+
sessions: TimelineSession[]
|
|
350
376
|
}
|
|
351
377
|
|
|
352
378
|
function groupTimeline(timeline: VisitorTimelineAction[]): TimelineDateGroup[] {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
379
|
+
const dateGroups = new Map<string, VisitorTimelineAction[]>()
|
|
380
|
+
|
|
381
|
+
// Group by date
|
|
382
|
+
for (const action of timeline) {
|
|
383
|
+
const date = formatTimelineDate(action.timestamp)
|
|
384
|
+
if (!dateGroups.has(date)) {
|
|
385
|
+
dateGroups.set(date, [])
|
|
386
|
+
}
|
|
387
|
+
dateGroups.get(date)!.push(action)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Convert to array and group by session within each date
|
|
391
|
+
const result: TimelineDateGroup[] = []
|
|
392
|
+
|
|
393
|
+
for (const [date, actions] of dateGroups) {
|
|
394
|
+
const sessionGroups = new Map<string, VisitorTimelineAction[]>()
|
|
395
|
+
const nonSessionActions: VisitorTimelineAction[] = []
|
|
396
|
+
|
|
397
|
+
for (const action of actions) {
|
|
398
|
+
if (action.type === "pageview" && action.session_id) {
|
|
399
|
+
if (!sessionGroups.has(action.session_id)) {
|
|
400
|
+
sessionGroups.set(action.session_id, [])
|
|
401
|
+
}
|
|
402
|
+
sessionGroups.get(action.session_id)!.push(action)
|
|
403
|
+
} else {
|
|
404
|
+
nonSessionActions.push(action)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const sessions: TimelineSession[] = []
|
|
409
|
+
let sessionNumber = 1
|
|
410
|
+
|
|
411
|
+
// Add session-grouped actions
|
|
412
|
+
for (const [sessionId, sessionActions] of sessionGroups) {
|
|
413
|
+
sessions.push({
|
|
414
|
+
sessionNumber: sessionNumber++,
|
|
415
|
+
sessionId,
|
|
416
|
+
isGrouped: true,
|
|
417
|
+
actions: sessionActions,
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Add non-session actions
|
|
422
|
+
if (nonSessionActions.length > 0) {
|
|
423
|
+
sessions.push({
|
|
424
|
+
sessionNumber: 0,
|
|
425
|
+
sessionId: "",
|
|
426
|
+
isGrouped: false,
|
|
427
|
+
actions: nonSessionActions,
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
result.push({ date, sessions })
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return result
|
|
409
435
|
}
|
|
410
436
|
|
|
411
437
|
function formatTimelineAction(action: VisitorTimelineAction): string {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
438
|
+
const time = formatTime(action.timestamp)
|
|
439
|
+
const indent = " "
|
|
440
|
+
|
|
441
|
+
if (action.type === "pageview") {
|
|
442
|
+
const url = action.url_path || "/"
|
|
443
|
+
const badges: string[] = []
|
|
444
|
+
|
|
445
|
+
if (action.is_entry_page === 1) {
|
|
446
|
+
badges.push(chalk.dim("[Entry]"))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (action.referrer) {
|
|
450
|
+
badges.push(chalk.dim(`[${action.referrer}]`))
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (action.utm_source) {
|
|
454
|
+
badges.push(chalk.dim(`[utm: ${action.utm_source}]`))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (action.scroll_depth !== undefined && action.scroll_depth !== null) {
|
|
458
|
+
badges.push(chalk.dim(`Scroll: ${action.scroll_depth}%`))
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const badgeStr = badges.length > 0 ? " " + badges.join(" ") : ""
|
|
462
|
+
|
|
463
|
+
return `${indent}${chalk.dim(time)} ${chalk.bold("PAGEVIEW")} ${chalk.cyan(url)}${badgeStr}`
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (action.type === "event" || action.type === "auto") {
|
|
467
|
+
const eventName = action.event_name || "unknown"
|
|
468
|
+
const label = action.type === "auto" ? "AUTO" : "EVENT"
|
|
469
|
+
let propsStr = ""
|
|
470
|
+
|
|
471
|
+
if (action.event_properties) {
|
|
472
|
+
try {
|
|
473
|
+
const props = JSON.parse(action.event_properties)
|
|
474
|
+
const pairs = Object.entries(props)
|
|
475
|
+
.slice(0, 2)
|
|
476
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
477
|
+
.join(", ")
|
|
478
|
+
propsStr = chalk.dim(` { ${pairs} }`)
|
|
479
|
+
} catch {
|
|
480
|
+
// Ignore parse errors
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return `${indent}${chalk.dim(time)} ${chalk.bold(label.padEnd(9))} ${eventName}${propsStr}`
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (action.type === "revenue") {
|
|
488
|
+
const amount = action.amount_cents ? formatRevenue(action.amount_cents, action.currency) : "$0"
|
|
489
|
+
const revenueType = action.revenue_event || "payment"
|
|
490
|
+
const txn = action.transaction_id
|
|
491
|
+
? chalk.dim(` (txn: ${action.transaction_id.slice(0, 12)})`)
|
|
492
|
+
: ""
|
|
493
|
+
|
|
494
|
+
return `${indent}${chalk.dim(time)} ${chalk.bold("REVENUE")} ${revenueType.padEnd(15)} ${amount}${txn}`
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return `${indent}${chalk.dim(time)} ${action.type}`
|
|
470
498
|
}
|