hazo_umetrics 1.11.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 CHANGED
@@ -1,5 +1,53 @@
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
+
34
+ ## 1.12.0 — 404 / broken-link analytics
35
+
36
+ **New behavioral cut for `action.404` events (minor — purely additive; no existing signatures changed):**
37
+
38
+ ### Server (`src/server/behavior.ts`)
39
+ - `notFoundQueries(adapter, opts)` — app-side aggregation over `action.404` events (mirrors `searchQueries`). Reuses the existing `fetchWindow`/`parseDims`/`tsBucket`/`defaultSince` helpers. Filters `metric_key === 'action.404'`, skips rows with an empty/missing `requested_path`, defaults `referrer_kind` to `direct` and folds unrecognized kinds into `external`.
40
+ - `NotFoundQueriesResult`: `total_404s`, `unique_paths`, `internal_referrer_count`, `top_paths` (`{ path, count, lastAt, internalCount, sampleReferrer }`, desc by count), `by_source` (`{ internal, search, external, direct }`), `over_time` (`EventDayBucket[]`, second-precision, most-recent 100 chronological). New exported types `NotFoundPathEntry`, `NotFoundSourceBreakdown`.
41
+
42
+ ### API (`src/api/index.ts`)
43
+ - `createBehaviorHandlers` gains `getNotFound(req)` — `GET /...?app_id=<id>[&since=<iso>][&top_n=<n>]`, requires `metrics.view`, returns `jsonOk(result)`. No per-user cut (404s have no `user_ids` param).
44
+
45
+ ### UI (`src/ui/not_found_panel.tsx`, new; exported from `src/ui/index.ts`)
46
+ - `NotFoundPanel({ appId, apiBase })` — self-contained window tabs (Total / 24h / 7d / 28d / 90d), summary cards (Total 404s / Unique Paths / Internal Broken Links / Direct-Typed), Top Missing Paths (amber "broken internal link" badge when `internalCount > 0`), Traffic Source breakdown, and 404s Over Time. Consumes `GET {apiBase}/analytics/not-found`.
47
+
48
+ ### Tests
49
+ - `__tests__/not_found.test.ts` — mirrors `search_queries.test.ts` (counts, `internal_referrer_count`, `by_source`, `top_paths` ordering, empty-path skip).
50
+
3
51
  ## 1.11.0 — M1 GA4 live (OAuth connect + Admin provisioner + Data API funnels + windowed ga4 metrics)
4
52
 
5
53
  **M1 GA4 half shipped (minor — stubs replaced with real implementations; signatures widened, not broken):**
@@ -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
  /**
@@ -208,5 +227,18 @@ export declare function createBehaviorHandlers(options: ApiFactoryOptions): {
208
227
  * Returns search query analytics. Requires metrics.view.
209
228
  */
210
229
  getSearchQueries(req: Request): Promise<Response>;
230
+ /**
231
+ * GET /...?app_id=<id>[&since=<iso>][&top_n=<n>]
232
+ * Returns 404 / broken-link analytics. Requires metrics.view.
233
+ */
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>;
211
243
  };
212
244
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAuC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAU1D,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE;IAAE,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAC3E,OAAO,CAAC;IACX,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAC;IACnE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C;;;;;;;;;;;;;;;;OAgBG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACpD;AAiGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB;IAIzD;;;OAGG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqB9C;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBpD;;;;OAIG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA4CjD;;;;OAIG;0BACyB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA4B1D;AAqBD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAW/D;;;OAGG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqBtD;;;;OAIG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAiDlD;;;;OAIG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA8CtD;;;;OAIG;wBACuB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA8CxD;AAsCD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAW/D;;;;;;;;OAQG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA0F9C;;;;;OAKG;mBACkB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoFhD;;;;OAIG;gBACe,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAuDhD;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB;IAI1D,oDAAoD;eACnC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAe/C;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,qBAAqB,CAAC;IAEtG;;;;;;;;;OASG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA6CrD;AAMD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB;IAI3D;;;OAGG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBlD;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBpD;;;;;OAKG;sBACqB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkDnD;;;;;OAKG;2BAC0B,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAsE3D;AAMD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB;IAI7D;;;OAGG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAyBrD;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB;IAIzD;;;OAGG;kBACiB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkB/C;;;;OAIG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoC9C;;;;OAIG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAiCpD;AAMD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB;IAI7D;;;;OAIG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA0DlD;;;OAGG;2BAC0B,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAyBxD;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqCpD;;;OAGG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA2BjD;;;OAGG;0BACyB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAsBvD;;;OAGG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBlD;;;;;;OAMG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAyCvD"}
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, } 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';
@@ -1144,5 +1144,73 @@ export function createBehaviorHandlers(options) {
1144
1144
  const result = await _searchQueries(adapter, { app_id, since, topN, userIds });
1145
1145
  return jsonOk(result);
1146
1146
  },
1147
+ /**
1148
+ * GET /...?app_id=<id>[&since=<iso>][&top_n=<n>]
1149
+ * Returns 404 / broken-link analytics. Requires metrics.view.
1150
+ */
1151
+ async getNotFound(req) {
1152
+ const url = new URL(req.url);
1153
+ const app_id = url.searchParams.get('app_id') ?? '';
1154
+ const since = url.searchParams.get('since') ?? undefined;
1155
+ const topNParam = url.searchParams.get('top_n');
1156
+ const topN = topNParam != null ? parseInt(topNParam, 10) : undefined;
1157
+ const gate = await gateAuth(req, {
1158
+ required_permissions: ['metrics.view'],
1159
+ app_id: app_id || undefined,
1160
+ getAuth,
1161
+ });
1162
+ if (!gate.ok)
1163
+ return gate.response;
1164
+ if (!app_id)
1165
+ return jsonError('BAD_REQUEST', 'app_id is required', 400);
1166
+ const adapter = await options.getAdapter();
1167
+ const result = await _notFoundQueries(adapter, { app_id, since, topN });
1168
+ return jsonOk(result);
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
+ },
1147
1215
  };
1148
1216
  }
@@ -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;AAmDpE,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"}
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"}
@@ -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 = crypto.randomUUID();
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;
@@ -184,4 +240,28 @@ export interface SearchQueriesResult {
184
240
  export declare function searchQueries(adapter: HazoConnectAdapter, opts: BehaviorOptions & {
185
241
  topN?: number;
186
242
  }): Promise<SearchQueriesResult>;
243
+ export interface NotFoundPathEntry {
244
+ path: string;
245
+ count: number;
246
+ lastAt: string;
247
+ internalCount: number;
248
+ sampleReferrer: string;
249
+ }
250
+ export interface NotFoundSourceBreakdown {
251
+ internal: number;
252
+ search: number;
253
+ external: number;
254
+ direct: number;
255
+ }
256
+ export interface NotFoundQueriesResult {
257
+ total_404s: number;
258
+ unique_paths: number;
259
+ internal_referrer_count: number;
260
+ top_paths: NotFoundPathEntry[];
261
+ by_source: NotFoundSourceBreakdown;
262
+ over_time: EventDayBucket[];
263
+ }
264
+ export declare function notFoundQueries(adapter: HazoConnectAdapter, opts: BehaviorOptions & {
265
+ topN?: number;
266
+ }): Promise<NotFoundQueriesResult>;
187
267
  //# sourceMappingURL=behavior.d.ts.map
@@ -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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"}
@@ -586,94 +586,249 @@ 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
- export async function searchQueries(adapter, opts) {
590
- const { app_id, scope_id, since, limit = 10000 } = opts;
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 rows = await fetchWindow(adapter, { app_id, scope_id, since: since ?? defaultSince(), cap: limit });
593
- const searchRows = rows.filter((r) => r.metric_key === 'action.search');
594
- const termMap = new Map();
595
- const dayMap = new Map();
596
- const userMap = new Map();
597
- let zeroResultSearches = 0;
598
- const allSearchers = new Set();
599
- for (const row of searchRows) {
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 rawTerm = typeof dims.term === 'string' ? dims.term : String(dims.term ?? '');
602
- if (!rawTerm)
653
+ const rawValue = typeof dims[value_dim] === 'string' ? dims[value_dim] : String(dims[value_dim] ?? '');
654
+ if (!rawValue)
603
655
  continue;
604
- const normTerm = rawTerm.trim().toLowerCase();
605
- const uid = dims.user_id;
606
- const sid = dims.session_id;
607
- const identity = uid ?? sid;
608
- const resultCount = dims.result_count != null ? Number(dims.result_count) : null;
609
- const isZero = resultCount === 0;
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
- allSearchers.add(identity);
612
- // Term accumulator
613
- if (!termMap.has(normTerm)) {
614
- termMap.set(normTerm, {
664
+ allSubjects.add(identity);
665
+ // Value accumulator
666
+ if (!valueMap.has(normValue)) {
667
+ valueMap.set(normValue, {
615
668
  count: 0,
616
- searchers: new Set(),
617
- zeroResults: 0,
618
- firstCasing: rawTerm.trim(),
669
+ subjects: new Set(),
670
+ flagged: 0,
671
+ firstCasing: rawValue.trim(),
619
672
  lastAt: row.captured_at,
620
673
  });
621
674
  }
622
- const acc = termMap.get(normTerm);
675
+ const acc = valueMap.get(normValue);
623
676
  acc.count++;
624
677
  if (identity)
625
- acc.searchers.add(identity);
626
- if (isZero) {
627
- acc.zeroResults++;
628
- zeroResultSearches++;
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
- dayMap.set(ts, (dayMap.get(ts) ?? 0) + 1);
635
- // Per-user
636
- if (uid) {
637
- if (!userMap.has(uid))
638
- userMap.set(uid, { count: 0, terms: new Set() });
639
- const ua = userMap.get(uid);
640
- ua.count++;
641
- ua.terms.add(normTerm);
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 allTermEntries = [...termMap.entries()].map(([, acc]) => ({
645
- term: acc.firstCasing,
703
+ const allValueEntries = [...valueMap.entries()].map(([, acc]) => ({
704
+ value: acc.firstCasing,
646
705
  count: acc.count,
647
- searchers: acc.searchers.size,
648
- zeroResults: acc.zeroResults,
649
- lastSearchedAt: acc.lastAt,
706
+ subjects: acc.subjects.size,
707
+ flagged: acc.flagged,
708
+ lastAt: acc.lastAt,
650
709
  }));
651
- const top_terms = [...allTermEntries].sort((a, b) => b.count - a.count).slice(0, topN_);
652
- const zero_result_terms = allTermEntries
653
- .filter((e) => e.zeroResults > 0)
654
- .sort((a, b) => b.zeroResults - a.zeroResults)
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 = [...dayMap.entries()]
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 per_user = [...userMap.entries()]
662
- .map(([user_id, { count, terms }]) => ({
663
- user_id,
720
+ const per_subject = [...subjectMap.entries()]
721
+ .map(([subject_id, { count, values }]) => ({
722
+ subject_id,
664
723
  count,
665
- distinct_terms: terms.size,
666
- terms: [...terms],
724
+ distinct_values: values.size,
725
+ values: [...values],
667
726
  }))
668
727
  .sort((a, b) => b.count - a.count);
669
- return {
670
- total_searches: searchRows.length,
671
- unique_terms: termMap.size,
672
- unique_searchers: allSearchers.size,
673
- zero_result_searches: zeroResultSearches,
674
- top_terms,
675
- zero_result_terms,
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
- per_user,
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
+ })),
767
+ };
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
+ }
782
+ export async function notFoundQueries(adapter, opts) {
783
+ const { app_id, scope_id, since, limit = 10000 } = opts;
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
+ });
796
+ const rows = await fetchWindow(adapter, { app_id, scope_id, since: since ?? defaultSince(), cap: limit });
797
+ const notFoundRows = rows.filter((r) => r.metric_key === 'action.404');
798
+ const bySource = { internal: 0, search: 0, external: 0, direct: 0 };
799
+ const sampleReferrerMap = new Map();
800
+ let total404s = 0;
801
+ for (const row of notFoundRows) {
802
+ const dims = parseDims(row.dimensions);
803
+ const requestedPath = typeof dims.requested_path === 'string' ? dims.requested_path : '';
804
+ if (!requestedPath)
805
+ continue;
806
+ total404s++;
807
+ const referrerKind = typeof dims.referrer_kind === 'string' && dims.referrer_kind ? dims.referrer_kind : 'direct';
808
+ const referrer = typeof dims.referrer === 'string' ? dims.referrer : '';
809
+ if (referrerKind === 'internal' || referrerKind === 'search' || referrerKind === 'external' || referrerKind === 'direct') {
810
+ bySource[referrerKind]++;
811
+ }
812
+ else {
813
+ bySource.external++;
814
+ }
815
+ if (!sampleReferrerMap.has(requestedPath) && referrer) {
816
+ sampleReferrerMap.set(requestedPath, referrer);
817
+ }
818
+ }
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
+ }));
826
+ return {
827
+ total_404s: total404s,
828
+ unique_paths: generic.unique_values,
829
+ internal_referrer_count: generic.flagged_count,
830
+ top_paths,
831
+ by_source: bySource,
832
+ over_time: generic.over_time,
678
833
  };
679
834
  }
@@ -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;AAE/C,MAAM,WAAW,SAAS;IACxB,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,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,cAAc,GAAG,QAAQ,GAAG,UAAU,CAAC;IAC/C,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,gBAAgB,EAAE,eAAe,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,aAAa,GAAG,YAAY,GAAG,KAAK,GAAG,cAAc,GAAG,QAAQ,GAAG,kBAAkB,CAAC,CAAC,CAAC;AAEvJ,MAAM,WAAW,UAAW,SAAQ,SAAS;IAC3C,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1B,MAAM,EAAE,IAAI,GAAG,KAAK,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC5B,MAAM,EAAE,IAAI,GAAG,KAAK,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,iBAAiB,EAAE,CAAC;CAC7B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@ 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';
9
+ export * from './not_found_panel.js';
8
10
  export * from './ga4_connection_card.js';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -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,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,5 +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';
11
+ export * from './not_found_panel.js';
10
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
+ }
@@ -0,0 +1,6 @@
1
+ export interface NotFoundPanelProps {
2
+ appId: string;
3
+ apiBase: string;
4
+ }
5
+ export declare function NotFoundPanel({ appId, apiBase }: NotFoundPanelProps): import("react").JSX.Element;
6
+ //# sourceMappingURL=not_found_panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"not_found_panel.d.ts","sourceRoot":"","sources":["../../src/ui/not_found_panel.tsx"],"names":[],"mappings":"AA4FA,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,kBAAkB,+BAiKnE"}
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from 'react';
4
+ const WINDOW_KEYS = ['Total', '24h', '7d', '28d', '90d'];
5
+ function windowKeyToSince(key) {
6
+ if (key === 'Total')
7
+ return undefined;
8
+ const days = key === '24h' ? 1 : key === '7d' ? 7 : key === '28d' ? 28 : 90;
9
+ const d = new Date();
10
+ d.setDate(d.getDate() - days);
11
+ return d.toISOString();
12
+ }
13
+ // ---------------------------------------------------------------------------
14
+ // MiniBar
15
+ // ---------------------------------------------------------------------------
16
+ // Format a stored timestamp ("…T…Z" or "YYYY-MM-DD HH:MM:SS") to second precision.
17
+ function fmtTs(iso) {
18
+ if (!iso)
19
+ return '';
20
+ return iso.slice(0, 19).replace('T', ' ');
21
+ }
22
+ function MiniBar({ value, max, color = '#6366f1' }) {
23
+ const pct = max > 0 ? (value / max) * 100 : 0;
24
+ 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 })] }));
25
+ }
26
+ // ---------------------------------------------------------------------------
27
+ // SummaryCard
28
+ // ---------------------------------------------------------------------------
29
+ function SummaryCard({ label, value }) {
30
+ 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 })] }));
31
+ }
32
+ export function NotFoundPanel({ appId, apiBase }) {
33
+ const [windowKey, setWindowKey] = useState('Total');
34
+ const [data, setData] = useState(null);
35
+ const [loading, setLoading] = useState(false);
36
+ const [error, setError] = useState(null);
37
+ const since = windowKeyToSince(windowKey);
38
+ useEffect(() => {
39
+ if (!appId)
40
+ return;
41
+ let cancelled = false;
42
+ setLoading(true);
43
+ setError(null);
44
+ const params = new URLSearchParams({ app_id: appId });
45
+ if (since)
46
+ params.set('since', since);
47
+ fetch(`${apiBase}/analytics/not-found?${params}`, { credentials: 'include' })
48
+ .then((r) => r.json().catch(() => null))
49
+ .then((body) => {
50
+ if (cancelled)
51
+ return;
52
+ setData(body?.data ?? null);
53
+ setLoading(false);
54
+ })
55
+ .catch((err) => {
56
+ if (cancelled)
57
+ return;
58
+ setError(err instanceof Error ? err.message : 'Load error');
59
+ setLoading(false);
60
+ });
61
+ return () => { cancelled = true; };
62
+ }, [appId, apiBase, since]);
63
+ const windowTabs = (_jsx("div", { className: "flex flex-wrap gap-2", children: WINDOW_KEYS.map((key) => (_jsx("button", { type: "button", onClick: () => setWindowKey(key), className: key === windowKey
64
+ ? 'px-3 py-1 rounded text-xs font-medium bg-gray-800 text-white'
65
+ : 'px-3 py-1 rounded text-xs font-medium border border-gray-200 text-gray-500 hover:bg-gray-50', children: key }, key))) }));
66
+ if (loading) {
67
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [windowTabs, _jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading\u2026" })] }));
68
+ }
69
+ if (error) {
70
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [windowTabs, _jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error })] }));
71
+ }
72
+ if (!data) {
73
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [windowTabs, _jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No data yet." })] }));
74
+ }
75
+ const maxPathCount = data.top_paths.length > 0 ? Math.max(...data.top_paths.map((p) => p.count), 1) : 1;
76
+ const maxDayCount = data.over_time.length > 0 ? Math.max(...data.over_time.map((d) => d.count), 1) : 1;
77
+ const maxSourceCount = Math.max(data.by_source.internal, data.by_source.search, data.by_source.external, data.by_source.direct, 1);
78
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [windowTabs, _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsx(SummaryCard, { label: "Total 404s", value: data.total_404s }), _jsx(SummaryCard, { label: "Unique Paths", value: data.unique_paths }), _jsx(SummaryCard, { label: "Internal Broken Links", value: data.internal_referrer_count }), _jsx(SummaryCard, { label: "Direct / Typed", value: data.by_source.direct })] }), _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 Missing Paths" }), data.top_paths.length === 0 ? (_jsx("p", { className: "text-xs text-gray-400", children: "No 404s recorded yet." })) : (_jsx("div", { className: "flex flex-col gap-1", children: data.top_paths.map((p) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-40 truncate", title: p.path, children: p.path }), p.internalCount > 0 && (_jsx("span", { className: "text-[10px] font-medium px-1.5 py-0.5 rounded-full border border-amber-200 bg-amber-50 text-amber-800 whitespace-nowrap", children: "broken internal link" })), _jsx(MiniBar, { value: p.count, max: maxPathCount }), _jsx("span", { className: "text-[10px] text-gray-400 tabular-nums whitespace-nowrap", title: `Last seen ${p.lastAt}`, children: fmtTs(p.lastAt) })] }, p.path))) }))] }), _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: "Traffic Source" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-40", children: "Internal (broken links)" }), _jsx(MiniBar, { value: data.by_source.internal, max: maxSourceCount, color: "#f59e0b" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-40", children: "Search" }), _jsx(MiniBar, { value: data.by_source.search, max: maxSourceCount })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-40", children: "External" }), _jsx(MiniBar, { value: data.by_source.external, max: maxSourceCount })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-40", children: "Direct" }), _jsx(MiniBar, { value: data.by_source.direct, max: maxSourceCount })] })] })] }), 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: "404s 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))) })] }))] }));
79
+ }
@@ -1,9 +1,4 @@
1
- interface UserProfile {
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":"AA4BA,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoDD,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,+BAmJzG"}
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, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect } from 'react';
4
- // ---------------------------------------------------------------------------
5
- // MiniBar
6
- // ---------------------------------------------------------------------------
7
- // Format a stored timestamp ("…T…Z" or "YYYY-MM-DD HH:MM:SS") to second precision.
8
- function fmtTs(iso) {
9
- if (!iso)
10
- return '';
11
- return iso.slice(0, 19).replace('T', ' ');
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
- const [data, setData] = useState(null);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_umetrics",
3
- "version": "1.11.0",
3
+ "version": "1.13.1",
4
4
  "description": "Product analytics for hazo apps — GA4 hybrid + first-party stat store + feature flags",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -51,11 +51,11 @@
51
51
  "hazo_logs": "^2.1.1",
52
52
  "hazo_config": "^2.4.1",
53
53
  "hazo_connect": "^3.9.2",
54
- "hazo_api": "^2.5.1",
55
- "hazo_auth": "^10.8.2",
54
+ "hazo_api": "^2.6.0",
55
+ "hazo_auth": "^10.9.0",
56
56
  "hazo_secure": "^1.4.0",
57
57
  "hazo_audit": "^2.1.2",
58
- "hazo_ui": "^6.0.0",
58
+ "hazo_ui": "^6.1.0",
59
59
  "next": "^14.0.0 || ^16.0.0",
60
60
  "react": "^18.0.0 || ^19.0.0",
61
61
  "react-dom": "^18.0.0 || ^19.0.0"