hazo_umetrics 1.9.1 → 1.11.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 (51) hide show
  1. package/CHANGE_LOG.md +108 -0
  2. package/config/hazo_umetrics_config.ini.sample +6 -2
  3. package/dist/api/index.d.ts +27 -2
  4. package/dist/api/index.d.ts.map +1 -1
  5. package/dist/api/index.js +248 -7
  6. package/dist/index.client.d.ts +2 -1
  7. package/dist/index.client.d.ts.map +1 -1
  8. package/dist/index.client.js +1 -0
  9. package/dist/lib/index.d.ts +5 -0
  10. package/dist/lib/index.d.ts.map +1 -1
  11. package/dist/lib/index.js +5 -0
  12. package/dist/lib/track_flow_step.d.ts +21 -0
  13. package/dist/lib/track_flow_step.d.ts.map +1 -0
  14. package/dist/lib/track_flow_step.js +33 -0
  15. package/dist/server/funnel.d.ts +38 -0
  16. package/dist/server/funnel.d.ts.map +1 -0
  17. package/dist/server/funnel.js +60 -0
  18. package/dist/server/ga4.d.ts +18 -15
  19. package/dist/server/ga4.d.ts.map +1 -1
  20. package/dist/server/ga4.js +84 -14
  21. package/dist/server/ga4_manifest.d.ts +58 -0
  22. package/dist/server/ga4_manifest.d.ts.map +1 -0
  23. package/dist/server/ga4_manifest.js +127 -0
  24. package/dist/server/ga4_metrics.d.ts +20 -0
  25. package/dist/server/ga4_metrics.d.ts.map +1 -0
  26. package/dist/server/ga4_metrics.js +84 -0
  27. package/dist/server/ga4_provision.d.ts +39 -0
  28. package/dist/server/ga4_provision.d.ts.map +1 -0
  29. package/dist/server/ga4_provision.js +313 -0
  30. package/dist/server/metrics.d.ts +88 -0
  31. package/dist/server/metrics.d.ts.map +1 -0
  32. package/dist/server/metrics.js +260 -0
  33. package/dist/server/stats.d.ts +3 -0
  34. package/dist/server/stats.d.ts.map +1 -1
  35. package/dist/server/stats.js +3 -1
  36. package/dist/server/types.d.ts +26 -0
  37. package/dist/server/types.d.ts.map +1 -1
  38. package/dist/ui/ga4_connection_card.d.ts +16 -0
  39. package/dist/ui/ga4_connection_card.d.ts.map +1 -0
  40. package/dist/ui/ga4_connection_card.js +249 -0
  41. package/dist/ui/index.d.ts +2 -0
  42. package/dist/ui/index.d.ts.map +1 -1
  43. package/dist/ui/index.js +2 -0
  44. package/dist/ui/journey_panel.d.ts.map +1 -1
  45. package/dist/ui/journey_panel.js +23 -22
  46. package/dist/ui/metrics_tab.d.ts +36 -0
  47. package/dist/ui/metrics_tab.d.ts.map +1 -0
  48. package/dist/ui/metrics_tab.js +148 -0
  49. package/migrations/db_setup_postgres.sql +4 -1
  50. package/migrations/db_setup_sqlite.sql +4 -1
  51. package/package.json +12 -8
package/CHANGE_LOG.md CHANGED
@@ -1,5 +1,113 @@
1
1
  # hazo_umetrics — Change Log
2
2
 
3
+ ## 1.11.0 — M1 GA4 live (OAuth connect + Admin provisioner + Data API funnels + windowed ga4 metrics)
4
+
5
+ **M1 GA4 half shipped (minor — stubs replaced with real implementations; signatures widened, not broken):**
6
+
7
+ ### GA4 Data API client (`src/server/ga4.ts`, rewritten)
8
+ - `createGa4Client({ propertyId, fetchToken, fetchImpl?, apiBaseUrl?, cacheTtlMs? })` replaces the M0 stub with a real client that issues raw `fetch` calls to `analyticsdata.googleapis.com/v1beta/properties/{propertyId}:runReport`. Token comes from the caller-supplied `fetchToken()` (delegated to `hazo_auth`, see D-12) — the client never persists or reads credentials itself.
9
+ - In-memory ~5min TTL cache, keyed per client instance; stale-if-error (a failed refetch serves the last good cached value rather than throwing). `cacheTtlMs` overridable for tests. (D-11, D-14)
10
+ - `Ga4Report` return shape unchanged from M0 — existing callers of the stub keep working.
11
+
12
+ ### GA4 manifest + PII guard (`src/server/ga4_manifest.ts`, new)
13
+ - `DEFAULT_GA4_MANIFEST` — the standard set of custom dimensions/metrics/key events `hazo_umetrics` provisions per app.
14
+ - `mergeManifest` — combines the default manifest with an app-supplied override, de-duped by name.
15
+ - `isPII` / `assertNoPII` — blocks dimension/metric names that look like PII (email, name, phone, etc.) before they're sent to the Admin API.
16
+ - `MAX_EVENT_SCOPED_DIMENSIONS = 50` — GA4's own event-scoped custom dimension cap, enforced client-side with a hard error rather than letting the Admin API reject it opaquely.
17
+ - `validateManifest` — runs both guards over a full manifest in one pass.
18
+ - Metric `step_duration_ms` display name is `Step Duration` (not `Step Duration (ms)`) — GA4 Admin `display_name` allows only `[A-Za-z0-9_ ]`; the `MILLISECONDS` `measurementUnit` already conveys the unit. Caught in live provision smoke (400 `INVALID_ARGUMENT`).
19
+
20
+ ### GA4 Admin API provisioner (`src/server/ga4_provision.ts`, new)
21
+ - Idempotent **list → create-missing → patch-drifted** flow for custom dimensions, custom metrics, and key events. Re-running provision on an already-provisioned property is a no-op (aside from patching any drifted display names/scopes).
22
+ - Runs the manifest through `validateManifest` (PII + cap) before touching the Admin API.
23
+ - Partial failures don't abort the whole run — the result comes back `status: 'error'` with a per-item error list, and any items that did succeed still have their resolved IDs returned.
24
+ - Returns resolved `dimension_ids` / `metric_ids` / `key_event_ids` for persistence on the connection row.
25
+
26
+ ### Funnel read (`src/server/funnel.ts`, new)
27
+ - `getFunnel(client, flow, dateRange)` — one `runReport` call (`eventCount` dimensioned by `eventName`, filtered with an `inListFilter` on the flow's step event names), then JS-side: reorders rows to match the flow's declared step order, computes per-step and cumulative drop-off, and overall conversion. (D-13)
28
+ - Output shaped to drop straight into `hazo_ui`'s `FunnelChart`.
29
+
30
+ ### Windowed ga4 metrics live (`src/server/metrics.ts`, `src/server/ga4_metrics.ts` new)
31
+ - `getMetricWindows` gains an optional third arg `{ ga4Reader }` — when supplied, `source: 'ga4'` metrics are no longer forced to `pending`.
32
+ - `createGa4MetricWindowReader(...)` (new) looks up the app's GA4 connection row, reads the Data API through `ga4.ts`, and fills `total`/`24h`/`7d`/`28d`/`90d` for each registered `ga4` metric. Falls back to `pending: true` when the app has no connection yet — unchanged behaviour for unconnected apps.
33
+ - Wired into `createStatHandlers.getMetricWindows` in `src/api/index.ts` so the `/stats/windows` route serves live GA4 numbers once an app is connected.
34
+
35
+ ### Connect / provision / funnel handlers (`src/api/index.ts`)
36
+ - `createGa4ConnectHandlers` — `connect` now actually persists `connected_user_id` + the property/measurement IDs to the connection row (`disconnect: true` clears the row instead of deleting it, keeping history). `provision` delegates the OAuth token fetch to `hazo_auth`'s `getGoogleToken`, runs `ga4_provision.ts`, persists the resolved dimension/metric/key-event IDs, and writes an audit entry. New `funnel` handler wraps `getFunnel` for the `/ga4/funnel` route. (D-12)
37
+
38
+ ### Canonical flow helper (`src/lib/track_flow_step.ts`, exported from `hazo_umetrics/client`)
39
+ - `trackFlowStep(flowId, step, params)` — gtag-guarded no-op when `window.gtag` isn't present (SSR-safe, no-op in tests). This is the shape intended to replace gotimer's `src/lib/ga-events.ts` — the gotimer migration itself is a separate, later session.
40
+
41
+ ### UI
42
+ - `GA4ConnectionCard` (`src/ui/ga4_connection_card.tsx`, exported from `hazo_umetrics/ui`) — connect/disconnect + provision status card.
43
+ - Test-app: new `/ga4` tab, plus `provision`, `funnel`, and jobs-snapshot demo routes.
44
+
45
+ ### DB migration
46
+ - `hazo_umetrics_ga4_connection` gains `connected_user_id`, `metric_ids`, `key_event_ids` columns — both SQLite and PostgreSQL DDL, plus seed data updated. `credential_ref` stays in the schema but reserved/unused: the OAuth token itself is never stored by `hazo_umetrics` (delegated to `hazo_auth`, D-12).
47
+
48
+ ### Decisions
49
+ - **D-11** Raw `fetch` to the GA4 Data/Admin REST APIs, not the `googleapis` SDK — keeps the dependency tree shallow.
50
+ - **D-12** GA4 token delegated to `hazo_auth`'s `getGoogleToken` — no second credential store.
51
+ - **D-13** Funnel step ordering/drop-off/conversion computed client-side in JS from a single `eventCount`×`eventName` report, not a multi-call server-side funnel API.
52
+ - **D-14** In-memory ~5min TTL cache, stale-if-error, per client instance — scheduled background refresh deferred.
53
+
54
+ ### Tests
55
+ - New mocked-`fetch` (no real Google calls) jest suites: manifest/PII guard, client + cache behaviour, funnel computation, provisioner (idempotency + partial failure), connect/provision/funnel handlers, live windowed-metrics read, and the flow-step helper. All green.
56
+
57
+ ## 1.10.1 — Windowed-metrics wiring fixes (exports `default` + `MetricsTab` `apiBase`)
58
+
59
+ **Fixes (patch — surfaced while wiring consumer apps; both additive/back-compatible):**
60
+
61
+ ### `package.json` `exports` — add a `"default"` condition to every subpath
62
+ - Each subpath (`.`, `./client`, `./api`, `./ui`) now declares `"default"` alongside `"import"`, mirroring `hazo_connect`/`hazo_jobs`. Without it, a static `import … from "hazo_umetrics"` under tooling that doesn't resolve the `"import"` condition (e.g. `tsx` standalone scripts) failed with `ERR_PACKAGE_PATH_NOT_EXPORTED`. Consumer backfill/snapshot scripts had to use dynamic `await import()` as a workaround; they can now import statically.
63
+
64
+ ### `MetricsTab` — optional `apiBase` prop
65
+ - `MetricsTab({ appId, apiBase = '/api/hazo_umetrics' })` builds the windows fetch from `${apiBase}/stats/windows` (trailing slash tolerated) instead of hardcoding `/api/hazo_umetrics/stats/windows`. Apps mounting umetrics under a different base can now point the tab at their route. Default preserves the previous behaviour (back-compatible). Matches `JourneyPanel`'s `apiBase` pattern.
66
+
67
+ ## 1.10.0 — Per-app windowed metrics (registry + Total/24h/7d/28d/90d + graph)
68
+
69
+ **New features (minor — additive; existing `MetricsPanel`/`statSnapshotJob` unaffected):**
70
+
71
+ ### Per-app metric registry + windowed read (`src/server/metrics.ts`, new)
72
+ - `registerMetrics({ app_id, metrics })` / `getRegisteredMetrics(app_id)` — module-level `Map<app_id, MetricDef[]>`, idempotent/merge-by-`(app_id, key)` (HMR/cold-start safe). Mirrors `registerStatCollector` but keyed per app; code is the single source of truth (`stat_def` stays dormant).
73
+ - `getMetricWindows(adapter, { app_id, scope_id?, days=90 })` → `MetricWindow[]`. Per registered metric: `ga4` → `pending` (all-null, empty series); `db` → per-key `getStatSeries`, collapse to one point per UTC day (last-write-wins), `total = latest`, `delta_Nd = total − value_as_of(now−Nd)` where `value_as_of` walks back to the last daily point `≤ target` (gap-day safe); no point ≤ target → `null`. Per-key reads only — never pulls `ui.*`/`action.*` rows from the shared table.
74
+ - `runMetricSnapshot(adapter, { app_id })` — records one row per `db` metric via `collect`; **idempotent per UTC day** (skips a metric already snapshotted today). Guards nightly + manual re-runs.
75
+ - `metricSnapshotJob: MaintenanceJob` — `type: 'hazo_umetrics.metric_snapshot'`, `defaultCron: '0 3 * * *'`.
76
+ - `backfillDailySnapshots(adapter, { app_id, metric_key, valueAsOf, from, to? })` — one-time historical fill, one row per UTC day, skips days already present.
77
+
78
+ ### Types (`src/server/types.ts`)
79
+ - Added `MetricDef` (`key/label/format/source:'db'|'ga4'/collect?/backfill?`), `MetricSeriesPoint`, `MetricWindow`.
80
+
81
+ ### `recordStat` (`src/server/stats.ts`) — optional `captured_at`
82
+ - Added an optional `captured_at?: string` param (non-breaking; defaults to now) so snapshot/backfill can write day-stamped rows.
83
+
84
+ ### API (`src/api/index.ts`)
85
+ - `createStatHandlers` gains `getMetricWindows` — `GET …/stats/windows?app_id=&days=`, gated on `metrics.view`, returns `{ windows }`. Mirrors the `getStatSeries` handler.
86
+
87
+ ### UI (`src/ui/metrics_tab.tsx`, new)
88
+ - `MetricsTab({ appId })` — fetches `/api/hazo_umetrics/stats/windows`, renders **WindowGrid** (row per metric: `Total | 24h | 7d | 28d | 90d`, sign-coloured deltas, `null`→"—", `ga4` rows show a muted "GA4 pending" badge) + **Graph** (metric `<select>` + `[24h][7d][28d][90d][All]` filter → `LineChart`). 401/403 → same `metrics.view` empty state as `MetricsPanel`.
89
+
90
+ ### Tests / test-app
91
+ - New `__tests__/metrics.test.ts` (8 cases: window diffing, gap days, before-first-point null, ga4 pending, same-day collapse, snapshot idempotency, backfill idempotency).
92
+ - Test-app: new `/metrics-windows` demo page + `stats/windows` route + `register_window_metrics` module; seeder now emits 90-day daily snapshots (with deliberate gap days) for demo `db` metrics.
93
+
94
+ ## 1.9.2 — Journey panel: name-first user labels, calmer flow-graph arrows
95
+
96
+ **Fixes / UI polish (patch — no public API or type changes):**
97
+
98
+ ### `IdentityDropdown` — show name before email
99
+ - `labelFor(id)` now falls back **name → email → short-uuid** (was email → name → short-uuid), matching `SessionDetail`'s ordering. The raw uuid only shows when a profile resolves to neither a name nor an email.
100
+
101
+ ### `JourneyFlowGraph` — arrows no longer render "messed up"
102
+ - Arrowhead `<marker>` now sets `markerUnits="userSpaceOnUse"` so the head stays a constant size instead of scaling with edge `strokeWidth` (thick edges previously got oversized, broken-looking heads).
103
+ - Edge routing simplified: forward edges always use a clean right→left horizontal cubic (steep forward edges no longer get shoved into top/bottom ports). Back-edge and same-column vertical routing unchanged; both `multiSidePorts`/`backEdgeLoop` props remain as escape hatches.
104
+ - Curvature clamped: control distance is now `max(30, min(dist*0.4, 120))` (was `max(40, dist*0.5)`) so long edges don't balloon.
105
+
106
+ ## 1.9.1 — Journey panel: absolute per-step timestamps
107
+
108
+ ### `SessionDetail` — absolute per-step timestamps
109
+ - Each step row now shows a local `HH:MM:SS` clock time (`fmtClock`) alongside the existing relative gap; the session header shows the start date (`fmtDateTime`). Data (`step.captured_at`) was already present, just unrendered.
110
+
3
111
  ## 1.9.0 — Search queries: per-term last-searched timestamps, second-precision "over time"
4
112
 
5
113
  **New features (minor — no code-level breaking changes):**
@@ -33,9 +33,13 @@ app_id = my_app
33
33
  ; env = development
34
34
 
35
35
  [ga4]
36
- ; GA4 property ID (format: "properties/123456789"). Leave blank until M1 GA4 connect.
36
+ ; These three keys are OPTIONAL fallbacks only. Once an app completes the
37
+ ; /ga4 connect flow, the hazo_umetrics_ga4_connection table (per app_id) is
38
+ ; the source of truth for property_id/measurement_id — these ini values are
39
+ ; not read at runtime for a connected app. Leave commented in normal use.
40
+ ; GA4 property ID — BARE numeric id (the code prefixes "properties/"). e.g. 497179388
37
41
  ; property_id =
38
- ; GA4 Measurement ID (format: "G-XXXXXXXX"). Required for client-side event tracking.
42
+ ; GA4 Measurement ID (format: "G-XXXXXXXX"). Required for client-side event tracking. e.g. G-CBY26RF5JD
39
43
  ; measurement_id =
40
44
  ; GA4 Data API base URL (default: https://analyticsdata.googleapis.com/v1beta/)
41
45
  ; api_base_url =
@@ -55,6 +55,12 @@ export declare function createStatHandlers(options: ApiFactoryOptions): {
55
55
  * Appends a new stat reading.
56
56
  */
57
57
  recordStat(req: Request): Promise<Response>;
58
+ /**
59
+ * GET /...?app_id=<id>[&days=<n>]
60
+ * Returns windowed roll-ups (total/d1/d7/d28/d90 + daily series) for
61
+ * every metric registered against app_id via registerMetrics().
62
+ */
63
+ getMetricWindows(req: Request): Promise<Response>;
58
64
  };
59
65
  export declare function createExperimentHandlers(options: ApiFactoryOptions): {
60
66
  /**
@@ -82,10 +88,29 @@ export declare function createExperimentHandlers(options: ApiFactoryOptions): {
82
88
  stopExperiment(req: Request): Promise<Response>;
83
89
  };
84
90
  export declare function createGa4ConnectHandlers(options: ApiFactoryOptions): {
85
- /** STUB (M1): Initiate GA4 OAuth connection for an app. */
91
+ /**
92
+ * POST /...?app_id=<id>
93
+ * Body: { property_id, measurement_id, connected_user_id } | { disconnect: true }
94
+ * Persists (or clears) the per-app GA4 connection row. The browser has
95
+ * already driven Google consent (`requestGoogleScopes`) before calling this —
96
+ * this handler only records the resulting property/user linkage. Requires
97
+ * metrics.manage. Revocation of the Google token itself is delegated to
98
+ * hazo_auth's DELETE /google/token (not called here).
99
+ */
86
100
  connect(req: Request): Promise<Response>;
87
- /** STUB (M1): Provision a GA4 property for an app. */
101
+ /**
102
+ * POST /...?app_id=<id>
103
+ * Idempotent GA4 Admin provision — creates/patches custom dimensions,
104
+ * custom metrics, and key events from DEFAULT_GA4_MANIFEST, then persists
105
+ * the resolved IDs + provision_status. Requires metrics.manage.
106
+ */
88
107
  provision(req: Request): Promise<Response>;
108
+ /**
109
+ * GET /...?app_id=<id>[&flow_id=<id>]
110
+ * Reads a funnel via the GA4 Data API for the manifest flow matching
111
+ * flow_id (defaults to the first flow). Requires metrics.view.
112
+ */
113
+ funnel(req: Request): Promise<Response>;
89
114
  };
90
115
  export declare function createQueryHandlers(options: ApiFactoryOptions): {
91
116
  /** STUB (M1): Execute a GA4 or stat-store query. */
@@ -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
@@ -6,10 +6,16 @@
6
6
  // or callers supply their own `getAuth` implementation.
7
7
  import { createCrudService } from 'hazo_connect/server';
8
8
  import { recordStat as _recordStat, getLatestStat as _getLatestStat, getStatSeries as _getStatSeries, } from '../server/stats.js';
9
+ import { getMetricWindows as _getMetricWindows } from '../server/metrics.js';
10
+ import { createGa4MetricWindowReader } from '../server/ga4_metrics.js';
9
11
  import { resolveVariant } from '../server/flags.js';
10
12
  import { getActivitySummary as _getActivitySummary, getActivityTrend as _getActivityTrend, } from '../server/activity.js';
11
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';
12
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';
15
+ import { createGa4Client } from '../server/ga4.js';
16
+ import { createGa4Provisioner } from '../server/ga4_provision.js';
17
+ import { getFunnel } from '../server/funnel.js';
18
+ import { DEFAULT_GA4_MANIFEST } from '../server/ga4_manifest.js';
13
19
  // ---------------------------------------------------------------------------
14
20
  // Default auth — dynamically imports hazo_auth/server-lib to avoid the
15
21
  // server-only guard firing at module-evaluation time (e.g. in test runners).
@@ -162,6 +168,36 @@ export function createStatHandlers(options) {
162
168
  });
163
169
  return jsonOk({ recorded: true }, 201);
164
170
  },
171
+ /**
172
+ * GET /...?app_id=<id>[&days=<n>]
173
+ * Returns windowed roll-ups (total/d1/d7/d28/d90 + daily series) for
174
+ * every metric registered against app_id via registerMetrics().
175
+ */
176
+ async getMetricWindows(req) {
177
+ const url = new URL(req.url);
178
+ const app_id = url.searchParams.get('app_id') ?? '';
179
+ const daysParam = url.searchParams.get('days');
180
+ const days = daysParam != null ? parseInt(daysParam, 10) : 90;
181
+ const gate = await gateAuth(req, {
182
+ required_permissions: ['metrics.view'],
183
+ app_id: app_id || undefined,
184
+ getAuth,
185
+ });
186
+ if (!gate.ok)
187
+ return gate.response;
188
+ if (!app_id) {
189
+ return jsonError('BAD_REQUEST', 'app_id is required', 400);
190
+ }
191
+ const adapter = await options.getAdapter();
192
+ const ga4Reader = createGa4MetricWindowReader(adapter, {
193
+ getToken: async (userId, o) => {
194
+ const { getGoogleToken } = await import('hazo_auth/server-lib');
195
+ return getGoogleToken(userId, o);
196
+ },
197
+ });
198
+ const windows = await _getMetricWindows(adapter, { app_id, days }, { ga4Reader });
199
+ return jsonOk({ windows });
200
+ },
165
201
  };
166
202
  }
167
203
  export function createExperimentHandlers(options) {
@@ -319,12 +355,39 @@ export function createExperimentHandlers(options) {
319
355
  };
320
356
  }
321
357
  // ---------------------------------------------------------------------------
322
- // createGa4ConnectHandlers — STUB (M1)
358
+ // createGa4ConnectHandlers — GA4 OAuth connection, Admin provisioning, and
359
+ // funnel reads (M1 step 5, plan_m1_ga4.md).
360
+ //
361
+ // D-12/D-9: the Google OAuth token itself is never stored here — it's
362
+ // delegated to hazo_auth's `getGoogleToken(userId, opts)`, lazily imported
363
+ // inside each handler (mirrors `defaultGetAuth` above) so the `server-only`
364
+ // guard in `hazo_auth/server-lib` never fires at module-evaluation time.
323
365
  // ---------------------------------------------------------------------------
366
+ /** Google OAuth scopes needed to read + manage a GA4 property. */
367
+ const ANALYTICS_SCOPES = [
368
+ 'https://www.googleapis.com/auth/analytics.readonly',
369
+ 'https://www.googleapis.com/auth/analytics.edit',
370
+ ];
371
+ /** Read-only subset — sufficient for Data API funnel reads. */
372
+ const ANALYTICS_READONLY_SCOPES = [ANALYTICS_SCOPES[0]];
324
373
  export function createGa4ConnectHandlers(options) {
325
374
  const getAuth = options.getAuth ?? defaultGetAuth;
375
+ function getConnectionCrud(adapter) {
376
+ return createCrudService(adapter, 'hazo_umetrics_ga4_connection', {
377
+ primaryKeys: ['app_id'],
378
+ autoId: false,
379
+ });
380
+ }
326
381
  return {
327
- /** STUB (M1): Initiate GA4 OAuth connection for an app. */
382
+ /**
383
+ * POST /...?app_id=<id>
384
+ * Body: { property_id, measurement_id, connected_user_id } | { disconnect: true }
385
+ * Persists (or clears) the per-app GA4 connection row. The browser has
386
+ * already driven Google consent (`requestGoogleScopes`) before calling this —
387
+ * this handler only records the resulting property/user linkage. Requires
388
+ * metrics.manage. Revocation of the Google token itself is delegated to
389
+ * hazo_auth's DELETE /google/token (not called here).
390
+ */
328
391
  async connect(req) {
329
392
  const url = new URL(req.url);
330
393
  const app_id = url.searchParams.get('app_id') ?? '';
@@ -335,10 +398,70 @@ export function createGa4ConnectHandlers(options) {
335
398
  });
336
399
  if (!gate.ok)
337
400
  return gate.response;
338
- // STUB (M1)
339
- return jsonOk({ status: 'stub' });
401
+ let body;
402
+ try {
403
+ body = (await req.json());
404
+ }
405
+ catch {
406
+ return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
407
+ }
408
+ const { app_id: bodyAppId, property_id, measurement_id, connected_user_id, disconnect, } = body;
409
+ const resolvedAppId = (bodyAppId ?? app_id);
410
+ if (!resolvedAppId) {
411
+ return jsonError('BAD_REQUEST', 'app_id is required', 400);
412
+ }
413
+ const adapter = await options.getAdapter();
414
+ const crud = getConnectionCrud(adapter);
415
+ if (disconnect) {
416
+ await crud.deleteWhere({ app_id: resolvedAppId });
417
+ await options.onAudit?.({
418
+ action: 'ga4.disconnect',
419
+ actor_id: gate.userId,
420
+ app_id: resolvedAppId,
421
+ });
422
+ return jsonOk({ status: 'disconnected', app_id: resolvedAppId });
423
+ }
424
+ if (!property_id) {
425
+ return jsonError('BAD_REQUEST', 'property_id is required', 400);
426
+ }
427
+ const existing = await crud.findOneBy({ app_id: resolvedAppId });
428
+ const now = new Date().toISOString();
429
+ const merged = {
430
+ app_id: resolvedAppId,
431
+ scope_id: existing?.scope_id ?? '',
432
+ property_id,
433
+ measurement_id: measurement_id ?? existing?.measurement_id ?? null,
434
+ credential_ref: existing?.credential_ref ?? null,
435
+ provision_status: existing?.provision_status ?? 'unprovisioned',
436
+ last_provisioned_at: existing?.last_provisioned_at ?? null,
437
+ dimension_ids: existing?.dimension_ids ?? '[]',
438
+ connected_user_id: connected_user_id ?? existing?.connected_user_id ?? null,
439
+ metric_ids: existing?.metric_ids ?? '[]',
440
+ key_event_ids: existing?.key_event_ids ?? '[]',
441
+ created_at: existing?.created_at ?? now,
442
+ updated_at: now,
443
+ };
444
+ await crud.upsert(merged, ['app_id']);
445
+ await options.onAudit?.({
446
+ action: 'ga4.connect',
447
+ actor_id: gate.userId,
448
+ app_id: resolvedAppId,
449
+ details: { property_id },
450
+ });
451
+ return jsonOk({
452
+ status: 'connected',
453
+ app_id: resolvedAppId,
454
+ property_id: merged.property_id,
455
+ measurement_id: merged.measurement_id,
456
+ connected_user_id: merged.connected_user_id,
457
+ });
340
458
  },
341
- /** STUB (M1): Provision a GA4 property for an app. */
459
+ /**
460
+ * POST /...?app_id=<id>
461
+ * Idempotent GA4 Admin provision — creates/patches custom dimensions,
462
+ * custom metrics, and key events from DEFAULT_GA4_MANIFEST, then persists
463
+ * the resolved IDs + provision_status. Requires metrics.manage.
464
+ */
342
465
  async provision(req) {
343
466
  const url = new URL(req.url);
344
467
  const app_id = url.searchParams.get('app_id') ?? '';
@@ -349,8 +472,126 @@ export function createGa4ConnectHandlers(options) {
349
472
  });
350
473
  if (!gate.ok)
351
474
  return gate.response;
352
- // STUB (M1)
353
- return jsonOk({ status: 'stub' });
475
+ let body;
476
+ try {
477
+ body = (await req.json());
478
+ }
479
+ catch {
480
+ return jsonError('BAD_REQUEST', 'Invalid JSON body', 400);
481
+ }
482
+ const bodyAppId = body.app_id;
483
+ const resolvedAppId = (bodyAppId ?? app_id);
484
+ if (!resolvedAppId) {
485
+ return jsonError('BAD_REQUEST', 'app_id is required', 400);
486
+ }
487
+ const adapter = await options.getAdapter();
488
+ const crud = getConnectionCrud(adapter);
489
+ const row = await crud.findOneBy({ app_id: resolvedAppId });
490
+ if (!row) {
491
+ return jsonOk({ status: 'error', error: 'not_connected' });
492
+ }
493
+ const { getGoogleToken } = await import('hazo_auth/server-lib');
494
+ let result;
495
+ try {
496
+ const provisioner = createGa4Provisioner({
497
+ propertyId: row.property_id,
498
+ fetchToken: async () => {
499
+ const t = await getGoogleToken(row.connected_user_id, {
500
+ scopes: ANALYTICS_SCOPES,
501
+ });
502
+ if (!t.ok)
503
+ throw new Error('ga4_token_' + t.error);
504
+ return t.access_token;
505
+ },
506
+ });
507
+ result = await provisioner.provision();
508
+ }
509
+ catch (err) {
510
+ return jsonOk({
511
+ status: 'error',
512
+ error: err instanceof Error ? err.message : String(err),
513
+ });
514
+ }
515
+ const now = new Date().toISOString();
516
+ const updatedRow = {
517
+ ...row,
518
+ dimension_ids: JSON.stringify(result.dimension_ids),
519
+ metric_ids: JSON.stringify(result.metric_ids),
520
+ key_event_ids: JSON.stringify(result.key_event_ids),
521
+ provision_status: result.status,
522
+ last_provisioned_at: now,
523
+ updated_at: now,
524
+ };
525
+ await crud.upsert(updatedRow, ['app_id']);
526
+ await options.onAudit?.({
527
+ action: 'ga4.provision',
528
+ actor_id: gate.userId,
529
+ app_id: resolvedAppId,
530
+ details: { created: result.created, patched: result.patched, status: result.status, errors: result.errors },
531
+ });
532
+ return jsonOk({
533
+ status: result.status,
534
+ property_id: row.property_id,
535
+ provision: { created: result.created, patched: result.patched, skipped: result.skipped },
536
+ dimension_ids: result.dimension_ids,
537
+ metric_ids: result.metric_ids,
538
+ key_event_ids: result.key_event_ids,
539
+ errors: result.errors,
540
+ });
541
+ },
542
+ /**
543
+ * GET /...?app_id=<id>[&flow_id=<id>]
544
+ * Reads a funnel via the GA4 Data API for the manifest flow matching
545
+ * flow_id (defaults to the first flow). Requires metrics.view.
546
+ */
547
+ async funnel(req) {
548
+ const url = new URL(req.url);
549
+ const app_id = url.searchParams.get('app_id') ?? '';
550
+ const flow_id = url.searchParams.get('flow_id') ?? undefined;
551
+ const gate = await gateAuth(req, {
552
+ required_permissions: ['metrics.view'],
553
+ app_id: app_id || undefined,
554
+ getAuth,
555
+ });
556
+ if (!gate.ok)
557
+ return gate.response;
558
+ if (!app_id) {
559
+ return jsonError('BAD_REQUEST', 'app_id is required', 400);
560
+ }
561
+ const adapter = await options.getAdapter();
562
+ const crud = getConnectionCrud(adapter);
563
+ const row = await crud.findOneBy({ app_id });
564
+ if (!row) {
565
+ return jsonOk({ status: 'error', error: 'not_connected' });
566
+ }
567
+ const flow = flow_id
568
+ ? DEFAULT_GA4_MANIFEST.flows.find((f) => f.flowId === flow_id)
569
+ : DEFAULT_GA4_MANIFEST.flows[0];
570
+ if (!flow) {
571
+ return jsonOk({ status: 'error', error: 'unknown_flow' });
572
+ }
573
+ try {
574
+ const { getGoogleToken } = await import('hazo_auth/server-lib');
575
+ const client = createGa4Client({
576
+ propertyId: row.property_id,
577
+ fetchToken: async () => {
578
+ const t = await getGoogleToken(row.connected_user_id, {
579
+ scopes: ANALYTICS_READONLY_SCOPES,
580
+ });
581
+ if (!t.ok)
582
+ throw new Error('ga4_token_' + t.error);
583
+ return t.access_token;
584
+ },
585
+ });
586
+ const result = await getFunnel(client, flow, { startDate: '28daysAgo', endDate: 'today' });
587
+ return jsonOk({ status: 'ok', funnel: result, as_of: new Date().toISOString() });
588
+ }
589
+ catch (err) {
590
+ return jsonOk({
591
+ status: 'error',
592
+ error: err instanceof Error ? err.message : String(err),
593
+ });
594
+ }
354
595
  },
355
596
  };
356
597
  }
@@ -1,4 +1,6 @@
1
1
  import React from 'react';
2
+ export { trackFlowStep } from './lib/track_flow_step.js';
3
+ export type { TrackFlowStepParams } from './lib/track_flow_step.js';
2
4
  interface MetricsContextValue {
3
5
  appId: string | null | undefined;
4
6
  track: (metricKey: string, dimensions?: Record<string, string | number>) => void;
@@ -25,5 +27,4 @@ export declare function getIdentity(): {
25
27
  export declare function recordStat(_key: string, _value: number, _opts?: {
26
28
  unit?: string;
27
29
  }): void;
28
- export {};
29
30
  //# sourceMappingURL=index.client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAoD1B,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IACjF,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1G,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;CAClE;AASD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,wBAAwB,4EAsJxF;AAMD,wBAAgB,UAAU,IAAI,mBAAmB,CAEhD;AAMD,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAK7C;AAED,wBAAgB,WAAW,IAAI;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAE5E;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAExF"}
1
+ {"version":3,"file":"index.client.d.ts","sourceRoot":"","sources":["../src/index.client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAmDpE,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IACjF,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1G,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;CAClE;AASD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,wBAAwB,4EAsJxF;AAMD,wBAAgB,UAAU,IAAI,mBAAmB,CAEhD;AAMD,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAK7C;AAED,wBAAgB,WAAW,IAAI;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAE5E;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAExF"}
@@ -3,6 +3,7 @@
3
3
  'use client';
4
4
  import React from 'react';
5
5
  import { usePathname } from 'next/navigation';
6
+ export { trackFlowStep } from './lib/track_flow_step.js';
6
7
  // ---------------------------------------------------------------------------
7
8
  // Anonymous session identity (localStorage, SSR-guarded)
8
9
  // ---------------------------------------------------------------------------
@@ -1,9 +1,14 @@
1
1
  export * from '../server/types.js';
2
2
  export * from '../server/stats.js';
3
+ export * from '../server/metrics.js';
3
4
  export * from '../server/actions.js';
4
5
  export * from '../server/flags.js';
5
6
  export * from '../server/cookie.js';
6
7
  export * from '../server/ga4.js';
8
+ export * from '../server/ga4_manifest.js';
9
+ export * from '../server/ga4_provision.js';
10
+ export * from '../server/funnel.js';
11
+ export * from '../server/ga4_metrics.js';
7
12
  export * from '../server/experiments.js';
8
13
  export * from '../server/activity.js';
9
14
  export * from '../server/behavior.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AACA,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AACA,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC"}
package/dist/lib/index.js CHANGED
@@ -1,10 +1,15 @@
1
1
  // hazo_umetrics/src/lib/index.ts — re-exports server-side modules
2
2
  export * from '../server/types.js';
3
3
  export * from '../server/stats.js';
4
+ export * from '../server/metrics.js';
4
5
  export * from '../server/actions.js';
5
6
  export * from '../server/flags.js';
6
7
  export * from '../server/cookie.js';
7
8
  export * from '../server/ga4.js';
9
+ export * from '../server/ga4_manifest.js';
10
+ export * from '../server/ga4_provision.js';
11
+ export * from '../server/funnel.js';
12
+ export * from '../server/ga4_metrics.js';
8
13
  export * from '../server/experiments.js';
9
14
  export * from '../server/activity.js';
10
15
  export * from '../server/behavior.js';
@@ -0,0 +1,21 @@
1
+ export interface TrackFlowStepParams {
2
+ variant?: string;
3
+ featureFlag?: string;
4
+ appSection?: string;
5
+ stepDurationMs?: number;
6
+ [extra: string]: string | number | undefined;
7
+ }
8
+ declare global {
9
+ interface Window {
10
+ gtag?: (...args: unknown[]) => void;
11
+ }
12
+ }
13
+ /**
14
+ * Emits a GA4 event named `step`, with manifest dimension params:
15
+ * `flow_id`, `flow_step`, `variant`, `feature_flag`, `app_section`, `step_duration_ms`,
16
+ * plus any extra keys passed through as-is. Undefined params are omitted.
17
+ *
18
+ * Safe no-op when `window.gtag` isn't present (SSR, no GA loaded).
19
+ */
20
+ export declare function trackFlowStep(flowId: string, step: string, params?: TrackFlowStepParams): void;
21
+ //# sourceMappingURL=track_flow_step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track_flow_step.d.ts","sourceRoot":"","sources":["../../src/lib/track_flow_step.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC9C;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;KACrC;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAoB9F"}
@@ -0,0 +1,33 @@
1
+ // hazo_umetrics/src/lib/track_flow_step.ts — canonical client-safe flow-event helper.
2
+ // Emits a GA4 event via window.gtag using the manifest's dimension params. Mirrors the
3
+ // shape of gotimer's src/lib/ga-events.ts (which this replaces — that repo is untouched here).
4
+ // Must stay client-safe: no Node.js builtins, no server-only imports.
5
+ /**
6
+ * Emits a GA4 event named `step`, with manifest dimension params:
7
+ * `flow_id`, `flow_step`, `variant`, `feature_flag`, `app_section`, `step_duration_ms`,
8
+ * plus any extra keys passed through as-is. Undefined params are omitted.
9
+ *
10
+ * Safe no-op when `window.gtag` isn't present (SSR, no GA loaded).
11
+ */
12
+ export function trackFlowStep(flowId, step, params) {
13
+ if (typeof window === 'undefined' || typeof window.gtag !== 'function')
14
+ return;
15
+ const { variant, featureFlag, appSection, stepDurationMs, ...extra } = params ?? {};
16
+ const eventParams = {
17
+ flow_id: flowId,
18
+ flow_step: step,
19
+ };
20
+ if (variant !== undefined)
21
+ eventParams.variant = variant;
22
+ if (featureFlag !== undefined)
23
+ eventParams.feature_flag = featureFlag;
24
+ if (appSection !== undefined)
25
+ eventParams.app_section = appSection;
26
+ if (stepDurationMs !== undefined)
27
+ eventParams.step_duration_ms = stepDurationMs;
28
+ for (const [key, value] of Object.entries(extra)) {
29
+ if (value !== undefined)
30
+ eventParams[key] = value;
31
+ }
32
+ window.gtag('event', step, eventParams);
33
+ }