hazo_umetrics 1.5.1 → 1.6.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.
@@ -5,6 +5,7 @@ export type TopEntry = {
5
5
  };
6
6
  export type ActivitySummary = {
7
7
  activeUsers: number;
8
+ activeUserIds: string[];
8
9
  pageViews: number;
9
10
  clicks: number;
10
11
  topPages: TopEntry[];
@@ -1 +1 @@
1
- {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/server/activity.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAsB9D,MAAM,MAAM,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,UAAU,EAAE,QAAQ,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA8DvD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,eAAe,CAAC,CAoC1B;AAMD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,UAAU,EAAE,CAAC,CAqBvB"}
1
+ {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/server/activity.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAsB9D,MAAM,MAAM,QAAQ,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,UAAU,EAAE,QAAQ,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA8DvD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,eAAe,CAAC,CAwC1B;AAMD;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE;IACN,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,UAAU,EAAE,CAAC,CAqBvB"}
@@ -63,6 +63,7 @@ export async function getActivitySummary(adapter, params) {
63
63
  const rows = await fetchWindow(adapter, { app_id: params.app_id, scope_id: params.scope_id, since, cap });
64
64
  const uiRows = rows.filter((r) => r.metric_key.startsWith('ui.'));
65
65
  const sessions = new Set();
66
+ const userIds = new Set();
66
67
  const pageTally = new Map();
67
68
  const btnTally = new Map();
68
69
  let pageViews = 0;
@@ -72,6 +73,9 @@ export async function getActivitySummary(adapter, params) {
72
73
  const sid = dims.session_id;
73
74
  if (sid)
74
75
  sessions.add(sid);
76
+ const uid = dims.user_id;
77
+ if (uid)
78
+ userIds.add(uid);
75
79
  if (row.metric_key.startsWith('ui.pageview.')) {
76
80
  pageViews++;
77
81
  const path = dims.path ?? row.metric_key.replace('ui.pageview.', '/');
@@ -85,6 +89,7 @@ export async function getActivitySummary(adapter, params) {
85
89
  }
86
90
  return {
87
91
  activeUsers: sessions.size,
92
+ activeUserIds: Array.from(userIds),
88
93
  pageViews,
89
94
  clicks,
90
95
  topPages: topN(pageTally, 5),
@@ -1 +1 @@
1
- {"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,KAAK,EAAE,YAAY,CAAC;CACrB;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC3I;AA0QD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,EAAE,iBAAiB,+BA8IzG"}
1
+ {"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,KAAK,EAAE,YAAY,CAAC;CACrB;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC3I;AA+VD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,EAAE,iBAAiB,+BA8IzG"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState, useEffect } from 'react';
3
+ import { useState, useEffect, useRef } from 'react';
4
4
  // ---------------------------------------------------------------------------
5
5
  // Helpers
6
6
  // ---------------------------------------------------------------------------
@@ -21,6 +21,7 @@ function kindColor(kind) {
21
21
  default: return '#94a3b8';
22
22
  }
23
23
  }
24
+ function clampZoom(z) { return Math.max(0.25, Math.min(3, z)); }
24
25
  // ---------------------------------------------------------------------------
25
26
  // JourneyFlowGraph — SVG left-to-right DAG visualization
26
27
  // ---------------------------------------------------------------------------
@@ -102,6 +103,25 @@ function computeFlowLayout(nodes, edges) {
102
103
  return pos;
103
104
  }
104
105
  function JourneyFlowGraph({ graph }) {
106
+ const [zoom, setZoom] = useState(1.0);
107
+ const [pan, setPan] = useState({ x: 0, y: 0 });
108
+ const [dragging, setDragging] = useState(false);
109
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
110
+ const [selectedNode, setSelectedNode] = useState(null);
111
+ const containerRef = useRef(null);
112
+ // Non-passive wheel listener so preventDefault() actually suppresses page scroll.
113
+ // React 17+ registers onWheel as passive, which silently ignores preventDefault.
114
+ useEffect(() => {
115
+ const el = containerRef.current;
116
+ if (!el)
117
+ return;
118
+ const onWheel = (e) => {
119
+ e.preventDefault();
120
+ setZoom(z => clampZoom(z * (e.deltaY < 0 ? 1.1 : 0.9)));
121
+ };
122
+ el.addEventListener('wheel', onWheel, { passive: false });
123
+ return () => el.removeEventListener('wheel', onWheel);
124
+ }, []);
105
125
  if (graph.nodes.length === 0)
106
126
  return null;
107
127
  const pos = computeFlowLayout(graph.nodes, graph.edges);
@@ -120,8 +140,43 @@ function JourneyFlowGraph({ graph }) {
120
140
  const offsetY = minY < 0 ? -minY + PAD : PAD;
121
141
  const svgH = maxY - minY + PAD * 2;
122
142
  const svgW = maxX;
143
+ function handleMouseDown(e) {
144
+ if (e.target.closest('[data-node]'))
145
+ return;
146
+ setDragging(true);
147
+ setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y });
148
+ }
149
+ function handleMouseMove(e) {
150
+ if (!dragging)
151
+ return;
152
+ setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
153
+ }
154
+ function handleMouseUp() { setDragging(false); }
155
+ function fitToScreen() {
156
+ if (!containerRef.current)
157
+ return;
158
+ const cw = containerRef.current.clientWidth;
159
+ const ch = containerRef.current.clientHeight;
160
+ const scale = clampZoom(Math.min(cw / svgW, ch / svgH) * 0.9);
161
+ setZoom(scale);
162
+ setPan({ x: (cw - svgW * scale) / 2, y: (ch - svgH * scale) / 2 });
163
+ }
164
+ const highlightedEdges = new Set();
165
+ const highlightedNodes = new Set();
166
+ if (selectedNode) {
167
+ highlightedNodes.add(selectedNode);
168
+ graph.edges.forEach((edge, i) => {
169
+ if (edge.from === selectedNode || edge.to === selectedNode) {
170
+ highlightedEdges.add(i);
171
+ highlightedNodes.add(edge.from);
172
+ highlightedNodes.add(edge.to);
173
+ }
174
+ });
175
+ }
176
+ const hasSelection = selectedNode !== null;
123
177
  const maxCount = Math.max(...graph.nodes.map(n => n.count), 1);
124
- return (_jsxs("div", { className: "rounded-lg border bg-white overflow-x-auto", children: [_jsxs("div", { className: "px-4 py-2 border-b flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wide", children: "Flow Graph" }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", graph.nodes.length, " nodes \u00B7 ", graph.edges.length, " transitions)"] }), _jsxs("div", { className: "ml-auto flex items-center gap-3 text-[10px] text-gray-400", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#6366f1' } }), " action"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#10b981' } }), " pageview"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#ef4444' } }), " error"] })] })] }), _jsx("div", { style: { overflowX: 'auto' }, children: _jsxs("svg", { width: svgW, height: Math.max(svgH, 200), style: { display: 'block' }, children: [_jsx("defs", { children: _jsx("marker", { id: "arrow", markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: "auto", children: _jsx("path", { d: "M0,0 L0,6 L8,3 z", fill: "#94a3b8" }) }) }), graph.edges.map((edge, i) => {
178
+ return (_jsxs("div", { className: "rounded-lg border bg-white", children: [_jsxs("div", { className: "px-4 py-2 border-b flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-semibold text-gray-600 uppercase tracking-wide", children: "Flow Graph" }), _jsxs("span", { className: "text-xs text-gray-400", children: ["(", graph.nodes.length, " nodes \u00B7 ", graph.edges.length, " transitions)"] }), _jsxs("div", { className: "ml-auto flex items-center gap-3 text-[10px] text-gray-400", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#6366f1' } }), " action"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#10b981' } }), " pageview"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "inline-block w-2 h-2 rounded-sm", style: { backgroundColor: '#ef4444' } }), " error"] }), _jsxs("div", { className: "flex items-center gap-1 ml-2", children: [_jsx("button", { onClick: () => setZoom(z => clampZoom(z * 1.25)), className: "px-1.5 py-0.5 text-xs border rounded hover:bg-gray-50", title: "Zoom in", children: "+" }), _jsx("button", { onClick: () => setZoom(z => clampZoom(z / 1.25)), className: "px-1.5 py-0.5 text-xs border rounded hover:bg-gray-50", title: "Zoom out", children: "\u2212" }), _jsx("button", { onClick: fitToScreen, className: "px-1.5 py-0.5 text-xs border rounded hover:bg-gray-50", title: "Fit to screen", children: "\u22A1" })] })] })] }), _jsx("div", { ref: containerRef, style: { position: 'relative', height: '500px', overflow: 'hidden', cursor: dragging ? 'grabbing' : 'grab' }, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, children: _jsxs("svg", { width: svgW, height: Math.max(svgH, 200), style: { display: 'block', transform: `translate(${pan.x}px,${pan.y}px) scale(${zoom})`, transformOrigin: '0 0', userSelect: 'none' }, onClick: (e) => { if (e.target === e.currentTarget)
179
+ setSelectedNode(null); }, children: [_jsx("defs", { children: _jsx("marker", { id: "arrow", markerWidth: "8", markerHeight: "8", refX: "7", refY: "3", orient: "auto", children: _jsx("path", { d: "M0,0 L0,6 L8,3 z", fill: "#94a3b8" }) }) }), graph.edges.map((edge, i) => {
125
180
  const from = pos.get(edge.from);
126
181
  const to = pos.get(edge.to);
127
182
  if (!from || !to)
@@ -131,9 +186,11 @@ function JourneyFlowGraph({ graph }) {
131
186
  const x2 = to.x;
132
187
  const y2 = to.y + offsetY + NODE_H / 2;
133
188
  const cx = (x1 + x2) / 2;
134
- const opacity = Math.max(0.2, edge.count / maxCount);
135
- const strokeW = Math.max(1, Math.min(4, edge.count / maxCount * 4));
136
- return (_jsxs("g", { children: [_jsx("path", { d: `M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`, fill: "none", stroke: "#94a3b8", strokeWidth: strokeW, strokeOpacity: opacity, markerEnd: "url(#arrow)" }), edge.count > 1 && (_jsxs("text", { x: cx, y: (y1 + y2) / 2 - 4, textAnchor: "middle", fontSize: "9", fill: "#94a3b8", opacity: opacity, children: [edge.count, "\u00D7"] }))] }, i));
189
+ const isHighlighted = hasSelection ? highlightedEdges.has(i) : true;
190
+ const edgeOpacity = isHighlighted ? Math.max(0.4, edge.count / maxCount) : 0.05;
191
+ const edgeStroke = isHighlighted && hasSelection ? '#6366f1' : '#94a3b8';
192
+ const edgeStrokeW = isHighlighted ? Math.max(1.5, Math.min(4, edge.count / maxCount * 4)) : 1;
193
+ return (_jsxs("g", { children: [_jsx("path", { d: `M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`, fill: "none", stroke: edgeStroke, strokeWidth: edgeStrokeW, strokeOpacity: edgeOpacity, markerEnd: "url(#arrow)" }), edge.count > 1 && (_jsxs("text", { x: cx, y: (y1 + y2) / 2 - 4, textAnchor: "middle", fontSize: "9", fill: "#94a3b8", opacity: edgeOpacity, children: [edge.count, "\u00D7"] }))] }, i));
137
194
  }), graph.nodes.map((node) => {
138
195
  const p = pos.get(node.id);
139
196
  if (!p)
@@ -141,9 +198,12 @@ function JourneyFlowGraph({ graph }) {
141
198
  const x = p.x;
142
199
  const y = p.y + offsetY;
143
200
  const fill = kindColor(node.kind);
144
- const alpha = Math.max(0.25, node.count / maxCount);
201
+ const isNodeHighlighted = hasSelection ? highlightedNodes.has(node.id) : true;
202
+ const nodeAlpha = isNodeHighlighted ? Math.max(0.55, node.count / maxCount) : 0.1;
203
+ const nodeStrokeW = node.id === selectedNode ? 2.5 : 1.5;
204
+ const nodeStrokeOpacity = isNodeHighlighted ? Math.min(1, nodeAlpha + 0.3) : 0.1;
145
205
  const label = node.label.length > 18 ? node.label.slice(0, 17) + '…' : node.label;
146
- return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: NODE_W, height: NODE_H, rx: 6, fill: fill, fillOpacity: alpha, stroke: fill, strokeOpacity: Math.min(1, alpha + 0.3), strokeWidth: 1.5 }), _jsx("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 - 5, textAnchor: "middle", fontSize: "11", fontWeight: "600", fill: "#1e293b", children: label }), _jsxs("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 + 9, textAnchor: "middle", fontSize: "9", fill: "#475569", children: [node.count, "\u00D7"] })] }, node.id));
206
+ return (_jsxs("g", { "data-node": "true", onClick: (e) => { e.stopPropagation(); setSelectedNode(prev => prev === node.id ? null : node.id); }, style: { cursor: 'pointer' }, children: [_jsx("rect", { x: x, y: y, width: NODE_W, height: NODE_H, rx: 6, fill: fill, fillOpacity: nodeAlpha, stroke: fill, strokeOpacity: nodeStrokeOpacity, strokeWidth: nodeStrokeW }), _jsx("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 - 5, textAnchor: "middle", fontSize: "11", fontWeight: "600", fill: "#1e293b", children: label }), _jsxs("text", { x: x + NODE_W / 2, y: y + NODE_H / 2 + 9, textAnchor: "middle", fontSize: "9", fill: "#475569", children: [node.count, "\u00D7"] })] }, node.id));
147
207
  })] }) })] }));
148
208
  }
149
209
  function SessionDetail({ session, index, profile }) {
@@ -1,3 +1,4 @@
1
+ import type { UserProfileInfo } from './analytics_panel.js';
1
2
  export interface StatCardsProps {
2
3
  stats: Array<{
3
4
  key: string;
@@ -28,6 +29,7 @@ export interface ExperimentListProps {
28
29
  export declare function ExperimentList({ experiments }: ExperimentListProps): import("react").JSX.Element;
29
30
  export interface MetricsPanelProps {
30
31
  appId: string;
32
+ resolveUserProfiles?: (ids: string[]) => Promise<UserProfileInfo[]>;
31
33
  }
32
- export declare function MetricsPanel({ appId }: MetricsPanelProps): import("react").JSX.Element;
34
+ export declare function MetricsPanel({ appId, resolveUserProfiles }: MetricsPanelProps): import("react").JSX.Element;
33
35
  //# sourceMappingURL=metrics_panel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics_panel.d.ts","sourceRoot":"","sources":["../../src/ui/metrics_panel.tsx"],"names":[],"mappings":"AA8DA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5F;AAWD,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,cAAc,+BAkBlD;AAMD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,cAAc,+BAmBxD;AA4CD,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnG;AAcD,wBAAgB,cAAc,CAAC,EAAE,WAAW,EAAE,EAAE,mBAAmB,+BAwBlE;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;CACf;AAwBD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE,iBAAiB,+BA0JxD"}
1
+ {"version":3,"file":"metrics_panel.d.ts","sourceRoot":"","sources":["../../src/ui/metrics_panel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA2D5D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5F;AAWD,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,cAAc,+BAkBlD;AAMD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD,wBAAgB,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,cAAc,+BAmBxD;AA4CD,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnG;AAcD,wBAAgB,cAAc,CAAC,EAAE,WAAW,EAAE,EAAE,mBAAmB,+BAwBlE;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACrE;AAwBD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,iBAAiB,+BAyM7E"}
@@ -64,10 +64,13 @@ const INITIAL_STATE = {
64
64
  topButtons: [],
65
65
  experiments: [],
66
66
  };
67
- export function MetricsPanel({ appId }) {
67
+ export function MetricsPanel({ appId, resolveUserProfiles }) {
68
68
  const [state, setState] = useState(INITIAL_STATE);
69
69
  const [refreshKey, setRefreshKey] = useState(0);
70
70
  const refresh = useCallback(() => setRefreshKey((k) => k + 1), []);
71
+ const [activeUserIds, setActiveUserIds] = useState([]);
72
+ const [userProfiles, setUserProfiles] = useState([]);
73
+ const [showUsers, setShowUsers] = useState(false);
71
74
  useEffect(() => {
72
75
  if (!appId)
73
76
  return;
@@ -106,6 +109,19 @@ export function MetricsPanel({ appId }) {
106
109
  ];
107
110
  topPages = summary.topPages ?? [];
108
111
  topButtons = summary.topButtons ?? [];
112
+ const ids = summary.activeUserIds ?? [];
113
+ if (!cancelled) {
114
+ setActiveUserIds(ids);
115
+ if (resolveUserProfiles && ids.length > 0) {
116
+ resolveUserProfiles(ids).then(profiles => {
117
+ if (!cancelled)
118
+ setUserProfiles(profiles);
119
+ }).catch(() => { });
120
+ }
121
+ else {
122
+ setUserProfiles([]);
123
+ }
124
+ }
109
125
  }
110
126
  trend = actBody?.data?.trend ?? [];
111
127
  }
@@ -120,7 +136,7 @@ export function MetricsPanel({ appId }) {
120
136
  }
121
137
  });
122
138
  return () => { cancelled = true; };
123
- }, [appId, refreshKey]);
139
+ }, [appId, refreshKey, resolveUserProfiles]);
124
140
  // -------------------------------------------------------------------------
125
141
  // Render states
126
142
  // -------------------------------------------------------------------------
@@ -136,5 +152,13 @@ export function MetricsPanel({ appId }) {
136
152
  // -------------------------------------------------------------------------
137
153
  // Main render
138
154
  // -------------------------------------------------------------------------
139
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", {}), _jsxs("button", { onClick: refresh, className: "flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-muted transition-colors", title: "Refresh metrics", children: [_jsx(RotateCw, { className: "h-3.5 w-3.5" }), "Refresh"] })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Activity (last 14 days)" }), _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: "Active Users" })] }), _jsxs("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2", children: [_jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Top Pages" }), _jsx(TopList, { items: state.topPages, empty: "No page views yet" })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Top Buttons" }), _jsx(TopList, { items: state.topButtons, empty: "No clicks yet" })] })] }), _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 })] })] }));
155
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", {}), _jsxs("button", { onClick: refresh, className: "flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-muted transition-colors", title: "Refresh metrics", children: [_jsx(RotateCw, { className: "h-3.5 w-3.5" }), "Refresh"] })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Activity (last 14 days)" }), _jsx(StatCards, { stats: state.stats }), activeUserIds.length > 0 && (_jsxs("div", { className: "mt-2", children: [_jsx("button", { onClick: () => setShowUsers((v) => !v), className: "text-xs text-indigo-500 hover:underline", children: showUsers ? 'Hide users' : 'View users' }), showUsers && (_jsxs("div", { className: "mt-3 space-y-2", children: [userProfiles.map((p) => (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [p.profile_picture_url
156
+ ? _jsx("img", { src: p.profile_picture_url, className: "w-7 h-7 rounded-full", alt: "" })
157
+ : _jsx("div", { className: "w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center text-xs text-gray-500", children: (p.name || p.email || '?')[0].toUpperCase() }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-gray-800", children: p.name ?? p.email ?? p.user_id }), p.name && p.email && _jsx("div", { className: "text-xs text-gray-400", children: p.email })] })] }, p.user_id))), (() => {
158
+ const totalSessions = state.stats.find((s) => s.key === 'active_users')?.value ?? 0;
159
+ const anonSessions = Math.max(0, totalSessions - userProfiles.length);
160
+ return anonSessions > 0
161
+ ? _jsxs("div", { className: "text-xs text-gray-400 pl-9", children: [anonSessions, " anonymous session", anonSessions > 1 ? 's' : ''] })
162
+ : null;
163
+ })()] }))] }))] }), _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: "Active Users" })] }), _jsxs("div", { className: "grid grid-cols-1 gap-6 sm:grid-cols-2", children: [_jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Top Pages" }), _jsx(TopList, { items: state.topPages, empty: "No page views yet" })] }), _jsxs("section", { children: [_jsx("h2", { className: "mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: "Top Buttons" }), _jsx(TopList, { items: state.topButtons, empty: "No clicks yet" })] })] }), _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 })] })] }));
140
164
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_umetrics",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "description": "Product analytics for hazo apps — GA4 hybrid + first-party stat store + feature flags",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",