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.
- package/dist/server/activity.d.ts +1 -0
- package/dist/server/activity.d.ts.map +1 -1
- package/dist/server/activity.js +5 -0
- package/dist/ui/journey_panel.d.ts.map +1 -1
- package/dist/ui/journey_panel.js +67 -7
- package/dist/ui/metrics_panel.d.ts +3 -1
- package/dist/ui/metrics_panel.d.ts.map +1 -1
- package/dist/ui/metrics_panel.js +27 -3
- package/package.json +1 -1
|
@@ -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,
|
|
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"}
|
package/dist/server/activity.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/ui/journey_panel.js
CHANGED
|
@@ -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
|
|
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
|
|
135
|
-
const
|
|
136
|
-
|
|
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
|
|
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:
|
|
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":"
|
|
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"}
|
package/dist/ui/metrics_panel.js
CHANGED
|
@@ -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 })
|
|
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
|
}
|