hazo_umetrics 0.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGE_LOG.md +117 -0
  2. package/README.md +88 -12
  3. package/SETUP_CHECKLIST.md +26 -9
  4. package/config/hazo_umetrics_config.ini.sample +26 -0
  5. package/dist/api/index.d.ts +82 -11
  6. package/dist/api/index.d.ts.map +1 -1
  7. package/dist/api/index.js +442 -69
  8. package/dist/index.client.d.ts +10 -8
  9. package/dist/index.client.d.ts.map +1 -1
  10. package/dist/index.client.js +180 -20
  11. package/dist/lib/index.d.ts +4 -0
  12. package/dist/lib/index.d.ts.map +1 -1
  13. package/dist/lib/index.js +4 -0
  14. package/dist/server/actions.d.ts +66 -0
  15. package/dist/server/actions.d.ts.map +1 -0
  16. package/dist/server/actions.js +234 -0
  17. package/dist/server/activity.d.ts +37 -0
  18. package/dist/server/activity.d.ts.map +1 -0
  19. package/dist/server/activity.js +118 -0
  20. package/dist/server/behavior.d.ts +64 -0
  21. package/dist/server/behavior.d.ts.map +1 -0
  22. package/dist/server/behavior.js +346 -0
  23. package/dist/server/connect.d.ts +11 -0
  24. package/dist/server/connect.d.ts.map +1 -0
  25. package/dist/server/connect.js +58 -0
  26. package/dist/server/flags.d.ts +2 -1
  27. package/dist/server/flags.d.ts.map +1 -1
  28. package/dist/server/flags.js +17 -12
  29. package/dist/server/ga4.d.ts +1 -1
  30. package/dist/server/ga4.d.ts.map +1 -1
  31. package/dist/server/stats.d.ts +10 -8
  32. package/dist/server/stats.d.ts.map +1 -1
  33. package/dist/server/stats.js +25 -18
  34. package/dist/server/types.d.ts +31 -3
  35. package/dist/server/types.d.ts.map +1 -1
  36. package/dist/ui/actions_panel.d.ts +6 -0
  37. package/dist/ui/actions_panel.d.ts.map +1 -0
  38. package/dist/ui/actions_panel.js +218 -0
  39. package/dist/ui/analytics_panel.d.ts +6 -0
  40. package/dist/ui/analytics_panel.d.ts.map +1 -0
  41. package/dist/ui/analytics_panel.js +138 -0
  42. package/dist/ui/index.d.ts +2 -0
  43. package/dist/ui/index.d.ts.map +1 -1
  44. package/dist/ui/index.js +2 -0
  45. package/dist/ui/metrics_panel.d.ts +2 -2
  46. package/dist/ui/metrics_panel.d.ts.map +1 -1
  47. package/dist/ui/metrics_panel.js +49 -104
  48. package/migrations/db_setup_postgres.sql +48 -20
  49. package/migrations/db_setup_sqlite.sql +48 -20
  50. package/package.json +11 -4
package/CHANGE_LOG.md CHANGED
@@ -1,5 +1,122 @@
1
1
  # hazo_umetrics — Change Log
2
2
 
3
+ ## 1.3.0 — Analytics dashboard + behavioral fixes + createUmetricsConnect
4
+
5
+ **New features + bug fixes (no breaking changes):**
6
+
7
+ ### New: `createUmetricsConnect` — dedicated DB factory
8
+ - `createUmetricsConnect(opts?)` builds a `HazoConnectAdapter` from `HAZO_UMETRICS_DATABASE_*`
9
+ env vars (type / URL / API key / SQLite path), with INI `[database]` as fallback. Returns a
10
+ cached adapter on no-arg calls; creates fresh when opts are passed.
11
+ - Exported from main `hazo_umetrics` entry: `import { createUmetricsConnect } from 'hazo_umetrics'`
12
+ - `UmetricsConnectOptions` type exported alongside the factory.
13
+ - Enables wiring umetrics to a **separate DB** from the host app in one line:
14
+ `createStatHandlers({ getAdapter: createUmetricsConnect })`
15
+ - Updated `config/hazo_umetrics_config.ini.sample` `[database]` section with dedicated env-var docs.
16
+
17
+ ### New: `topActions` behavior aggregation
18
+ - `topActions` function in `src/server/behavior.ts` — ranks all `action.*` events by frequency, no `user_id` required. Answers "what is the most common action?" without needing session segmentation.
19
+ - Exposed as `metric=top_actions` in `GET /api/hazo_umetrics/analytics/behavior`.
20
+ - New "Top actions (all users)" panel in `AnalyticsPanel` (first panel in behavioral grid).
21
+
22
+ ### New: `UserIdentifier` client component (test-app)
23
+ - Fetches `/api/hazo_auth/me` on mount and calls `identify(user_id)` automatically so logged-in users are tracked with `user_id` in all subsequent events. Required for first-session, post-relogin, and returning-user panels to populate.
24
+
25
+ ### Bug fix: `tabDwell` elapsed time
26
+ - `tabDwell` was reading `row.value` (hardcoded `1`) instead of `dims.value` (the real accumulated foreground ms from the client-side visibility timer). Fixed to use `dims.value` when present. `Time per tab` panel now shows actual seconds, not event count.
27
+
28
+ ### Bug fix: `AnalyticsPanel` bar chart invisible bars
29
+ - `SimpleBarChart` column divs lacked `h-full`, so `height: X%` resolved to 0px (percentage of auto-height container). Removed `items-end` from parent flex container (default `items-stretch`) and added `h-full` to each column div. Bars now render at correct proportional height.
30
+
31
+ ### Enhanced seed (test-app)
32
+ - `buildEventRows()` rewritten with 4 realistic user personas (alice/bob/carol/dave) carrying `user_id` + `session_id` in all dimensions. Generates `action.*`, `ui.dwell.*`, and `ui.error.*` events spread across 14 days. Seeds `hazo_umetrics_action_def` catalog on every run so Action Catalog shows pre-classified actions with correct hierarchy and key-event flags.
33
+
34
+ ## 1.2.0 — Behavioral analytics + catalog UX
35
+
36
+ **New features (additive, no breaking changes):**
37
+
38
+ ### Phase 1 — Capture foundation
39
+ - `identify(userId)` now stores user ID in module var + `localStorage['hazo_umetrics_uid']`; `getUserId()` restores on page reload. `postEvent` attaches `user_id` to every event's dimensions automatically.
40
+ - `HazoMetricsProvider` gains optional `userId` prop — calls `identify` when set (SSR/known-user apps).
41
+ - Global `error` + `unhandledrejection` listeners post `ui.error.<slug>` events with `type`, `message` (truncated 200 chars), `path`.
42
+ - Tab-dwell tracking: `ui.dwell.<path_slug>` posted on path change with `value=ms`; `pagehide`/`visibilitychange` flushes remaining dwell.
43
+ - `trackError(err, ctx?)` exported from context for manual error capture.
44
+
45
+ ### Phase 2 — Catalog UX
46
+ - New `hazo_umetrics_action_link` table (SQLite + Postgres) — undirected many-to-many related-action pairs. PK `(action_key, related_key, app_id)`; canonical order (alpha sort) prevents duplicate rows.
47
+ - `getLinks`, `addLink`, `removeLink` in `src/server/actions.ts`. Self-link guard (`action_key === related_key` → null/400). Idempotent add.
48
+ - `createLinkHandlers` in `hazo_umetrics/api`: `GET`, `POST`, `DELETE` for `/actions/links`. Auth-gated (`metrics.view` / `metrics.manage`).
49
+ - `actions_panel.tsx` updated: free-text parent_key field replaced with `ActionKeyPicker` (searchable single-select with clear button); new `RelatedActionsPicker` multi-select with removable pills. Both source options from the flat action catalog.
50
+
51
+ ### Phase 3 — Analytics query layer
52
+ - New `src/server/behavior.ts` — 7 behavioral aggregation functions using the established JS-aggregation pattern (windowed fetch, in-memory grouping, backend-agnostic):
53
+ - `firstSessionActions` — actions in each user's first session
54
+ - `postReloginActions` — actions in each user's second session
55
+ - `actionsBeforeGoal(goalKey, n)` — N actions preceding a goal event
56
+ - `topErrors` — `ui.error.*` frequency ranking
57
+ - `tabDwell` — summed dwell ms by path
58
+ - `frequentActionsReturning` — actions by users with >1 session
59
+ - `eventsOverTime` — day buckets with optional per-user breakdown
60
+ - All functions accept `catalogMap`, `includeArchived`, `keyEventsOnly` filters — archived actions excluded by default; key events given first-class treatment.
61
+ - `createBehaviorHandlers` in `hazo_umetrics/api`: `GET /analytics/behavior?metric=<name>` and `GET /analytics/events-over-time`.
62
+
63
+ ### Phase 4 — Analytics dashboard
64
+ - New `AnalyticsPanel` component (`hazo_umetrics/ui`) — date-range filter, user multi-select, show-archived toggle, key-events-only toggle, goal-key input.
65
+ - Events-over-time bar chart (inline `SimpleBarChart`, no extra peer dep) with per-user stacked segments when user filter active.
66
+ - Six behavioral question cards: first-session, post-relogin, before-goal, errors, tab-dwell, frequent-returning.
67
+ - Test-app: `/analytics` page, `Analytics` sidebar nav link, three new API routes (`/actions/links`, `/analytics/behavior`, `/analytics/events-over-time`).
68
+ - Autotest scenarios for client capture (identify) and link API.
69
+
70
+ ---
71
+
72
+ ## 1.1.0 — Action tracking + catalog
73
+
74
+ **New features (additive, no breaking changes):**
75
+
76
+ ### Action catalog
77
+ - New `hazo_umetrics_action_def` table (SQLite + Postgres) — governance layer for user action tracking. Composite PK `(action_key, app_id)`, self-referencing `parent_key` for hierarchy, `is_key_event` flag, `status` (`unclassified | active | archived`).
78
+ - `src/server/actions.ts` — catalog service: `ensureActionDef`, `listActions`, `getActionTree`, `updateActionDef`. In-process seen-set cache prevents repeated DB writes per event.
79
+ - `ActionDef`, `ActionNode`, `ActionDefPatch` types added to `src/server/types.ts` and re-exported from the package root.
80
+
81
+ ### Broadened event ingestion
82
+ - `createEventHandlers.recordEvent` now accepts `metric_key` starting with `action.*` in addition to `ui.*`. On first ingest of a new key, auto-creates an `unclassified` catalog row via `ensureActionDef`.
83
+ - Arbitrary keys still rejected (guard unchanged for anything else).
84
+
85
+ ### Catalog API
86
+ - `createActionHandlers(options)` — new handler factory in `hazo_umetrics/api`:
87
+ - `listActions` — `GET`, requires `metrics.view`.
88
+ - `getActionTree` — `GET`, requires `metrics.view`. Returns catalog nested by `parent_key`.
89
+ - `updateAction` — `PATCH`, requires `metrics.manage`. Patches mutable fields; emits `action.catalog.update` audit entry.
90
+
91
+ ### Client
92
+ - `useMetrics()` context now includes `trackAction(key, opts?)` — thin wrapper that posts `action.<key>` via the existing `postEvent`. Optional `opts.url` and `opts.dims` forwarded as dimensions.
93
+
94
+ ### Admin UI
95
+ - `ActionsPanel` component exported from `hazo_umetrics/ui` — catalog tree view with inline editing (label, description, parent reparent, URL, key-event toggle, status).
96
+
97
+ ### Test-app
98
+ - New `/actions` page with demo `trackAction` buttons and live `ActionsPanel`.
99
+ - New API routes: `GET/PATCH /api/hazo_umetrics/actions`, `GET /api/hazo_umetrics/actions/tree`.
100
+ - 4 browser autotest scenarios in `hazo_umetrics_actions` scenario group.
101
+ - Sidebar: Actions nav item added.
102
+
103
+ ### Tests
104
+ - 22 new Jest tests in `__tests__/actions.test.ts`. Total: 83 (all green).
105
+
106
+ ## 1.0.0 — Breaking rename: scope_id→app_id, app_id→scope_id
107
+
108
+ **Breaking changes:**
109
+ - `scope_id` (required site-isolation key) renamed to `app_id` in all tables, TypeScript interfaces, function signatures, and API query parameters.
110
+ - Old optional `app_id` column on `hazo_umetrics_stat` renamed to `scope_id` (now means: optional tenant subdivision within a site).
111
+ - New optional `scope_id TEXT NOT NULL DEFAULT ''` column added to all 7 tables.
112
+ - `StatCollectorDef.collect(scope_id)` → `collect(app_id)`.
113
+ - `MaintenanceJob` handler ctx: `scope_id` → `app_id`.
114
+ - `MetricsPanel` prop: `scopeId` → `appId`.
115
+ - `Ga4Provisioner.provision(scopeId, ...)` → `provision(appId, ...)`.
116
+ - API query params: `?scope_id=` → `?app_id=` on all stat/experiment/GA4/query endpoints.
117
+
118
+ **No in-place migration provided** — this package is pre-release. Re-apply fresh schema via `db_setup_postgres.sql` or `db_setup_sqlite.sql`.
119
+
3
120
  ## 0.1.2 — 2026-06-14
4
121
 
5
122
  **Changed:**
package/README.md CHANGED
@@ -8,7 +8,7 @@ Gives every hazo Next.js app one consistent, auth-gated way to understand user b
8
8
  - **Feature flags** — deterministic weighted-variant bucketing, signed consent cookie, sticky assignment.
9
9
  - **A/B experiments** — same engine as flags + significance (deferred until traffic justifies it).
10
10
 
11
- All surfaces mount inside `hazo_admin` via the existing `kind:'metrics'` section; every read/write is `scope_id`-gated.
11
+ All surfaces mount inside `hazo_admin` via the existing `kind:'metrics'` section; every read/write is `app_id`-gated.
12
12
 
13
13
  ## Status
14
14
 
@@ -41,9 +41,9 @@ HAZO_UMETRICS_COOKIE_KEY_v1=<base64-encoded 32-byte AES-256 key>
41
41
  | Import path | Contents |
42
42
  |---|---|
43
43
  | `hazo_umetrics` | Server: recordStat, getLatestStat, getStatSeries, resolveVariant, weightedBucket, GA4/experiment stubs |
44
- | `hazo_umetrics/api` | Route factories: createStatHandlers, createExperimentHandlers, createGa4ConnectHandlers, createQueryHandlers |
45
- | `hazo_umetrics/client` | Client: HazoMetricsProvider, useMetrics, getVariant, isEnabled, identify, recordStat |
46
- | `hazo_umetrics/ui` | UI: MetricsPanel, StatCards, StatTrend (uses hazo_dataviz LineChart — requires hazo_dataviz peer dep) |
44
+ | `hazo_umetrics/api` | Route factories: createStatHandlers, createExperimentHandlers, createGa4ConnectHandlers, createQueryHandlers, createEventHandlers (anon ingestion), createBehaviorHandlers, createActionHandlers, createLinkHandlers, createActivityHandlers |
45
+ | `hazo_umetrics/client` | Client: HazoMetricsProvider, useMetrics, getVariant, isEnabled, identify, trackAction, trackError, recordStat |
46
+ | `hazo_umetrics/ui` | UI: MetricsPanel, StatCards, StatTrend, AnalyticsPanel, ActionsPanel |
47
47
 
48
48
  ## Stat store
49
49
 
@@ -51,24 +51,24 @@ HAZO_UMETRICS_COOKIE_KEY_v1=<base64-encoded 32-byte AES-256 key>
51
51
  import { recordStat, getLatestStat, getStatSeries, registerStatCollector, runStatSnapshot } from 'hazo_umetrics';
52
52
 
53
53
  // Append a reading (always inserts, never upserts)
54
- await recordStat(adapter, { scope_id: 'my-app', metric_key: 'active_users', value: 42 });
54
+ await recordStat(adapter, { app_id: 'my-app', metric_key: 'active_users', value: 42 });
55
55
 
56
56
  // Latest value
57
- const stat = await getLatestStat(adapter, { scope_id: 'my-app', metric_key: 'active_users' });
57
+ const stat = await getLatestStat(adapter, { app_id: 'my-app', metric_key: 'active_users' });
58
58
 
59
59
  // Series (ascending by captured_at)
60
- const series = await getStatSeries(adapter, { scope_id: 'my-app', metric_key: 'active_users', limit: 30 });
60
+ const series = await getStatSeries(adapter, { app_id: 'my-app', metric_key: 'active_users', limit: 30 });
61
61
 
62
62
  // Register a collector (runs in runStatSnapshot)
63
63
  registerStatCollector({
64
64
  key: 'active_users',
65
65
  label: 'Active users',
66
66
  format: 'count',
67
- collect: async (scope_id) => countActiveUsers(scope_id),
67
+ collect: async (app_id) => countActiveUsers(app_id),
68
68
  });
69
69
 
70
70
  // Run all collectors (call on a schedule via hazo_jobs in M1)
71
- const results = await runStatSnapshot(adapter, { scope_id: 'my-app' });
71
+ const results = await runStatSnapshot(adapter, { app_id: 'my-app' });
72
72
  ```
73
73
 
74
74
  ## Feature flags
@@ -79,15 +79,52 @@ import { resolveVariant, getVariant, isEnabled } from 'hazo_umetrics';
79
79
  // Evaluate a flag with logged-in user (sticky assignment)
80
80
  const { variant } = await resolveVariant(adapter, {
81
81
  experimentKey: 'new_dashboard',
82
- scope_id: 'my-app',
82
+ app_id: 'my-app',
83
83
  subjectId: user.id,
84
84
  consent: true,
85
85
  });
86
86
 
87
87
  // Boolean flag shorthand
88
- const enabled = await isEnabled(adapter, { experimentKey: 'new_feature', scope_id, subjectId });
88
+ const enabled = await isEnabled(adapter, { experimentKey: 'new_feature', app_id, subjectId });
89
89
  ```
90
90
 
91
+ ## Analytics dashboard
92
+
93
+ ```tsx
94
+ import { AnalyticsPanel } from 'hazo_umetrics/ui';
95
+
96
+ // Mount in a server or client page — fetches all behavioral data from /api/hazo_umetrics/analytics/*
97
+ <AnalyticsPanel appId="my-app" />
98
+ ```
99
+
100
+ Seven behavioral panels answer:
101
+ - **Top actions (all users)** — most frequent `action.*` events, no user_id required
102
+ - **First-session actions** — what new users do first (requires `user_id` in events)
103
+ - **Post re-login actions** — what users do on their second session
104
+ - **Before "share"** — the N actions preceding a goal event (configurable goal key)
105
+ - **Common errors** — ranked `ui.error.*` frequency
106
+ - **Time per tab** — real foreground dwell time per path (from `ui.dwell.*` events)
107
+ - **Returning-user actions** — most common actions for users with >1 session
108
+
109
+ For `user_id` to be captured, call `identify(userId)` after login or mount `UserIdentifier` (see test-app `src/components/user_identifier.tsx`).
110
+
111
+ ## Action tracking (client)
112
+
113
+ ```tsx
114
+ import { HazoMetricsProvider, useMetrics } from 'hazo_umetrics/client';
115
+
116
+ // Wrap your app
117
+ <HazoMetricsProvider appId="my-app" userId={currentUser?.id}>
118
+ {children}
119
+ </HazoMetricsProvider>
120
+
121
+ // In any component
122
+ const { trackAction, trackError } = useMetrics();
123
+ trackAction('timer.complete', { url: '/timer' });
124
+ ```
125
+
126
+ Auto-tracked on mount: page views (`ui.pageview.*`), clicks (`ui.click.*`), errors (`ui.error.*`), dwell time (`ui.dwell.*`).
127
+
91
128
  ## API route factories
92
129
 
93
130
  ```ts
@@ -102,7 +139,46 @@ const expHandlers = createExperimentHandlers({ getAdapter: () => myAdapter });
102
139
 
103
140
  ## DB setup
104
141
 
105
- Run `db_setup_sqlite.sql` (SQLite) or `db_setup_postgres.sql` (PostgreSQL) once. Creates 7 tables, all prefixed `hazo_umetrics_`.
142
+ Run `db_setup_sqlite.sql` (SQLite) or `db_setup_postgres.sql` (PostgreSQL) once against your **umetrics DB**. Creates 7 tables, all prefixed `hazo_umetrics_`.
143
+
144
+ > Run the migration against the **dedicated umetrics database**, not the host app's main DB.
145
+ > If you use `createUmetricsConnect()` (recommended), the umetrics DB is the one pointed to
146
+ > by `HAZO_UMETRICS_DATABASE_URL`.
147
+
148
+ ## Separate database for umetrics
149
+
150
+ `hazo_umetrics` data (events, stats, flags, experiments) should live in its own database,
151
+ **separate from the host app's main DB**. This keeps analytics writes from competing with
152
+ product data, lets you point multiple apps at one shared analytics store, and lets you
153
+ scale each independently.
154
+
155
+ Use `createUmetricsConnect()` to build a dedicated adapter in one line:
156
+
157
+ ```ts
158
+ import { createUmetricsConnect } from 'hazo_umetrics';
159
+
160
+ // Reads HAZO_UMETRICS_DATABASE_URL + HAZO_UMETRICS_DATABASE_API_KEY from env.
161
+ // Falls back to ./umetrics.sqlite when type=sqlite.
162
+ export const getUmetricsAdapter = createUmetricsConnect;
163
+ ```
164
+
165
+ Then inject it into every route factory:
166
+
167
+ ```ts
168
+ const handlers = createStatHandlers({ getAdapter: createUmetricsConnect });
169
+ ```
170
+
171
+ ### Env vars
172
+
173
+ | Variable | Required | Description |
174
+ |---|---|---|
175
+ | `HAZO_UMETRICS_DATABASE_URL` | PostgREST only | Base URL of the umetrics PostgREST server |
176
+ | `HAZO_UMETRICS_DATABASE_API_KEY` | PostgREST only | Bearer token for the umetrics PostgREST server |
177
+ | `HAZO_UMETRICS_DATABASE_TYPE` | No | `postgrest` (default) or `sqlite` |
178
+ | `HAZO_UMETRICS_DATABASE_PATH` | SQLite only | Path to the SQLite file (default: `./umetrics.sqlite`) |
179
+
180
+ These are independent of the host app's `POSTGREST_URL` / `POSTGREST_API_KEY` — the
181
+ analytics DB can be a completely different PostgREST instance (or a local SQLite file for dev).
106
182
 
107
183
  ## Config
108
184
 
@@ -33,7 +33,24 @@ db.close();
33
33
 
34
34
  **PostgreSQL:** Run `node_modules/hazo_umetrics/db_setup_postgres.sql` against your database.
35
35
 
36
- ## 3. Set env vars for cookie signing
36
+ ## 3. Set env vars for the umetrics database
37
+
38
+ `createUmetricsConnect()` reads these variables to connect to the dedicated umetrics DB:
39
+
40
+ ```bash
41
+ # PostgREST mode (production / staging)
42
+ HAZO_UMETRICS_DATABASE_URL=https://your-umetrics-postgrest.example.com
43
+ HAZO_UMETRICS_DATABASE_API_KEY=<bearer-token>
44
+
45
+ # SQLite mode (local dev — omit DATABASE_URL to fall back to ./umetrics.sqlite)
46
+ # HAZO_UMETRICS_DATABASE_TYPE=sqlite
47
+ # HAZO_UMETRICS_DATABASE_PATH=./umetrics.sqlite
48
+ ```
49
+
50
+ > Run `db_setup_postgres.sql` (or `db_setup_sqlite.sql`) against this **umetrics DB**, not
51
+ > the host app's main database.
52
+
53
+ ## 4. Set env vars for cookie signing
37
54
 
38
55
  ```bash
39
56
  # Generate a 32-byte key: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
@@ -41,11 +58,11 @@ HAZO_UMETRICS_COOKIE_KEY_CURRENT=v1
41
58
  HAZO_UMETRICS_COOKIE_KEY_v1=<base64-encoded 32-byte key>
42
59
  ```
43
60
 
44
- ## 4. Add config file (optional)
61
+ ## 5. Add config file (optional)
45
62
 
46
63
  Copy `node_modules/hazo_umetrics/config/hazo_umetrics_config.ini.sample` to your app's config directory as `hazo_umetrics_config.ini` and fill in `[ga4]` when ready for M1.
47
64
 
48
- ## 5. Mount API routes (Next.js)
65
+ ## 6. Mount API routes (Next.js)
49
66
 
50
67
  ```ts
51
68
  // app/api/hazo_umetrics/stats/route.ts
@@ -58,7 +75,7 @@ export async function GET(req: Request) { return handlers.getStat(req); }
58
75
  export async function POST(req: Request) { return handlers.recordStat(req); }
59
76
  ```
60
77
 
61
- ## 6. Wrap your app in HazoMetricsProvider (client)
78
+ ## 7. Wrap your app in HazoMetricsProvider (client)
62
79
 
63
80
  ```tsx
64
81
  // app/layout.tsx
@@ -66,14 +83,14 @@ import { HazoMetricsProvider } from 'hazo_umetrics/client';
66
83
 
67
84
  export default function RootLayout({ children }) {
68
85
  return (
69
- <HazoMetricsProvider scopeId={process.env.NEXT_PUBLIC_SCOPE_ID}>
86
+ <HazoMetricsProvider appId={process.env.NEXT_PUBLIC_APP_ID}>
70
87
  {children}
71
88
  </HazoMetricsProvider>
72
89
  );
73
90
  }
74
91
  ```
75
92
 
76
- ## 7. Configure Tailwind (if you use hazo_umetrics UI components)
93
+ ## 8. Configure Tailwind (if you use hazo_umetrics UI components)
77
94
 
78
95
  `MetricsPanel` uses shadcn token utilities (`bg-background`, `text-muted-foreground`, `border`, etc.) from `hazo_ui`. Without the token definitions the dialog panel will render transparent. In your `globals.css`:
79
96
 
@@ -112,11 +129,11 @@ export default function RootLayout({ children }) {
112
129
 
113
130
  (Adjust `@import` and `@source` depth to match your project's distance from `node_modules`.)
114
131
 
115
- ## 8. Smoke test
132
+ ## 9. Smoke test
116
133
 
117
134
  ```ts
118
135
  import { recordStat, getLatestStat } from 'hazo_umetrics';
119
- await recordStat(adapter, { scope_id: 'my-app', metric_key: 'setup_test', value: 1 });
120
- const stat = await getLatestStat(adapter, { scope_id: 'my-app', metric_key: 'setup_test' });
136
+ await recordStat(adapter, { app_id: 'my-app', metric_key: 'setup_test', value: 1 });
137
+ const stat = await getLatestStat(adapter, { app_id: 'my-app', metric_key: 'setup_test' });
121
138
  console.assert(stat?.value === 1, 'hazo_umetrics setup OK');
122
139
  ```
@@ -2,6 +2,32 @@
2
2
  ; Copy to hazo_umetrics_config.ini and fill in values for your environment.
3
3
  ; Env-var overrides: HAZO_UMETRICS_<SECTION>_<KEY> (uppercase, underscores).
4
4
 
5
+ [umetrics]
6
+ ; The site identifier for this application instance. Used as the primary
7
+ ; isolation key (app_id) on all hazo_umetrics tables.
8
+ app_id = my_app
9
+
10
+ [database]
11
+ ; Dedicated hazo_umetrics database — separate from the host app's main DB.
12
+ ; Use createUmetricsConnect() from hazo_umetrics to wire this adapter.
13
+ ;
14
+ ; type: postgrest (default) | sqlite
15
+ ; Env-var override: HAZO_UMETRICS_DATABASE_TYPE
16
+ ; type = postgrest
17
+ ;
18
+ ; PostgREST mode (recommended for production):
19
+ ; base_url — the PostgREST server base URL
20
+ ; Env-var override: HAZO_UMETRICS_DATABASE_URL
21
+ ; base_url = http://your-postgrest-host:4444
22
+ ; api_key — bearer token (env only, never store in INI)
23
+ ; Set: HAZO_UMETRICS_DATABASE_API_KEY=<your-token>
24
+ ;
25
+ ; SQLite mode (local development without a Postgres instance):
26
+ ; database_path — path to the SQLite file (default: ./umetrics.sqlite)
27
+ ; Env-var override: HAZO_UMETRICS_DATABASE_PATH
28
+ ; type = sqlite
29
+ ; database_path = ./umetrics.sqlite
30
+
5
31
  [env]
6
32
  ; Uncomment to hard-code the environment name (overrides HAZO_ENV / NODE_ENV).
7
33
  ; env = development
@@ -18,61 +18,132 @@ export interface ApiFactoryOptions {
18
18
  onAudit?: (entry: {
19
19
  action: string;
20
20
  actor_id: string;
21
- scope_id: string;
21
+ app_id: string;
22
22
  details?: unknown;
23
23
  }) => Promise<void>;
24
24
  }
25
25
  export declare function createStatHandlers(options: ApiFactoryOptions): {
26
26
  /**
27
- * GET /...?scope_id=<id>&metric_key=<key>
27
+ * GET /...?app_id=<id>&metric_key=<key>
28
28
  * Returns the latest reading for the metric, or null if none.
29
29
  */
30
30
  getStat(req: Request): Promise<Response>;
31
31
  /**
32
- * GET /...?scope_id=<id>&metric_key=<key>[&since=<iso>][&limit=<n>]
32
+ * GET /...?app_id=<id>&metric_key=<key>[&since=<iso>][&limit=<n>]
33
33
  * Returns time-series readings.
34
34
  */
35
35
  getStatSeries(req: Request): Promise<Response>;
36
36
  /**
37
37
  * POST /...
38
- * Body: { scope_id, metric_key, value, app_id?, unit?, dimensions? }
38
+ * Body: { app_id, metric_key, value, scope_id?, unit?, dimensions? }
39
39
  * Appends a new stat reading.
40
40
  */
41
41
  recordStat(req: Request): Promise<Response>;
42
42
  };
43
43
  export declare function createExperimentHandlers(options: ApiFactoryOptions): {
44
44
  /**
45
- * GET /...?scope_id=<id>
46
- * Lists all experiments for a scope.
45
+ * GET /...?app_id=<id>
46
+ * Lists all experiments for an app.
47
47
  */
48
48
  listExperiments(req: Request): Promise<Response>;
49
49
  /**
50
50
  * POST /...
51
- * Body: { experimentKey, scope_id, subjectId?, consent? }
51
+ * Body: { experimentKey, app_id, subjectId?, consent? }
52
52
  * Resolves the variant for a subject.
53
53
  */
54
54
  evalVariant(req: Request): Promise<Response>;
55
55
  /**
56
56
  * POST /...
57
- * Body: { key, scope_id }
57
+ * Body: { key, app_id }
58
58
  * Transitions an experiment to 'running'. Requires metrics.manage.
59
59
  */
60
60
  startExperiment(req: Request): Promise<Response>;
61
61
  /**
62
62
  * POST /...
63
- * Body: { key, scope_id }
63
+ * Body: { key, app_id }
64
64
  * Transitions an experiment to 'stopped'. Requires metrics.manage.
65
65
  */
66
66
  stopExperiment(req: Request): Promise<Response>;
67
67
  };
68
68
  export declare function createGa4ConnectHandlers(options: ApiFactoryOptions): {
69
- /** STUB (M1): Initiate GA4 OAuth connection for a scope. */
69
+ /** STUB (M1): Initiate GA4 OAuth connection for an app. */
70
70
  connect(req: Request): Promise<Response>;
71
- /** STUB (M1): Provision a GA4 property for a scope. */
71
+ /** STUB (M1): Provision a GA4 property for an app. */
72
72
  provision(req: Request): Promise<Response>;
73
73
  };
74
74
  export declare function createQueryHandlers(options: ApiFactoryOptions): {
75
75
  /** STUB (M1): Execute a GA4 or stat-store query. */
76
76
  query(req: Request): Promise<Response>;
77
77
  };
78
+ export declare function createEventHandlers(options: Pick<ApiFactoryOptions, 'getAdapter'>): {
79
+ /**
80
+ * POST /...
81
+ * Body: { app_id, metric_key, value?, dimensions? }
82
+ * Records a UI or action event stat without auth. metric_key must start with
83
+ * 'ui.' or 'action.'. Intentional auth carve-out: instrumentation must work
84
+ * without login.
85
+ *
86
+ * For action.* keys: auto-creates an 'unclassified' catalog row on first sight
87
+ * (seen-set cached in-process). Accepts optional `url` dimension.
88
+ */
89
+ recordEvent(req: Request): Promise<Response>;
90
+ };
91
+ export declare function createActionHandlers(options: ApiFactoryOptions): {
92
+ /**
93
+ * GET /...?app_id=<id>
94
+ * Returns all action defs as a flat list. Requires metrics.view.
95
+ */
96
+ listActions(req: Request): Promise<Response>;
97
+ /**
98
+ * GET /...?app_id=<id>
99
+ * Returns action defs nested by parent_key. Requires metrics.view.
100
+ */
101
+ getActionTree(req: Request): Promise<Response>;
102
+ /**
103
+ * PATCH /...
104
+ * Body: { app_id, action_key, patch: { label?, description?, parent_key?, url?, is_key_event?, status? } }
105
+ * Updates mutable fields of an action def. Requires metrics.manage.
106
+ * Emits an audit entry 'action.catalog.update'.
107
+ */
108
+ updateAction(req: Request): Promise<Response>;
109
+ };
110
+ export declare function createActivityHandlers(options: ApiFactoryOptions): {
111
+ /**
112
+ * GET /...?app_id=<id>[&since=<iso>]
113
+ * Returns { summary, trend } aggregated from ui.* events in the stat store.
114
+ */
115
+ getActivity(req: Request): Promise<Response>;
116
+ };
117
+ export declare function createLinkHandlers(options: ApiFactoryOptions): {
118
+ /**
119
+ * GET /...?app_id=<id>[&action_key=<key>]
120
+ * Returns related-action links. Requires metrics.view.
121
+ */
122
+ getLinks(req: Request): Promise<Response>;
123
+ /**
124
+ * POST /...
125
+ * Body: { app_id, action_key, related_key }
126
+ * Adds a link (idempotent). Requires metrics.manage.
127
+ */
128
+ addLink(req: Request): Promise<Response>;
129
+ /**
130
+ * DELETE /...
131
+ * Body: { app_id, action_key, related_key }
132
+ * Removes a link. Requires metrics.manage.
133
+ */
134
+ removeLink(req: Request): Promise<Response>;
135
+ };
136
+ export declare function createBehaviorHandlers(options: ApiFactoryOptions): {
137
+ /**
138
+ * GET /...?app_id=<id>&metric=<name>[&since=<iso>][&goal=<key>][&n=<int>][&include_archived=1][&key_events_only=1][&user_ids=u1,u2]
139
+ * Dispatches to behavioral aggregation functions. Requires metrics.view.
140
+ * metric: first_session | post_relogin | before_goal | errors | tab_dwell | frequent_returning
141
+ */
142
+ getBehavior(req: Request): Promise<Response>;
143
+ /**
144
+ * GET /...?app_id=<id>[&since=<iso>][&user_ids=u1,u2][&key_events_only=1][&include_archived=1]
145
+ * Returns events bucketed by day. Requires metrics.view.
146
+ */
147
+ getEventsOverTime(req: Request): Promise<Response>;
148
+ };
78
149
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAY9D,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE;IAAE,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAC3E,OAAO,CAAC;IACX,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAC;IACnE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAiGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB;IAIzD;;;OAGG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqB9C;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBpD;;;;OAIG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA4CpD;AAoBD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAW/D;;;OAGG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqBtD;;;;OAIG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAiDlD;;;;OAIG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA8CtD;;;;OAIG;wBACuB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA8CxD;AAMD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAI/D,4DAA4D;iBACzC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAe9C,uDAAuD;mBAClC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAenD;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB;IAI1D,oDAAoD;eACnC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAe/C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAmC9D,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE;IAAE,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAC3E,OAAO,CAAC;IACX,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAC;IACnE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrB;AAiGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB;IAIzD;;;OAGG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqB9C;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBpD;;;;OAIG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA4CpD;AAqBD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAW/D;;;OAGG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqBtD;;;;OAIG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAiDlD;;;;OAIG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA8CtD;;;;OAIG;wBACuB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA8CxD;AAMD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAI/D,2DAA2D;iBACxC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAe9C,sDAAsD;mBACjC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAenD;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB;IAI1D,oDAAoD;eACnC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAe/C;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC;IAE9E;;;;;;;;;OASG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAwCrD;AAMD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB;IAI3D;;;OAGG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBlD;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBpD;;;;;OAKG;sBACqB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAkDtD;AAMD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB;IAI7D;;;OAGG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAyBrD;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB;IAIzD;;;OAGG;kBACiB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkB/C;;;;OAIG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoC9C;;;;OAIG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAiCpD;AAMD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB;IAI7D;;;;OAIG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA0DlD;;;OAGG;2BAC0B,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAyB3D"}