hazo_umetrics 1.12.0 → 1.13.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/CHANGE_LOG.md +31 -0
- package/dist/api/index.d.ts +27 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +46 -1
- package/dist/index.client.d.ts +1 -0
- package/dist/index.client.d.ts.map +1 -1
- package/dist/index.client.js +26 -1
- package/dist/server/behavior.d.ts +56 -0
- package/dist/server/behavior.d.ts.map +1 -1
- package/dist/server/behavior.js +188 -96
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/input_field_panel.d.ts +43 -0
- package/dist/ui/input_field_panel.d.ts.map +1 -0
- package/dist/ui/input_field_panel.js +173 -0
- package/dist/ui/search_queries_panel.d.ts +1 -7
- package/dist/ui/search_queries_panel.d.ts.map +1 -1
- package/dist/ui/search_queries_panel.js +12 -74
- package/package.json +1 -1
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# hazo_umetrics — Change Log
|
|
2
2
|
|
|
3
|
+
## 1.13.1 — Insecure-context-safe session id (bugfix)
|
|
4
|
+
|
|
5
|
+
**The client tracker no longer crashes the host app when opened over a bare-http origin (patch — no signature changes):**
|
|
6
|
+
|
|
7
|
+
- `src/index.client.ts` `getSessionId()` used `crypto.randomUUID()`, which is only defined in a **secure context** (https or localhost). Opening the app over a bare-http LAN IP (e.g. `http://192.168.x.x:3025` while testing on another device) left `randomUUID` undefined, so generating the anonymous session id threw and took down the whole React tree with it.
|
|
8
|
+
- New `safeUuid()` helper: uses `crypto.randomUUID()` when available (secure contexts), falls back to a v4 UUID built from `crypto.getRandomValues()` (available in insecure contexts too), and finally to a non-crypto id (fine for a throwaway anonymous session key). An analytics session id must never crash the host app.
|
|
9
|
+
- Test: `__tests__/safe_uuid.test.ts` — covers the `randomUUID` path, the `getRandomValues` fallback (v4 shape), and the no-crypto last resort.
|
|
10
|
+
|
|
11
|
+
## 1.13.0 — Generic manual-input field analyzer (`inputFieldAnalysis`)
|
|
12
|
+
|
|
13
|
+
**Generalizes the search-query analysis to any manual-input field — free-text labels, numeric amounts, override targets — per `design/hazo_umetrics-input-analyzer-spec.md` (minor — additive, no signatures changed):**
|
|
14
|
+
|
|
15
|
+
### Server (`src/server/behavior.ts`, `src/server/types.ts`)
|
|
16
|
+
- `inputFieldAnalysis(adapter, opts)` — generic analyzer over `action.<metric_key>` events for any `value_dim`, with an optional numeric-distribution path (`numeric: true` + optional `numeric_dim`). New types `InputFieldQuery`, `InputValueEntry`, `NumericSummary` (`n, min, p25, median, p75, max, mean, histogram`), `InputFieldResult`.
|
|
17
|
+
- `searchQueries` and `notFoundQueries` refactored into thin presets delegating to `inputFieldAnalysis`, mapped byte-identical to their frozen legacy shapes (`SearchQueriesResult`/`SearchTermEntry`, `NotFoundQueriesResult`) — no consumer-facing signature changes. New `case_sensitive` option on `InputFieldQuery` preserves `notFoundQueries`' raw-path dedup.
|
|
18
|
+
- `AnalyzableField` (extends `DatapointField`): `{ metric_key, value_dim, numeric_dim?, aggregatable, flag? }` — the privacy gate. Only fields with `aggregatable: true` may ever be captured server-side or returned by the API. `app_id`/`scope_id` tenant isolation inherited unchanged.
|
|
19
|
+
|
|
20
|
+
### API (`src/api/index.ts`)
|
|
21
|
+
- `createBehaviorHandlers` gains `getInputField(req)` — `GET /...?app_id=<id>&field_key=<key>[&since=<iso>][&top_n=<n>]`, requires `metrics.view`. Resolves `field_key` against a new `analyzableFields?: Record<string, AnalyzableField>` registry on `ApiFactoryOptions`, rejects unknown fields (404) and non-aggregatable fields (403), then delegates to `inputFieldAnalysis`. Legacy `getSearchQueries`/`getNotFound` untouched (now delegating internally, same routes/shapes).
|
|
22
|
+
|
|
23
|
+
### UI (`src/ui/input_field_panel.tsx`, new; exported from `src/ui/index.ts`)
|
|
24
|
+
- `InputFieldPanel({ appId, apiBase, fieldKey, labels?, since?, resolveUserProfiles? })` — generalized `search_queries_panel` (extracted `SummaryCard`/`MiniBar` locals). Same summary-cards + top-values + over-time + per-subject layout, with configurable copy via `InputFieldPanelLabels`. New numeric mode: when `numeric_summary` is present, renders a percentile strip (min/p25/median/p75/max/mean) + histogram instead of the top-values list.
|
|
25
|
+
- `SearchQueriesPanel` (`src/ui/search_queries_panel.tsx`, rewritten) is now `<InputFieldPanel fieldKey="search_query" labels={SEARCH_LABELS} .../>` — zero behaviour change. `InputFieldPanel` special-cases `fieldKey="search_query"` to keep talking to the legacy `GET /analytics/search-queries` route (byte-identical wire format, normalized client-side for shared rendering) instead of the new `/analytics/input-field` route, so existing consumer apps (gotimer/stocktools/kinstripe) keep working untouched on upgrade.
|
|
26
|
+
|
|
27
|
+
### Tests
|
|
28
|
+
- `__tests__/input_field_numeric.test.ts` — `inputFieldAnalysis` numeric path over synthetic `action.expense_other` rows (dim `amount`): percentiles + 10-bucket histogram against a hand-verified distribution, a single-bucket degenerate case, and categorical-path non-interference.
|
|
29
|
+
- `__tests__/search_queries.test.ts` and `__tests__/not_found.test.ts` (pre-existing) continue to pass unmodified — parity proof that the preset wrappers are byte-identical through the new `inputFieldAnalysis` path.
|
|
30
|
+
|
|
31
|
+
### Test-app
|
|
32
|
+
- `test-app/app/api/hazo_umetrics/analytics/input-field/route.ts` (new) — mirrors the `analytics/datapoints` route pattern; registers a demo `timer_score` numeric field (reusing the existing "Seed datapoint events" quick-seed action) so `GET .../analytics/input-field?app_id=test_app&field_key=timer_score` exercises the numeric path end to end.
|
|
33
|
+
|
|
3
34
|
## 1.12.0 — 404 / broken-link analytics
|
|
4
35
|
|
|
5
36
|
**New behavioral cut for `action.404` events (minor — purely additive; no existing signatures changed):**
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HazoConnectAdapter } from 'hazo_connect/server';
|
|
2
|
+
import type { AnalyzableField } from '../server/types.js';
|
|
2
3
|
export type GetAuthFn = (req: Request, opts: {
|
|
3
4
|
required_permissions?: string[];
|
|
4
5
|
scope_id?: string;
|
|
@@ -37,6 +38,24 @@ export interface ApiFactoryOptions {
|
|
|
37
38
|
* ```
|
|
38
39
|
*/
|
|
39
40
|
defaultActionLabels?: Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Registry of manual-input fields analyzable via `getInputField`
|
|
43
|
+
* (`GET /analytics/input-field?field_key=<key>`), keyed by field_key.
|
|
44
|
+
* Only entries with `aggregatable: true` are ever returned by the API —
|
|
45
|
+
* getInputField rejects a resolved field with `aggregatable: false`.
|
|
46
|
+
*
|
|
47
|
+
* Example:
|
|
48
|
+
* ```ts
|
|
49
|
+
* analyzableFields: {
|
|
50
|
+
* expense_other: {
|
|
51
|
+
* key: 'expense_other', label: 'Expense (Other)', type: 'number',
|
|
52
|
+
* metric_key: 'action.expense_other', value_dim: 'label',
|
|
53
|
+
* numeric_dim: 'amount', aggregatable: true,
|
|
54
|
+
* },
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
analyzableFields?: Record<string, AnalyzableField>;
|
|
40
59
|
}
|
|
41
60
|
export declare function createStatHandlers(options: ApiFactoryOptions): {
|
|
42
61
|
/**
|
|
@@ -213,5 +232,13 @@ export declare function createBehaviorHandlers(options: ApiFactoryOptions): {
|
|
|
213
232
|
* Returns 404 / broken-link analytics. Requires metrics.view.
|
|
214
233
|
*/
|
|
215
234
|
getNotFound(req: Request): Promise<Response>;
|
|
235
|
+
/**
|
|
236
|
+
* GET /...?app_id=<id>&field_key=<key>[&since=<iso>][&top_n=<n>]
|
|
237
|
+
* Generic manual-input field analytics — resolves field_key against the
|
|
238
|
+
* analyzableFields registry (see ApiFactoryOptions), enforces the
|
|
239
|
+
* aggregatable privacy gate, then delegates to inputFieldAnalysis.
|
|
240
|
+
* Requires metrics.view.
|
|
241
|
+
*/
|
|
242
|
+
getInputField(req: Request): Promise<Response>;
|
|
216
243
|
};
|
|
217
244
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
|
package/dist/api/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createGa4MetricWindowReader } from '../server/ga4_metrics.js';
|
|
|
11
11
|
import { resolveVariant } from '../server/flags.js';
|
|
12
12
|
import { getActivitySummary as _getActivitySummary, getActivityTrend as _getActivityTrend, } from '../server/activity.js';
|
|
13
13
|
import { ensureActionDef as _ensureActionDef, listActions as _listActions, getActionTree as _getActionTree, updateActionDef as _updateActionDef, updateActionDefBulk as _updateActionDefBulk, applyLinksBulk as _applyLinksBulk, getLinks as _getLinks, addLink as _addLink, removeLink as _removeLink, } from '../server/actions.js';
|
|
14
|
-
import { topActions as _topActions, firstSessionActions as _firstSessionActions, postReloginActions as _postReloginActions, actionsBeforeGoal as _actionsBeforeGoal, topErrors as _topErrors, tabDwell as _tabDwell, frequentActionsReturning as _frequentActionsReturning, eventsOverTime as _eventsOverTime, datapointStats as _datapointStats, userJourney as _userJourney, searchQueries as _searchQueries, notFoundQueries as _notFoundQueries, } from '../server/behavior.js';
|
|
14
|
+
import { topActions as _topActions, firstSessionActions as _firstSessionActions, postReloginActions as _postReloginActions, actionsBeforeGoal as _actionsBeforeGoal, topErrors as _topErrors, tabDwell as _tabDwell, frequentActionsReturning as _frequentActionsReturning, eventsOverTime as _eventsOverTime, datapointStats as _datapointStats, userJourney as _userJourney, searchQueries as _searchQueries, notFoundQueries as _notFoundQueries, inputFieldAnalysis as _inputFieldAnalysis, } from '../server/behavior.js';
|
|
15
15
|
import { createGa4Client } from '../server/ga4.js';
|
|
16
16
|
import { createGa4Provisioner } from '../server/ga4_provision.js';
|
|
17
17
|
import { getFunnel } from '../server/funnel.js';
|
|
@@ -1167,5 +1167,50 @@ export function createBehaviorHandlers(options) {
|
|
|
1167
1167
|
const result = await _notFoundQueries(adapter, { app_id, since, topN });
|
|
1168
1168
|
return jsonOk(result);
|
|
1169
1169
|
},
|
|
1170
|
+
/**
|
|
1171
|
+
* GET /...?app_id=<id>&field_key=<key>[&since=<iso>][&top_n=<n>]
|
|
1172
|
+
* Generic manual-input field analytics — resolves field_key against the
|
|
1173
|
+
* analyzableFields registry (see ApiFactoryOptions), enforces the
|
|
1174
|
+
* aggregatable privacy gate, then delegates to inputFieldAnalysis.
|
|
1175
|
+
* Requires metrics.view.
|
|
1176
|
+
*/
|
|
1177
|
+
async getInputField(req) {
|
|
1178
|
+
const url = new URL(req.url);
|
|
1179
|
+
const app_id = url.searchParams.get('app_id') ?? '';
|
|
1180
|
+
const field_key = url.searchParams.get('field_key') ?? '';
|
|
1181
|
+
const since = url.searchParams.get('since') ?? undefined;
|
|
1182
|
+
const topNParam = url.searchParams.get('top_n');
|
|
1183
|
+
const topN = topNParam != null ? parseInt(topNParam, 10) : undefined;
|
|
1184
|
+
const gate = await gateAuth(req, {
|
|
1185
|
+
required_permissions: ['metrics.view'],
|
|
1186
|
+
app_id: app_id || undefined,
|
|
1187
|
+
getAuth,
|
|
1188
|
+
});
|
|
1189
|
+
if (!gate.ok)
|
|
1190
|
+
return gate.response;
|
|
1191
|
+
if (!app_id || !field_key) {
|
|
1192
|
+
return jsonError('BAD_REQUEST', 'app_id and field_key are required', 400);
|
|
1193
|
+
}
|
|
1194
|
+
const field = options.analyzableFields?.[field_key];
|
|
1195
|
+
if (!field) {
|
|
1196
|
+
return jsonError('NOT_FOUND', `Unknown field_key "${field_key}"`, 404);
|
|
1197
|
+
}
|
|
1198
|
+
if (!field.aggregatable) {
|
|
1199
|
+
return jsonError('FORBIDDEN', `Field "${field_key}" is not aggregatable`, 403);
|
|
1200
|
+
}
|
|
1201
|
+
const adapter = await options.getAdapter();
|
|
1202
|
+
const result = await _inputFieldAnalysis(adapter, {
|
|
1203
|
+
app_id,
|
|
1204
|
+
since,
|
|
1205
|
+
metric_key: field.metric_key,
|
|
1206
|
+
value_dim: field.value_dim,
|
|
1207
|
+
numeric: field.type === 'number',
|
|
1208
|
+
numeric_dim: field.numeric_dim,
|
|
1209
|
+
flag_dim: field.flag?.dim,
|
|
1210
|
+
flag_predicate: field.flag?.predicate,
|
|
1211
|
+
topN,
|
|
1212
|
+
});
|
|
1213
|
+
return jsonOk(result);
|
|
1214
|
+
},
|
|
1170
1215
|
};
|
|
1171
1216
|
}
|
package/dist/index.client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
export { trackFlowStep } from './lib/track_flow_step.js';
|
|
3
3
|
export type { TrackFlowStepParams } from './lib/track_flow_step.js';
|
|
4
|
+
export declare function safeUuid(): string;
|
|
4
5
|
interface MetricsContextValue {
|
|
5
6
|
appId: string | null | undefined;
|
|
6
7
|
track: (metricKey: string, dimensions?: Record<string, string | number>) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAYpE,wBAAgB,QAAQ,IAAI,MAAM,CAiBjC;AA+CD,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IACjF,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1G,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;CAClE;AASD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,wBAAwB,4EAsJxF;AAMD,wBAAgB,UAAU,IAAI,mBAAmB,CAEhD;AAMD,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAK7C;AAED,wBAAgB,WAAW,IAAI;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAE5E;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAExF"}
|
package/dist/index.client.js
CHANGED
|
@@ -7,13 +7,38 @@ export { trackFlowStep } from './lib/track_flow_step.js';
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Anonymous session identity (localStorage, SSR-guarded)
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
+
// `crypto.randomUUID` only exists in a *secure context* (https or localhost).
|
|
11
|
+
// When the app is opened over a bare-http origin — e.g. a LAN IP like
|
|
12
|
+
// http://192.168.x.x:3025 while testing on another device — it is `undefined`,
|
|
13
|
+
// and calling it throws. An anonymous analytics session id must never crash the
|
|
14
|
+
// host app, so fall back to `getRandomValues` (available in insecure contexts)
|
|
15
|
+
// and finally to a non-crypto id (fine for a throwaway session key).
|
|
16
|
+
export function safeUuid() {
|
|
17
|
+
const c = typeof crypto !== 'undefined' ? crypto : undefined;
|
|
18
|
+
if (c && typeof c.randomUUID === 'function') {
|
|
19
|
+
try {
|
|
20
|
+
return c.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* secure-context throw — fall through */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (c && typeof c.getRandomValues === 'function') {
|
|
27
|
+
const b = c.getRandomValues(new Uint8Array(16));
|
|
28
|
+
b[6] = (b[6] & 0x0f) | 0x40; // version 4
|
|
29
|
+
b[8] = (b[8] & 0x3f) | 0x80; // variant 10
|
|
30
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, '0'));
|
|
31
|
+
return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
|
|
32
|
+
}
|
|
33
|
+
return `sid-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
34
|
+
}
|
|
10
35
|
function getSessionId() {
|
|
11
36
|
if (typeof window === 'undefined')
|
|
12
37
|
return '';
|
|
13
38
|
const key = 'hazo_umetrics_sid';
|
|
14
39
|
let sid = localStorage.getItem(key);
|
|
15
40
|
if (!sid) {
|
|
16
|
-
sid =
|
|
41
|
+
sid = safeUuid();
|
|
17
42
|
localStorage.setItem(key, sid);
|
|
18
43
|
}
|
|
19
44
|
return sid;
|
|
@@ -159,6 +159,62 @@ export declare function datapointStats(adapter: HazoConnectAdapter, opts: {
|
|
|
159
159
|
topN?: number;
|
|
160
160
|
schema?: DatapointSchema;
|
|
161
161
|
}): Promise<DatapointStatsResult>;
|
|
162
|
+
export interface InputFieldQuery {
|
|
163
|
+
app_id: string;
|
|
164
|
+
scope_id?: string;
|
|
165
|
+
since?: string;
|
|
166
|
+
metric_key: string;
|
|
167
|
+
value_dim: string;
|
|
168
|
+
numeric?: boolean;
|
|
169
|
+
numeric_dim?: string;
|
|
170
|
+
subject_dims?: string[];
|
|
171
|
+
flag_dim?: string;
|
|
172
|
+
flag_predicate?: (v: unknown) => boolean;
|
|
173
|
+
topN?: number;
|
|
174
|
+
/** Dedup key casing for value_dim. Default false = case-insensitive (the
|
|
175
|
+
* original searchQueries behavior: values are trim()+toLowerCase()'d for
|
|
176
|
+
* the dedup key, with the first-seen casing preserved for display). When
|
|
177
|
+
* true, the dedup key is the raw trimmed value — no case folding — which
|
|
178
|
+
* is what notFoundQueries needs ('/Foo' and '/foo' must stay separate). */
|
|
179
|
+
case_sensitive?: boolean;
|
|
180
|
+
}
|
|
181
|
+
export interface InputValueEntry {
|
|
182
|
+
value: string;
|
|
183
|
+
count: number;
|
|
184
|
+
subjects: number;
|
|
185
|
+
flagged: number;
|
|
186
|
+
lastAt: string;
|
|
187
|
+
}
|
|
188
|
+
export interface NumericSummary {
|
|
189
|
+
n: number;
|
|
190
|
+
min: number;
|
|
191
|
+
p25: number;
|
|
192
|
+
median: number;
|
|
193
|
+
p75: number;
|
|
194
|
+
max: number;
|
|
195
|
+
mean: number;
|
|
196
|
+
histogram: Array<{
|
|
197
|
+
bucket: string;
|
|
198
|
+
count: number;
|
|
199
|
+
}>;
|
|
200
|
+
}
|
|
201
|
+
export interface InputFieldResult {
|
|
202
|
+
total: number;
|
|
203
|
+
unique_values: number;
|
|
204
|
+
unique_subjects: number;
|
|
205
|
+
flagged_count: number;
|
|
206
|
+
top_values: InputValueEntry[];
|
|
207
|
+
flagged_values: InputValueEntry[];
|
|
208
|
+
over_time: EventDayBucket[];
|
|
209
|
+
per_subject: Array<{
|
|
210
|
+
subject_id: string;
|
|
211
|
+
count: number;
|
|
212
|
+
distinct_values: number;
|
|
213
|
+
values: string[];
|
|
214
|
+
}>;
|
|
215
|
+
numeric_summary?: NumericSummary;
|
|
216
|
+
}
|
|
217
|
+
export declare function inputFieldAnalysis(adapter: HazoConnectAdapter, opts: InputFieldQuery): Promise<InputFieldResult>;
|
|
162
218
|
export interface SearchTermEntry {
|
|
163
219
|
term: string;
|
|
164
220
|
count: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/server/behavior.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/server/behavior.ts"],"names":[],"mappings":"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"}
|
package/dist/server/behavior.js
CHANGED
|
@@ -586,107 +586,218 @@ export async function datapointStats(adapter, opts) {
|
|
|
586
586
|
keys.sort((a, b) => a.key.localeCompare(b.key));
|
|
587
587
|
return { action_key: opts.action_key, total_events, keys };
|
|
588
588
|
}
|
|
589
|
-
|
|
590
|
-
|
|
589
|
+
const NUMERIC_HISTOGRAM_BUCKETS = 10;
|
|
590
|
+
function percentile(sortedAsc, p) {
|
|
591
|
+
const n = sortedAsc.length;
|
|
592
|
+
if (n === 0)
|
|
593
|
+
return 0;
|
|
594
|
+
if (n === 1)
|
|
595
|
+
return sortedAsc[0];
|
|
596
|
+
const idx = (p / 100) * (n - 1);
|
|
597
|
+
const lo = Math.floor(idx);
|
|
598
|
+
const hi = Math.ceil(idx);
|
|
599
|
+
if (lo === hi)
|
|
600
|
+
return sortedAsc[lo];
|
|
601
|
+
const frac = idx - lo;
|
|
602
|
+
return sortedAsc[lo] + (sortedAsc[hi] - sortedAsc[lo]) * frac;
|
|
603
|
+
}
|
|
604
|
+
function computeNumericSummary(values) {
|
|
605
|
+
const n = values.length;
|
|
606
|
+
if (n === 0) {
|
|
607
|
+
return { n: 0, min: 0, p25: 0, median: 0, p75: 0, max: 0, mean: 0, histogram: [] };
|
|
608
|
+
}
|
|
609
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
610
|
+
const min = sorted[0];
|
|
611
|
+
const max = sorted[n - 1];
|
|
612
|
+
const mean = sorted.reduce((a, b) => a + b, 0) / n;
|
|
613
|
+
const p25 = percentile(sorted, 25);
|
|
614
|
+
const median = percentile(sorted, 50);
|
|
615
|
+
const p75 = percentile(sorted, 75);
|
|
616
|
+
const numBuckets = min === max ? 1 : NUMERIC_HISTOGRAM_BUCKETS;
|
|
617
|
+
const binWidth = min === max ? 1 : (max - min) / numBuckets;
|
|
618
|
+
const counts = new Array(numBuckets).fill(0);
|
|
619
|
+
for (const v of sorted) {
|
|
620
|
+
let idx = binWidth > 0 ? Math.floor((v - min) / binWidth) : 0;
|
|
621
|
+
if (idx >= numBuckets)
|
|
622
|
+
idx = numBuckets - 1;
|
|
623
|
+
if (idx < 0)
|
|
624
|
+
idx = 0;
|
|
625
|
+
counts[idx]++;
|
|
626
|
+
}
|
|
627
|
+
const histogram = counts.map((count, i) => {
|
|
628
|
+
const lo = min + i * binWidth;
|
|
629
|
+
const hi = i === numBuckets - 1 ? max : lo + binWidth;
|
|
630
|
+
return { bucket: `${round2(lo)}–${round2(hi)}`, count };
|
|
631
|
+
});
|
|
632
|
+
return { n, min, p25, median, p75, max, mean, histogram };
|
|
633
|
+
}
|
|
634
|
+
function round2(v) {
|
|
635
|
+
return Math.round(v * 100) / 100;
|
|
636
|
+
}
|
|
637
|
+
export async function inputFieldAnalysis(adapter, opts) {
|
|
638
|
+
const { app_id, scope_id, since, metric_key, value_dim } = opts;
|
|
591
639
|
const topN_ = opts.topN ?? 20;
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
|
|
640
|
+
const subjectDims = opts.subject_dims ?? ['user_id', 'session_id'];
|
|
641
|
+
const primaryDim = subjectDims[0];
|
|
642
|
+
const fallbackDim = subjectDims[1];
|
|
643
|
+
const rows = await fetchWindow(adapter, { app_id, scope_id, since: since ?? defaultSince(), cap: 10000 });
|
|
644
|
+
const fieldRows = rows.filter((r) => r.metric_key === metric_key);
|
|
645
|
+
const valueMap = new Map();
|
|
646
|
+
const tsMap = new Map();
|
|
647
|
+
const subjectMap = new Map();
|
|
648
|
+
const numericValues = [];
|
|
649
|
+
let flaggedCount = 0;
|
|
650
|
+
const allSubjects = new Set();
|
|
651
|
+
for (const row of fieldRows) {
|
|
600
652
|
const dims = parseDims(row.dimensions);
|
|
601
|
-
const
|
|
602
|
-
if (!
|
|
653
|
+
const rawValue = typeof dims[value_dim] === 'string' ? dims[value_dim] : String(dims[value_dim] ?? '');
|
|
654
|
+
if (!rawValue)
|
|
603
655
|
continue;
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
const identity =
|
|
608
|
-
const
|
|
609
|
-
|
|
656
|
+
const normValue = opts.case_sensitive ? rawValue.trim() : rawValue.trim().toLowerCase();
|
|
657
|
+
const primary = primaryDim ? dims[primaryDim] : undefined;
|
|
658
|
+
const fallback = fallbackDim ? dims[fallbackDim] : undefined;
|
|
659
|
+
const identity = primary ?? fallback;
|
|
660
|
+
const isFlagged = opts.flag_dim != null && opts.flag_predicate != null
|
|
661
|
+
? Boolean(opts.flag_predicate(dims[opts.flag_dim]))
|
|
662
|
+
: false;
|
|
610
663
|
if (identity)
|
|
611
|
-
|
|
612
|
-
//
|
|
613
|
-
if (!
|
|
614
|
-
|
|
664
|
+
allSubjects.add(identity);
|
|
665
|
+
// Value accumulator
|
|
666
|
+
if (!valueMap.has(normValue)) {
|
|
667
|
+
valueMap.set(normValue, {
|
|
615
668
|
count: 0,
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
firstCasing:
|
|
669
|
+
subjects: new Set(),
|
|
670
|
+
flagged: 0,
|
|
671
|
+
firstCasing: rawValue.trim(),
|
|
619
672
|
lastAt: row.captured_at,
|
|
620
673
|
});
|
|
621
674
|
}
|
|
622
|
-
const acc =
|
|
675
|
+
const acc = valueMap.get(normValue);
|
|
623
676
|
acc.count++;
|
|
624
677
|
if (identity)
|
|
625
|
-
acc.
|
|
626
|
-
if (
|
|
627
|
-
acc.
|
|
628
|
-
|
|
678
|
+
acc.subjects.add(identity);
|
|
679
|
+
if (isFlagged) {
|
|
680
|
+
acc.flagged++;
|
|
681
|
+
flaggedCount++;
|
|
629
682
|
}
|
|
630
683
|
if (row.captured_at > acc.lastAt)
|
|
631
684
|
acc.lastAt = row.captured_at;
|
|
632
685
|
// Timestamp bucket (second precision) for fine-grained "over time"
|
|
633
686
|
const ts = tsBucket(row.captured_at);
|
|
634
|
-
|
|
635
|
-
// Per-
|
|
636
|
-
if (
|
|
637
|
-
if (!
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
687
|
+
tsMap.set(ts, (tsMap.get(ts) ?? 0) + 1);
|
|
688
|
+
// Per-subject (real primary-identity only — no session fallback)
|
|
689
|
+
if (primary) {
|
|
690
|
+
if (!subjectMap.has(primary))
|
|
691
|
+
subjectMap.set(primary, { count: 0, values: new Set() });
|
|
692
|
+
const sa = subjectMap.get(primary);
|
|
693
|
+
sa.count++;
|
|
694
|
+
sa.values.add(normValue);
|
|
695
|
+
}
|
|
696
|
+
if (opts.numeric) {
|
|
697
|
+
const numDim = opts.numeric_dim ?? value_dim;
|
|
698
|
+
const n = Number(dims[numDim]);
|
|
699
|
+
if (isFinite(n))
|
|
700
|
+
numericValues.push(n);
|
|
642
701
|
}
|
|
643
702
|
}
|
|
644
|
-
const
|
|
645
|
-
|
|
703
|
+
const allValueEntries = [...valueMap.entries()].map(([, acc]) => ({
|
|
704
|
+
value: acc.firstCasing,
|
|
646
705
|
count: acc.count,
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
706
|
+
subjects: acc.subjects.size,
|
|
707
|
+
flagged: acc.flagged,
|
|
708
|
+
lastAt: acc.lastAt,
|
|
650
709
|
}));
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
.filter((e) => e.
|
|
654
|
-
.sort((a, b) => b.
|
|
710
|
+
const top_values = [...allValueEntries].sort((a, b) => b.count - a.count).slice(0, topN_);
|
|
711
|
+
const flagged_values = allValueEntries
|
|
712
|
+
.filter((e) => e.flagged > 0)
|
|
713
|
+
.sort((a, b) => b.flagged - a.flagged)
|
|
655
714
|
.slice(0, topN_);
|
|
656
715
|
// Keep the most recent 100 timestamp buckets, chronological order.
|
|
657
|
-
const over_time = [...
|
|
716
|
+
const over_time = [...tsMap.entries()]
|
|
658
717
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
659
718
|
.slice(-100)
|
|
660
719
|
.map(([day, count]) => ({ day, count }));
|
|
661
|
-
const
|
|
662
|
-
.map(([
|
|
663
|
-
|
|
720
|
+
const per_subject = [...subjectMap.entries()]
|
|
721
|
+
.map(([subject_id, { count, values }]) => ({
|
|
722
|
+
subject_id,
|
|
664
723
|
count,
|
|
665
|
-
|
|
666
|
-
|
|
724
|
+
distinct_values: values.size,
|
|
725
|
+
values: [...values],
|
|
667
726
|
}))
|
|
668
727
|
.sort((a, b) => b.count - a.count);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
728
|
+
const result = {
|
|
729
|
+
total: fieldRows.length,
|
|
730
|
+
unique_values: valueMap.size,
|
|
731
|
+
unique_subjects: allSubjects.size,
|
|
732
|
+
flagged_count: flaggedCount,
|
|
733
|
+
top_values,
|
|
734
|
+
flagged_values,
|
|
676
735
|
over_time,
|
|
677
|
-
|
|
736
|
+
per_subject,
|
|
737
|
+
};
|
|
738
|
+
if (opts.numeric) {
|
|
739
|
+
result.numeric_summary = computeNumericSummary(numericValues);
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
function toSearchTermEntry(e) {
|
|
744
|
+
return {
|
|
745
|
+
term: e.value,
|
|
746
|
+
count: e.count,
|
|
747
|
+
searchers: e.subjects,
|
|
748
|
+
zeroResults: e.flagged,
|
|
749
|
+
lastSearchedAt: e.lastAt,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function mapToLegacySearchShape(r) {
|
|
753
|
+
return {
|
|
754
|
+
total_searches: r.total,
|
|
755
|
+
unique_terms: r.unique_values,
|
|
756
|
+
unique_searchers: r.unique_subjects,
|
|
757
|
+
zero_result_searches: r.flagged_count,
|
|
758
|
+
top_terms: r.top_values.map(toSearchTermEntry),
|
|
759
|
+
zero_result_terms: r.flagged_values.map(toSearchTermEntry),
|
|
760
|
+
over_time: r.over_time,
|
|
761
|
+
per_user: r.per_subject.map((s) => ({
|
|
762
|
+
user_id: s.subject_id,
|
|
763
|
+
count: s.count,
|
|
764
|
+
distinct_terms: s.distinct_values,
|
|
765
|
+
terms: s.values,
|
|
766
|
+
})),
|
|
678
767
|
};
|
|
679
768
|
}
|
|
769
|
+
export async function searchQueries(adapter, opts) {
|
|
770
|
+
const result = await inputFieldAnalysis(adapter, {
|
|
771
|
+
app_id: opts.app_id,
|
|
772
|
+
scope_id: opts.scope_id,
|
|
773
|
+
since: opts.since,
|
|
774
|
+
metric_key: 'action.search',
|
|
775
|
+
value_dim: 'term',
|
|
776
|
+
flag_dim: 'result_count',
|
|
777
|
+
flag_predicate: (v) => Number(v) === 0,
|
|
778
|
+
topN: opts.topN,
|
|
779
|
+
});
|
|
780
|
+
return mapToLegacySearchShape(result);
|
|
781
|
+
}
|
|
680
782
|
export async function notFoundQueries(adapter, opts) {
|
|
681
783
|
const { app_id, scope_id, since, limit = 10000 } = opts;
|
|
682
784
|
const topN_ = opts.topN ?? 20;
|
|
785
|
+
const generic = await inputFieldAnalysis(adapter, {
|
|
786
|
+
app_id,
|
|
787
|
+
scope_id,
|
|
788
|
+
since,
|
|
789
|
+
metric_key: 'action.404',
|
|
790
|
+
value_dim: 'requested_path',
|
|
791
|
+
flag_dim: 'referrer_kind',
|
|
792
|
+
flag_predicate: (v) => v === 'internal',
|
|
793
|
+
topN: topN_,
|
|
794
|
+
case_sensitive: true, // paths are case-sensitive — '/Foo' and '/foo' must stay separate
|
|
795
|
+
});
|
|
683
796
|
const rows = await fetchWindow(adapter, { app_id, scope_id, since: since ?? defaultSince(), cap: limit });
|
|
684
797
|
const notFoundRows = rows.filter((r) => r.metric_key === 'action.404');
|
|
685
|
-
const pathMap = new Map();
|
|
686
798
|
const bySource = { internal: 0, search: 0, external: 0, direct: 0 };
|
|
687
|
-
const
|
|
799
|
+
const sampleReferrerMap = new Map();
|
|
688
800
|
let total404s = 0;
|
|
689
|
-
let internalReferrerCount = 0;
|
|
690
801
|
for (const row of notFoundRows) {
|
|
691
802
|
const dims = parseDims(row.dimensions);
|
|
692
803
|
const requestedPath = typeof dims.requested_path === 'string' ? dims.requested_path : '';
|
|
@@ -695,48 +806,29 @@ export async function notFoundQueries(adapter, opts) {
|
|
|
695
806
|
total404s++;
|
|
696
807
|
const referrerKind = typeof dims.referrer_kind === 'string' && dims.referrer_kind ? dims.referrer_kind : 'direct';
|
|
697
808
|
const referrer = typeof dims.referrer === 'string' ? dims.referrer : '';
|
|
698
|
-
if (!pathMap.has(requestedPath)) {
|
|
699
|
-
pathMap.set(requestedPath, { count: 0, lastAt: row.captured_at, internalCount: 0, sampleReferrer: '' });
|
|
700
|
-
}
|
|
701
|
-
const acc = pathMap.get(requestedPath);
|
|
702
|
-
acc.count++;
|
|
703
|
-
if (row.captured_at > acc.lastAt)
|
|
704
|
-
acc.lastAt = row.captured_at;
|
|
705
|
-
if (referrerKind === 'internal')
|
|
706
|
-
acc.internalCount++;
|
|
707
|
-
if (!acc.sampleReferrer && referrer)
|
|
708
|
-
acc.sampleReferrer = referrer;
|
|
709
|
-
if (referrerKind === 'internal')
|
|
710
|
-
internalReferrerCount++;
|
|
711
809
|
if (referrerKind === 'internal' || referrerKind === 'search' || referrerKind === 'external' || referrerKind === 'direct') {
|
|
712
810
|
bySource[referrerKind]++;
|
|
713
811
|
}
|
|
714
812
|
else {
|
|
715
813
|
bySource.external++;
|
|
716
814
|
}
|
|
717
|
-
|
|
718
|
-
|
|
815
|
+
if (!sampleReferrerMap.has(requestedPath) && referrer) {
|
|
816
|
+
sampleReferrerMap.set(requestedPath, referrer);
|
|
817
|
+
}
|
|
719
818
|
}
|
|
720
|
-
const top_paths =
|
|
721
|
-
.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}))
|
|
728
|
-
.sort((a, b) => b.count - a.count)
|
|
729
|
-
.slice(0, topN_);
|
|
730
|
-
const over_time = [...dayMap.entries()]
|
|
731
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
732
|
-
.slice(-100)
|
|
733
|
-
.map(([day, count]) => ({ day, count }));
|
|
819
|
+
const top_paths = generic.top_values.map((v) => ({
|
|
820
|
+
path: v.value,
|
|
821
|
+
count: v.count,
|
|
822
|
+
lastAt: v.lastAt,
|
|
823
|
+
internalCount: v.flagged,
|
|
824
|
+
sampleReferrer: sampleReferrerMap.get(v.value) ?? '',
|
|
825
|
+
}));
|
|
734
826
|
return {
|
|
735
827
|
total_404s: total404s,
|
|
736
|
-
unique_paths:
|
|
737
|
-
internal_referrer_count:
|
|
828
|
+
unique_paths: generic.unique_values,
|
|
829
|
+
internal_referrer_count: generic.flagged_count,
|
|
738
830
|
top_paths,
|
|
739
831
|
by_source: bySource,
|
|
740
|
-
over_time,
|
|
832
|
+
over_time: generic.over_time,
|
|
741
833
|
};
|
|
742
834
|
}
|
package/dist/server/types.d.ts
CHANGED
|
@@ -66,6 +66,21 @@ export interface DatapointField {
|
|
|
66
66
|
options?: string[];
|
|
67
67
|
}
|
|
68
68
|
export type DatapointSchema = DatapointField[];
|
|
69
|
+
export interface AnalyzableField extends DatapointField {
|
|
70
|
+
/** Event name this field's values are captured under, e.g. 'action.search'. */
|
|
71
|
+
metric_key: string;
|
|
72
|
+
/** Dimension holding the value to analyze, e.g. 'term' | 'label'. */
|
|
73
|
+
value_dim: string;
|
|
74
|
+
/** Optional separate numeric dimension (defaults to value_dim), e.g. 'amount'. */
|
|
75
|
+
numeric_dim?: string;
|
|
76
|
+
/** Privacy gate — false means never captured server-side or returned by the API. */
|
|
77
|
+
aggregatable: boolean;
|
|
78
|
+
/** Optional "content gap" signal, e.g. zero-result searches or broken internal links. */
|
|
79
|
+
flag?: {
|
|
80
|
+
dim: string;
|
|
81
|
+
predicate: (v: unknown) => boolean;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
69
84
|
export interface ActionDef {
|
|
70
85
|
action_key: string;
|
|
71
86
|
app_id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;IACtD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,cAAc,EAAE,kBAAkB,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5G;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IAC/C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GAAG,cAAc,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './actions_panel.js';
|
|
|
4
4
|
export * from './analytics_panel.js';
|
|
5
5
|
export * from './datapoint_panel.js';
|
|
6
6
|
export * from './journey_panel.js';
|
|
7
|
+
export * from './input_field_panel.js';
|
|
7
8
|
export * from './search_queries_panel.js';
|
|
8
9
|
export * from './not_found_panel.js';
|
|
9
10
|
export * from './ga4_connection_card.js';
|
package/dist/ui/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAGA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAGA,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC"}
|
package/dist/ui/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './actions_panel.js';
|
|
|
6
6
|
export * from './analytics_panel.js';
|
|
7
7
|
export * from './datapoint_panel.js';
|
|
8
8
|
export * from './journey_panel.js';
|
|
9
|
+
export * from './input_field_panel.js';
|
|
9
10
|
export * from './search_queries_panel.js';
|
|
10
11
|
export * from './not_found_panel.js';
|
|
11
12
|
export * from './ga4_connection_card.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface UserProfile {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
avatar_url?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function MiniBar({ value, max, color }: {
|
|
8
|
+
value: number;
|
|
9
|
+
max: number;
|
|
10
|
+
color?: string;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export declare function SummaryCard({ label, value }: {
|
|
13
|
+
label: string;
|
|
14
|
+
value: number | string;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
export interface InputFieldPanelLabels {
|
|
17
|
+
totalLabel: string;
|
|
18
|
+
uniqueValuesLabel: string;
|
|
19
|
+
uniqueSubjectsLabel: string;
|
|
20
|
+
flaggedLabel: string;
|
|
21
|
+
topValuesTitle: string;
|
|
22
|
+
emptyTopValues: string;
|
|
23
|
+
flaggedSectionTitle: string;
|
|
24
|
+
flaggedSectionSubtitle: string;
|
|
25
|
+
overTimeTitle: string;
|
|
26
|
+
perSubjectTitle: string;
|
|
27
|
+
subjectColumnLabel: string;
|
|
28
|
+
countColumnLabel: string;
|
|
29
|
+
distinctColumnLabel: string;
|
|
30
|
+
recentValuesColumnLabel: string;
|
|
31
|
+
distributionTitle: string;
|
|
32
|
+
}
|
|
33
|
+
export declare const SEARCH_LABELS: InputFieldPanelLabels;
|
|
34
|
+
export interface InputFieldPanelProps {
|
|
35
|
+
appId: string;
|
|
36
|
+
apiBase: string;
|
|
37
|
+
fieldKey: string;
|
|
38
|
+
labels?: Partial<InputFieldPanelLabels>;
|
|
39
|
+
since?: string;
|
|
40
|
+
resolveUserProfiles?: (ids: string[]) => Promise<UserProfile[]>;
|
|
41
|
+
}
|
|
42
|
+
export declare function InputFieldPanel({ appId, apiBase, fieldKey, labels: labelsOverride, since, resolveUserProfiles }: InputFieldPanelProps): import("react").JSX.Element;
|
|
43
|
+
//# sourceMappingURL=input_field_panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input_field_panel.d.ts","sourceRoot":"","sources":["../../src/ui/input_field_panel.tsx"],"names":[],"mappings":"AAyGA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAiB,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,+BAUxG;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,+BAOtF;AAMD,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uBAAuB,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAuBD,eAAO,MAAM,aAAa,EAAE,qBAgB3B,CAAC;AAMF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;CACjE;AAWD,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,oBAAoB,+BAsKrI"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
// hazo_umetrics/src/ui/input_field_panel.tsx — generic manual-input field
|
|
4
|
+
// analytics panel. Generalizes search_queries_panel.tsx per input-analyzer
|
|
5
|
+
// spec §7: same summary-cards + top-values + over-time + per-subject layout,
|
|
6
|
+
// configurable labels, plus a numeric mode (percentile strip + histogram)
|
|
7
|
+
// for fields analyzed with `numeric: true` (e.g. expense amounts).
|
|
8
|
+
import { useState, useEffect } from 'react';
|
|
9
|
+
function normalizeSearchQueriesResult(r) {
|
|
10
|
+
const toEntry = (t) => ({
|
|
11
|
+
value: t.term,
|
|
12
|
+
count: t.count,
|
|
13
|
+
subjects: t.searchers,
|
|
14
|
+
flagged: t.zeroResults,
|
|
15
|
+
lastAt: t.lastSearchedAt,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
total: r.total_searches,
|
|
19
|
+
unique_values: r.unique_terms,
|
|
20
|
+
unique_subjects: r.unique_searchers,
|
|
21
|
+
flagged_count: r.zero_result_searches,
|
|
22
|
+
top_values: r.top_terms.map(toEntry),
|
|
23
|
+
flagged_values: r.zero_result_terms.map(toEntry),
|
|
24
|
+
over_time: r.over_time,
|
|
25
|
+
per_subject: r.per_user.map((u) => ({
|
|
26
|
+
subject_id: u.user_id,
|
|
27
|
+
count: u.count,
|
|
28
|
+
distinct_values: u.distinct_terms,
|
|
29
|
+
values: u.terms,
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Shared primitives (extracted from the original search_queries_panel)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Format a stored timestamp ("…T…Z" or "YYYY-MM-DD HH:MM:SS") to second precision.
|
|
37
|
+
function fmtTs(iso) {
|
|
38
|
+
if (!iso)
|
|
39
|
+
return '';
|
|
40
|
+
return iso.slice(0, 19).replace('T', ' ');
|
|
41
|
+
}
|
|
42
|
+
export function MiniBar({ value, max, color = '#6366f1' }) {
|
|
43
|
+
const pct = max > 0 ? (value / max) * 100 : 0;
|
|
44
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1 h-2 rounded bg-gray-100 overflow-hidden", children: _jsx("div", { className: "h-full rounded", style: { width: `${pct}%`, backgroundColor: color } }) }), _jsx("span", { className: "text-xs tabular-nums text-gray-600 w-8 text-right", children: value })] }));
|
|
45
|
+
}
|
|
46
|
+
export function SummaryCard({ label, value }) {
|
|
47
|
+
return (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-1", children: [_jsx("p", { className: "text-[10px] text-gray-400 uppercase tracking-wide", children: label }), _jsx("p", { className: "text-2xl font-semibold text-gray-800 tabular-nums", children: value })] }));
|
|
48
|
+
}
|
|
49
|
+
const DEFAULT_LABELS = {
|
|
50
|
+
totalLabel: 'Total',
|
|
51
|
+
uniqueValuesLabel: 'Unique Values',
|
|
52
|
+
uniqueSubjectsLabel: 'Unique Subjects',
|
|
53
|
+
flaggedLabel: 'Flagged',
|
|
54
|
+
topValuesTitle: 'Top Values',
|
|
55
|
+
emptyTopValues: 'No values recorded yet.',
|
|
56
|
+
flaggedSectionTitle: 'Flagged Values',
|
|
57
|
+
flaggedSectionSubtitle: '(content gaps)',
|
|
58
|
+
overTimeTitle: 'Over Time',
|
|
59
|
+
perSubjectTitle: 'Per-Subject',
|
|
60
|
+
subjectColumnLabel: 'Subject',
|
|
61
|
+
countColumnLabel: 'Count',
|
|
62
|
+
distinctColumnLabel: 'Distinct Values',
|
|
63
|
+
recentValuesColumnLabel: 'Recent Values',
|
|
64
|
+
distributionTitle: 'Distribution',
|
|
65
|
+
};
|
|
66
|
+
// The exact copy the original search_queries_panel shipped — reused by the
|
|
67
|
+
// SearchQueriesPanel preset wrapper (search_queries_panel.tsx) so its labels
|
|
68
|
+
// are unchanged.
|
|
69
|
+
export const SEARCH_LABELS = {
|
|
70
|
+
totalLabel: 'Total Searches',
|
|
71
|
+
uniqueValuesLabel: 'Unique Terms',
|
|
72
|
+
uniqueSubjectsLabel: 'Unique Searchers',
|
|
73
|
+
flaggedLabel: 'Zero-Result Searches',
|
|
74
|
+
topValuesTitle: 'Top Search Terms',
|
|
75
|
+
emptyTopValues: 'No searches recorded yet.',
|
|
76
|
+
flaggedSectionTitle: 'Zero-Result Terms',
|
|
77
|
+
flaggedSectionSubtitle: '(content gaps)',
|
|
78
|
+
overTimeTitle: 'Searches Over Time',
|
|
79
|
+
perSubjectTitle: 'Per-User Searches',
|
|
80
|
+
subjectColumnLabel: 'User',
|
|
81
|
+
countColumnLabel: 'Searches',
|
|
82
|
+
distinctColumnLabel: 'Distinct Terms',
|
|
83
|
+
recentValuesColumnLabel: 'Recent Terms',
|
|
84
|
+
distributionTitle: 'Distribution',
|
|
85
|
+
};
|
|
86
|
+
// field_key -> endpoint. Only 'search_query' talks to the legacy
|
|
87
|
+
// GET /analytics/search-queries route (byte-identical wire format to the
|
|
88
|
+
// original SearchQueriesPanel) so existing consumer apps keep working
|
|
89
|
+
// untouched on upgrade. Every other field_key hits the generic
|
|
90
|
+
// GET /analytics/input-field route.
|
|
91
|
+
function endpointFor(fieldKey) {
|
|
92
|
+
return fieldKey === 'search_query' ? '/analytics/search-queries' : '/analytics/input-field';
|
|
93
|
+
}
|
|
94
|
+
export function InputFieldPanel({ appId, apiBase, fieldKey, labels: labelsOverride, since, resolveUserProfiles }) {
|
|
95
|
+
const labels = { ...DEFAULT_LABELS, ...labelsOverride };
|
|
96
|
+
const [data, setData] = useState(null);
|
|
97
|
+
const [profiles, setProfiles] = useState(new Map());
|
|
98
|
+
const [loading, setLoading] = useState(false);
|
|
99
|
+
const [error, setError] = useState(null);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!appId)
|
|
102
|
+
return;
|
|
103
|
+
let cancelled = false;
|
|
104
|
+
setLoading(true);
|
|
105
|
+
setError(null);
|
|
106
|
+
const endpoint = endpointFor(fieldKey);
|
|
107
|
+
const params = new URLSearchParams({ app_id: appId });
|
|
108
|
+
if (since)
|
|
109
|
+
params.set('since', since);
|
|
110
|
+
if (endpoint === '/analytics/input-field')
|
|
111
|
+
params.set('field_key', fieldKey);
|
|
112
|
+
fetch(`${apiBase}${endpoint}?${params}`, { credentials: 'include' })
|
|
113
|
+
.then((r) => r.json().catch(() => null))
|
|
114
|
+
.then((body) => {
|
|
115
|
+
if (cancelled)
|
|
116
|
+
return;
|
|
117
|
+
const raw = body?.data;
|
|
118
|
+
const normalized = raw == null
|
|
119
|
+
? null
|
|
120
|
+
: endpoint === '/analytics/search-queries'
|
|
121
|
+
? normalizeSearchQueriesResult(raw)
|
|
122
|
+
: raw;
|
|
123
|
+
setData(normalized);
|
|
124
|
+
setLoading(false);
|
|
125
|
+
})
|
|
126
|
+
.catch((err) => {
|
|
127
|
+
if (cancelled)
|
|
128
|
+
return;
|
|
129
|
+
setError(err instanceof Error ? err.message : 'Load error');
|
|
130
|
+
setLoading(false);
|
|
131
|
+
});
|
|
132
|
+
return () => { cancelled = true; };
|
|
133
|
+
}, [appId, apiBase, fieldKey, since]);
|
|
134
|
+
// Resolve user profiles after data loads
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!data || !resolveUserProfiles || data.per_subject.length === 0)
|
|
137
|
+
return;
|
|
138
|
+
const ids = data.per_subject.map((s) => s.subject_id);
|
|
139
|
+
resolveUserProfiles(ids).then((resolved) => {
|
|
140
|
+
const map = new Map(resolved.map((p) => [p.id, p]));
|
|
141
|
+
setProfiles(map);
|
|
142
|
+
}).catch(() => { });
|
|
143
|
+
}, [data, resolveUserProfiles]);
|
|
144
|
+
if (loading)
|
|
145
|
+
return _jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading\u2026" });
|
|
146
|
+
if (error)
|
|
147
|
+
return _jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error });
|
|
148
|
+
if (!data)
|
|
149
|
+
return _jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No data yet." });
|
|
150
|
+
const isNumeric = data.numeric_summary != null;
|
|
151
|
+
const maxValueCount = data.top_values.length > 0 ? Math.max(...data.top_values.map((t) => t.count), 1) : 1;
|
|
152
|
+
const maxFlaggedCount = data.flagged_values.length > 0 ? Math.max(...data.flagged_values.map((t) => t.flagged), 1) : 1;
|
|
153
|
+
const maxDayCount = data.over_time.length > 0 ? Math.max(...data.over_time.map((d) => d.count), 1) : 1;
|
|
154
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsx(SummaryCard, { label: labels.totalLabel, value: data.total }), _jsx(SummaryCard, { label: labels.uniqueValuesLabel, value: data.unique_values }), _jsx(SummaryCard, { label: labels.uniqueSubjectsLabel, value: data.unique_subjects }), _jsx(SummaryCard, { label: labels.flaggedLabel, value: data.flagged_count })] }), isNumeric ? (_jsx(NumericSummarySection, { summary: data.numeric_summary, title: labels.distributionTitle })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: labels.topValuesTitle }), data.top_values.length === 0 ? (_jsx("p", { className: "text-xs text-gray-400", children: labels.emptyTopValues })) : (_jsx("div", { className: "flex flex-col gap-1", children: data.top_values.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-32 truncate", title: t.value, children: t.value }), _jsx(MiniBar, { value: t.count, max: maxValueCount }), _jsx("span", { className: "text-[10px] text-gray-400 tabular-nums whitespace-nowrap w-32 text-right", title: `Last seen ${t.lastAt}`, children: fmtTs(t.lastAt) })] }, t.value))) }))] }), data.flagged_values.length > 0 && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-4 flex flex-col gap-3", children: [_jsxs("h3", { className: "text-sm font-semibold text-amber-800", children: [labels.flaggedSectionTitle, " ", _jsx("span", { className: "text-xs font-normal text-amber-600", children: labels.flaggedSectionSubtitle })] }), _jsx("div", { className: "flex flex-col gap-1", children: data.flagged_values.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-amber-700 w-32 truncate", title: t.value, children: t.value }), _jsx(MiniBar, { value: t.flagged, max: maxFlaggedCount, color: "#f59e0b" }), _jsx("span", { className: "text-[10px] text-amber-600 tabular-nums whitespace-nowrap w-32 text-right", title: `Last seen ${t.lastAt}`, children: fmtTs(t.lastAt) })] }, t.value))) })] }))] })), data.over_time.length > 0 && (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: labels.overTimeTitle }), _jsx("div", { className: "flex flex-col gap-1", children: data.over_time.map((d) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[10px] text-gray-400 tabular-nums w-36 whitespace-nowrap", children: fmtTs(d.day) }), _jsx(MiniBar, { value: d.count, max: maxDayCount, color: "#10b981" })] }, d.day))) })] })), data.per_subject.length > 0 && (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: labels.perSubjectTitle }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-xs border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-left text-gray-400 border-b", children: [_jsx("th", { className: "py-1 pr-4 font-medium", children: labels.subjectColumnLabel }), _jsx("th", { className: "py-1 pr-4 font-medium tabular-nums", children: labels.countColumnLabel }), _jsx("th", { className: "py-1 pr-4 font-medium tabular-nums", children: labels.distinctColumnLabel }), _jsx("th", { className: "py-1 font-medium", children: labels.recentValuesColumnLabel })] }) }), _jsx("tbody", { children: data.per_subject.map((s) => {
|
|
155
|
+
const profile = profiles.get(s.subject_id);
|
|
156
|
+
const displayName = profile?.name ?? profile?.email ?? s.subject_id.slice(0, 8) + '…';
|
|
157
|
+
return (_jsxs("tr", { className: "border-b border-gray-50", children: [_jsxs("td", { className: "py-1 pr-4 text-gray-700", children: [profile?.avatar_url && (_jsx("img", { src: profile.avatar_url, alt: "", className: "inline-block w-4 h-4 rounded-full mr-1 align-middle" })), displayName] }), _jsx("td", { className: "py-1 pr-4 tabular-nums text-gray-600", children: s.count }), _jsx("td", { className: "py-1 pr-4 tabular-nums text-gray-600", children: s.distinct_values }), _jsxs("td", { className: "py-1 text-gray-500", children: [s.values.slice(0, 5).join(', '), s.values.length > 5 ? '…' : ''] })] }, s.subject_id));
|
|
158
|
+
}) })] }) })] }))] }));
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// NumericSummarySection — percentile strip + histogram, replaces the
|
|
162
|
+
// top-values list when the field is numeric (e.g. expense amounts).
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
function fmtNum(v) {
|
|
165
|
+
return (Math.round(v * 100) / 100).toLocaleString();
|
|
166
|
+
}
|
|
167
|
+
function NumericSummarySection({ summary, title }) {
|
|
168
|
+
const maxBucketCount = summary.histogram.length > 0 ? Math.max(...summary.histogram.map((b) => b.count), 1) : 1;
|
|
169
|
+
return (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-4", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: title }), _jsxs("div", { className: "grid grid-cols-3 md:grid-cols-6 gap-3", children: [_jsx(PercentileStat, { label: "Min", value: summary.min }), _jsx(PercentileStat, { label: "P25", value: summary.p25 }), _jsx(PercentileStat, { label: "Median", value: summary.median }), _jsx(PercentileStat, { label: "P75", value: summary.p75 }), _jsx(PercentileStat, { label: "Max", value: summary.max }), _jsx(PercentileStat, { label: "Mean", value: summary.mean })] }), summary.histogram.length > 0 && (_jsx("div", { className: "flex flex-col gap-1", children: summary.histogram.map((b) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[10px] text-gray-400 tabular-nums w-28 whitespace-nowrap", title: b.bucket, children: b.bucket }), _jsx(MiniBar, { value: b.count, max: maxBucketCount, color: "#8b5cf6" })] }, b.bucket))) }))] }));
|
|
170
|
+
}
|
|
171
|
+
function PercentileStat({ label, value }) {
|
|
172
|
+
return (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("p", { className: "text-[10px] text-gray-400 uppercase tracking-wide", children: label }), _jsx("p", { className: "text-sm font-semibold text-gray-800 tabular-nums", children: fmtNum(value) })] }));
|
|
173
|
+
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
id: string;
|
|
3
|
-
name?: string;
|
|
4
|
-
email?: string;
|
|
5
|
-
avatar_url?: string;
|
|
6
|
-
}
|
|
1
|
+
import type { UserProfile } from './input_field_panel.js';
|
|
7
2
|
export interface SearchQueriesPanelProps {
|
|
8
3
|
appId: string;
|
|
9
4
|
apiBase: string;
|
|
@@ -11,5 +6,4 @@ export interface SearchQueriesPanelProps {
|
|
|
11
6
|
resolveUserProfiles?: (ids: string[]) => Promise<UserProfile[]>;
|
|
12
7
|
}
|
|
13
8
|
export declare function SearchQueriesPanel({ appId, apiBase, since, resolveUserProfiles }: SearchQueriesPanelProps): import("react").JSX.Element;
|
|
14
|
-
export {};
|
|
15
9
|
//# sourceMappingURL=search_queries_panel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search_queries_panel.d.ts","sourceRoot":"","sources":["../../src/ui/search_queries_panel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"search_queries_panel.d.ts","sourceRoot":"","sources":["../../src/ui/search_queries_panel.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;CACjE;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,uBAAuB,+BAWzG"}
|
|
@@ -1,77 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
function MiniBar({ value, max, color = '#6366f1' }) {
|
|
14
|
-
const pct = max > 0 ? (value / max) * 100 : 0;
|
|
15
|
-
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1 h-2 rounded bg-gray-100 overflow-hidden", children: _jsx("div", { className: "h-full rounded", style: { width: `${pct}%`, backgroundColor: color } }) }), _jsx("span", { className: "text-xs tabular-nums text-gray-600 w-8 text-right", children: value })] }));
|
|
16
|
-
}
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// SummaryCard
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
function SummaryCard({ label, value }) {
|
|
21
|
-
return (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-1", children: [_jsx("p", { className: "text-[10px] text-gray-400 uppercase tracking-wide", children: label }), _jsx("p", { className: "text-2xl font-semibold text-gray-800 tabular-nums", children: value })] }));
|
|
22
|
-
}
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// hazo_umetrics/src/ui/search_queries_panel.tsx — back-compat preset.
|
|
4
|
+
//
|
|
5
|
+
// Generalized into ui/input_field_panel.tsx (input-analyzer spec §7). This
|
|
6
|
+
// file is now a thin wrapper around InputFieldPanel so existing consumer
|
|
7
|
+
// apps (gotimer/stocktools/kinstripe) keep importing SearchQueriesPanel with
|
|
8
|
+
// unchanged props, unchanged copy, and — critically — an unchanged wire
|
|
9
|
+
// format: InputFieldPanel special-cases fieldKey="search_query" to keep
|
|
10
|
+
// talking to the original GET /analytics/search-queries route rather than
|
|
11
|
+
// the new generic /analytics/input-field route. Zero behaviour change.
|
|
12
|
+
import { InputFieldPanel, SEARCH_LABELS } from './input_field_panel.js';
|
|
23
13
|
export function SearchQueriesPanel({ appId, apiBase, since, resolveUserProfiles }) {
|
|
24
|
-
|
|
25
|
-
const [profiles, setProfiles] = useState(new Map());
|
|
26
|
-
const [loading, setLoading] = useState(false);
|
|
27
|
-
const [error, setError] = useState(null);
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if (!appId)
|
|
30
|
-
return;
|
|
31
|
-
let cancelled = false;
|
|
32
|
-
setLoading(true);
|
|
33
|
-
setError(null);
|
|
34
|
-
const params = new URLSearchParams({ app_id: appId });
|
|
35
|
-
if (since)
|
|
36
|
-
params.set('since', since);
|
|
37
|
-
fetch(`${apiBase}/analytics/search-queries?${params}`, { credentials: 'include' })
|
|
38
|
-
.then((r) => r.json().catch(() => null))
|
|
39
|
-
.then((body) => {
|
|
40
|
-
if (cancelled)
|
|
41
|
-
return;
|
|
42
|
-
setData(body?.data ?? null);
|
|
43
|
-
setLoading(false);
|
|
44
|
-
})
|
|
45
|
-
.catch((err) => {
|
|
46
|
-
if (cancelled)
|
|
47
|
-
return;
|
|
48
|
-
setError(err instanceof Error ? err.message : 'Load error');
|
|
49
|
-
setLoading(false);
|
|
50
|
-
});
|
|
51
|
-
return () => { cancelled = true; };
|
|
52
|
-
}, [appId, apiBase, since]);
|
|
53
|
-
// Resolve user profiles after data loads
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (!data || !resolveUserProfiles || data.per_user.length === 0)
|
|
56
|
-
return;
|
|
57
|
-
const ids = data.per_user.map((u) => u.user_id);
|
|
58
|
-
resolveUserProfiles(ids).then((resolved) => {
|
|
59
|
-
const map = new Map(resolved.map((p) => [p.id, p]));
|
|
60
|
-
setProfiles(map);
|
|
61
|
-
}).catch(() => { });
|
|
62
|
-
}, [data, resolveUserProfiles]);
|
|
63
|
-
if (loading)
|
|
64
|
-
return _jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading\u2026" });
|
|
65
|
-
if (error)
|
|
66
|
-
return _jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error });
|
|
67
|
-
if (!data)
|
|
68
|
-
return _jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No data yet." });
|
|
69
|
-
const maxTermCount = data.top_terms.length > 0 ? Math.max(...data.top_terms.map((t) => t.count), 1) : 1;
|
|
70
|
-
const maxZeroCount = data.zero_result_terms.length > 0 ? Math.max(...data.zero_result_terms.map((t) => t.zeroResults), 1) : 1;
|
|
71
|
-
const maxDayCount = data.over_time.length > 0 ? Math.max(...data.over_time.map((d) => d.count), 1) : 1;
|
|
72
|
-
return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsx(SummaryCard, { label: "Total Searches", value: data.total_searches }), _jsx(SummaryCard, { label: "Unique Terms", value: data.unique_terms }), _jsx(SummaryCard, { label: "Unique Searchers", value: data.unique_searchers }), _jsx(SummaryCard, { label: "Zero-Result Searches", value: data.zero_result_searches })] }), _jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Top Search Terms" }), data.top_terms.length === 0 ? (_jsx("p", { className: "text-xs text-gray-400", children: "No searches recorded yet." })) : (_jsx("div", { className: "flex flex-col gap-1", children: data.top_terms.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-32 truncate", title: t.term, children: t.term }), _jsx(MiniBar, { value: t.count, max: maxTermCount }), _jsx("span", { className: "text-[10px] text-gray-400 tabular-nums whitespace-nowrap w-32 text-right", title: `Last searched ${t.lastSearchedAt}`, children: fmtTs(t.lastSearchedAt) })] }, t.term))) }))] }), data.zero_result_terms.length > 0 && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-4 flex flex-col gap-3", children: [_jsxs("h3", { className: "text-sm font-semibold text-amber-800", children: ["Zero-Result Terms ", _jsx("span", { className: "text-xs font-normal text-amber-600", children: "(content gaps)" })] }), _jsx("div", { className: "flex flex-col gap-1", children: data.zero_result_terms.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-amber-700 w-32 truncate", title: t.term, children: t.term }), _jsx(MiniBar, { value: t.zeroResults, max: maxZeroCount, color: "#f59e0b" }), _jsx("span", { className: "text-[10px] text-amber-600 tabular-nums whitespace-nowrap w-32 text-right", title: `Last searched ${t.lastSearchedAt}`, children: fmtTs(t.lastSearchedAt) })] }, t.term))) })] })), data.over_time.length > 0 && (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Searches Over Time" }), _jsx("div", { className: "flex flex-col gap-1", children: data.over_time.map((d) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[10px] text-gray-400 tabular-nums w-36 whitespace-nowrap", children: fmtTs(d.day) }), _jsx(MiniBar, { value: d.count, max: maxDayCount, color: "#10b981" })] }, d.day))) })] })), data.per_user.length > 0 && (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Per-User Searches" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-xs border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-left text-gray-400 border-b", children: [_jsx("th", { className: "py-1 pr-4 font-medium", children: "User" }), _jsx("th", { className: "py-1 pr-4 font-medium tabular-nums", children: "Searches" }), _jsx("th", { className: "py-1 pr-4 font-medium tabular-nums", children: "Distinct Terms" }), _jsx("th", { className: "py-1 font-medium", children: "Recent Terms" })] }) }), _jsx("tbody", { children: data.per_user.map((u) => {
|
|
73
|
-
const profile = profiles.get(u.user_id);
|
|
74
|
-
const displayName = profile?.name ?? profile?.email ?? u.user_id.slice(0, 8) + '…';
|
|
75
|
-
return (_jsxs("tr", { className: "border-b border-gray-50", children: [_jsxs("td", { className: "py-1 pr-4 text-gray-700", children: [profile?.avatar_url && (_jsx("img", { src: profile.avatar_url, alt: "", className: "inline-block w-4 h-4 rounded-full mr-1 align-middle" })), displayName] }), _jsx("td", { className: "py-1 pr-4 tabular-nums text-gray-600", children: u.count }), _jsx("td", { className: "py-1 pr-4 tabular-nums text-gray-600", children: u.distinct_terms }), _jsxs("td", { className: "py-1 text-gray-500", children: [u.terms.slice(0, 5).join(', '), u.terms.length > 5 ? '…' : ''] })] }, u.user_id));
|
|
76
|
-
}) })] }) })] }))] }));
|
|
14
|
+
return (_jsx(InputFieldPanel, { appId: appId, apiBase: apiBase, fieldKey: "search_query", labels: SEARCH_LABELS, since: since, resolveUserProfiles: resolveUserProfiles }));
|
|
77
15
|
}
|