Skip to content

Commit 8c08480

Browse files
committed
feat: better analytic safety
1 parent 4ddd8f1 commit 8c08480

5 files changed

Lines changed: 397 additions & 12 deletions

File tree

apps/web/src/components/inbox-analytics/inbox-analytics-display.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ const analyticsData = {
99
medianResolutionTimeSeconds: 3600,
1010
aiHandledRate: 80,
1111
satisfactionIndex: 73,
12-
uniqueVisitors: 42,
12+
uniqueVisitors: 987,
1313
},
1414
previous: {
1515
medianResponseTimeSeconds: 250,
1616
medianResolutionTimeSeconds: 5400,
1717
aiHandledRate: 60,
1818
satisfactionIndex: 68,
19-
uniqueVisitors: 21,
19+
uniqueVisitors: 321,
2020
},
2121
} as InboxAnalyticsResponse;
2222

@@ -59,6 +59,7 @@ describe("InboxAnalyticsDisplay", () => {
5959
expect(html).toContain('data-slot="inbox-analytics-live-dot-pulse"');
6060
expect(html).toContain("bg-emerald-600");
6161
expect(html).toContain("1,234");
62+
expect(html).toContain("987");
6263
expect(html).toContain("Median response time");
6364
expect(
6465
html.indexOf('data-slot="inbox-analytics-live-presence"')
@@ -79,6 +80,7 @@ describe("InboxAnalyticsDisplay", () => {
7980
expect(html).toContain("1h");
8081
expect(html).toContain("80%");
8182
expect(html).toContain('aria-label="Analytics date range"');
83+
expect(html).toContain(">30d<");
8284
expect(html).toContain("shrink-0");
8385
});
8486

apps/web/src/components/inbox-analytics/inbox-analytics-display.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const numberFormatter = new Intl.NumberFormat("en-US", {
5858
const rangeOptions = [
5959
{ value: "7", label: "7d" },
6060
{ value: "14", label: "14d" },
61-
{ value: "30", label: "Month" },
61+
{ value: "30", label: "30d" },
6262
] as const satisfies readonly SegmentedControlOption<string>[];
6363

6464
const formatDuration = (value: number | null): string => {
@@ -149,7 +149,8 @@ const metricConfigs: MetricConfig[] = [
149149
{
150150
key: "uniqueVisitors",
151151
label: "Unique visitors",
152-
description: "Number of distinct widget visitors active in this period.",
152+
description:
153+
"Number of distinct website visitors who loaded the widget during this period.",
153154
higherIsBetter: true,
154155
formatValue: formatCount,
155156
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { beforeEach, describe, expect, it, mock } from "bun:test";
2+
import { renderToStaticMarkup } from "react-dom/server";
3+
4+
let capturedSegmentedControlProps: {
5+
"aria-label": string;
6+
onValueChange: (value: string) => void;
7+
options: Array<{ value: string; label: string }>;
8+
value: string;
9+
} | null = null;
10+
11+
mock.module("@/components/ui/segmented-control", () => ({
12+
SegmentedControl: (props: {
13+
"aria-label": string;
14+
onValueChange: (value: string) => void;
15+
options: Array<{ value: string; label: string }>;
16+
value: string;
17+
}) => {
18+
capturedSegmentedControlProps = props;
19+
return <div data-slot="mock-segmented-control" />;
20+
},
21+
}));
22+
23+
const modulePromise = import("./inbox-analytics-display");
24+
25+
describe("InboxAnalyticsRangeControl", () => {
26+
beforeEach(() => {
27+
capturedSegmentedControlProps = null;
28+
});
29+
30+
it("maps segmented-control selections to supported analytics ranges", async () => {
31+
const onRangeChange = mock(((_rangeDays: number) => {}) as (
32+
value: number
33+
) => void);
34+
const { InboxAnalyticsRangeControl } = await modulePromise;
35+
36+
renderToStaticMarkup(
37+
<InboxAnalyticsRangeControl
38+
onRangeChange={onRangeChange}
39+
rangeDays={14}
40+
/>
41+
);
42+
43+
expect(capturedSegmentedControlProps).not.toBeNull();
44+
expect(capturedSegmentedControlProps?.["aria-label"]).toBe(
45+
"Analytics date range"
46+
);
47+
expect(capturedSegmentedControlProps?.value).toBe("14");
48+
expect(capturedSegmentedControlProps?.options).toEqual([
49+
{ value: "7", label: "7d" },
50+
{ value: "14", label: "14d" },
51+
{ value: "30", label: "30d" },
52+
]);
53+
54+
capturedSegmentedControlProps?.onValueChange("30");
55+
capturedSegmentedControlProps?.onValueChange("999");
56+
57+
expect(onRangeChange).toHaveBeenCalledTimes(1);
58+
expect(onRangeChange).toHaveBeenCalledWith(30);
59+
});
60+
});

apps/web/src/components/inbox-analytics/inbox-analytics.test.tsx

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,25 @@ let onlineNowQueryState = {
1717
isLoading: false,
1818
};
1919

20+
const useInboxAnalyticsMock = mock(
21+
((_: { websiteSlug: string; rangeDays: number; enabled?: boolean }) =>
22+
inboxAnalyticsQueryState) as (args: {
23+
websiteSlug: string;
24+
rangeDays: number;
25+
enabled?: boolean;
26+
}) => typeof inboxAnalyticsQueryState
27+
);
28+
const useOnlineNowMock = mock(
29+
((_: { websiteSlug: string; minutes?: number; enabled?: boolean }) =>
30+
onlineNowQueryState) as (args: {
31+
websiteSlug: string;
32+
minutes?: number;
33+
enabled?: boolean;
34+
}) => typeof onlineNowQueryState
35+
);
36+
2037
mock.module("@/data/use-inbox-analytics", () => ({
21-
useInboxAnalytics: () => inboxAnalyticsQueryState,
38+
useInboxAnalytics: useInboxAnalyticsMock,
2239
}));
2340

2441
mock.module("@/data/use-online-now", () => ({
@@ -27,14 +44,16 @@ mock.module("@/data/use-online-now", () => ({
2744
["tinybird", "online-now", websiteSlug] as const,
2845
isVisitorOnlineEntity: (entity: { entity_type: string }) =>
2946
entity.entity_type === "visitor",
30-
useOnlineNow: () => onlineNowQueryState,
47+
useOnlineNow: useOnlineNowMock,
3148
}));
3249

3350
const modulePromise = import("./inbox-analytics");
3451

3552
describe("InboxAnalytics", () => {
3653
beforeEach(() => {
3754
capturedControllerState = null;
55+
useInboxAnalyticsMock.mockClear();
56+
useOnlineNowMock.mockClear();
3857
inboxAnalyticsQueryState = {
3958
data: null,
4059
isError: false,
@@ -78,4 +97,88 @@ describe("InboxAnalytics", () => {
7897
isLoading: false,
7998
});
8099
});
100+
101+
it("keeps unique visitors separate from the live presence count", async () => {
102+
inboxAnalyticsQueryState = {
103+
data: {
104+
range: {
105+
rangeDays: 7,
106+
currentStart: "2026-03-21T00:00:00.000Z",
107+
currentEnd: "2026-03-28T00:00:00.000Z",
108+
previousStart: "2026-03-14T00:00:00.000Z",
109+
previousEnd: "2026-03-21T00:00:00.000Z",
110+
},
111+
current: {
112+
medianResponseTimeSeconds: 120,
113+
medianResolutionTimeSeconds: 1800,
114+
aiHandledRate: 60,
115+
satisfactionIndex: 75,
116+
uniqueVisitors: 987,
117+
},
118+
previous: {
119+
medianResponseTimeSeconds: 150,
120+
medianResolutionTimeSeconds: 2100,
121+
aiHandledRate: 55,
122+
satisfactionIndex: 70,
123+
uniqueVisitors: 654,
124+
},
125+
},
126+
isError: false,
127+
isFetching: false,
128+
isLoading: false,
129+
};
130+
onlineNowQueryState = {
131+
data: [
132+
{ entity_id: "entity_1", entity_type: "visitor" },
133+
{ entity_id: "entity_2", entity_type: "visitor" },
134+
{ entity_id: "entity_3", entity_type: "user" },
135+
],
136+
isFetching: false,
137+
isLoading: false,
138+
};
139+
140+
const { useInboxAnalyticsController } = await modulePromise;
141+
142+
function ControllerProbe() {
143+
capturedControllerState = useInboxAnalyticsController({
144+
websiteSlug: "acme",
145+
}) as Record<string, unknown>;
146+
return null;
147+
}
148+
149+
renderToStaticMarkup(<ControllerProbe />);
150+
151+
const inboxAnalyticsArgs = useInboxAnalyticsMock.mock.calls[0]?.[0] as
152+
| {
153+
websiteSlug: string;
154+
rangeDays: number;
155+
enabled: boolean;
156+
}
157+
| undefined;
158+
const onlineNowArgs = useOnlineNowMock.mock.calls[0]?.[0] as
159+
| {
160+
websiteSlug: string;
161+
enabled: boolean;
162+
}
163+
| undefined;
164+
165+
expect(inboxAnalyticsArgs).toEqual({
166+
websiteSlug: "acme",
167+
rangeDays: 7,
168+
enabled: true,
169+
});
170+
expect(onlineNowArgs).toEqual({
171+
websiteSlug: "acme",
172+
enabled: true,
173+
});
174+
expect(
175+
(capturedControllerState?.data as InboxAnalyticsResponse | null)?.current
176+
.uniqueVisitors
177+
).toBe(987);
178+
expect(capturedControllerState?.livePresence).toEqual({
179+
count: 2,
180+
isFetching: false,
181+
isLoading: false,
182+
});
183+
});
81184
});

0 commit comments

Comments
 (0)