hazo_umetrics 1.4.0 → 1.5.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.5.1 — `ui.pageexit.*` rename, smart auto-labels, `defaultActionLabels`, `ActionsPanel.refreshTrigger`
4
+
5
+ **Fixes / improvements (patch — no breaking changes for new installs; existing `ui.dwell.*` catalog rows need re-seeding):**
6
+
7
+ ### Rename: `ui.dwell.*` → `ui.pageexit.*`
8
+ - `HazoMetricsProvider` now fires `ui.pageexit.<slug>` (instead of `ui.dwell.<slug>`) on path change and page hide. The name better describes the event: it fires when a user *leaves* a page, carrying the foreground dwell time in `dims.value`.
9
+ - `tabDwell` behavior aggregation updated to match — reads `ui.pageexit.*` from the stat store.
10
+ - Seed and test-app updated accordingly. Existing `ui.dwell.*` rows in the catalog are inert; re-seed or rename manually.
11
+
12
+ ### Smarter auto-labels for `ui.*` events
13
+ - `ensureActionDef` now uses `deriveLabel()` instead of the generic `humanize()` for auto-created catalog rows. `ui.pageview.*` → `PageEntry:<Page>`, `ui.pageexit.*` → `PageExit:<Page>`, `ui.click.*` → `Click:<Label>`, `action.*` → `Action:<Label>`. Keeps auto-created rows recognizable without manual editing.
14
+
15
+ ### New: `defaultActionLabels` option on `createEventHandlers`
16
+ - Pass `defaultActionLabels: Record<string, string>` to `createEventHandlers` to override the derived label for specific event keys on first ingest. Useful for wiring friendly display names before an operator touches the catalog:
17
+ ```ts
18
+ createEventHandlers({
19
+ getAdapter: () => myAdapter,
20
+ defaultActionLabels: {
21
+ 'ui.pageview.root': 'PageEntry:Homepage',
22
+ 'ui.click.sign_in': 'Click:Sign In',
23
+ },
24
+ })
25
+ ```
26
+
27
+ ### New: `ActionsPanel.refreshTrigger` prop
28
+ - `ActionsPanel` now accepts an optional `refreshTrigger?: number` prop. Increment it from the parent to trigger an external catalog refresh (e.g. after seeding events via a button click), without requiring the user to scroll back to a manual refresh control.
29
+
30
+ ### Edit form: shows technical name
31
+ - The `ActionsPanel` inline edit form now shows a read-only "Technical name" field above the label input, so operators know which `action_key` they're renaming. Also adds a placeholder to the label input showing the raw key.
32
+
33
+ ---
34
+
35
+ ## 1.4.1 — Self-contained journey flow graph, journey user filter, smarter datapoint default
36
+
37
+ **Fixes / improvements (patch — no breaking API changes):**
38
+
39
+ ### JourneyPanel: inline-SVG flow graph (replaces hazo_canvas)
40
+ - `JourneyPanel` now renders a self-contained inline-SVG left-to-right DAG. Removed the optional `hazo_canvas` peer dependency entirely — no external graph engine needed.
41
+ - Layout uses **shortest-path BFS** ranking (source nodes on the left, depth growing rightward). Journey graphs are cyclic (users revisit steps); the previous longest-path approach inflated every node to the same column and could grow its queue without bound (`RangeError: Invalid array length`). Shortest-path sets each node's rank once, so columns stay stable and bounded.
42
+ - Node fill encodes step `kind` (action / pageview / error); node opacity scales with visit count. Edge stroke width + opacity scale with transition count; counts >1 are labelled.
43
+
44
+ ### JourneyPanel: built-in user filter
45
+ - Added a user-filter dropdown (top-right of the panel). "All users" by default; selecting a user refetches the journey scoped to `&user_id=<id>`.
46
+ - The known-user option list only grows across loads, so it stays stable while a single-user filter is active. Falls back to the `userId` prop when the internal filter is unset.
47
+
48
+ ### DatapointPanel: smarter default action selection
49
+ - The action dropdown now defaults to the most useful action: first one with a declared `datapoint_schema`, else the first app-domain action (skipping auto-tracked `btn.*` / `ui.*` events), else the first action. Avoids landing on an empty auto-tracked event with no dimensions.
50
+
3
51
  ## 1.4.0 — Bulk catalog edits, per-action datapoints, user-journey viz, real user identity
4
52
 
5
53
  **New features (minor bump — no breaking changes):**
package/README.md CHANGED
@@ -103,7 +103,7 @@ Seven behavioral panels answer:
103
103
  - **Post re-login actions** — what users do on their second session
104
104
  - **Before "share"** — the N actions preceding a goal event (configurable goal key)
105
105
  - **Common errors** — ranked `ui.error.*` frequency
106
- - **Time per tab** — real foreground dwell time per path (from `ui.dwell.*` events)
106
+ - **Time per tab** — real foreground dwell time per path (from `ui.pageexit.*` events)
107
107
  - **Returning-user actions** — most common actions for users with >1 session
108
108
 
109
109
  For `user_id` to be captured, call `identify(userId)` after login or mount `UserIdentifier` (see test-app `src/components/user_identifier.tsx`).
@@ -123,7 +123,7 @@ const { trackAction, trackError } = useMetrics();
123
123
  trackAction('timer.complete', { url: '/timer' });
124
124
  ```
125
125
 
126
- Auto-tracked on mount: page views (`ui.pageview.*`), clicks (`ui.click.*`), errors (`ui.error.*`), dwell time (`ui.dwell.*`).
126
+ Auto-tracked on mount: page views (`ui.pageview.*`), clicks (`ui.click.*`), errors (`ui.error.*`), page-exit dwell time (`ui.pageexit.*`).
127
127
 
128
128
  ## Bulk Action Catalog edits
129
129
 
@@ -201,7 +201,7 @@ import { JourneyPanel } from 'hazo_umetrics/ui';
201
201
  />
202
202
  ```
203
203
 
204
- `JourneyPanel` renders a dagre LR graph via `hazo_canvas` when installed (`npm install hazo_canvas@^1.9.1`). Without `hazo_canvas`, it falls back to an ordered step list. Dagre edge timing labels require `hazo_canvas 1.9.1`.
204
+ `JourneyPanel` renders a self-contained inline-SVG flow graph a left-to-right DAG laid out by shortest-path BFS (source nodes on the left, depth growing rightward). Node fill encodes step `kind` (action / pageview / error) and opacity scales with visit count; edge stroke width and opacity scale with transition count. No external graph dependency is required. A built-in **user filter** dropdown (top-right of the panel) scopes the graph and session list to a single user; it refetches with `&user_id=<id>`. The known-user option list only grows, so it stays stable while a filter is active. Below the graph, per-session collapsible step lists show timing between steps.
205
205
 
206
206
  ## Datapoint analytics
207
207
 
@@ -211,7 +211,7 @@ import { JourneyPanel } from 'hazo_umetrics/ui';
211
211
  // Response: { ok: true, data: { action_key, total_events, keys: DatapointKeyStats[] } }
212
212
  ```
213
213
 
214
- Each entry in `keys` describes one dimension: `{ key, type, total, histogram | topValues }`. Use the `DatapointPanel` component to render these automatically.
214
+ Each entry in `keys` describes one dimension: `{ key, type, total, histogram | topValues }`. Use the `DatapointPanel` component to render these automatically. The panel's action dropdown defaults to the most useful action: first an action with a declared `datapoint_schema`, else the first app-domain action (skipping auto-tracked `btn.*` / `ui.*` events), else the first action — so a user lands on data-bearing dimensions instead of an empty auto-tracked event.
215
215
 
216
216
  ## resolveUserProfiles — wiring user identity
217
217
 
@@ -247,6 +247,42 @@ export async function POST(req: Request) {
247
247
  }
248
248
  ```
249
249
 
250
+ ## Event ingestion — `defaultActionLabels`
251
+
252
+ Pass `defaultActionLabels` to `createEventHandlers` to declare human-readable labels for auto-tracked `ui.*` events before an operator edits the catalog. On first ingest of a key, the label is stored instead of the derived `PageEntry:*` / `Click:*` fallback:
253
+
254
+ ```ts
255
+ import { createEventHandlers } from 'hazo_umetrics/api';
256
+
257
+ const handlers = createEventHandlers({
258
+ getAdapter: () => myAdapter,
259
+ defaultActionLabels: {
260
+ 'ui.pageview.root': 'Homepage',
261
+ 'ui.pageview.timer': 'Timer Page',
262
+ 'ui.click.sign_in': 'Sign In Button',
263
+ 'ui.pageexit.root': 'Homepage Exit',
264
+ },
265
+ });
266
+
267
+ // app/api/hazo_umetrics/events/route.ts
268
+ export const POST = (req: Request) => handlers.recordEvent(req);
269
+ ```
270
+
271
+ ## Action Catalog panel — `refreshTrigger`
272
+
273
+ `ActionsPanel` accepts an optional `refreshTrigger` prop. Increment it to force a catalog reload from the parent (e.g. after seeding events):
274
+
275
+ ```tsx
276
+ import { ActionsPanel } from 'hazo_umetrics/ui';
277
+
278
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
279
+
280
+ // After seeding:
281
+ setRefreshTrigger(n => n + 1);
282
+
283
+ <ActionsPanel appId="my-app" refreshTrigger={refreshTrigger} />
284
+ ```
285
+
250
286
  ## API route factories
251
287
 
252
288
  ```ts
@@ -21,6 +21,22 @@ export interface ApiFactoryOptions {
21
21
  app_id: string;
22
22
  details?: unknown;
23
23
  }) => Promise<void>;
24
+ /**
25
+ * Default human-readable labels for auto-tracked events.
26
+ * When an event key is first seen and a catalog entry is auto-created,
27
+ * the matching label is used instead of the raw technical name.
28
+ *
29
+ * Example:
30
+ * ```ts
31
+ * defaultActionLabels: {
32
+ * 'ui.pageview.root': 'Homepage',
33
+ * 'ui.pageview.timer': 'Timer Page',
34
+ * 'ui.click.sign_in': 'Sign In Button',
35
+ * 'ui.pageexit.root': 'Homepage Exit',
36
+ * }
37
+ * ```
38
+ */
39
+ defaultActionLabels?: Record<string, string>;
24
40
  }
25
41
  export declare function createStatHandlers(options: ApiFactoryOptions): {
26
42
  /**
@@ -75,7 +91,7 @@ export declare function createQueryHandlers(options: ApiFactoryOptions): {
75
91
  /** STUB (M1): Execute a GA4 or stat-store query. */
76
92
  query(req: Request): Promise<Response>;
77
93
  };
78
- export declare function createEventHandlers(options: Pick<ApiFactoryOptions, 'getAdapter'>): {
94
+ export declare function createEventHandlers(options: Pick<ApiFactoryOptions, 'getAdapter' | 'defaultActionLabels'>): {
79
95
  /**
80
96
  * POST /...
81
97
  * Body: { app_id, metric_key, value?, dimensions? }
@@ -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":"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"}
package/dist/api/index.js CHANGED
@@ -413,11 +413,17 @@ export function createEventHandlers(options) {
413
413
  value: typeof value === 'number' ? value : 1,
414
414
  dimensions,
415
415
  });
416
- // Auto-classify: upsert a draft catalog row for action.* events
416
+ // Auto-classify: upsert a draft catalog row for action.* and ui.* events
417
417
  if (metric_key.startsWith('action.')) {
418
418
  const action_key = metric_key.slice('action.'.length);
419
419
  const url = typeof dimensions?.url === 'string' ? dimensions.url : undefined;
420
- await _ensureActionDef(adapter, { app_id, action_key, url });
420
+ const defaultLabel = options.defaultActionLabels?.[action_key];
421
+ await _ensureActionDef(adapter, { app_id, action_key, url, source: 'manual', defaultLabel });
422
+ }
423
+ else if (metric_key.startsWith('ui.')) {
424
+ const url = typeof dimensions?.url === 'string' ? dimensions.url : undefined;
425
+ const defaultLabel = options.defaultActionLabels?.[metric_key];
426
+ await _ensureActionDef(adapter, { app_id, action_key: metric_key, url, source: 'auto', defaultLabel });
421
427
  }
422
428
  return jsonOk({ recorded: true }, 201);
423
429
  },
@@ -71,7 +71,7 @@ export function HazoMetricsProvider({ appId, userId, children }) {
71
71
  const elapsed = Date.now() - lastChangeAtRef.current;
72
72
  if (prev && elapsed > 0 && prev !== pathname) {
73
73
  const slug = prev.replace(/\//g, '_').replace(/^_/, '') || 'root';
74
- postEvent(appId, `ui.dwell.${slug}`, { path: prev, value: elapsed });
74
+ postEvent(appId, `ui.pageexit.${slug}`, { path: prev, value: elapsed });
75
75
  }
76
76
  // Update refs
77
77
  lastPathRef.current = pathname;
@@ -88,7 +88,7 @@ export function HazoMetricsProvider({ appId, userId, children }) {
88
88
  const elapsed = Date.now() - lastChangeAtRef.current;
89
89
  if (elapsed > 0 && lastPathRef.current) {
90
90
  const slug = lastPathRef.current.replace(/\//g, '_').replace(/^_/, '') || 'root';
91
- postEvent(appId, `ui.dwell.${slug}`, { path: lastPathRef.current, value: elapsed });
91
+ postEvent(appId, `ui.pageexit.${slug}`, { path: lastPathRef.current, value: elapsed });
92
92
  lastChangeAtRef.current = Date.now();
93
93
  }
94
94
  }
@@ -10,6 +10,7 @@ type ActionDefRow = {
10
10
  url: string | null;
11
11
  is_key_event: number | boolean;
12
12
  status: string;
13
+ source: string;
13
14
  datapoint_schema: string;
14
15
  created_at: string;
15
16
  updated_at: string;
@@ -25,6 +26,8 @@ export declare function ensureActionDef(adapter: HazoConnectAdapter, params: {
25
26
  scope_id?: string;
26
27
  action_key: string;
27
28
  url?: string | null;
29
+ source?: 'auto' | 'manual';
30
+ defaultLabel?: string;
28
31
  }): Promise<void>;
29
32
  /**
30
33
  * Returns all action defs for an app as a flat list.
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/server/actions.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAC;AAMrG,KAAK,YAAY,GAAG;IAClB,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,MAAM,GAAG,OAAO,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAwCF,wBAAgB,UAAU,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAU7F;AA6BD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,GACA,OAAO,CAAC,IAAI,CAAC,CAgCf;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,SAAS,EAAE,CAAC,CAUtB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,UAAU,EAAE,CAAC,CAwBvB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;CACvB,GACA,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAkB3B;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAsBD;;GAEG;AACH,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9C,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBvB;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACrF,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAsC5B;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAClE,OAAO,CAAC,IAAI,CAAC,CAiBf;AAMD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,cAAc,CAAA;CAAE,GACvE,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAgCnD;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B,GACA,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAoB7C"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/server/actions.ts"],"names":[],"mappings":"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"}
@@ -31,6 +31,7 @@ function rowToDef(row) {
31
31
  url: row.url ?? null,
32
32
  is_key_event: Boolean(row.is_key_event),
33
33
  status: row.status,
34
+ source: (row.source === 'auto' ? 'auto' : 'manual'),
34
35
  datapoint_schema: parseSchema(row.datapoint_schema),
35
36
  created_at: row.created_at,
36
37
  updated_at: row.updated_at,
@@ -65,6 +66,22 @@ function humanize(key) {
65
66
  .replace(/[._-]/g, ' ')
66
67
  .replace(/\b\w/g, (c) => c.toUpperCase());
67
68
  }
69
+ function deriveLabel(action_key, source) {
70
+ if (action_key.startsWith('ui.pageview.')) {
71
+ return `PageEntry:${humanize(action_key.slice('ui.pageview.'.length))}`;
72
+ }
73
+ if (action_key.startsWith('ui.pageexit.')) {
74
+ return `PageExit:${humanize(action_key.slice('ui.pageexit.'.length))}`;
75
+ }
76
+ if (action_key.startsWith('ui.click.')) {
77
+ return `Click:${humanize(action_key.slice('ui.click.'.length))}`;
78
+ }
79
+ if (source === 'manual') {
80
+ return `Action:${humanize(action_key)}`;
81
+ }
82
+ // other ui.* — raw key
83
+ return action_key;
84
+ }
68
85
  // ---------------------------------------------------------------------------
69
86
  // In-process seen-set cache — avoids a def write per event after first sight
70
87
  // ---------------------------------------------------------------------------
@@ -94,12 +111,13 @@ export async function ensureActionDef(adapter, params) {
94
111
  action_key: params.action_key,
95
112
  app_id: params.app_id,
96
113
  scope_id: params.scope_id ?? '',
97
- label: humanize(params.action_key),
114
+ label: params.defaultLabel ?? deriveLabel(params.action_key, params.source ?? 'manual'),
98
115
  description: '',
99
116
  parent_key: null,
100
117
  url: params.url ?? null,
101
118
  is_key_event: 0,
102
119
  status: 'unclassified',
120
+ source: params.source ?? 'manual',
103
121
  datapoint_schema: '[]',
104
122
  created_at: new Date().toISOString(),
105
123
  updated_at: new Date().toISOString(),
@@ -229,13 +229,13 @@ export async function tabDwell(adapter, opts) {
229
229
  const rows = await fetchWindow(adapter, { app_id, scope_id, since: since ?? defaultSince(), cap: limit });
230
230
  const tally = new Map();
231
231
  for (const row of rows) {
232
- if (!row.metric_key.startsWith('ui.dwell.'))
232
+ if (!row.metric_key.startsWith('ui.pageexit.'))
233
233
  continue;
234
234
  const dims = parseDims(row.dimensions);
235
235
  const uid = dims.user_id;
236
236
  if (userIds && uid && !userIds.includes(uid))
237
237
  continue;
238
- const path = dims.path ?? row.metric_key.slice('ui.dwell.'.length);
238
+ const path = dims.path ?? row.metric_key.slice('ui.pageexit.'.length);
239
239
  const elapsed = typeof dims.value === 'number' ? dims.value : row.value;
240
240
  tally.set(path, (tally.get(path) ?? 0) + elapsed);
241
241
  }
@@ -76,6 +76,7 @@ export interface ActionDef {
76
76
  url: string | null;
77
77
  is_key_event: boolean;
78
78
  status: 'unclassified' | 'active' | 'archived';
79
+ source: 'auto' | 'manual';
79
80
  datapoint_schema: DatapointSchema;
80
81
  created_at: string;
81
82
  updated_at: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  export interface ActionsPanelProps {
2
2
  appId: string;
3
3
  apiBase?: string;
4
+ /** Increment to trigger an external refresh (e.g. after seeding events). */
5
+ refreshTrigger?: number;
4
6
  }
5
- export declare function ActionsPanel({ appId, apiBase }: ActionsPanelProps): import("react").JSX.Element;
7
+ export declare function ActionsPanel({ appId, apiBase, refreshTrigger }: ActionsPanelProps): import("react").JSX.Element;
6
8
  //# sourceMappingURL=actions_panel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"actions_panel.d.ts","sourceRoot":"","sources":["../../src/ui/actions_panel.tsx"],"names":[],"mappings":"AAu1BA,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAWD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAA8B,EAAE,EAAE,iBAAiB,+BAkIxF"}
1
+ {"version":3,"file":"actions_panel.d.ts","sourceRoot":"","sources":["../../src/ui/actions_panel.tsx"],"names":[],"mappings":"AA42BA,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAWD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAA8B,EAAE,cAAkB,EAAE,EAAE,iBAAiB,+BAkI5G"}
@@ -12,6 +12,11 @@ function statusBadgeClass(status) {
12
12
  return 'inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-gray-100 text-gray-600';
13
13
  /* unclassified */ return 'inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-yellow-100 text-yellow-800';
14
14
  }
15
+ function sourceBadge(source) {
16
+ if (source !== 'auto')
17
+ return null;
18
+ return (_jsx("span", { className: "inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-gray-100 text-gray-500 ml-1", children: "auto" }));
19
+ }
15
20
  function ActionKeyPicker({ value, onChange, options, excludeKey, placeholder = 'Select action…' }) {
16
21
  const [open, setOpen] = useState(false);
17
22
  const [query, setQuery] = useState('');
@@ -169,7 +174,7 @@ function EditForm({ node, appId, apiBase, allActions, onSaved, onCancel }) {
169
174
  setSaving(false);
170
175
  }
171
176
  }, [draft, node, appId, apiBase, relatedKeys, initRelated, onSaved, datapointSchemaText, schemaError]);
172
- return (_jsxs("div", { className: "mt-1 ml-6 rounded-lg border border-indigo-200 bg-indigo-50 p-4 flex flex-col gap-3", children: [error != null && (_jsx("p", { className: "text-xs text-red-600 rounded bg-red-50 border border-red-200 px-2 py-1", children: error })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Label" }), _jsx("input", { type: "text", value: draft.label, onChange: (e) => setDraft((d) => ({ ...d, label: e.target.value })), className: "rounded border px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-400" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Description" }), _jsx("textarea", { value: draft.description, rows: 2, onChange: (e) => setDraft((d) => ({ ...d, description: e.target.value })), className: "rounded border px-2 py-1 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-indigo-400" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Parent action (optional)" }), _jsx(ActionKeyPicker, { value: draft.parent_key, onChange: (key) => setDraft((d) => ({ ...d, parent_key: key })), options: allActions, excludeKey: node.action_key, placeholder: "None (root node)" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Related actions" }), relatedLoading ? (_jsx("p", { className: "text-xs text-gray-400", children: "Loading\u2026" })) : (_jsx(RelatedActionsPicker, { selectedKeys: relatedKeys, onChange: setRelatedKeys, options: allActions, excludeKey: node.action_key }))] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "URL (optional)" }), _jsx("input", { type: "text", value: draft.url, placeholder: "https://\u2026", onChange: (e) => setDraft((d) => ({ ...d, url: e.target.value })), className: "rounded border px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-400" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Datapoint Schema (JSON)" }), _jsx("textarea", { value: datapointSchemaText, rows: 5, onChange: (e) => {
177
+ return (_jsxs("div", { className: "mt-1 ml-6 rounded-lg border border-indigo-200 bg-indigo-50 p-4 flex flex-col gap-3", children: [error != null && (_jsx("p", { className: "text-xs text-red-600 rounded bg-red-50 border border-red-200 px-2 py-1", children: error })), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Technical name" }), _jsx("input", { type: "text", value: node.action_key, readOnly: true, className: "rounded border px-2 py-1 text-sm font-mono bg-gray-50 text-gray-500 cursor-default focus:outline-none" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Label" }), _jsx("input", { type: "text", value: draft.label, placeholder: node.action_key, onChange: (e) => setDraft((d) => ({ ...d, label: e.target.value })), className: "rounded border px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-400" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Description" }), _jsx("textarea", { value: draft.description, rows: 2, onChange: (e) => setDraft((d) => ({ ...d, description: e.target.value })), className: "rounded border px-2 py-1 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-indigo-400" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Parent action (optional)" }), _jsx(ActionKeyPicker, { value: draft.parent_key, onChange: (key) => setDraft((d) => ({ ...d, parent_key: key })), options: allActions, excludeKey: node.action_key, placeholder: "None (root node)" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Related actions" }), relatedLoading ? (_jsx("p", { className: "text-xs text-gray-400", children: "Loading\u2026" })) : (_jsx(RelatedActionsPicker, { selectedKeys: relatedKeys, onChange: setRelatedKeys, options: allActions, excludeKey: node.action_key }))] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "URL (optional)" }), _jsx("input", { type: "text", value: draft.url, placeholder: "https://\u2026", onChange: (e) => setDraft((d) => ({ ...d, url: e.target.value })), className: "rounded border px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-400" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-gray-600", children: "Datapoint Schema (JSON)" }), _jsx("textarea", { value: datapointSchemaText, rows: 5, onChange: (e) => {
173
178
  setDatapointSchemaText(e.target.value);
174
179
  try {
175
180
  const parsed = JSON.parse(e.target.value);
@@ -242,10 +247,10 @@ function ActionRow({ node, depth, appId, apiBase, allActions, onRefresh, selecte
242
247
  const indent = depth * 20;
243
248
  return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-gray-50 group", style: { paddingLeft: `${indent + 8}px` }, children: [_jsx("input", { type: "checkbox", checked: selected.has(node.action_key), onChange: () => onToggleSelect(node.action_key), onClick: (e) => e.stopPropagation(), className: "flex-shrink-0 rounded", "aria-label": `Select ${node.action_key}` }), _jsx("button", { onClick: () => setExpanded((v) => !v), className: "flex-shrink-0 w-4 h-4 flex items-center justify-center text-gray-400 hover:text-gray-700", style: { visibility: hasChildren ? 'visible' : 'hidden' }, "aria-label": expanded ? 'Collapse' : 'Expand', children: expanded
244
249
  ? _jsx(ChevronDown, { className: "h-3.5 w-3.5" })
245
- : _jsx(ChevronRight, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "flex-1 min-w-0 text-sm text-gray-800 truncate font-medium", children: node.label }), _jsx("span", { className: "text-xs text-gray-400 font-mono truncate max-w-[160px]", children: node.action_key }), _jsx("span", { className: statusBadgeClass(node.status), children: node.status }), _jsx("span", { title: node.is_key_event ? 'Key event' : 'Not a key event', className: node.is_key_event ? 'text-yellow-500' : 'text-gray-300', children: _jsx(Star, { className: "h-3.5 w-3.5", fill: node.is_key_event ? 'currentColor' : 'none' }) }), _jsx("button", { onClick: () => setEditing((v) => !v), className: "flex-shrink-0 rounded p-1 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors opacity-0 group-hover:opacity-100", "aria-label": "Edit action", title: "Edit", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) })] }), editing && (_jsx(EditForm, { node: node, appId: appId, apiBase: apiBase, allActions: allActions, onSaved: () => { setEditing(false); onRefresh(); }, onCancel: () => setEditing(false) })), hasChildren && expanded && (_jsx("div", { children: node.children.map((child) => (_jsx(ActionRow, { node: child, depth: depth + 1, appId: appId, apiBase: apiBase, allActions: allActions, onRefresh: onRefresh, selected: selected, onToggleSelect: onToggleSelect }, child.action_key))) }))] }));
250
+ : _jsx(ChevronRight, { className: "h-3.5 w-3.5" }) }), _jsxs("span", { className: "flex-1 min-w-0 text-sm text-gray-800 truncate font-medium", children: [node.label, sourceBadge(node.source)] }), _jsx("span", { className: "text-xs text-gray-400 font-mono truncate max-w-[160px]", children: node.action_key }), _jsx("span", { className: statusBadgeClass(node.status), children: node.status }), _jsx("span", { title: node.is_key_event ? 'Key event' : 'Not a key event', className: node.is_key_event ? 'text-yellow-500' : 'text-gray-300', children: _jsx(Star, { className: "h-3.5 w-3.5", fill: node.is_key_event ? 'currentColor' : 'none' }) }), _jsx("button", { onClick: () => setEditing((v) => !v), className: "flex-shrink-0 rounded p-1 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors opacity-0 group-hover:opacity-100", "aria-label": "Edit action", title: "Edit", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) })] }), editing && (_jsx(EditForm, { node: node, appId: appId, apiBase: apiBase, allActions: allActions, onSaved: () => { setEditing(false); onRefresh(); }, onCancel: () => setEditing(false) })), hasChildren && expanded && (_jsx("div", { children: node.children.map((child) => (_jsx(ActionRow, { node: child, depth: depth + 1, appId: appId, apiBase: apiBase, allActions: allActions, onRefresh: onRefresh, selected: selected, onToggleSelect: onToggleSelect }, child.action_key))) }))] }));
246
251
  }
247
252
  const INITIAL_STATE = { loading: true, error: null, tree: [], allActions: [] };
248
- export function ActionsPanel({ appId, apiBase = '/api/hazo_umetrics' }) {
253
+ export function ActionsPanel({ appId, apiBase = '/api/hazo_umetrics', refreshTrigger = 0 }) {
249
254
  const [state, setState] = useState(INITIAL_STATE);
250
255
  const [refreshKey, setRefreshKey] = useState(0);
251
256
  const [selected, setSelected] = useState(new Set());
@@ -292,7 +297,7 @@ export function ActionsPanel({ appId, apiBase = '/api/hazo_umetrics' }) {
292
297
  }
293
298
  });
294
299
  return () => { cancelled = true; };
295
- }, [appId, apiBase, refreshKey]);
300
+ }, [appId, apiBase, refreshKey, refreshTrigger]);
296
301
  // -------------------------------------------------------------------------
297
302
  // Render states
298
303
  // -------------------------------------------------------------------------
@@ -1 +1 @@
1
- {"version":3,"file":"datapoint_panel.d.ts","sourceRoot":"","sources":["../../src/ui/datapoint_panel.tsx"],"names":[],"mappings":"AAQA,UAAU,cAAc;IACtB,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,UAAU,SAAS;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,cAAc,EAAE,CAAC;CACpC;AA0HD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,mBAAmB,+BAyG9E"}
1
+ {"version":3,"file":"datapoint_panel.d.ts","sourceRoot":"","sources":["../../src/ui/datapoint_panel.tsx"],"names":[],"mappings":"AAQA,UAAU,cAAc;IACtB,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,UAAU,SAAS;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,cAAc,EAAE,CAAC;CACpC;AA0HD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAeD,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,mBAAmB,+BAuG9E"}
@@ -24,18 +24,28 @@ function CategoricalCard({ stat, schemaLabel }) {
24
24
  const title = schemaLabel ? `${stat.key} (${schemaLabel})` : stat.key;
25
25
  return (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h4", { className: "text-xs font-semibold text-gray-700 font-mono", children: title }), _jsxs("span", { className: "text-[10px] text-gray-400", children: [stat.distinct, " distinct \u00B7 ", stat.count, " total"] })] }), stat.top && stat.top.length > 0 ? (_jsx("div", { className: "flex flex-col gap-1", children: stat.top.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[10px] text-gray-600 w-24 truncate", children: t.value }), _jsx(MiniBar, { value: t.count, max: maxCount, color: "#10b981" })] }, t.value))) })) : (_jsx("p", { className: "text-xs text-gray-400", children: "No values." }))] }));
26
26
  }
27
+ function preferredAction(actions) {
28
+ // 1. First with a declared datapoint schema
29
+ const withSchema = actions.find((a) => a.datapoint_schema.length > 0);
30
+ if (withSchema)
31
+ return withSchema;
32
+ // 2. First app-domain action (not auto-tracked btn./ui. events)
33
+ const appAction = actions.find((a) => !a.action_key.startsWith('btn.') && !a.action_key.startsWith('ui.'));
34
+ if (appAction)
35
+ return appAction;
36
+ // 3. Fallback to first
37
+ return actions[0];
38
+ }
27
39
  export function DatapointPanel({ appId, apiBase, actions }) {
28
- const firstWithSchema = actions.find((a) => a.datapoint_schema.length > 0) ?? actions[0];
29
- const [selectedKey, setSelectedKey] = useState(firstWithSchema?.action_key ?? '');
40
+ const [selectedKey, setSelectedKey] = useState(preferredAction(actions)?.action_key ?? '');
30
41
  const [stats, setStats] = useState(null);
31
42
  const [loading, setLoading] = useState(false);
32
43
  const [error, setError] = useState(null);
33
44
  useEffect(() => {
34
45
  if (!actions.length)
35
46
  return;
36
- const first = actions.find((a) => a.datapoint_schema.length > 0) ?? actions[0];
37
- if (first && !selectedKey)
38
- setSelectedKey(first.action_key);
47
+ if (!selectedKey)
48
+ setSelectedKey(preferredAction(actions)?.action_key ?? '');
39
49
  }, [actions, selectedKey]);
40
50
  useEffect(() => {
41
51
  if (!selectedKey || !appId)
@@ -1 +1 @@
1
- {"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;IAChD,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,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;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;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC3I;AAgJD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,EAAE,iBAAiB,+BAuHzG"}
1
+ {"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;IAChD,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,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;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;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC3I;AA0QD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,EAAE,iBAAiB,+BA8IzG"}
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from 'react';
4
4
  // ---------------------------------------------------------------------------
5
5
  // Helpers
@@ -21,10 +21,130 @@ function kindColor(kind) {
21
21
  default: return '#94a3b8';
22
22
  }
23
23
  }
24
- function GraphSummary({ graph }) {
25
- const topNodes = [...graph.nodes].sort((a, b) => b.count - a.count).slice(0, 10);
26
- const topEdges = [...graph.edges].sort((a, b) => b.count - a.count).slice(0, 8);
27
- return (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-4", children: [_jsxs("div", { children: [_jsxs("h4", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2", children: ["Top steps (", graph.nodes.length, " unique)"] }), _jsx("div", { className: "flex flex-col gap-1", children: topNodes.map((node) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-full flex-shrink-0", style: { backgroundColor: kindColor(node.kind) } }), _jsx("span", { className: "flex-1 text-xs font-mono text-gray-700 truncate", children: node.label }), _jsxs("span", { className: "text-xs font-semibold text-indigo-600 tabular-nums", children: [node.count, "\u00D7"] })] }, node.id))) })] }), topEdges.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2", children: "Common transitions" }), _jsx("div", { className: "flex flex-col gap-1", children: topEdges.map((edge, i) => (_jsxs("div", { className: "flex items-center gap-1 text-xs text-gray-600", children: [_jsx("span", { className: "font-mono truncate max-w-[140px]", children: edge.from.replace(/^action\./, '') }), _jsx("span", { className: "text-gray-400 flex-shrink-0", children: "\u2192" }), _jsx("span", { className: "font-mono truncate max-w-[140px]", children: edge.to.replace(/^action\./, '') }), _jsxs("span", { className: "ml-auto text-gray-400 flex-shrink-0 tabular-nums", children: [edge.count, "\u00D7 \u00B7 ", fmtMs(edge.avgMs)] })] }, i))) })] }))] }));
24
+ // ---------------------------------------------------------------------------
25
+ // JourneyFlowGraph SVG left-to-right DAG visualization
26
+ // ---------------------------------------------------------------------------
27
+ const NODE_W = 148;
28
+ const NODE_H = 44;
29
+ const COL_GAP = 200;
30
+ const ROW_GAP = 64;
31
+ const PAD = 24;
32
+ function computeFlowLayout(nodes, edges) {
33
+ const ids = new Set(nodes.map(n => n.id));
34
+ const inDeg = new Map(nodes.map(n => [n.id, 0]));
35
+ const outEdges = new Map(nodes.map(n => [n.id, []]));
36
+ for (const e of edges) {
37
+ if (!ids.has(e.from) || !ids.has(e.to))
38
+ continue;
39
+ inDeg.set(e.to, (inDeg.get(e.to) ?? 0) + 1);
40
+ outEdges.get(e.from)?.push(e.to);
41
+ }
42
+ // Shortest-path rank assignment (BFS distance from a source). Journey graphs
43
+ // contain cycles (users revisit steps); longest-path ranking would let
44
+ // back-edges inflate every node to the same max column. Shortest-path sets
45
+ // each node's rank exactly once on first visit, so columns stay stable.
46
+ const rank = new Map();
47
+ const queue = [];
48
+ // Seed with all source nodes (in-degree 0), processed highest-count first.
49
+ const sources = nodes.filter((n) => (inDeg.get(n.id) ?? 0) === 0).sort((a, b) => b.count - a.count);
50
+ for (const n of sources) {
51
+ rank.set(n.id, 0);
52
+ queue.push(n.id);
53
+ }
54
+ // Pure-cycle fallback: every node has an in-edge → seed from highest count.
55
+ if (queue.length === 0) {
56
+ const top = [...nodes].sort((a, b) => b.count - a.count)[0];
57
+ if (top) {
58
+ rank.set(top.id, 0);
59
+ queue.push(top.id);
60
+ }
61
+ }
62
+ let qi = 0;
63
+ while (qi < queue.length) {
64
+ const cur = queue[qi++];
65
+ const r = rank.get(cur) ?? 0;
66
+ for (const next of (outEdges.get(cur) ?? [])) {
67
+ if (!rank.has(next)) {
68
+ rank.set(next, r + 1);
69
+ queue.push(next);
70
+ } // set once
71
+ }
72
+ }
73
+ // Nodes unreachable from any source → trailing column after the deepest rank.
74
+ let maxAssigned = 0;
75
+ for (const r of rank.values())
76
+ if (r > maxAssigned)
77
+ maxAssigned = r;
78
+ for (const n of nodes) {
79
+ if (!rank.has(n.id))
80
+ rank.set(n.id, maxAssigned + 1);
81
+ }
82
+ // Group by rank, sort each column by count desc
83
+ const cols = new Map();
84
+ for (const n of nodes) {
85
+ const r = rank.get(n.id) ?? 0;
86
+ if (!cols.has(r))
87
+ cols.set(r, []);
88
+ cols.get(r).push(n);
89
+ }
90
+ for (const col of cols.values())
91
+ col.sort((a, b) => b.count - a.count);
92
+ // Assign positions
93
+ const pos = new Map();
94
+ for (const [col, colNodes] of cols.entries()) {
95
+ const totalH = colNodes.length * NODE_H + (colNodes.length - 1) * ROW_GAP;
96
+ let y = PAD - totalH / 2;
97
+ for (const n of colNodes) {
98
+ pos.set(n.id, { x: PAD + col * (NODE_W + COL_GAP), y });
99
+ y += NODE_H + ROW_GAP;
100
+ }
101
+ }
102
+ return pos;
103
+ }
104
+ function JourneyFlowGraph({ graph }) {
105
+ if (graph.nodes.length === 0)
106
+ return null;
107
+ const pos = computeFlowLayout(graph.nodes, graph.edges);
108
+ // Compute SVG dimensions
109
+ let maxX = 0;
110
+ let maxY = 0;
111
+ let minY = 0;
112
+ for (const p of pos.values()) {
113
+ if (p.x + NODE_W + PAD > maxX)
114
+ maxX = p.x + NODE_W + PAD;
115
+ if (p.y + NODE_H + PAD > maxY)
116
+ maxY = p.y + NODE_H + PAD;
117
+ if (p.y < minY)
118
+ minY = p.y;
119
+ }
120
+ const offsetY = minY < 0 ? -minY + PAD : PAD;
121
+ const svgH = maxY - minY + PAD * 2;
122
+ const svgW = maxX;
123
+ const maxCount = Math.max(...graph.nodes.map(n => n.count), 1);
124
+ return (_jsxs("div", { className: "rounded-lg border bg-white overflow-x-auto", children: [_jsxs("div", { className: "px-4 py-2 border-b flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wide", children: "Flow Graph" }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", graph.nodes.length, " nodes \u00B7 ", graph.edges.length, " transitions)"] }), _jsxs("div", { className: "ml-auto flex items-center gap-3 text-[10px] text-gray-400", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#6366f1' } }), " action"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#10b981' } }), " pageview"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#ef4444' } }), " error"] })] })] }), _jsx("div", { style: { overflowX: 'auto' }, children: _jsxs("svg", { width: svgW, height: Math.max(svgH, 200), style: { display: 'block' }, children: [_jsx("defs", { children: _jsx("marker", { id: "arrow", markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: "auto", children: _jsx("path", { d: "M0,0 L0,6 L8,3 z", fill: "#94a3b8" }) }) }), graph.edges.map((edge, i) => {
125
+ const from = pos.get(edge.from);
126
+ const to = pos.get(edge.to);
127
+ if (!from || !to)
128
+ return null;
129
+ const x1 = from.x + NODE_W;
130
+ const y1 = from.y + offsetY + NODE_H / 2;
131
+ const x2 = to.x;
132
+ const y2 = to.y + offsetY + NODE_H / 2;
133
+ const cx = (x1 + x2) / 2;
134
+ const opacity = Math.max(0.2, edge.count / maxCount);
135
+ const strokeW = Math.max(1, Math.min(4, edge.count / maxCount * 4));
136
+ return (_jsxs("g", { children: [_jsx("path", { d: `M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`, fill: "none", stroke: "#94a3b8", strokeWidth: strokeW, strokeOpacity: opacity, markerEnd: "url(#arrow)" }), edge.count > 1 && (_jsxs("text", { x: cx, y: (y1 + y2) / 2 - 4, textAnchor: "middle", fontSize: "9", fill: "#94a3b8", opacity: opacity, children: [edge.count, "\u00D7"] }))] }, i));
137
+ }), graph.nodes.map((node) => {
138
+ const p = pos.get(node.id);
139
+ if (!p)
140
+ return null;
141
+ const x = p.x;
142
+ const y = p.y + offsetY;
143
+ const fill = kindColor(node.kind);
144
+ const alpha = Math.max(0.25, node.count / maxCount);
145
+ const label = node.label.length > 18 ? node.label.slice(0, 17) + '…' : node.label;
146
+ return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: NODE_W, height: NODE_H, rx: 6, fill: fill, fillOpacity: alpha, stroke: fill, strokeOpacity: Math.min(1, alpha + 0.3), strokeWidth: 1.5 }), _jsx("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 - 5, textAnchor: "middle", fontSize: "11", fontWeight: "600", fill: "#1e293b", children: label }), _jsxs("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 + 9, textAnchor: "middle", fontSize: "9", fill: "#475569", children: [node.count, "\u00D7"] })] }, node.id));
147
+ })] }) })] }));
28
148
  }
29
149
  function SessionDetail({ session, index, profile }) {
30
150
  const [open, setOpen] = useState(index === 0);
@@ -40,14 +160,18 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
40
160
  const [result, setResult] = useState(null);
41
161
  const [loading, setLoading] = useState(false);
42
162
  const [error, setError] = useState(null);
43
- const [canvasAvailable, setCanvasAvailable] = useState(false);
44
163
  const [userProfiles, setUserProfiles] = useState(new Map());
45
- // Check if hazo_canvas is available (optional peer dep)
46
- useEffect(() => {
47
- import('hazo_canvas/client').catch(() => null).then((mod) => {
48
- setCanvasAvailable(mod !== null);
49
- });
50
- }, []);
164
+ // Persistent set of all user IDs ever seen (so the filter dropdown keeps its
165
+ // full option list even while a single-user filter is active).
166
+ const [knownUserIds, setKnownUserIds] = useState([]);
167
+ // Internal filter (falls back to the userId prop when unset, e.g. from the
168
+ // Behavior tab's single-user selection).
169
+ const [selectedUserId, setSelectedUserId] = useState('');
170
+ const effectiveUserId = selectedUserId || userId;
171
+ function userLabel(id) {
172
+ const p = userProfiles.get(id);
173
+ return p?.name ?? p?.email ?? `${id.slice(0, 12)}…`;
174
+ }
51
175
  useEffect(() => {
52
176
  if (!appId)
53
177
  return;
@@ -55,8 +179,8 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
55
179
  setLoading(true);
56
180
  setError(null);
57
181
  const params = new URLSearchParams({ app_id: appId });
58
- if (userId)
59
- params.set('user_id', userId);
182
+ if (effectiveUserId)
183
+ params.set('user_id', effectiveUserId);
60
184
  if (sessionId)
61
185
  params.set('session_id', sessionId);
62
186
  fetch(`${apiBase}/analytics/journey?${params.toString()}`, { credentials: 'include' })
@@ -68,14 +192,21 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
68
192
  setResult(data);
69
193
  setLoading(false);
70
194
  // Resolve user profiles for all sessions that have a user_id
71
- if (resolveUserProfiles && data?.sessions) {
195
+ if (data?.sessions) {
72
196
  const ids = [...new Set(data.sessions.map((s) => s.user_id).filter((id) => Boolean(id)))];
73
- if (ids.length > 0) {
197
+ // Grow the known-users set (never shrink — keeps the filter options stable)
198
+ setKnownUserIds((prev) => [...new Set([...prev, ...ids])]);
199
+ if (resolveUserProfiles && ids.length > 0) {
74
200
  resolveUserProfiles(ids)
75
201
  .then((profiles) => {
76
202
  if (cancelled)
77
203
  return;
78
- setUserProfiles(new Map(profiles.map((p) => [p.user_id, p])));
204
+ setUserProfiles((prev) => {
205
+ const next = new Map(prev);
206
+ for (const p of profiles)
207
+ next.set(p.user_id, p);
208
+ return next;
209
+ });
79
210
  })
80
211
  .catch(() => { });
81
212
  }
@@ -88,8 +219,8 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
88
219
  setLoading(false);
89
220
  });
90
221
  return () => { cancelled = true; };
91
- }, [appId, apiBase, userId, sessionId]);
222
+ }, [appId, apiBase, effectiveUserId, sessionId]);
92
223
  const sessionCount = result?.sessions.length ?? 0;
93
224
  const totalSteps = result?.sessions.reduce((s, sess) => s + sess.steps.length, 0) ?? 0;
94
- return (_jsxs("div", { className: "flex flex-col gap-4 p-4", children: [_jsxs("div", { className: "flex items-center gap-4 text-xs text-gray-500", children: [_jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: sessionCount }), " session", sessionCount !== 1 ? 's' : ''] }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: totalSteps }), " total steps"] }), canvasAvailable && (_jsx("span", { className: "text-indigo-400 text-[10px]", children: "canvas available" }))] }), error && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error })), loading && (_jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading journey data\u2026" })), !loading && result && (_jsxs(_Fragment, { children: [result.graph.nodes.length > 0 && (_jsx(GraphSummary, { graph: result.graph })), result.sessions.length === 0 ? (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No journey sessions found." })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: "Sessions" }), result.sessions.map((session, i) => (_jsx(SessionDetail, { session: session, index: i, profile: session.user_id ? userProfiles.get(session.user_id) : undefined }, session.session_id)))] }))] })), !loading && !result && !error && (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No data yet." }))] }));
225
+ return (_jsxs("div", { className: "flex flex-col gap-4 p-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4 text-xs text-gray-500", children: [_jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: sessionCount }), " session", sessionCount !== 1 ? 's' : ''] }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: totalSteps }), " total steps"] }), knownUserIds.length > 0 && (_jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [_jsx("label", { className: "text-gray-500", children: "User" }), _jsxs("select", { value: selectedUserId, onChange: (e) => setSelectedUserId(e.target.value), className: "rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-indigo-400", children: [_jsx("option", { value: "", children: "All users" }), knownUserIds.map((id) => (_jsx("option", { value: id, children: userLabel(id) }, id)))] })] }))] }), error && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error })), loading && (_jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading journey data\u2026" })), !loading && result && (_jsxs(_Fragment, { children: [result.graph.nodes.length > 0 && (_jsx(JourneyFlowGraph, { graph: result.graph })), result.sessions.length === 0 ? (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No journey sessions found." })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: "Sessions" }), result.sessions.map((session, i) => (_jsx(SessionDetail, { session: session, index: i, profile: session.user_id ? userProfiles.get(session.user_id) : undefined }, session.session_id)))] }))] })), !loading && !result && !error && (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No data yet." }))] }));
95
226
  }
@@ -60,6 +60,7 @@ CREATE TABLE IF NOT EXISTS hazo_umetrics_action_def (
60
60
  url TEXT, -- nullable, actions without URL leave null
61
61
  is_key_event BOOLEAN NOT NULL DEFAULT FALSE,
62
62
  status TEXT NOT NULL DEFAULT 'unclassified', -- unclassified | active | archived
63
+ source TEXT NOT NULL DEFAULT 'manual',
63
64
  datapoint_schema JSONB NOT NULL DEFAULT '[]',
64
65
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
65
66
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@@ -60,6 +60,7 @@ CREATE TABLE IF NOT EXISTS hazo_umetrics_action_def (
60
60
  url TEXT, -- nullable, actions without URL leave null
61
61
  is_key_event INTEGER NOT NULL DEFAULT 0, -- 0/1 boolean
62
62
  status TEXT NOT NULL DEFAULT 'unclassified', -- unclassified | active | archived
63
+ source TEXT NOT NULL DEFAULT 'manual', -- 'auto' | 'manual'
63
64
  datapoint_schema TEXT NOT NULL DEFAULT '[]',
64
65
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
65
66
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_umetrics",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",
@@ -42,7 +42,6 @@
42
42
  "build:test-app:pg": "HAZO_CONNECT_TYPE=postgrest npm run build:test-app"
43
43
  },
44
44
  "peerDependencies": {
45
- "hazo_canvas": "^1.9.0",
46
45
  "hazo_core": "^1.2.0",
47
46
  "hazo_dataviz": "^0.3.0",
48
47
  "hazo_logs": "^2.0.3",
@@ -58,9 +57,6 @@
58
57
  "react-dom": "^18.0.0 || ^19.0.0"
59
58
  },
60
59
  "peerDependenciesMeta": {
61
- "hazo_canvas": {
62
- "optional": true
63
- },
64
60
  "hazo_logs": {
65
61
  "optional": true
66
62
  },