hazo_umetrics 0.1.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.
Files changed (43) hide show
  1. package/CHANGE_LOG.md +28 -0
  2. package/README.md +129 -0
  3. package/SETUP_CHECKLIST.md +118 -0
  4. package/config/hazo_umetrics_config.ini.sample +35 -0
  5. package/dist/api/index.d.ts +78 -0
  6. package/dist/api/index.d.ts.map +1 -0
  7. package/dist/api/index.js +375 -0
  8. package/dist/index.client.d.ts +23 -0
  9. package/dist/index.client.d.ts.map +1 -0
  10. package/dist/index.client.js +44 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +5 -0
  14. package/dist/lib/index.d.ts +7 -0
  15. package/dist/lib/index.d.ts.map +1 -0
  16. package/dist/lib/index.js +7 -0
  17. package/dist/server/cookie.d.ts +9 -0
  18. package/dist/server/cookie.d.ts.map +1 -0
  19. package/dist/server/cookie.js +32 -0
  20. package/dist/server/experiments.d.ts +30 -0
  21. package/dist/server/experiments.d.ts.map +1 -0
  22. package/dist/server/experiments.js +17 -0
  23. package/dist/server/flags.d.ts +44 -0
  24. package/dist/server/flags.d.ts.map +1 -0
  25. package/dist/server/flags.js +207 -0
  26. package/dist/server/ga4.d.ts +30 -0
  27. package/dist/server/ga4.d.ts.map +1 -0
  28. package/dist/server/ga4.js +19 -0
  29. package/dist/server/stats.d.ts +51 -0
  30. package/dist/server/stats.d.ts.map +1 -0
  31. package/dist/server/stats.js +129 -0
  32. package/dist/server/types.d.ts +65 -0
  33. package/dist/server/types.d.ts.map +1 -0
  34. package/dist/server/types.js +2 -0
  35. package/dist/ui/index.d.ts +2 -0
  36. package/dist/ui/index.d.ts.map +1 -0
  37. package/dist/ui/index.js +3 -0
  38. package/dist/ui/metrics_panel.d.ts +33 -0
  39. package/dist/ui/metrics_panel.d.ts.map +1 -0
  40. package/dist/ui/metrics_panel.js +195 -0
  41. package/migrations/db_setup_postgres.sql +116 -0
  42. package/migrations/db_setup_sqlite.sql +116 -0
  43. package/package.json +116 -0
@@ -0,0 +1,195 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { LineChart } from 'hazo_ui';
5
+ // ---------------------------------------------------------------------------
6
+ // Metric metadata (display labels + format hints)
7
+ // ---------------------------------------------------------------------------
8
+ const METRIC_KEYS = [
9
+ 'active_users',
10
+ 'timers_completed',
11
+ 'funnel_land',
12
+ 'funnel_start',
13
+ 'funnel_complete',
14
+ ];
15
+ const METRIC_META = {
16
+ active_users: { label: 'Active Users', format: 'count' },
17
+ timers_completed: { label: 'Timers Completed', format: 'count' },
18
+ funnel_land: { label: 'Funnel: Landed', format: 'count' },
19
+ funnel_start: { label: 'Funnel: Started', format: 'count' },
20
+ funnel_complete: { label: 'Funnel: Completed', format: 'count' },
21
+ };
22
+ function formatValue(value, format) {
23
+ switch (format) {
24
+ case 'currency':
25
+ return `$${value.toLocaleString()}`;
26
+ case 'percent':
27
+ return `${value}%`;
28
+ case 'duration':
29
+ return `${value}s`;
30
+ default:
31
+ return value.toLocaleString();
32
+ }
33
+ }
34
+ export function StatCards({ stats }) {
35
+ if (stats.length === 0) {
36
+ return (_jsx("div", { className: "rounded-lg border p-4 text-sm text-muted-foreground", children: "No stats" }));
37
+ }
38
+ return (_jsx("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-4", children: stats.map((s) => (_jsxs("div", { className: "rounded-lg border p-4", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: s.label }), _jsx("p", { className: "text-2xl font-semibold", children: formatValue(s.value, s.format) })] }, s.key))) }));
39
+ }
40
+ function formatTrendLabel(ts) {
41
+ const d = new Date(ts);
42
+ if (Number.isNaN(d.getTime()))
43
+ return ts;
44
+ return d.toLocaleString(undefined, {
45
+ month: 'short',
46
+ day: 'numeric',
47
+ hour: '2-digit',
48
+ minute: '2-digit',
49
+ });
50
+ }
51
+ export function StatTrend({ data, label }) {
52
+ if (data.length < 2) {
53
+ return (_jsx("div", { className: "rounded-lg border p-4 text-sm text-muted-foreground", children: "No trend data yet" }));
54
+ }
55
+ // hazo_ui LineChart gives us labelled X/Y axes + a hover cursor & value bubble.
56
+ const values = data.map((d) => d.value);
57
+ const dates = data.map((d) => formatTrendLabel(d.ts));
58
+ return (_jsxs("div", { className: "rounded-lg border p-4", children: [label != null && _jsx("p", { className: "mb-2 text-sm font-medium", children: label }), _jsx(LineChart, { data: values, dates: dates, color: "#6366f1", showTooltip: true })] }));
59
+ }
60
+ function typeBadgeClass(type) {
61
+ return type === 'flag'
62
+ ? 'inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-purple-100 text-purple-700'
63
+ : 'inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-700';
64
+ }
65
+ function statusBadgeClass(status) {
66
+ if (status === 'running') {
67
+ return 'inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-700';
68
+ }
69
+ if (status === 'stopped') {
70
+ return 'inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-red-100 text-red-700';
71
+ }
72
+ return 'inline-block rounded px-1.5 py-0.5 text-xs font-medium bg-gray-100 text-gray-600';
73
+ }
74
+ export function ExperimentList({ experiments }) {
75
+ if (experiments.length === 0) {
76
+ return (_jsx("div", { className: "rounded-lg border p-4 text-sm text-muted-foreground", children: "No experiments" }));
77
+ }
78
+ return (_jsx("div", { className: "flex flex-col gap-3", children: experiments.map((exp) => (_jsxs("div", { className: "rounded-lg border p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "font-semibold", children: exp.name }), _jsxs("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [_jsx("span", { className: typeBadgeClass(exp.type), children: exp.type }), _jsx("span", { className: statusBadgeClass(exp.status), children: exp.status })] })] }), _jsx("p", { className: "mt-1 font-mono text-xs text-muted-foreground", children: exp.key })] }, exp.key))) }));
79
+ }
80
+ const INITIAL_STATE = {
81
+ loading: true,
82
+ unauthenticated: false,
83
+ error: null,
84
+ stats: [],
85
+ trend: [],
86
+ experiments: [],
87
+ };
88
+ export function MetricsPanel({ scopeId }) {
89
+ const [state, setState] = useState(INITIAL_STATE);
90
+ useEffect(() => {
91
+ if (!scopeId)
92
+ return;
93
+ let cancelled = false;
94
+ async function fetchAll() {
95
+ setState(INITIAL_STATE);
96
+ // -----------------------------------------------------------------------
97
+ // 1. Experiments
98
+ // -----------------------------------------------------------------------
99
+ const expRes = await fetch(`/api/hazo_umetrics/experiments?scope_id=${encodeURIComponent(scopeId)}`);
100
+ if (expRes.status === 401) {
101
+ if (!cancelled)
102
+ setState({ ...INITIAL_STATE, loading: false, unauthenticated: true });
103
+ return;
104
+ }
105
+ if (!expRes.ok) {
106
+ const body = (await expRes.json().catch(() => null));
107
+ if (!cancelled)
108
+ setState({
109
+ ...INITIAL_STATE,
110
+ loading: false,
111
+ error: body?.error?.message ?? `HTTP ${expRes.status}`,
112
+ });
113
+ return;
114
+ }
115
+ const expBody = (await expRes.json());
116
+ const experiments = expBody.data?.experiments ?? [];
117
+ // -----------------------------------------------------------------------
118
+ // 2. Latest stat readings — fetch all metric keys in parallel
119
+ // -----------------------------------------------------------------------
120
+ const statResults = await Promise.all(METRIC_KEYS.map((key) => fetch(`/api/hazo_umetrics/stats?scope_id=${encodeURIComponent(scopeId)}&metric_key=${encodeURIComponent(key)}`)
121
+ .then((r) => {
122
+ if (r.status === 401)
123
+ return null; // propagate unauth up
124
+ if (!r.ok)
125
+ return null;
126
+ return r.json();
127
+ })
128
+ .catch(() => null)));
129
+ // Check for any 401 inside stat results (null return from 401 path)
130
+ // We already checked experiments for 401 — any null here is a soft failure.
131
+ const stats = METRIC_KEYS.reduce((acc, key, idx) => {
132
+ const result = statResults[idx];
133
+ const stat = result?.data?.stat;
134
+ if (stat != null) {
135
+ acc.push({
136
+ key,
137
+ label: METRIC_META[key].label,
138
+ value: stat.value,
139
+ unit: stat.unit ?? undefined,
140
+ format: METRIC_META[key].format,
141
+ });
142
+ }
143
+ return acc;
144
+ }, []);
145
+ // -----------------------------------------------------------------------
146
+ // 3. Trend series for the first metric key
147
+ // -----------------------------------------------------------------------
148
+ const trendKey = 'active_users';
149
+ const seriesRes = await fetch(`/api/hazo_umetrics/stats/series?scope_id=${encodeURIComponent(scopeId)}&metric_key=${encodeURIComponent(trendKey)}`).catch(() => null);
150
+ let trend = [];
151
+ if (seriesRes != null && seriesRes.ok) {
152
+ const seriesBody = (await seriesRes.json().catch(() => null));
153
+ trend = (seriesBody?.data?.series ?? []).map((s) => ({
154
+ ts: s.captured_at,
155
+ value: s.value,
156
+ }));
157
+ }
158
+ if (!cancelled) {
159
+ setState({
160
+ loading: false,
161
+ unauthenticated: false,
162
+ error: null,
163
+ stats,
164
+ trend,
165
+ experiments,
166
+ });
167
+ }
168
+ }
169
+ fetchAll().catch((err) => {
170
+ if (!cancelled) {
171
+ const message = err instanceof Error ? err.message : 'Unexpected error';
172
+ setState({ ...INITIAL_STATE, loading: false, error: message });
173
+ }
174
+ });
175
+ return () => {
176
+ cancelled = true;
177
+ };
178
+ }, [scopeId]);
179
+ // -------------------------------------------------------------------------
180
+ // Render states
181
+ // -------------------------------------------------------------------------
182
+ if (state.loading) {
183
+ return (_jsx("div", { className: "flex items-center justify-center p-8 text-sm text-muted-foreground", children: "Loading metrics\u2026" }));
184
+ }
185
+ if (state.unauthenticated) {
186
+ return (_jsx("div", { className: "flex items-center justify-center rounded-lg border p-8 text-sm text-muted-foreground", children: "Log in to view metrics" }));
187
+ }
188
+ if (state.error != null) {
189
+ return (_jsxs("div", { className: "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700", children: ["Failed to load metrics: ", state.error] }));
190
+ }
191
+ // -------------------------------------------------------------------------
192
+ // Main render
193
+ // -------------------------------------------------------------------------
194
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Stats" }), _jsx(StatCards, { stats: state.stats })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Active Users Trend" }), _jsx(StatTrend, { data: state.trend, label: METRIC_META.active_users.label })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Experiments & Flags" }), _jsx(ExperimentList, { experiments: state.experiments })] })] }));
195
+ }
@@ -0,0 +1,116 @@
1
+ -- hazo_umetrics database schema — PostgreSQL
2
+ --
3
+ -- Supports both PostgreSQL (this file) and SQLite (db_setup_sqlite.sql).
4
+ -- All tables use the `hazo_umetrics_` prefix per workspace standards.
5
+ -- Every table carries `scope_id` (TEXT) for multi-tenant isolation.
6
+ --
7
+ -- Tables:
8
+ -- hazo_umetrics_stat — append-only metric readings
9
+ -- hazo_umetrics_stat_def — metric key definitions / display metadata
10
+ -- hazo_umetrics_experiment — feature flags and A/B experiments
11
+ -- hazo_umetrics_assignment — sticky subject→variant assignments
12
+ -- hazo_umetrics_exposure — exposure + goal events
13
+ -- hazo_umetrics_ga4_connection — per-scope GA4 property credentials
14
+ -- hazo_umetrics_dashboard — saved dashboard layouts
15
+
16
+ -- ---------------------------------------------------------------------------
17
+ -- Stat store
18
+ -- ---------------------------------------------------------------------------
19
+
20
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_stat (
21
+ id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
22
+ scope_id TEXT NOT NULL,
23
+ app_id TEXT NOT NULL DEFAULT '',
24
+ metric_key TEXT NOT NULL,
25
+ value DOUBLE PRECISION NOT NULL,
26
+ unit TEXT,
27
+ dimensions JSONB DEFAULT NULL,
28
+ captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
29
+ );
30
+ -- Primary query pattern: latest/series for a (scope, key) over time
31
+ CREATE INDEX IF NOT EXISTS idx_hazo_umetrics_stat_scope_key_ts
32
+ ON hazo_umetrics_stat (scope_id, metric_key, captured_at);
33
+
34
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_stat_def (
35
+ metric_key TEXT NOT NULL,
36
+ scope_id TEXT NOT NULL,
37
+ label TEXT NOT NULL,
38
+ description TEXT NOT NULL DEFAULT '',
39
+ format TEXT NOT NULL DEFAULT 'count', -- count | currency | duration | percent
40
+ cadence TEXT,
41
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
42
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
43
+ PRIMARY KEY (metric_key, scope_id)
44
+ );
45
+
46
+ -- ---------------------------------------------------------------------------
47
+ -- Feature flags / experiments
48
+ -- ---------------------------------------------------------------------------
49
+
50
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_experiment (
51
+ key TEXT NOT NULL,
52
+ scope_id TEXT NOT NULL,
53
+ name TEXT NOT NULL,
54
+ type TEXT NOT NULL DEFAULT 'flag',
55
+ variants JSONB NOT NULL DEFAULT '[]',
56
+ traffic_allocation DOUBLE PRECISION NOT NULL DEFAULT 100.0,
57
+ status TEXT NOT NULL DEFAULT 'draft', -- draft | running | stopped
58
+ targeting JSONB NOT NULL DEFAULT '{}',
59
+ goal_event TEXT,
60
+ target_sample_size INTEGER,
61
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
62
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
63
+ PRIMARY KEY (key, scope_id)
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_assignment (
67
+ experiment_key TEXT NOT NULL,
68
+ scope_id TEXT NOT NULL,
69
+ subject_id TEXT NOT NULL,
70
+ variant TEXT NOT NULL,
71
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
72
+ PRIMARY KEY (experiment_key, scope_id, subject_id)
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_exposure (
76
+ id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
77
+ experiment_key TEXT NOT NULL,
78
+ scope_id TEXT NOT NULL,
79
+ subject_id TEXT NOT NULL,
80
+ variant TEXT NOT NULL,
81
+ event TEXT NOT NULL DEFAULT 'exposure',
82
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
83
+ );
84
+ CREATE INDEX IF NOT EXISTS idx_hazo_umetrics_exposure_key_scope
85
+ ON hazo_umetrics_exposure (experiment_key, scope_id, subject_id);
86
+
87
+ -- ---------------------------------------------------------------------------
88
+ -- GA4 integration
89
+ -- ---------------------------------------------------------------------------
90
+
91
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_ga4_connection (
92
+ scope_id TEXT NOT NULL PRIMARY KEY,
93
+ property_id TEXT,
94
+ measurement_id TEXT,
95
+ credential_ref TEXT,
96
+ provision_status TEXT NOT NULL DEFAULT 'unprovisioned',
97
+ last_provisioned_at TIMESTAMPTZ,
98
+ dimension_ids JSONB NOT NULL DEFAULT '[]',
99
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
100
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
101
+ );
102
+
103
+ -- ---------------------------------------------------------------------------
104
+ -- Dashboard
105
+ -- ---------------------------------------------------------------------------
106
+
107
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_dashboard (
108
+ id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
109
+ scope_id TEXT NOT NULL,
110
+ name TEXT NOT NULL,
111
+ layout JSONB NOT NULL DEFAULT '[]',
112
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
113
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
114
+ );
115
+ CREATE INDEX IF NOT EXISTS idx_hazo_umetrics_dashboard_scope
116
+ ON hazo_umetrics_dashboard (scope_id);
@@ -0,0 +1,116 @@
1
+ -- hazo_umetrics database schema — SQLite
2
+ --
3
+ -- Supports both PostgreSQL (db_setup_postgres.sql) and SQLite (this file).
4
+ -- All tables use the `hazo_umetrics_` prefix per workspace standards.
5
+ -- Every table carries `scope_id` (TEXT) for multi-tenant isolation.
6
+ --
7
+ -- Tables:
8
+ -- hazo_umetrics_stat — append-only metric readings
9
+ -- hazo_umetrics_stat_def — metric key definitions / display metadata
10
+ -- hazo_umetrics_experiment — feature flags and A/B experiments
11
+ -- hazo_umetrics_assignment — sticky subject→variant assignments
12
+ -- hazo_umetrics_exposure — exposure + goal events
13
+ -- hazo_umetrics_ga4_connection — per-scope GA4 property credentials
14
+ -- hazo_umetrics_dashboard — saved dashboard layouts
15
+
16
+ -- ---------------------------------------------------------------------------
17
+ -- Stat store
18
+ -- ---------------------------------------------------------------------------
19
+
20
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_stat (
21
+ id TEXT NOT NULL PRIMARY KEY,
22
+ scope_id TEXT NOT NULL,
23
+ app_id TEXT NOT NULL DEFAULT '',
24
+ metric_key TEXT NOT NULL,
25
+ value REAL NOT NULL,
26
+ unit TEXT,
27
+ dimensions TEXT DEFAULT NULL, -- JSON object or null
28
+ captured_at TEXT NOT NULL DEFAULT (datetime('now'))
29
+ );
30
+ -- Primary query pattern: latest/series for a (scope, key) over time
31
+ CREATE INDEX IF NOT EXISTS idx_hazo_umetrics_stat_scope_key_ts
32
+ ON hazo_umetrics_stat (scope_id, metric_key, captured_at);
33
+
34
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_stat_def (
35
+ metric_key TEXT NOT NULL,
36
+ scope_id TEXT NOT NULL,
37
+ label TEXT NOT NULL,
38
+ description TEXT NOT NULL DEFAULT '',
39
+ format TEXT NOT NULL DEFAULT 'count', -- count | currency | duration | percent
40
+ cadence TEXT, -- e.g. 'daily', 'hourly', or null
41
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
42
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
43
+ PRIMARY KEY (metric_key, scope_id)
44
+ );
45
+
46
+ -- ---------------------------------------------------------------------------
47
+ -- Feature flags / experiments
48
+ -- ---------------------------------------------------------------------------
49
+
50
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_experiment (
51
+ key TEXT NOT NULL,
52
+ scope_id TEXT NOT NULL,
53
+ name TEXT NOT NULL,
54
+ type TEXT NOT NULL DEFAULT 'flag', -- flag | experiment
55
+ variants TEXT NOT NULL DEFAULT '[]', -- JSON [{name,weight}]
56
+ traffic_allocation REAL NOT NULL DEFAULT 100.0, -- 0-100 %
57
+ status TEXT NOT NULL DEFAULT 'draft', -- draft | running | stopped
58
+ targeting TEXT NOT NULL DEFAULT '{}', -- JSON targeting rules
59
+ goal_event TEXT, -- null for flags
60
+ target_sample_size INTEGER, -- null until powered
61
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
62
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
63
+ PRIMARY KEY (key, scope_id)
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_assignment (
67
+ experiment_key TEXT NOT NULL,
68
+ scope_id TEXT NOT NULL,
69
+ subject_id TEXT NOT NULL,
70
+ variant TEXT NOT NULL,
71
+ assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
72
+ PRIMARY KEY (experiment_key, scope_id, subject_id)
73
+ );
74
+
75
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_exposure (
76
+ id TEXT NOT NULL PRIMARY KEY,
77
+ experiment_key TEXT NOT NULL,
78
+ scope_id TEXT NOT NULL,
79
+ subject_id TEXT NOT NULL,
80
+ variant TEXT NOT NULL,
81
+ event TEXT NOT NULL DEFAULT 'exposure', -- exposure | <goal-event-name>
82
+ timestamp TEXT NOT NULL DEFAULT (datetime('now'))
83
+ );
84
+ CREATE INDEX IF NOT EXISTS idx_hazo_umetrics_exposure_key_scope
85
+ ON hazo_umetrics_exposure (experiment_key, scope_id, subject_id);
86
+
87
+ -- ---------------------------------------------------------------------------
88
+ -- GA4 integration
89
+ -- ---------------------------------------------------------------------------
90
+
91
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_ga4_connection (
92
+ scope_id TEXT NOT NULL PRIMARY KEY,
93
+ property_id TEXT,
94
+ measurement_id TEXT,
95
+ credential_ref TEXT, -- encrypted JSON (AES-GCM via hazo_secure)
96
+ provision_status TEXT NOT NULL DEFAULT 'unprovisioned',
97
+ last_provisioned_at TEXT,
98
+ dimension_ids TEXT NOT NULL DEFAULT '[]', -- JSON string array
99
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
100
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
101
+ );
102
+
103
+ -- ---------------------------------------------------------------------------
104
+ -- Dashboard
105
+ -- ---------------------------------------------------------------------------
106
+
107
+ CREATE TABLE IF NOT EXISTS hazo_umetrics_dashboard (
108
+ id TEXT NOT NULL PRIMARY KEY,
109
+ scope_id TEXT NOT NULL,
110
+ name TEXT NOT NULL,
111
+ layout TEXT NOT NULL DEFAULT '[]', -- JSON layout config
112
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
113
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
114
+ );
115
+ CREATE INDEX IF NOT EXISTS idx_hazo_umetrics_dashboard_scope
116
+ ON hazo_umetrics_dashboard (scope_id);
package/package.json ADDED
@@ -0,0 +1,116 @@
1
+ {
2
+ "name": "hazo_umetrics",
3
+ "version": "0.1.1",
4
+ "description": "Product analytics for hazo apps — GA4 hybrid + first-party stat store + feature flags",
5
+ "type": "module",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./dist/index.client.d.ts",
15
+ "import": "./dist/index.client.js"
16
+ },
17
+ "./api": {
18
+ "types": "./dist/api/index.d.ts",
19
+ "import": "./dist/api/index.js"
20
+ },
21
+ "./ui": {
22
+ "types": "./dist/ui/index.d.ts",
23
+ "import": "./dist/ui/index.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "migrations",
30
+ "config",
31
+ "README.md",
32
+ "CHANGE_LOG.md",
33
+ "SETUP_CHECKLIST.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "type-check": "tsc --noEmit",
38
+ "test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --config jest.config.cjs",
39
+ "dev:test-app": "npm run build && cd test-app && npm run dev",
40
+ "build:test-app": "npm run build && cd test-app && npm run build"
41
+ },
42
+ "peerDependencies": {
43
+ "hazo_core": "^1.1.0",
44
+ "hazo_logs": "^2.0.3",
45
+ "hazo_config": "^2.1.10",
46
+ "hazo_connect": "^3.5.1",
47
+ "hazo_api": "^2.3.1",
48
+ "hazo_auth": "^10.1.0",
49
+ "hazo_secure": "^1.3.0",
50
+ "hazo_audit": "^2.1.1",
51
+ "hazo_ui": "^3.5.0",
52
+ "react": "^18.0.0 || ^19.0.0",
53
+ "react-dom": "^18.0.0 || ^19.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "hazo_logs": {
57
+ "optional": true
58
+ },
59
+ "hazo_config": {
60
+ "optional": true
61
+ },
62
+ "hazo_connect": {
63
+ "optional": true
64
+ },
65
+ "hazo_api": {
66
+ "optional": true
67
+ },
68
+ "hazo_auth": {
69
+ "optional": true
70
+ },
71
+ "hazo_secure": {
72
+ "optional": true
73
+ },
74
+ "hazo_audit": {
75
+ "optional": true
76
+ },
77
+ "hazo_ui": {
78
+ "optional": true
79
+ },
80
+ "react": {
81
+ "optional": true
82
+ },
83
+ "react-dom": {
84
+ "optional": true
85
+ }
86
+ },
87
+ "dependencies": {
88
+ "clsx": "^2.1.1",
89
+ "tailwind-merge": "^3.5.0"
90
+ },
91
+ "devDependencies": {
92
+ "typescript": "^5.7.2",
93
+ "jest": "^30.2.0",
94
+ "ts-jest": "^29.4.5",
95
+ "@types/jest": "^30.0.0",
96
+ "jest-environment-node": "^30.2.0",
97
+ "@types/node": "^22.10.0",
98
+ "@types/react": "^19.0.0",
99
+ "@types/react-dom": "^19.0.0",
100
+ "tailwindcss": "^4.2.4",
101
+ "@tailwindcss/postcss": "^4.2.4",
102
+ "postcss": "^8.4.49"
103
+ },
104
+ "keywords": [
105
+ "analytics",
106
+ "product-analytics",
107
+ "metrics",
108
+ "feature-flags",
109
+ "ab-testing",
110
+ "experiments",
111
+ "ga4",
112
+ "hazo"
113
+ ],
114
+ "author": "Pubs Abayasiri",
115
+ "license": "MIT"
116
+ }