hazo_umetrics 1.7.1 → 1.9.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.
- package/CHANGE_LOG.md +44 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +3 -1
- package/dist/server/behavior.d.ts +3 -0
- package/dist/server/behavior.d.ts.map +1 -1
- package/dist/server/behavior.js +13 -3
- package/dist/ui/journey_panel.d.ts.map +1 -1
- package/dist/ui/journey_panel.js +140 -18
- package/dist/ui/search_queries_panel.d.ts.map +1 -1
- package/dist/ui/search_queries_panel.js +7 -1
- package/package.json +10 -10
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# hazo_umetrics — Change Log
|
|
2
2
|
|
|
3
|
+
## 1.9.0 — Search queries: per-term last-searched timestamps, second-precision "over time"
|
|
4
|
+
|
|
5
|
+
**New features (minor — no code-level breaking changes):**
|
|
6
|
+
|
|
7
|
+
### `searchQueries()` — finer-grained `over_time`
|
|
8
|
+
|
|
9
|
+
- `over_time` buckets are now **second-precision timestamps** (`YYYY-MM-DD HH:MM:SS`) instead of day-only (`YYYY-MM-DD`), capped to the **most recent 100** buckets in chronological order.
|
|
10
|
+
- Added a `tsBucket()` helper in `src/server/behavior.ts`; the existing `dayBucket()` (used by other aggregations) is unchanged.
|
|
11
|
+
- Per-term `lastSearchedAt` was already computed and returned — no change there.
|
|
12
|
+
- **Behavior note for consumers:** the `EventDayBucket.day` field returned by `searchQueries` now carries a full timestamp string rather than a date. Shape is unchanged, so consumers upgrade without code changes, but any UI that rendered `day` as date-only will now display the full timestamp.
|
|
13
|
+
|
|
14
|
+
### `SearchQueriesPanel` — visible timestamps
|
|
15
|
+
|
|
16
|
+
- **Top Search Terms** and **Zero-Result Terms** rows now show a right-aligned **last-searched** timestamp (from `lastSearchedAt`).
|
|
17
|
+
- **Searches Over Time** now renders the full second-precision timestamp per bucket (was date-only).
|
|
18
|
+
- Added an `fmtTs()` formatter; no prop or public-type changes, so consumers upgrade with no code changes.
|
|
19
|
+
|
|
20
|
+
## 1.8.0 — Journey: anonymous-session bucket, session drilldown, rich user dropdown
|
|
21
|
+
|
|
22
|
+
**New features (minor — no breaking changes):**
|
|
23
|
+
|
|
24
|
+
### `userJourney` — `anonymous` filter
|
|
25
|
+
|
|
26
|
+
- New `anonymous?: boolean` option on `userJourney(adapter, opts)` in `src/server/behavior.ts`.
|
|
27
|
+
- When true, keeps only sessions with **no** `user_id` (anonymous visitors); mutually exclusive with `user_id`. The aggregated graph then reflects anonymous traffic only.
|
|
28
|
+
|
|
29
|
+
### `getJourney` API handler — `anonymous=1`
|
|
30
|
+
|
|
31
|
+
- `GET analytics/journey` now parses `anonymous=1` and passes it through to `userJourney`.
|
|
32
|
+
- Existing `user_id` / `session_id` params unchanged.
|
|
33
|
+
|
|
34
|
+
### `JourneyPanel` — anonymous bucket, drilldown, avatar+email dropdown
|
|
35
|
+
|
|
36
|
+
- The user filter is now a custom dropdown (replaces the native `<select>`): renders **profile picture + email** per logged-in user with a session count, plus an **`Anonymous (N)`** bucket that filters to `anonymous=1`.
|
|
37
|
+
- Options and counts come from an identity summary computed on the all-users fetch, so they stay stable while a single identity is selected.
|
|
38
|
+
- New **session drilldown**: once a user or the anonymous bucket is selected, a second picker lists that group's individual sessions (short `session_id` + step count) and filters to one via the existing `session_id` param.
|
|
39
|
+
- No prop or public-type changes — `JourneyPanelProps` and `resolveUserProfiles` are unchanged, so consumers upgrade with no code changes.
|
|
40
|
+
|
|
41
|
+
### `JourneyPanel` — date-range filter + session timestamps
|
|
42
|
+
|
|
43
|
+
- New **date filter** in the toolbar: a preset dropdown (`24 hours` · `7 days` · `30 days` · `90 days` · `All time`) plus optional **from/to date pickers** that override the preset. Maps to the `since` / `until` query params.
|
|
44
|
+
- `getJourney` now also parses `until=<iso>` (previously only `since`) and passes it through to `userJourney`.
|
|
45
|
+
- Session step lists now show an **absolute timestamp** per step (local `HH:MM:SS`, above the existing time-to-next duration), and each session header shows its **start time** (`Mon D, HH:MM`).
|
|
46
|
+
|
|
3
47
|
## 1.7.0 — Search query analytics: `searchQueries`, `SearchQueriesPanel`, `getIdentity`
|
|
4
48
|
|
|
5
49
|
**New features (minor — no breaking changes):**
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAwC9D,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE;IAAE,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAC3E,OAAO,CAAC;IACX,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAC;IACnE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9C;AAiGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB;IAIzD;;;OAGG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqB9C;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAwBpD;;;;OAIG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA4CpD;AAqBD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAW/D;;;OAGG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqBtD;;;;OAIG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAiDlD;;;;OAIG;yBACwB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA8CtD;;;;OAIG;wBACuB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA8CxD;AAMD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB;IAI/D,2DAA2D;iBACxC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAe9C,sDAAsD;mBACjC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAenD;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB;IAI1D,oDAAoD;eACnC,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAe/C;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,qBAAqB,CAAC;IAEtG;;;;;;;;;OASG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EA6CrD;AAMD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB;IAI3D;;;OAGG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBlD;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoBpD;;;;;OAKG;sBACqB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkDnD;;;;;OAKG;2BAC0B,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAsE3D;AAMD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB;IAI7D;;;OAGG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAyBrD;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB;IAIzD;;;OAGG;kBACiB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkB/C;;;;OAIG;iBACgB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoC9C;;;;OAIG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;EAiCpD;AAMD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB;IAI7D;;;;OAIG;qBACoB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IA0DlD;;;OAGG;2BAC0B,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAyBxD;;;OAGG;uBACsB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAqCpD;;;OAGG;oBACmB,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;
|
|
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
|
@@ -858,7 +858,9 @@ export function createBehaviorHandlers(options) {
|
|
|
858
858
|
const app_id = url.searchParams.get('app_id') ?? '';
|
|
859
859
|
const user_id = url.searchParams.get('user_id') ?? undefined;
|
|
860
860
|
const session_id = url.searchParams.get('session_id') ?? undefined;
|
|
861
|
+
const anonymous = url.searchParams.get('anonymous') === '1';
|
|
861
862
|
const since = url.searchParams.get('since') ?? undefined;
|
|
863
|
+
const until = url.searchParams.get('until') ?? undefined;
|
|
862
864
|
const splitByPage = url.searchParams.get('split_by_page') === '1';
|
|
863
865
|
const pageFlow = url.searchParams.get('page_flow') === '1';
|
|
864
866
|
const gate = await gateAuth(req, {
|
|
@@ -873,7 +875,7 @@ export function createBehaviorHandlers(options) {
|
|
|
873
875
|
const adapter = await options.getAdapter();
|
|
874
876
|
const catalog = await _listActions(adapter, { app_id });
|
|
875
877
|
const catalogMap = new Map(catalog.map((a) => [a.action_key, { label: a.label, datapoint_schema: a.datapoint_schema }]));
|
|
876
|
-
const result = await _userJourney(adapter, { app_id, user_id, session_id, since, catalogMap, splitByPage, pageFlow });
|
|
878
|
+
const result = await _userJourney(adapter, { app_id, user_id, session_id, anonymous, since, until, catalogMap, splitByPage, pageFlow });
|
|
877
879
|
return jsonOk(result);
|
|
878
880
|
},
|
|
879
881
|
/**
|
|
@@ -108,6 +108,9 @@ export declare function userJourney(adapter: HazoConnectAdapter, opts: {
|
|
|
108
108
|
app_id: string;
|
|
109
109
|
user_id?: string;
|
|
110
110
|
session_id?: string;
|
|
111
|
+
/** When true, keep only sessions with NO user_id (anonymous visitors).
|
|
112
|
+
* Mutually exclusive with user_id — the graph aggregates anon traffic only. */
|
|
113
|
+
anonymous?: boolean;
|
|
111
114
|
since?: string;
|
|
112
115
|
until?: string;
|
|
113
116
|
catalogMap?: Map<string, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/server/behavior.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAsB7D,MAAM,MAAM,WAAW,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAM7F,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"behavior.d.ts","sourceRoot":"","sources":["../../src/server/behavior.ts"],"names":[],"mappings":"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"}
|
package/dist/server/behavior.js
CHANGED
|
@@ -41,6 +41,11 @@ function topN(tally, n, catalogMap) {
|
|
|
41
41
|
function dayBucket(iso) {
|
|
42
42
|
return iso.slice(0, 10);
|
|
43
43
|
}
|
|
44
|
+
// Second-precision bucket: "YYYY-MM-DD HH:MM:SS". Handles both ISO ("…T…Z")
|
|
45
|
+
// and SQLite space-separated timestamps.
|
|
46
|
+
function tsBucket(iso) {
|
|
47
|
+
return iso.slice(0, 19).replace('T', ' ');
|
|
48
|
+
}
|
|
44
49
|
async function fetchWindow(adapter, params) {
|
|
45
50
|
const crud = getCrud(adapter);
|
|
46
51
|
return crud.list((qb) => {
|
|
@@ -374,6 +379,9 @@ export async function userJourney(adapter, opts) {
|
|
|
374
379
|
// Drop rows with neither user_id nor session_id
|
|
375
380
|
if (!uid && !sid)
|
|
376
381
|
continue;
|
|
382
|
+
// Apply anonymous filter: keep only rows with no user_id
|
|
383
|
+
if (opts.anonymous && uid != null)
|
|
384
|
+
continue;
|
|
377
385
|
// Apply user_id filter
|
|
378
386
|
if (opts.user_id != null && uid !== opts.user_id)
|
|
379
387
|
continue;
|
|
@@ -621,9 +629,9 @@ export async function searchQueries(adapter, opts) {
|
|
|
621
629
|
}
|
|
622
630
|
if (row.captured_at > acc.lastAt)
|
|
623
631
|
acc.lastAt = row.captured_at;
|
|
624
|
-
//
|
|
625
|
-
const
|
|
626
|
-
dayMap.set(
|
|
632
|
+
// Timestamp bucket (second precision) for fine-grained "over time"
|
|
633
|
+
const ts = tsBucket(row.captured_at);
|
|
634
|
+
dayMap.set(ts, (dayMap.get(ts) ?? 0) + 1);
|
|
627
635
|
// Per-user
|
|
628
636
|
if (uid) {
|
|
629
637
|
if (!userMap.has(uid))
|
|
@@ -645,8 +653,10 @@ export async function searchQueries(adapter, opts) {
|
|
|
645
653
|
.filter((e) => e.zeroResults > 0)
|
|
646
654
|
.sort((a, b) => b.zeroResults - a.zeroResults)
|
|
647
655
|
.slice(0, topN_);
|
|
656
|
+
// Keep the most recent 100 timestamp buckets, chronological order.
|
|
648
657
|
const over_time = [...dayMap.entries()]
|
|
649
658
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
659
|
+
.slice(-100)
|
|
650
660
|
.map(([day, count]) => ({ day, count }));
|
|
651
661
|
const per_user = [...userMap.entries()]
|
|
652
662
|
.map(([user_id, { count, terms }]) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAiBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,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,GAAG,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;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;IAC1I,iGAAiG;IACjG,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oFAAoF;IACpF,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"journey_panel.d.ts","sourceRoot":"","sources":["../../src/ui/journey_panel.tsx"],"names":[],"mappings":"AAiBA,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC1D,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,GAAG,OAAO,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;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;IAC1I,iGAAiG;IACjG,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oFAAoF;IACpF,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAotBD,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,cAAqB,EAAE,YAAmB,EAAE,EAAE,iBAAiB,+BA+QrJ"}
|
package/dist/ui/journey_panel.js
CHANGED
|
@@ -14,6 +14,29 @@ function fmtMs(ms) {
|
|
|
14
14
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
15
15
|
return `${(ms / 60000).toFixed(1)}m`;
|
|
16
16
|
}
|
|
17
|
+
/** Absolute clock time for a step (local), e.g. "22:29:07". */
|
|
18
|
+
function fmtClock(iso) {
|
|
19
|
+
const d = new Date(iso);
|
|
20
|
+
if (isNaN(d.getTime()))
|
|
21
|
+
return '—';
|
|
22
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
|
23
|
+
}
|
|
24
|
+
/** Session-start stamp for a header, e.g. "Jul 9, 22:29". */
|
|
25
|
+
function fmtDateTime(iso) {
|
|
26
|
+
const d = new Date(iso);
|
|
27
|
+
if (isNaN(d.getTime()))
|
|
28
|
+
return '—';
|
|
29
|
+
return d.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false });
|
|
30
|
+
}
|
|
31
|
+
/** Date-range presets for the journey filter. `days: null` = all time. */
|
|
32
|
+
const RANGE_PRESETS = [
|
|
33
|
+
{ key: '24h', label: '24 hours', days: 1 },
|
|
34
|
+
{ key: '7d', label: '7 days', days: 7 },
|
|
35
|
+
{ key: '30d', label: '30 days', days: 30 },
|
|
36
|
+
{ key: '90d', label: '90 days', days: 90 },
|
|
37
|
+
{ key: 'all', label: 'All time', days: null },
|
|
38
|
+
];
|
|
39
|
+
const ALL_TIME_SINCE = '2000-01-01T00:00:00.000Z';
|
|
17
40
|
function kindColor(kind) {
|
|
18
41
|
switch (kind) {
|
|
19
42
|
case 'action': return '#6366f1';
|
|
@@ -329,7 +352,53 @@ function SessionDetail({ session, index, profile }) {
|
|
|
329
352
|
const headingText = session.user_id
|
|
330
353
|
? (profile?.name ?? profile?.email ?? session.user_id.slice(0, 16))
|
|
331
354
|
: `Anonymous (sid ${session.session_id.slice(0, 8)}…)`;
|
|
332
|
-
return (_jsxs("div", { className: "rounded-lg border bg-white", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-gray-50 transition-colors", children: [profile?.profile_picture_url && (_jsx("img", { src: profile.profile_picture_url, alt: "", width: 20, height: 20, className: "rounded-full object-cover flex-shrink-0", style: { width: 20, height: 20 } })), _jsx("span", { className: "text-sm font-mono text-gray-700 flex-1 truncate", children: headingText }), _jsxs("span", { className: "text-xs text-gray-400", children: [session.steps.length, " steps"] }), _jsx("span", { className: "text-gray-400 text-xs", children: open ? '▴' : '▾' })] }), open && (_jsx("div", { className: "border-t divide-y", children: session.steps.map((step, i) => (_jsxs("div", { className: "flex items-start gap-3 px-4 py-2", children: [_jsx("span", { className: "mt-0.5 w-2 h-2 rounded-full flex-shrink-0", style: { backgroundColor: kindColor(step.kind) } }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-xs font-medium text-gray-800 truncate", children: step.label }), step.path && (_jsx("p", { className: "text-[10px] text-gray-400 font-mono truncate", children: step.path }))] }), _jsx("span", { className: "text-[10px] text-gray-
|
|
355
|
+
return (_jsxs("div", { className: "rounded-lg border bg-white", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-gray-50 transition-colors", children: [profile?.profile_picture_url && (_jsx("img", { src: profile.profile_picture_url, alt: "", width: 20, height: 20, className: "rounded-full object-cover flex-shrink-0", style: { width: 20, height: 20 } })), _jsx("span", { className: "text-sm font-mono text-gray-700 flex-1 truncate", children: headingText }), session.steps[0] && (_jsx("span", { className: "text-[10px] text-gray-400 tabular-nums flex-shrink-0", title: "Session start", children: fmtDateTime(session.steps[0].captured_at) })), _jsxs("span", { className: "text-xs text-gray-400", children: [session.steps.length, " steps"] }), _jsx("span", { className: "text-gray-400 text-xs", children: open ? '▴' : '▾' })] }), open && (_jsx("div", { className: "border-t divide-y", children: session.steps.map((step, i) => (_jsxs("div", { className: "flex items-start gap-3 px-4 py-2", children: [_jsx("span", { className: "mt-0.5 w-2 h-2 rounded-full flex-shrink-0", style: { backgroundColor: kindColor(step.kind) } }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-xs font-medium text-gray-800 truncate", children: step.label }), step.path && (_jsx("p", { className: "text-[10px] text-gray-400 font-mono truncate", children: step.path }))] }), _jsxs("span", { className: "flex flex-col items-end flex-shrink-0 mt-0.5 leading-tight", children: [_jsx("span", { className: "text-[10px] text-gray-500 tabular-nums", title: "Timestamp", children: fmtClock(step.captured_at) }), _jsx("span", { className: "text-[10px] text-gray-400 tabular-nums", title: "Time to next step", children: fmtMs(step.durationMsToNext) })] })] }, i))) }))] }));
|
|
356
|
+
}
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// IdentityDropdown — custom user filter (avatars + email, anonymous bucket)
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
/** Sentinel identity value meaning "anonymous sessions only" (no user_id). */
|
|
361
|
+
const ANON_IDENTITY = '__anon__';
|
|
362
|
+
function IdentityDropdown({ value, onChange, userIds, userSessions, anonCount, profiles }) {
|
|
363
|
+
const [open, setOpen] = useState(false);
|
|
364
|
+
const rootRef = useRef(null);
|
|
365
|
+
// Close on outside click / Escape.
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (!open)
|
|
368
|
+
return;
|
|
369
|
+
const onDown = (e) => {
|
|
370
|
+
if (rootRef.current && !rootRef.current.contains(e.target))
|
|
371
|
+
setOpen(false);
|
|
372
|
+
};
|
|
373
|
+
const onKey = (e) => { if (e.key === 'Escape')
|
|
374
|
+
setOpen(false); };
|
|
375
|
+
document.addEventListener('mousedown', onDown);
|
|
376
|
+
document.addEventListener('keydown', onKey);
|
|
377
|
+
return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
|
|
378
|
+
}, [open]);
|
|
379
|
+
const options = [
|
|
380
|
+
{ value: '', sessionCount: 0 },
|
|
381
|
+
...(anonCount > 0 ? [{ value: ANON_IDENTITY, sessionCount: anonCount }] : []),
|
|
382
|
+
...userIds.map((id) => ({ value: id, sessionCount: userSessions[id] ?? 0 })),
|
|
383
|
+
];
|
|
384
|
+
function labelFor(id) {
|
|
385
|
+
if (id === '')
|
|
386
|
+
return 'All users';
|
|
387
|
+
if (id === ANON_IDENTITY)
|
|
388
|
+
return 'Anonymous';
|
|
389
|
+
const p = profiles.get(id);
|
|
390
|
+
return p?.email ?? p?.name ?? `${id.slice(0, 12)}…`;
|
|
391
|
+
}
|
|
392
|
+
function avatarFor(id) {
|
|
393
|
+
if (id === '' || id === ANON_IDENTITY)
|
|
394
|
+
return undefined;
|
|
395
|
+
return profiles.get(id)?.profile_picture_url;
|
|
396
|
+
}
|
|
397
|
+
const renderRow = (id, count) => {
|
|
398
|
+
const pic = avatarFor(id);
|
|
399
|
+
return (_jsxs("span", { className: "flex items-center gap-2 min-w-0", children: [id !== '' && id !== ANON_IDENTITY && (pic ? (_jsx("img", { src: pic, alt: "", width: 18, height: 18, className: "rounded-full object-cover flex-shrink-0", style: { width: 18, height: 18 } })) : (_jsx("span", { className: "inline-flex items-center justify-center w-[18px] h-[18px] rounded-full bg-gray-200 text-[9px] text-gray-500 flex-shrink-0", children: labelFor(id).charAt(0).toUpperCase() }))), id === ANON_IDENTITY && (_jsx("span", { className: "inline-flex items-center justify-center w-[18px] h-[18px] rounded-full bg-gray-100 text-gray-400 flex-shrink-0", title: "Anonymous visitors", children: "?" })), _jsx("span", { className: "truncate", children: labelFor(id) }), count > 0 && _jsxs("span", { className: "text-gray-400 tabular-nums flex-shrink-0", children: ["(", count, ")"] })] }));
|
|
400
|
+
};
|
|
401
|
+
return (_jsxs("div", { ref: rootRef, className: "relative inline-block", children: [_jsxs("button", { type: "button", onClick: () => setOpen((v) => !v), className: "flex items-center gap-1.5 rounded border px-2 py-1 text-xs bg-white hover:bg-gray-50 focus:outline-none focus:ring-1 focus:ring-indigo-400 max-w-[220px]", title: "Filter by user", children: [renderRow(value, value === '' ? 0 : (value === ANON_IDENTITY ? anonCount : (userSessions[value] ?? 0))), _jsx("span", { className: "text-gray-400 flex-shrink-0", children: "\u25BE" })] }), open && (_jsx("div", { className: "absolute right-0 z-20 mt-1 max-h-72 w-64 overflow-auto rounded-lg border bg-white py-1 shadow-lg", children: options.map((opt) => (_jsx("button", { type: "button", onClick: () => { onChange(opt.value); setOpen(false); }, className: `flex w-full items-center px-3 py-1.5 text-left text-xs hover:bg-gray-50 ${opt.value === value ? 'bg-indigo-50 font-medium' : ''}`, children: renderRow(opt.value, opt.sessionCount) }, opt.value || '__all__'))) }))] }));
|
|
333
402
|
}
|
|
334
403
|
// ---------------------------------------------------------------------------
|
|
335
404
|
// JourneyPanel — main export
|
|
@@ -339,20 +408,48 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
339
408
|
const [loading, setLoading] = useState(false);
|
|
340
409
|
const [error, setError] = useState(null);
|
|
341
410
|
const [userProfiles, setUserProfiles] = useState(new Map());
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
const
|
|
411
|
+
// Identity summary computed from the all-users fetch: distinct user_ids, their
|
|
412
|
+
// session counts, and the anonymous session count. Persisted so the dropdown
|
|
413
|
+
// keeps stable options/counts even while a single-identity filter is active.
|
|
414
|
+
const [identity, setIdentity] = useState({ userIds: [], userSessions: {}, anonCount: 0 });
|
|
415
|
+
// Selected identity: '' = all users, ANON_IDENTITY = anonymous, else a user_id.
|
|
416
|
+
// Falls back to the userId prop (e.g. the Behavior tab's single-user selection).
|
|
417
|
+
const [selectedIdentity, setSelectedIdentity] = useState('');
|
|
418
|
+
const effectiveIdentity = selectedIdentity || userId || '';
|
|
419
|
+
const isAnon = effectiveIdentity === ANON_IDENTITY;
|
|
420
|
+
const effectiveUserId = isAnon ? undefined : (effectiveIdentity || undefined);
|
|
421
|
+
// Drilldown: a specific session within the selected identity ('' = whole group).
|
|
422
|
+
const [selectedSessionId, setSelectedSessionId] = useState('');
|
|
423
|
+
// Sessions of the currently selected group — captured when a group is active
|
|
424
|
+
// and no single session is drilled, so the drilldown list stays populated.
|
|
425
|
+
const [groupSessions, setGroupSessions] = useState([]);
|
|
349
426
|
// Split-by-page: ask the server to key nodes by event+page (refetch on change).
|
|
350
427
|
const [splitByPage, setSplitByPage] = useState(false);
|
|
351
428
|
// Page-flow: server returns only pageviews, linked page→page (skips clicks).
|
|
352
429
|
const [pageFlow, setPageFlow] = useState(false);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
430
|
+
// Date range: a preset window, overridden by explicit from/to dates when set.
|
|
431
|
+
const [rangePreset, setRangePreset] = useState('30d');
|
|
432
|
+
const [fromDate, setFromDate] = useState(''); // yyyy-mm-dd
|
|
433
|
+
const [toDate, setToDate] = useState(''); // yyyy-mm-dd
|
|
434
|
+
const customRange = Boolean(fromDate || toDate);
|
|
435
|
+
// Resolve the active window to since/until ISO params.
|
|
436
|
+
let sinceParam;
|
|
437
|
+
let untilParam;
|
|
438
|
+
if (customRange) {
|
|
439
|
+
sinceParam = fromDate ? new Date(`${fromDate}T00:00:00`).toISOString() : ALL_TIME_SINCE;
|
|
440
|
+
if (toDate)
|
|
441
|
+
untilParam = new Date(`${toDate}T23:59:59.999`).toISOString();
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
const preset = RANGE_PRESETS.find((p) => p.key === rangePreset);
|
|
445
|
+
if (preset?.days != null) {
|
|
446
|
+
const d = new Date();
|
|
447
|
+
d.setDate(d.getDate() - preset.days);
|
|
448
|
+
sinceParam = d.toISOString();
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
sinceParam = ALL_TIME_SINCE; // all time
|
|
452
|
+
}
|
|
356
453
|
}
|
|
357
454
|
useEffect(() => {
|
|
358
455
|
if (!appId)
|
|
@@ -361,10 +458,19 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
361
458
|
setLoading(true);
|
|
362
459
|
setError(null);
|
|
363
460
|
const params = new URLSearchParams({ app_id: appId });
|
|
364
|
-
if (
|
|
461
|
+
if (isAnon)
|
|
462
|
+
params.set('anonymous', '1');
|
|
463
|
+
else if (effectiveUserId)
|
|
365
464
|
params.set('user_id', effectiveUserId);
|
|
366
|
-
|
|
367
|
-
|
|
465
|
+
// Drilldown session takes precedence; the sessionId prop (from the Behavior
|
|
466
|
+
// tab) still applies when no drilldown is active.
|
|
467
|
+
const effSession = selectedSessionId || sessionId;
|
|
468
|
+
if (effSession)
|
|
469
|
+
params.set('session_id', effSession);
|
|
470
|
+
if (sinceParam)
|
|
471
|
+
params.set('since', sinceParam);
|
|
472
|
+
if (untilParam)
|
|
473
|
+
params.set('until', untilParam);
|
|
368
474
|
if (splitByPage)
|
|
369
475
|
params.set('split_by_page', '1');
|
|
370
476
|
if (pageFlow)
|
|
@@ -380,8 +486,24 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
380
486
|
// Resolve user profiles for all sessions that have a user_id
|
|
381
487
|
if (data?.sessions) {
|
|
382
488
|
const ids = [...new Set(data.sessions.map((s) => s.user_id).filter((id) => Boolean(id)))];
|
|
383
|
-
//
|
|
384
|
-
|
|
489
|
+
// All-users view (no identity/session filter) → the result holds every
|
|
490
|
+
// session, so recompute the identity summary that drives the dropdown.
|
|
491
|
+
if (!effectiveUserId && !isAnon && !effSession) {
|
|
492
|
+
const userSessions = {};
|
|
493
|
+
let anonCount = 0;
|
|
494
|
+
for (const s of data.sessions) {
|
|
495
|
+
if (s.user_id)
|
|
496
|
+
userSessions[s.user_id] = (userSessions[s.user_id] ?? 0) + 1;
|
|
497
|
+
else
|
|
498
|
+
anonCount += 1;
|
|
499
|
+
}
|
|
500
|
+
setIdentity({ userIds: Object.keys(userSessions), userSessions, anonCount });
|
|
501
|
+
}
|
|
502
|
+
// A single group is selected but not yet drilled → capture its sessions
|
|
503
|
+
// so the drilldown picker keeps its full list.
|
|
504
|
+
if ((effectiveUserId || isAnon) && !selectedSessionId) {
|
|
505
|
+
setGroupSessions(data.sessions);
|
|
506
|
+
}
|
|
385
507
|
if (resolveUserProfiles && ids.length > 0) {
|
|
386
508
|
resolveUserProfiles(ids)
|
|
387
509
|
.then((profiles) => {
|
|
@@ -405,11 +527,11 @@ export function JourneyPanel({ appId, apiBase, userId, sessionId, resolveUserPro
|
|
|
405
527
|
setLoading(false);
|
|
406
528
|
});
|
|
407
529
|
return () => { cancelled = true; };
|
|
408
|
-
}, [appId, apiBase, effectiveUserId, sessionId, splitByPage, pageFlow]);
|
|
530
|
+
}, [appId, apiBase, effectiveUserId, isAnon, selectedSessionId, sessionId, sinceParam, untilParam, splitByPage, pageFlow]);
|
|
409
531
|
const sessionCount = result?.sessions.length ?? 0;
|
|
410
532
|
const totalSteps = result?.sessions.reduce((s, sess) => s + sess.steps.length, 0) ?? 0;
|
|
411
533
|
// Single source of truth for the data-fetch controls — rendered in the toolbar,
|
|
412
534
|
// and passed into the flow graph so they stay reachable in graph fullscreen.
|
|
413
|
-
const toolbarControls = (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex items-center gap-1 cursor-pointer select-none", title: "Show only page-to-page navigation (each page links to the next page visited)", children: [_jsx("input", { type: "checkbox", checked: pageFlow, onChange: (e) => setPageFlow(e.target.checked), className: "w-3 h-3 cursor-pointer", style: { accentColor: '#10b981' } }), "Pages only"] }), _jsxs("label", { className: "flex items-center gap-1 cursor-pointer select-none", title: "Split each event into one box per page", children: [_jsx("input", { type: "checkbox", checked: splitByPage, onChange: (e) => setSplitByPage(e.target.checked), className: "w-3 h-3 cursor-pointer" }), "Group by page"] }),
|
|
535
|
+
const toolbarControls = (_jsxs(_Fragment, { children: [_jsx("label", { className: "text-gray-500", children: "Date" }), _jsx("select", { value: rangePreset, onChange: (e) => { setRangePreset(e.target.value); setFromDate(''); setToDate(''); }, disabled: customRange, className: "rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-indigo-400 disabled:opacity-40", title: customRange ? 'Clear the from/to dates to use a preset' : 'Preset date range', children: RANGE_PRESETS.map((p) => (_jsx("option", { value: p.key, children: p.label }, p.key))) }), _jsx("input", { type: "date", value: fromDate, max: toDate || undefined, onChange: (e) => setFromDate(e.target.value), className: "rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-indigo-400", title: "From date (overrides preset)" }), _jsx("span", { className: "text-gray-400", children: "\u2013" }), _jsx("input", { type: "date", value: toDate, min: fromDate || undefined, onChange: (e) => setToDate(e.target.value), className: "rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-indigo-400", title: "To date (overrides preset)" }), customRange && (_jsx("button", { type: "button", onClick: () => { setFromDate(''); setToDate(''); }, className: "text-xs text-gray-400 hover:text-gray-600", title: "Clear date range", children: "\u2715" })), _jsxs("label", { className: "flex items-center gap-1 cursor-pointer select-none ml-1", title: "Show only page-to-page navigation (each page links to the next page visited)", children: [_jsx("input", { type: "checkbox", checked: pageFlow, onChange: (e) => setPageFlow(e.target.checked), className: "w-3 h-3 cursor-pointer", style: { accentColor: '#10b981' } }), "Pages only"] }), _jsxs("label", { className: "flex items-center gap-1 cursor-pointer select-none", title: "Split each event into one box per page", children: [_jsx("input", { type: "checkbox", checked: splitByPage, onChange: (e) => setSplitByPage(e.target.checked), className: "w-3 h-3 cursor-pointer" }), "Group by page"] }), _jsx("label", { className: "text-gray-500 ml-1", children: "User" }), _jsx(IdentityDropdown, { value: effectiveIdentity, onChange: (v) => { setSelectedIdentity(v); setSelectedSessionId(''); }, userIds: identity.userIds, userSessions: identity.userSessions, anonCount: identity.anonCount, profiles: userProfiles }), (effectiveUserId || isAnon) && groupSessions.length > 1 && (_jsxs("select", { value: selectedSessionId, onChange: (e) => setSelectedSessionId(e.target.value), className: "rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-indigo-400", title: "Drill into one session", children: [_jsxs("option", { value: "", children: ["All sessions (", groupSessions.length, ")"] }), groupSessions.map((s) => (_jsxs("option", { value: s.session_id, children: [s.session_id.slice(0, 8), "\u2026 \u00B7 ", s.steps.length, " steps"] }, s.session_id)))] }))] }));
|
|
414
536
|
return (_jsxs("div", { className: "flex flex-col gap-4 p-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4 text-xs text-gray-500", children: [_jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: sessionCount }), " session", sessionCount !== 1 ? 's' : ''] }), _jsxs("span", { children: [_jsx("span", { className: "font-semibold text-gray-700", children: totalSteps }), " total steps"] }), _jsx("div", { className: "ml-auto flex items-center gap-3", children: toolbarControls })] }), error && (_jsx("div", { className: "rounded-lg border border-red-200 bg-red-50 p-3 text-xs text-red-700", children: error })), loading && !result && (_jsx("div", { className: "p-4 text-xs text-gray-400", children: "Loading journey data\u2026" })), result && (_jsxs(_Fragment, { children: [result.graph.nodes.length > 0 && (_jsx(JourneyFlowGraph, { graph: result.graph, userFilter: toolbarControls, loading: loading, multiSidePorts: multiSidePorts, backEdgeLoop: backEdgeLoop })), result.sessions.length === 0 ? (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No journey sessions found." })) : (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("h3", { className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", children: "Sessions" }), result.sessions.map((session, i) => (_jsx(SessionDetail, { session: session, index: i, profile: session.user_id ? userProfiles.get(session.user_id) : undefined }, session.session_id)))] }))] })), !loading && !result && !error && (_jsx("div", { className: "rounded-lg border p-6 text-center text-sm text-gray-400", children: "No data yet." }))] }));
|
|
415
537
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search_queries_panel.d.ts","sourceRoot":"","sources":["../../src/ui/search_queries_panel.tsx"],"names":[],"mappings":"AA4BA,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"search_queries_panel.d.ts","sourceRoot":"","sources":["../../src/ui/search_queries_panel.tsx"],"names":[],"mappings":"AA4BA,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoDD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;CACjE;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,uBAAuB,+BAmJzG"}
|
|
@@ -4,6 +4,12 @@ import { useState, useEffect } from 'react';
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// MiniBar
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
|
+
// Format a stored timestamp ("…T…Z" or "YYYY-MM-DD HH:MM:SS") to second precision.
|
|
8
|
+
function fmtTs(iso) {
|
|
9
|
+
if (!iso)
|
|
10
|
+
return '';
|
|
11
|
+
return iso.slice(0, 19).replace('T', ' ');
|
|
12
|
+
}
|
|
7
13
|
function MiniBar({ value, max, color = '#6366f1' }) {
|
|
8
14
|
const pct = max > 0 ? (value / max) * 100 : 0;
|
|
9
15
|
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1 h-2 rounded bg-gray-100 overflow-hidden", children: _jsx("div", { className: "h-full rounded", style: { width: `${pct}%`, backgroundColor: color } }) }), _jsx("span", { className: "text-xs tabular-nums text-gray-600 w-8 text-right", children: value })] }));
|
|
@@ -63,7 +69,7 @@ export function SearchQueriesPanel({ appId, apiBase, since, resolveUserProfiles
|
|
|
63
69
|
const maxTermCount = data.top_terms.length > 0 ? Math.max(...data.top_terms.map((t) => t.count), 1) : 1;
|
|
64
70
|
const maxZeroCount = data.zero_result_terms.length > 0 ? Math.max(...data.zero_result_terms.map((t) => t.zeroResults), 1) : 1;
|
|
65
71
|
const maxDayCount = data.over_time.length > 0 ? Math.max(...data.over_time.map((d) => d.count), 1) : 1;
|
|
66
|
-
return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsx(SummaryCard, { label: "Total Searches", value: data.total_searches }), _jsx(SummaryCard, { label: "Unique Terms", value: data.unique_terms }), _jsx(SummaryCard, { label: "Unique Searchers", value: data.unique_searchers }), _jsx(SummaryCard, { label: "Zero-Result Searches", value: data.zero_result_searches })] }), _jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Top Search Terms" }), data.top_terms.length === 0 ? (_jsx("p", { className: "text-xs text-gray-400", children: "No searches recorded yet." })) : (_jsx("div", { className: "flex flex-col gap-1", children: data.top_terms.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-32 truncate", title: t.term, children: t.term }), _jsx(MiniBar, { value: t.count, max: maxTermCount })] }, t.term))) }))] }), data.zero_result_terms.length > 0 && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-4 flex flex-col gap-3", children: [_jsxs("h3", { className: "text-sm font-semibold text-amber-800", children: ["Zero-Result Terms ", _jsx("span", { className: "text-xs font-normal text-amber-600", children: "(content gaps)" })] }), _jsx("div", { className: "flex flex-col gap-1", children: data.zero_result_terms.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-amber-700 w-32 truncate", title: t.term, children: t.term }), _jsx(MiniBar, { value: t.zeroResults, max: maxZeroCount, color: "#f59e0b" })] }, t.term))) })] })), data.over_time.length > 0 && (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Searches Over Time" }), _jsx("div", { className: "flex flex-col gap-1", children: data.over_time.map((d) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[10px] text-gray-400 w-
|
|
72
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4", children: [_jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsx(SummaryCard, { label: "Total Searches", value: data.total_searches }), _jsx(SummaryCard, { label: "Unique Terms", value: data.unique_terms }), _jsx(SummaryCard, { label: "Unique Searchers", value: data.unique_searchers }), _jsx(SummaryCard, { label: "Zero-Result Searches", value: data.zero_result_searches })] }), _jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Top Search Terms" }), data.top_terms.length === 0 ? (_jsx("p", { className: "text-xs text-gray-400", children: "No searches recorded yet." })) : (_jsx("div", { className: "flex flex-col gap-1", children: data.top_terms.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-gray-600 w-32 truncate", title: t.term, children: t.term }), _jsx(MiniBar, { value: t.count, max: maxTermCount }), _jsx("span", { className: "text-[10px] text-gray-400 tabular-nums whitespace-nowrap w-32 text-right", title: `Last searched ${t.lastSearchedAt}`, children: fmtTs(t.lastSearchedAt) })] }, t.term))) }))] }), data.zero_result_terms.length > 0 && (_jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-4 flex flex-col gap-3", children: [_jsxs("h3", { className: "text-sm font-semibold text-amber-800", children: ["Zero-Result Terms ", _jsx("span", { className: "text-xs font-normal text-amber-600", children: "(content gaps)" })] }), _jsx("div", { className: "flex flex-col gap-1", children: data.zero_result_terms.map((t) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs text-amber-700 w-32 truncate", title: t.term, children: t.term }), _jsx(MiniBar, { value: t.zeroResults, max: maxZeroCount, color: "#f59e0b" }), _jsx("span", { className: "text-[10px] text-amber-600 tabular-nums whitespace-nowrap w-32 text-right", title: `Last searched ${t.lastSearchedAt}`, children: fmtTs(t.lastSearchedAt) })] }, t.term))) })] })), data.over_time.length > 0 && (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Searches Over Time" }), _jsx("div", { className: "flex flex-col gap-1", children: data.over_time.map((d) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-[10px] text-gray-400 tabular-nums w-36 whitespace-nowrap", children: fmtTs(d.day) }), _jsx(MiniBar, { value: d.count, max: maxDayCount, color: "#10b981" })] }, d.day))) })] })), data.per_user.length > 0 && (_jsxs("div", { className: "rounded-lg border bg-white p-4 flex flex-col gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "Per-User Searches" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-xs border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-left text-gray-400 border-b", children: [_jsx("th", { className: "py-1 pr-4 font-medium", children: "User" }), _jsx("th", { className: "py-1 pr-4 font-medium tabular-nums", children: "Searches" }), _jsx("th", { className: "py-1 pr-4 font-medium tabular-nums", children: "Distinct Terms" }), _jsx("th", { className: "py-1 font-medium", children: "Recent Terms" })] }) }), _jsx("tbody", { children: data.per_user.map((u) => {
|
|
67
73
|
const profile = profiles.get(u.user_id);
|
|
68
74
|
const displayName = profile?.name ?? profile?.email ?? u.user_id.slice(0, 8) + '…';
|
|
69
75
|
return (_jsxs("tr", { className: "border-b border-gray-50", children: [_jsxs("td", { className: "py-1 pr-4 text-gray-700", children: [profile?.avatar_url && (_jsx("img", { src: profile.avatar_url, alt: "", className: "inline-block w-4 h-4 rounded-full mr-1 align-middle" })), displayName] }), _jsx("td", { className: "py-1 pr-4 tabular-nums text-gray-600", children: u.count }), _jsx("td", { className: "py-1 pr-4 tabular-nums text-gray-600", children: u.distinct_terms }), _jsxs("td", { className: "py-1 text-gray-500", children: [u.terms.slice(0, 5).join(', '), u.terms.length > 5 ? '…' : ''] })] }, u.user_id));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_umetrics",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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",
|
|
@@ -42,16 +42,16 @@
|
|
|
42
42
|
"build:test-app:pg": "HAZO_CONNECT_TYPE=postgrest npm run build:test-app"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"hazo_core": "^1.2.
|
|
46
|
-
"hazo_dataviz": "^0.
|
|
47
|
-
"hazo_logs": "^2.
|
|
48
|
-
"hazo_config": "^2.
|
|
49
|
-
"hazo_connect": "^3.
|
|
50
|
-
"hazo_api": "^2.
|
|
51
|
-
"hazo_auth": "^10.
|
|
45
|
+
"hazo_core": "^1.2.1",
|
|
46
|
+
"hazo_dataviz": "^0.4.2",
|
|
47
|
+
"hazo_logs": "^2.1.1",
|
|
48
|
+
"hazo_config": "^2.4.1",
|
|
49
|
+
"hazo_connect": "^3.9.2",
|
|
50
|
+
"hazo_api": "^2.5.1",
|
|
51
|
+
"hazo_auth": "^10.8.0",
|
|
52
52
|
"hazo_secure": "^1.3.0",
|
|
53
|
-
"hazo_audit": "^2.1.
|
|
54
|
-
"hazo_ui": "^4.
|
|
53
|
+
"hazo_audit": "^2.1.2",
|
|
54
|
+
"hazo_ui": "^4.10.0",
|
|
55
55
|
"next": "^14.0.0 || ^16.0.0",
|
|
56
56
|
"react": "^18.0.0 || ^19.0.0",
|
|
57
57
|
"react-dom": "^18.0.0 || ^19.0.0"
|