@pitcher/cli-scripts 2.13.0 → 2.14.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/README.md
CHANGED
|
@@ -352,7 +352,7 @@ p org get --api-key myapikey
|
|
|
352
352
|
|
|
353
353
|
## To run the `analytics` command:
|
|
354
354
|
|
|
355
|
-
Read rollup analytics totals
|
|
355
|
+
Read rollup analytics totals (`org`/`instance`), KPI metrics, and content/user engagement analytics. `org`/`instance` are Django rollups; `kpi` and the 7 verbs below hit the **clickhouse-api** service (`clickhouse.pitcher.com`) via the same env resolution as everything else — `--org`/`--url` pick the environment (routed via `x-original-domain`). Access **follows the key user's real role**: `popularity`, `users`, `files`, and `canvases` need an **org-admin** key; `engagement`, `viewers`, and `sessions` work with a member key. `kpi` and `sessions` return the raw payload; the other row-heavy verbs return a shaped envelope (`summary`, `rows`, `total_count`, `returned`, `has_more`, and sometimes `truncated`/`warnings`) capped by `--limit` (default 20, max 100). Date-range verbs default to a **30-day window** when `--start-date`/`--end-date` are omitted. `viewers` and `sessions` require `--canvas-id`; `sessions` also requires `--user`.
|
|
356
356
|
|
|
357
357
|
```bash
|
|
358
358
|
# Organization-wide rollups (org-admin key required)
|
|
@@ -368,6 +368,30 @@ p analytics instance --instance-id 01... --api-key myapikey
|
|
|
368
368
|
p analytics kpi --instance-id 01... --type total-members --api-key myapikey
|
|
369
369
|
p analytics kpi --instance-id 01... --type total-active-users \
|
|
370
370
|
--start-date 2026-01-01 --end-date 2026-01-31 --api-key myapikey
|
|
371
|
+
|
|
372
|
+
# DSR/content engagement rows (member key)
|
|
373
|
+
p analytics engagement --instance-id 01... --engagement-level high --limit 50 --api-key myapikey
|
|
374
|
+
|
|
375
|
+
# Most-viewed canvases / view-analysis (org-admin key)
|
|
376
|
+
p analytics popularity --instance-id 01... --sort-by total_views --sort-order DESC --api-key myapikey
|
|
377
|
+
|
|
378
|
+
# User roster (org-admin key)
|
|
379
|
+
p analytics users --instance-id 01... --api-key myapikey
|
|
380
|
+
|
|
381
|
+
# Active-user statistics instead of the roster (org-admin key)
|
|
382
|
+
p analytics users --instance-id 01... --stats --start-date 2026-01-01 --end-date 2026-01-31 --api-key myapikey
|
|
383
|
+
|
|
384
|
+
# File usage analytics (org-admin key)
|
|
385
|
+
p analytics files --instance-id 01... --file-type pdf --sort-by views --sort-order desc --api-key myapikey
|
|
386
|
+
|
|
387
|
+
# Per-section canvas engagement list (org-admin key)
|
|
388
|
+
p analytics canvases --instance-id 01... --group-ids 01GROUPID... --api-key myapikey
|
|
389
|
+
|
|
390
|
+
# DSR canvas viewers — who viewed a given canvas (member key)
|
|
391
|
+
p analytics viewers --instance-id 01... --canvas-id 01CANVASID... --viewer-type external --api-key myapikey
|
|
392
|
+
|
|
393
|
+
# Per-viewer session event timeline for a DSR canvas (member key)
|
|
394
|
+
p analytics sessions --instance-id 01... --canvas-id 01CANVASID... --user rep@example.com --api-key myapikey
|
|
371
395
|
```
|
|
372
396
|
|
|
373
397
|
## To run the `users` command:
|
|
@@ -25,4 +25,152 @@ export interface AnalyticsKpiOpts extends CommonOpts {
|
|
|
25
25
|
* `total-members` need none) — that validation stays server-side.
|
|
26
26
|
*/
|
|
27
27
|
export declare function runAnalyticsKpi(opts: AnalyticsKpiOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
28
|
+
export interface AnalyticsEngagementOpts extends CommonOpts {
|
|
29
|
+
instanceId?: string;
|
|
30
|
+
startDate?: string;
|
|
31
|
+
endDate?: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
page?: number;
|
|
34
|
+
engagementLevel?: string;
|
|
35
|
+
search?: string;
|
|
36
|
+
view?: string;
|
|
37
|
+
groupIds?: string;
|
|
38
|
+
sortBy?: string;
|
|
39
|
+
sortOrder?: string;
|
|
40
|
+
hideMemberFiles?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* DSR/content engagement rows from the clickhouse-api analytics service.
|
|
44
|
+
* Row-heavy (member gate) — shaped through `shapeAnalytics` to cap the
|
|
45
|
+
* payload at `--limit` and surface a truncation notice, since the native
|
|
46
|
+
* response can carry hundreds of rows per page.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runAnalyticsEngagement(opts: AnalyticsEngagementOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
49
|
+
export interface AnalyticsPopularityOpts extends CommonOpts {
|
|
50
|
+
instanceId?: string;
|
|
51
|
+
startDate?: string;
|
|
52
|
+
endDate?: string;
|
|
53
|
+
limit?: number;
|
|
54
|
+
page?: number;
|
|
55
|
+
sortBy?: string;
|
|
56
|
+
sortOrder?: string;
|
|
57
|
+
groupIds?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Most-viewed canvases from the clickhouse-api analytics service. There is no
|
|
61
|
+
* dedicated `popularity` route — this maps to `GET /analytics/canvases/stats`
|
|
62
|
+
* with `type=view-analysis` forced (always sent, never taken from opts).
|
|
63
|
+
* Row-heavy, so shaped through `shapeAnalytics` like `engagement`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runAnalyticsPopularity(opts: AnalyticsPopularityOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
66
|
+
export interface AnalyticsUsersOpts extends CommonOpts {
|
|
67
|
+
instanceId?: string;
|
|
68
|
+
stats?: boolean;
|
|
69
|
+
limit?: number;
|
|
70
|
+
page?: number;
|
|
71
|
+
startDate?: string;
|
|
72
|
+
endDate?: string;
|
|
73
|
+
groupIds?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* User roster (default) or active-user statistics (`--stats`) from the
|
|
77
|
+
* clickhouse-api analytics service. The two branches hit different routes
|
|
78
|
+
* with different params — verified against `packages/clickhouse-api/src/routes/users.ts`:
|
|
79
|
+
*
|
|
80
|
+
* - Default → `GET /analytics/users/list`: `instance_id` only. Full roster,
|
|
81
|
+
* no dates, no page size. (`show_internal_users` is resolved server-side
|
|
82
|
+
* from an org LD flag — the route never reads it as a query param, so the
|
|
83
|
+
* CLI does not expose it.)
|
|
84
|
+
* - `--stats` → `GET /analytics/users/stats`: requires `start_date`/`end_date`
|
|
85
|
+
* (defaulted via `withDefaultWindow`), and `type` is ALWAYS forced to
|
|
86
|
+
* `'active-users'` — the wire default `type=all` returns a shape with no
|
|
87
|
+
* `.items`, which would otherwise silently shape to an empty row set. The
|
|
88
|
+
* page-size param on this route is the camelCase `pageSize` (verified
|
|
89
|
+
* `users.ts:230`) — the one route in the CLI that reads camelCase on the wire.
|
|
90
|
+
*/
|
|
91
|
+
export declare function runAnalyticsUsers(opts: AnalyticsUsersOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
92
|
+
export interface AnalyticsFilesOpts extends CommonOpts {
|
|
93
|
+
instanceId?: string;
|
|
94
|
+
startDate?: string;
|
|
95
|
+
endDate?: string;
|
|
96
|
+
limit?: number;
|
|
97
|
+
page?: number;
|
|
98
|
+
fileType?: string;
|
|
99
|
+
filter?: string;
|
|
100
|
+
sortBy?: string;
|
|
101
|
+
sortOrder?: string;
|
|
102
|
+
metadataFilters?: string;
|
|
103
|
+
groupIds?: string;
|
|
104
|
+
userEmails?: string;
|
|
105
|
+
hideMemberFiles?: boolean;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* File usage analytics from the clickhouse-api analytics service. There is
|
|
109
|
+
* no dedicated "list" type on the wire — `type` is ALWAYS forced to `'all'`
|
|
110
|
+
* (verified `files.ts`/`files-analytics.service.ts`): the `default` branch of
|
|
111
|
+
* `getFileUsageAnalysisData` throws `Unsupported type`, and only `type=all`
|
|
112
|
+
* yields the paginated row list. `page_size` is snake_case on this
|
|
113
|
+
* route (unlike `users/stats`). `sort_order` must be uppercased on the wire —
|
|
114
|
+
* the server zod is `z.enum(['ASC','DESC'])`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function runAnalyticsFiles(opts: AnalyticsFilesOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
117
|
+
export interface AnalyticsCanvasesOpts extends CommonOpts {
|
|
118
|
+
instanceId?: string;
|
|
119
|
+
startDate?: string;
|
|
120
|
+
endDate?: string;
|
|
121
|
+
limit?: number;
|
|
122
|
+
page?: number;
|
|
123
|
+
groupIds?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Per-section canvas engagement list from the clickhouse-api analytics
|
|
127
|
+
* service. There is no dedicated "list" type on the wire — `type` is ALWAYS
|
|
128
|
+
* forced to `'section-list'` (verified `canvases.ts`/`canvases-analytics.service.ts`):
|
|
129
|
+
* `getCanvasAnalysisData` has exactly one supported case (`section-list`); its
|
|
130
|
+
* `default` branch throws `Unsupported type`, so any other value would hard-fail
|
|
131
|
+
* server-side. `--status`/`--filter` are deliberately NOT exposed — the
|
|
132
|
+
* `section-list` code path (`fetchSectionsList`) ignores `filter`/`search`/`status`
|
|
133
|
+
* entirely, so they'd be dead flags. There is also no `sort_order` on this route.
|
|
134
|
+
*/
|
|
135
|
+
export declare function runAnalyticsCanvases(opts: AnalyticsCanvasesOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
136
|
+
export interface AnalyticsViewersOpts extends CommonOpts {
|
|
137
|
+
instanceId?: string;
|
|
138
|
+
canvasId?: string;
|
|
139
|
+
limit?: number;
|
|
140
|
+
page?: number;
|
|
141
|
+
viewerType?: string;
|
|
142
|
+
sortBy?: string;
|
|
143
|
+
sortOrder?: string;
|
|
144
|
+
search?: string;
|
|
145
|
+
startDate?: string;
|
|
146
|
+
endDate?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* DSR canvas viewers (who-viewed) from the clickhouse-api analytics service.
|
|
150
|
+
* Maps to `GET /analytics/canvas-details/viewers` (member-gated, not admin) —
|
|
151
|
+
* verified `canvas-details.ts` ~L578-1147. Unlike the other row-heavy verbs,
|
|
152
|
+
* this route has its OWN server-side default window, so dates are NOT run
|
|
153
|
+
* through `withDefaultWindow` here — `start_date`/`end_date` are sent only
|
|
154
|
+
* when the caller supplies them. `sort_order` must be lowercased on the
|
|
155
|
+
* wire — the server zod is `z.enum(['asc','desc'])` (opposite of `files`,
|
|
156
|
+
* which uppercases). `sort_by` is validated client-side against the server
|
|
157
|
+
* enum so bad values fail fast with VALIDATION instead of a server 400.
|
|
158
|
+
*/
|
|
159
|
+
export declare function runAnalyticsViewers(opts: AnalyticsViewersOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
160
|
+
export interface AnalyticsSessionsOpts extends CommonOpts {
|
|
161
|
+
instanceId?: string;
|
|
162
|
+
canvasId?: string;
|
|
163
|
+
userEmail?: string;
|
|
164
|
+
startDate?: string;
|
|
165
|
+
endDate?: string;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Per-viewer session event timeline (DSR drill-down) from the clickhouse-api
|
|
169
|
+
* analytics service. Maps to `GET /analytics/canvas-details/sessions`
|
|
170
|
+
* (member-gated) — verified `canvas-details.ts` ~L1152-1329. Returns RAW
|
|
171
|
+
* (like `kpi`), NOT run through `shapeAnalytics` — an event timeline must not
|
|
172
|
+
* be truncated. Like `viewers`, this route has its own server-side default
|
|
173
|
+
* window, so dates are sent only when the caller supplies them.
|
|
174
|
+
*/
|
|
175
|
+
export declare function runAnalyticsSessions(opts: AnalyticsSessionsOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
28
176
|
export declare function createAnalyticsCommand(): Command;
|
|
@@ -4,6 +4,7 @@ import { ok, fail } from '../core/envelope.js';
|
|
|
4
4
|
import { createCliError } from '../core/errors.js';
|
|
5
5
|
import { emitEnvelope } from '../core/output.js';
|
|
6
6
|
import { withAuthFlags } from './withAuth.js';
|
|
7
|
+
import { shapeAnalytics, pageSizeFor, withDefaultWindow, todayIso } from '../core/analytics-shape.js';
|
|
7
8
|
export async function runAnalyticsOrg(opts, deps = {}) {
|
|
8
9
|
try {
|
|
9
10
|
const client = clientFor(opts, deps);
|
|
@@ -57,6 +58,306 @@ export async function runAnalyticsKpi(opts, deps = {}) {
|
|
|
57
58
|
return fail(toCliError(e));
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* DSR/content engagement rows from the clickhouse-api analytics service.
|
|
63
|
+
* Row-heavy (member gate) — shaped through `shapeAnalytics` to cap the
|
|
64
|
+
* payload at `--limit` and surface a truncation notice, since the native
|
|
65
|
+
* response can carry hundreds of rows per page.
|
|
66
|
+
*/
|
|
67
|
+
export async function runAnalyticsEngagement(opts, deps = {}) {
|
|
68
|
+
try {
|
|
69
|
+
if (!opts.instanceId)
|
|
70
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics engagement');
|
|
71
|
+
const client = clickhouseClientFor(opts, deps);
|
|
72
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
73
|
+
const params = {
|
|
74
|
+
instance_id: opts.instanceId,
|
|
75
|
+
start_date: windowed.startDate,
|
|
76
|
+
end_date: windowed.endDate,
|
|
77
|
+
page_size: pageSizeFor(opts.limit),
|
|
78
|
+
};
|
|
79
|
+
if (opts.page !== undefined)
|
|
80
|
+
params.page = opts.page;
|
|
81
|
+
if (opts.engagementLevel)
|
|
82
|
+
params.engagement_level = opts.engagementLevel;
|
|
83
|
+
if (opts.search)
|
|
84
|
+
params.search = opts.search;
|
|
85
|
+
if (opts.view)
|
|
86
|
+
params.view = opts.view;
|
|
87
|
+
if (opts.groupIds)
|
|
88
|
+
params.group_ids = opts.groupIds;
|
|
89
|
+
if (opts.sortBy)
|
|
90
|
+
params.sort_by = opts.sortBy;
|
|
91
|
+
if (opts.sortOrder)
|
|
92
|
+
params.sort_order = opts.sortOrder;
|
|
93
|
+
if (opts.hideMemberFiles)
|
|
94
|
+
params.hide_member_files = 'true';
|
|
95
|
+
const res = await client.get('/analytics/engagement', { params });
|
|
96
|
+
return ok(shapeAnalytics(res.data, (n) => ({ summary: n.metrics, rows: n.data?.data ?? [], totalCount: n.data?.total }), {
|
|
97
|
+
limit: opts.limit,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return fail(toCliError(e));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Most-viewed canvases from the clickhouse-api analytics service. There is no
|
|
106
|
+
* dedicated `popularity` route — this maps to `GET /analytics/canvases/stats`
|
|
107
|
+
* with `type=view-analysis` forced (always sent, never taken from opts).
|
|
108
|
+
* Row-heavy, so shaped through `shapeAnalytics` like `engagement`.
|
|
109
|
+
*/
|
|
110
|
+
export async function runAnalyticsPopularity(opts, deps = {}) {
|
|
111
|
+
try {
|
|
112
|
+
if (!opts.instanceId)
|
|
113
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics popularity');
|
|
114
|
+
const client = clickhouseClientFor(opts, deps);
|
|
115
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
116
|
+
const params = {
|
|
117
|
+
instance_id: opts.instanceId,
|
|
118
|
+
start_date: windowed.startDate,
|
|
119
|
+
end_date: windowed.endDate,
|
|
120
|
+
type: 'view-analysis',
|
|
121
|
+
sort_by: opts.sortBy || 'total_views',
|
|
122
|
+
sort_order: opts.sortOrder || 'DESC',
|
|
123
|
+
page_size: pageSizeFor(opts.limit),
|
|
124
|
+
};
|
|
125
|
+
if (opts.page !== undefined)
|
|
126
|
+
params.page = opts.page;
|
|
127
|
+
if (opts.groupIds)
|
|
128
|
+
params.group_ids = opts.groupIds;
|
|
129
|
+
const res = await client.get('/analytics/canvases/stats', { params });
|
|
130
|
+
return ok(shapeAnalytics(res.data, (n) => ({ summary: n.metrics, rows: n.data ?? [], totalCount: n.pagination?.total }), {
|
|
131
|
+
limit: opts.limit,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return fail(toCliError(e));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* User roster (default) or active-user statistics (`--stats`) from the
|
|
140
|
+
* clickhouse-api analytics service. The two branches hit different routes
|
|
141
|
+
* with different params — verified against `packages/clickhouse-api/src/routes/users.ts`:
|
|
142
|
+
*
|
|
143
|
+
* - Default → `GET /analytics/users/list`: `instance_id` only. Full roster,
|
|
144
|
+
* no dates, no page size. (`show_internal_users` is resolved server-side
|
|
145
|
+
* from an org LD flag — the route never reads it as a query param, so the
|
|
146
|
+
* CLI does not expose it.)
|
|
147
|
+
* - `--stats` → `GET /analytics/users/stats`: requires `start_date`/`end_date`
|
|
148
|
+
* (defaulted via `withDefaultWindow`), and `type` is ALWAYS forced to
|
|
149
|
+
* `'active-users'` — the wire default `type=all` returns a shape with no
|
|
150
|
+
* `.items`, which would otherwise silently shape to an empty row set. The
|
|
151
|
+
* page-size param on this route is the camelCase `pageSize` (verified
|
|
152
|
+
* `users.ts:230`) — the one route in the CLI that reads camelCase on the wire.
|
|
153
|
+
*/
|
|
154
|
+
export async function runAnalyticsUsers(opts, deps = {}) {
|
|
155
|
+
try {
|
|
156
|
+
if (!opts.instanceId)
|
|
157
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics users');
|
|
158
|
+
const client = clickhouseClientFor(opts, deps);
|
|
159
|
+
if (!opts.stats) {
|
|
160
|
+
const params = { instance_id: opts.instanceId };
|
|
161
|
+
const res = await client.get('/analytics/users/list', { params });
|
|
162
|
+
return ok(shapeAnalytics(res.data, (n) => ({ summary: undefined, rows: n.data ?? [] }), { limit: opts.limit }));
|
|
163
|
+
}
|
|
164
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
165
|
+
const params = {
|
|
166
|
+
instance_id: opts.instanceId,
|
|
167
|
+
start_date: windowed.startDate,
|
|
168
|
+
end_date: windowed.endDate,
|
|
169
|
+
type: 'active-users',
|
|
170
|
+
pageSize: pageSizeFor(opts.limit),
|
|
171
|
+
};
|
|
172
|
+
if (opts.page !== undefined)
|
|
173
|
+
params.page = opts.page;
|
|
174
|
+
if (opts.groupIds)
|
|
175
|
+
params.group_ids = opts.groupIds;
|
|
176
|
+
const res = await client.get('/analytics/users/stats', { params });
|
|
177
|
+
return ok(shapeAnalytics(res.data, (n) => ({ summary: n.metrics, rows: n.data?.items ?? [], totalCount: n.data?.total }), {
|
|
178
|
+
limit: opts.limit,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
return fail(toCliError(e));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* File usage analytics from the clickhouse-api analytics service. There is
|
|
187
|
+
* no dedicated "list" type on the wire — `type` is ALWAYS forced to `'all'`
|
|
188
|
+
* (verified `files.ts`/`files-analytics.service.ts`): the `default` branch of
|
|
189
|
+
* `getFileUsageAnalysisData` throws `Unsupported type`, and only `type=all`
|
|
190
|
+
* yields the paginated row list. `page_size` is snake_case on this
|
|
191
|
+
* route (unlike `users/stats`). `sort_order` must be uppercased on the wire —
|
|
192
|
+
* the server zod is `z.enum(['ASC','DESC'])`.
|
|
193
|
+
*/
|
|
194
|
+
export async function runAnalyticsFiles(opts, deps = {}) {
|
|
195
|
+
try {
|
|
196
|
+
if (!opts.instanceId)
|
|
197
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics files');
|
|
198
|
+
const client = clickhouseClientFor(opts, deps);
|
|
199
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
200
|
+
const params = {
|
|
201
|
+
instance_id: opts.instanceId,
|
|
202
|
+
start_date: windowed.startDate,
|
|
203
|
+
end_date: windowed.endDate,
|
|
204
|
+
type: 'all',
|
|
205
|
+
page_size: pageSizeFor(opts.limit),
|
|
206
|
+
};
|
|
207
|
+
if (opts.page !== undefined)
|
|
208
|
+
params.page = opts.page;
|
|
209
|
+
if (opts.fileType)
|
|
210
|
+
params.file_type = opts.fileType;
|
|
211
|
+
if (opts.filter)
|
|
212
|
+
params.filter = opts.filter;
|
|
213
|
+
if (opts.sortBy)
|
|
214
|
+
params.sort_by = opts.sortBy;
|
|
215
|
+
if (opts.sortOrder)
|
|
216
|
+
params.sort_order = opts.sortOrder.toUpperCase();
|
|
217
|
+
if (opts.metadataFilters)
|
|
218
|
+
params.metadata_filters = opts.metadataFilters;
|
|
219
|
+
if (opts.groupIds)
|
|
220
|
+
params.group_ids = opts.groupIds;
|
|
221
|
+
if (opts.userEmails)
|
|
222
|
+
params.user_emails = opts.userEmails;
|
|
223
|
+
if (opts.hideMemberFiles)
|
|
224
|
+
params.hide_member_files = 'true';
|
|
225
|
+
const res = await client.get('/analytics/files', { params });
|
|
226
|
+
return ok(shapeAnalytics(res.data, (n) => ({ summary: n.metrics, rows: n.data?.data ?? [], totalCount: n.data?.pagination?.total }), { limit: opts.limit }));
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
return fail(toCliError(e));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Per-section canvas engagement list from the clickhouse-api analytics
|
|
234
|
+
* service. There is no dedicated "list" type on the wire — `type` is ALWAYS
|
|
235
|
+
* forced to `'section-list'` (verified `canvases.ts`/`canvases-analytics.service.ts`):
|
|
236
|
+
* `getCanvasAnalysisData` has exactly one supported case (`section-list`); its
|
|
237
|
+
* `default` branch throws `Unsupported type`, so any other value would hard-fail
|
|
238
|
+
* server-side. `--status`/`--filter` are deliberately NOT exposed — the
|
|
239
|
+
* `section-list` code path (`fetchSectionsList`) ignores `filter`/`search`/`status`
|
|
240
|
+
* entirely, so they'd be dead flags. There is also no `sort_order` on this route.
|
|
241
|
+
*/
|
|
242
|
+
export async function runAnalyticsCanvases(opts, deps = {}) {
|
|
243
|
+
try {
|
|
244
|
+
if (!opts.instanceId)
|
|
245
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics canvases');
|
|
246
|
+
const client = clickhouseClientFor(opts, deps);
|
|
247
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
248
|
+
const params = {
|
|
249
|
+
instance_id: opts.instanceId,
|
|
250
|
+
start_date: windowed.startDate,
|
|
251
|
+
end_date: windowed.endDate,
|
|
252
|
+
type: 'section-list',
|
|
253
|
+
page_size: pageSizeFor(opts.limit),
|
|
254
|
+
};
|
|
255
|
+
if (opts.page !== undefined)
|
|
256
|
+
params.page = opts.page;
|
|
257
|
+
if (opts.groupIds)
|
|
258
|
+
params.group_ids = opts.groupIds;
|
|
259
|
+
const res = await client.get('/analytics/canvases', { params });
|
|
260
|
+
return ok(shapeAnalytics(res.data, (n) => ({ summary: n.metrics, rows: n.data?.items ?? [], totalCount: n.data?.total }), {
|
|
261
|
+
limit: opts.limit,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
return fail(toCliError(e));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const VIEWERS_SORT_BY = new Set([
|
|
269
|
+
'email',
|
|
270
|
+
'session_count',
|
|
271
|
+
'total_engagement_time',
|
|
272
|
+
'last_active',
|
|
273
|
+
'sections_viewed',
|
|
274
|
+
'files_viewed',
|
|
275
|
+
]);
|
|
276
|
+
/**
|
|
277
|
+
* DSR canvas viewers (who-viewed) from the clickhouse-api analytics service.
|
|
278
|
+
* Maps to `GET /analytics/canvas-details/viewers` (member-gated, not admin) —
|
|
279
|
+
* verified `canvas-details.ts` ~L578-1147. Unlike the other row-heavy verbs,
|
|
280
|
+
* this route has its OWN server-side default window, so dates are NOT run
|
|
281
|
+
* through `withDefaultWindow` here — `start_date`/`end_date` are sent only
|
|
282
|
+
* when the caller supplies them. `sort_order` must be lowercased on the
|
|
283
|
+
* wire — the server zod is `z.enum(['asc','desc'])` (opposite of `files`,
|
|
284
|
+
* which uppercases). `sort_by` is validated client-side against the server
|
|
285
|
+
* enum so bad values fail fast with VALIDATION instead of a server 400.
|
|
286
|
+
*/
|
|
287
|
+
export async function runAnalyticsViewers(opts, deps = {}) {
|
|
288
|
+
try {
|
|
289
|
+
if (!opts.instanceId)
|
|
290
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics viewers');
|
|
291
|
+
if (!opts.canvasId)
|
|
292
|
+
throw createCliError('VALIDATION', '--canvas-id is required for analytics viewers');
|
|
293
|
+
if (opts.sortBy && !VIEWERS_SORT_BY.has(opts.sortBy)) {
|
|
294
|
+
throw createCliError('VALIDATION', `--sort-by must be one of: ${Array.from(VIEWERS_SORT_BY).join(', ')}`);
|
|
295
|
+
}
|
|
296
|
+
const client = clickhouseClientFor(opts, deps);
|
|
297
|
+
const params = {
|
|
298
|
+
instance_id: opts.instanceId,
|
|
299
|
+
canvas_id: opts.canvasId,
|
|
300
|
+
page_size: pageSizeFor(opts.limit),
|
|
301
|
+
};
|
|
302
|
+
if (opts.viewerType)
|
|
303
|
+
params.viewer_type = opts.viewerType;
|
|
304
|
+
if (opts.sortBy)
|
|
305
|
+
params.sort_by = opts.sortBy;
|
|
306
|
+
if (opts.sortOrder)
|
|
307
|
+
params.sort_order = opts.sortOrder.toLowerCase();
|
|
308
|
+
if (opts.search)
|
|
309
|
+
params.search = opts.search;
|
|
310
|
+
if (opts.page !== undefined)
|
|
311
|
+
params.page = opts.page;
|
|
312
|
+
if (opts.startDate)
|
|
313
|
+
params.start_date = opts.startDate;
|
|
314
|
+
if (opts.endDate)
|
|
315
|
+
params.end_date = opts.endDate;
|
|
316
|
+
const res = await client.get('/analytics/canvas-details/viewers', { params });
|
|
317
|
+
return ok(shapeAnalytics(res.data, (n) => ({
|
|
318
|
+
summary: { canvas_name: n.canvas_name, ...n.overview },
|
|
319
|
+
rows: n.viewers?.items ?? [],
|
|
320
|
+
totalCount: n.viewers?.total,
|
|
321
|
+
warnings: n._meta?.partial ? [`partial: ${(n._meta.unavailable || []).join(', ')}`] : undefined,
|
|
322
|
+
}), { limit: opts.limit }));
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
return fail(toCliError(e));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Per-viewer session event timeline (DSR drill-down) from the clickhouse-api
|
|
330
|
+
* analytics service. Maps to `GET /analytics/canvas-details/sessions`
|
|
331
|
+
* (member-gated) — verified `canvas-details.ts` ~L1152-1329. Returns RAW
|
|
332
|
+
* (like `kpi`), NOT run through `shapeAnalytics` — an event timeline must not
|
|
333
|
+
* be truncated. Like `viewers`, this route has its own server-side default
|
|
334
|
+
* window, so dates are sent only when the caller supplies them.
|
|
335
|
+
*/
|
|
336
|
+
export async function runAnalyticsSessions(opts, deps = {}) {
|
|
337
|
+
try {
|
|
338
|
+
if (!opts.instanceId)
|
|
339
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics sessions');
|
|
340
|
+
if (!opts.canvasId)
|
|
341
|
+
throw createCliError('VALIDATION', '--canvas-id is required for analytics sessions');
|
|
342
|
+
if (!opts.userEmail)
|
|
343
|
+
throw createCliError('VALIDATION', '--user is required for analytics sessions');
|
|
344
|
+
const client = clickhouseClientFor(opts, deps);
|
|
345
|
+
const params = {
|
|
346
|
+
instance_id: opts.instanceId,
|
|
347
|
+
canvas_id: opts.canvasId,
|
|
348
|
+
user_email: opts.userEmail,
|
|
349
|
+
};
|
|
350
|
+
if (opts.startDate)
|
|
351
|
+
params.start_date = opts.startDate;
|
|
352
|
+
if (opts.endDate)
|
|
353
|
+
params.end_date = opts.endDate;
|
|
354
|
+
const res = await client.get('/analytics/canvas-details/sessions', { params });
|
|
355
|
+
return ok(res.data);
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
return fail(toCliError(e));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
60
361
|
export function createAnalyticsCommand() {
|
|
61
362
|
const cmd = new Command('analytics').description('read rollup analytics totals (requires an org-admin API key)');
|
|
62
363
|
withAuthFlags(cmd.command('org'))
|
|
@@ -84,5 +385,108 @@ export function createAnalyticsCommand() {
|
|
|
84
385
|
const globals = command.optsWithGlobals();
|
|
85
386
|
emitEnvelope(await runAnalyticsKpi({ ...options, quiet: globals.quiet }), globals);
|
|
86
387
|
});
|
|
388
|
+
withAuthFlags(cmd.command('engagement'))
|
|
389
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
390
|
+
.option('--start-date <iso>', 'Window start (ISO date; defaults to 30 days ago)')
|
|
391
|
+
.option('--end-date <iso>', 'Window end (ISO date; defaults to today)')
|
|
392
|
+
.option('--engagement-level <level>', "Engagement level filter (default 'all')")
|
|
393
|
+
.option('--view <view>', "View context (default 'dsr')")
|
|
394
|
+
.option('--search <term>', 'Search term')
|
|
395
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs')
|
|
396
|
+
.option('--sort-by <field>', 'Sort field')
|
|
397
|
+
.option('--sort-order <dir>', 'Sort direction')
|
|
398
|
+
.option('--hide-member-files', 'Exclude rep-uploaded files')
|
|
399
|
+
.option('--page <n>', 'Page number', (v) => parseInt(v, 10))
|
|
400
|
+
.option('--limit <n>', 'Max rows to return (default 20, max 100)', (v) => parseInt(v, 10))
|
|
401
|
+
.description('DSR/content engagement rows from the clickhouse analytics service')
|
|
402
|
+
.action(async (options, command) => {
|
|
403
|
+
const globals = command.optsWithGlobals();
|
|
404
|
+
emitEnvelope(await runAnalyticsEngagement({ ...options, quiet: globals.quiet }), globals);
|
|
405
|
+
});
|
|
406
|
+
withAuthFlags(cmd.command('popularity'))
|
|
407
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
408
|
+
.option('--start-date <iso>', 'Window start (ISO date; defaults to 30 days ago)')
|
|
409
|
+
.option('--end-date <iso>', 'Window end (ISO date; defaults to today)')
|
|
410
|
+
.option('--sort-by <field>', "Sort field (default 'total_views')")
|
|
411
|
+
.option('--sort-order <dir>', "Sort direction (default 'DESC')")
|
|
412
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs')
|
|
413
|
+
.option('--page <n>', 'Page number', (v) => parseInt(v, 10))
|
|
414
|
+
.option('--limit <n>', 'Max rows to return (default 20, max 100)', (v) => parseInt(v, 10))
|
|
415
|
+
.description('most-viewed canvases (view-analysis) from the clickhouse analytics service')
|
|
416
|
+
.action(async (options, command) => {
|
|
417
|
+
const globals = command.optsWithGlobals();
|
|
418
|
+
emitEnvelope(await runAnalyticsPopularity({ ...options, quiet: globals.quiet }), globals);
|
|
419
|
+
});
|
|
420
|
+
withAuthFlags(cmd.command('users'))
|
|
421
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
422
|
+
.option('--stats', 'Return active-user statistics instead of the user roster')
|
|
423
|
+
.option('--start-date <iso>', 'Window start (ISO date; --stats only, defaults to 30 days ago)')
|
|
424
|
+
.option('--end-date <iso>', 'Window end (ISO date; --stats only, defaults to today)')
|
|
425
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs (--stats only)')
|
|
426
|
+
.option('--page <n>', 'Page number (--stats only)', (v) => parseInt(v, 10))
|
|
427
|
+
.option('--limit <n>', 'Max rows to return (default 20, max 100)', (v) => parseInt(v, 10))
|
|
428
|
+
.description('user roster (default) or active-user statistics (--stats) from the clickhouse analytics service (admin key)')
|
|
429
|
+
.action(async (options, command) => {
|
|
430
|
+
const globals = command.optsWithGlobals();
|
|
431
|
+
emitEnvelope(await runAnalyticsUsers({ ...options, quiet: globals.quiet }), globals);
|
|
432
|
+
});
|
|
433
|
+
withAuthFlags(cmd.command('files'))
|
|
434
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
435
|
+
.option('--start-date <iso>', 'Window start (ISO date; defaults to 30 days ago)')
|
|
436
|
+
.option('--end-date <iso>', 'Window end (ISO date; defaults to today)')
|
|
437
|
+
.option('--file-type <type>', 'Filter by file type')
|
|
438
|
+
.option('--filter <term>', 'Search term')
|
|
439
|
+
.option('--sort-by <field>', 'Sort field')
|
|
440
|
+
.option('--sort-order <dir>', 'Sort direction (asc/desc, normalized to ASC/DESC)')
|
|
441
|
+
.option('--metadata-filters <json>', 'Metadata filters')
|
|
442
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs')
|
|
443
|
+
.option('--user-emails <csv>', 'Comma-separated user emails to scope by')
|
|
444
|
+
.option('--hide-member-files', 'Exclude rep-uploaded files')
|
|
445
|
+
.option('--page <n>', 'Page number', (v) => parseInt(v, 10))
|
|
446
|
+
.option('--limit <n>', 'Max rows to return (default 20, max 100)', (v) => parseInt(v, 10))
|
|
447
|
+
.description('file usage analytics (admin key) from the clickhouse analytics service')
|
|
448
|
+
.action(async (options, command) => {
|
|
449
|
+
const globals = command.optsWithGlobals();
|
|
450
|
+
emitEnvelope(await runAnalyticsFiles({ ...options, quiet: globals.quiet }), globals);
|
|
451
|
+
});
|
|
452
|
+
withAuthFlags(cmd.command('canvases'))
|
|
453
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
454
|
+
.option('--start-date <iso>', 'Window start (ISO date; defaults to 30 days ago)')
|
|
455
|
+
.option('--end-date <iso>', 'Window end (ISO date; defaults to today)')
|
|
456
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs')
|
|
457
|
+
.option('--page <n>', 'Page number', (v) => parseInt(v, 10))
|
|
458
|
+
.option('--limit <n>', 'Max rows to return (default 20, max 100)', (v) => parseInt(v, 10))
|
|
459
|
+
.description('per-section canvas engagement list (admin key) from the clickhouse analytics service')
|
|
460
|
+
.action(async (options, command) => {
|
|
461
|
+
const globals = command.optsWithGlobals();
|
|
462
|
+
emitEnvelope(await runAnalyticsCanvases({ ...options, quiet: globals.quiet }), globals);
|
|
463
|
+
});
|
|
464
|
+
withAuthFlags(cmd.command('viewers'))
|
|
465
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
466
|
+
.option('--canvas-id <id>', 'Canvas ID (required)')
|
|
467
|
+
.option('--viewer-type <type>', "Viewer type filter: all, internal, external (default 'all')")
|
|
468
|
+
.option('--sort-by <field>', 'Sort field: email, session_count, total_engagement_time, last_active, sections_viewed, files_viewed')
|
|
469
|
+
.option('--sort-order <dir>', "Sort direction (asc/desc, normalized to lowercase, default 'desc')")
|
|
470
|
+
.option('--search <term>', 'Search term')
|
|
471
|
+
.option('--start-date <iso>', 'Window start (ISO date; server default applies if omitted)')
|
|
472
|
+
.option('--end-date <iso>', 'Window end (ISO date; server default applies if omitted)')
|
|
473
|
+
.option('--page <n>', 'Page number', (v) => parseInt(v, 10))
|
|
474
|
+
.option('--limit <n>', 'Max rows to return (default 20, max 100)', (v) => parseInt(v, 10))
|
|
475
|
+
.description('DSR canvas viewers (who viewed) from the clickhouse analytics service')
|
|
476
|
+
.action(async (options, command) => {
|
|
477
|
+
const globals = command.optsWithGlobals();
|
|
478
|
+
emitEnvelope(await runAnalyticsViewers({ ...options, quiet: globals.quiet }), globals);
|
|
479
|
+
});
|
|
480
|
+
withAuthFlags(cmd.command('sessions'))
|
|
481
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
482
|
+
.option('--canvas-id <id>', 'Canvas ID (required)')
|
|
483
|
+
.option('--user <email>', 'Viewer user email (required)')
|
|
484
|
+
.option('--start-date <iso>', 'Window start (ISO date; server default applies if omitted)')
|
|
485
|
+
.option('--end-date <iso>', 'Window end (ISO date; server default applies if omitted)')
|
|
486
|
+
.description('per-viewer session event timeline for a DSR canvas (clickhouse analytics service)')
|
|
487
|
+
.action(async (options, command) => {
|
|
488
|
+
const globals = command.optsWithGlobals();
|
|
489
|
+
emitEnvelope(await runAnalyticsSessions({ ...options, userEmail: options.user, quiet: globals.quiet }), globals);
|
|
490
|
+
});
|
|
87
491
|
return cmd;
|
|
88
492
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ShapedResult<T> {
|
|
2
|
+
summary: unknown;
|
|
3
|
+
rows: T[];
|
|
4
|
+
total_count: number;
|
|
5
|
+
returned: number;
|
|
6
|
+
has_more: boolean;
|
|
7
|
+
truncated?: string;
|
|
8
|
+
warnings?: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function shapeAnalytics<T>(native: unknown, adapter: (n: any) => {
|
|
11
|
+
summary: unknown;
|
|
12
|
+
rows: T[];
|
|
13
|
+
totalCount?: number;
|
|
14
|
+
warnings?: string[];
|
|
15
|
+
}, opts?: {
|
|
16
|
+
limit?: number;
|
|
17
|
+
}): ShapedResult<T>;
|
|
18
|
+
export declare function pageSizeFor(limit?: number): number;
|
|
19
|
+
/** Today as an ISO day string (UTC) — what callers pass as `nowIso` to `withDefaultWindow`. */
|
|
20
|
+
export declare function todayIso(): string;
|
|
21
|
+
export declare function withDefaultWindow<T extends {
|
|
22
|
+
startDate?: string;
|
|
23
|
+
endDate?: string;
|
|
24
|
+
}>(opts: T, nowIso: string): T;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createCliError } from './errors.js';
|
|
2
|
+
const DEFAULT_LIMIT = 20;
|
|
3
|
+
const MAX_LIMIT = 100;
|
|
4
|
+
export function shapeAnalytics(native, adapter, opts = {}) {
|
|
5
|
+
const { summary, rows, totalCount, warnings } = adapter(native);
|
|
6
|
+
const limit = pageSizeFor(opts.limit);
|
|
7
|
+
const all = Array.isArray(rows) ? rows : [];
|
|
8
|
+
const capped = all.slice(0, limit);
|
|
9
|
+
const total = totalCount ?? all.length;
|
|
10
|
+
const has_more = total > capped.length;
|
|
11
|
+
return {
|
|
12
|
+
summary,
|
|
13
|
+
rows: capped,
|
|
14
|
+
total_count: total,
|
|
15
|
+
returned: capped.length,
|
|
16
|
+
has_more,
|
|
17
|
+
...(has_more
|
|
18
|
+
? { truncated: `Showing ${capped.length} of ${total.toLocaleString('en-US')} — narrow with --limit or filters` }
|
|
19
|
+
: {}),
|
|
20
|
+
...(warnings && warnings.length ? { warnings } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Deliberately separate from src/core/pagination.ts (caps at 500, no default) —
|
|
24
|
+
// that helper serves a different set of routes. Analytics defaults to 20 and
|
|
25
|
+
// caps at 100, matching the viewers route's server-side cap. Also the single
|
|
26
|
+
// place the row limit is normalized (shapeAnalytics delegates here): a
|
|
27
|
+
// non-finite limit (NaN from `parseInt('abc')`, ±Infinity) falls back to the
|
|
28
|
+
// default, and 0/negative floors to 1 — so an invalid `--limit` never sends a
|
|
29
|
+
// bad page_size to the wire nor slices client rows to an empty set.
|
|
30
|
+
export function pageSizeFor(limit) {
|
|
31
|
+
const n = Number.isFinite(limit) ? limit : DEFAULT_LIMIT;
|
|
32
|
+
return Math.min(Math.max(n, 1), MAX_LIMIT);
|
|
33
|
+
}
|
|
34
|
+
function subUtcDays(iso, days) {
|
|
35
|
+
const d = new Date(`${iso}T00:00:00Z`);
|
|
36
|
+
if (Number.isNaN(d.getTime())) {
|
|
37
|
+
throw createCliError('VALIDATION', `Invalid date '${iso}' — expected ISO YYYY-MM-DD`);
|
|
38
|
+
}
|
|
39
|
+
d.setUTCDate(d.getUTCDate() - days);
|
|
40
|
+
return d.toISOString().slice(0, 10);
|
|
41
|
+
}
|
|
42
|
+
/** Today as an ISO day string (UTC) — what callers pass as `nowIso` to `withDefaultWindow`. */
|
|
43
|
+
export function todayIso() {
|
|
44
|
+
return new Date().toISOString().slice(0, 10);
|
|
45
|
+
}
|
|
46
|
+
// Completes whichever bound is missing independently — both present is left
|
|
47
|
+
// untouched; both absent get a 30-day window ending at nowIso; only end_date
|
|
48
|
+
// given derives start_date as end-30; only start_date given fills end_date
|
|
49
|
+
// with nowIso. The point is that a lone bound never leaves the other
|
|
50
|
+
// undefined, which axios would silently drop from the wire (both are required
|
|
51
|
+
// server-side).
|
|
52
|
+
export function withDefaultWindow(opts, nowIso) {
|
|
53
|
+
if (opts.startDate && opts.endDate)
|
|
54
|
+
return opts;
|
|
55
|
+
const endDate = opts.endDate ?? nowIso;
|
|
56
|
+
const startDate = opts.startDate ?? subUtcDays(endDate, 30);
|
|
57
|
+
return { ...opts, startDate, endDate };
|
|
58
|
+
}
|