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 +48 -0
- package/dist/api/index.d.ts +32 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +69 -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 +80 -0
- package/dist/server/behavior.d.ts.map +1 -1
- package/dist/server/behavior.js +216 -61
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +2 -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/not_found_panel.d.ts +6 -0
- package/dist/ui/not_found_panel.d.ts.map +1 -0
- package/dist/ui/not_found_panel.js +79 -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 +4 -4
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):**
|
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
|
/**
|
|
@@ -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
|
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, } 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
|
}
|
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;
|
|
@@ -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":"AAWA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAsB7D,MAAM,MAAM,WAAW,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAM7F,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA2ED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAgCxB;AAMD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAkDxB;AAMD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC,WAAW,EAAE,CAAC,CAmCxB;AAMD;;GAEG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAexB;AAMD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,UAAU,EAAE,CAAC,CAmBvB;AAMD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CA8BxB;AAMD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAgBxB;AAMD;;;GAGG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,cAAc,EAAE,CAAC,CAoC3B;AAMD,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd;8EAC0E;IAC1E,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,KAAK,EAAE,YAAY,CAAC;CACrB;AAWD,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;mFAC+E;IAC/E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;IACjF;kFAC8E;IAC9E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;iDAE6C;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GACA,OAAO,CAAC,aAAa,CAAC,CA+JxB;AAMD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,GAAG,aAAa,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACvD,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,iBAAiB,EAAE,CAAC;CAC3B;AAID,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B,GACA,OAAO,CAAC,oBAAoB,CAAC,CAqF/B;AAwBD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;+EAI2E;IAC3E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,cAAc,EAAE,eAAe,EAAE,CAAC;IAClC,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,WAAW,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrG,eAAe,CAAC,EAAE,cAAc,CAAC;CAClC;AAmDD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAyH3B;AAMD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC9F;AA8BD,wBAAsB,aAAa,CACjC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,mBAAmB,CAAC,CAY9B;AAeD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,EAAE,MAAM,CAAC;IAChC,SAAS,EAAE,iBAAiB,EAAE,CAAC;IAC/B,SAAS,EAAE,uBAAuB,CAAC;IACnC,SAAS,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,eAAe,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,qBAAqB,CAAC,CA6DhC"}
|
package/dist/server/behavior.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
})),
|
|
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
|
}
|
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,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
|
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,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 @@
|
|
|
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
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_umetrics",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
55
|
-
"hazo_auth": "^10.
|
|
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.
|
|
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"
|