Skip to content

Commit 08239ec

Browse files
authored
improved sessions (#5481)
1 parent e0cef85 commit 08239ec

File tree

6 files changed

+134
-31
lines changed

6 files changed

+134
-31
lines changed

valhalla/jawn/src/managers/SessionManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface SessionResult {
3333
completion_tokens: number;
3434
total_tokens: number;
3535
avg_latency: number;
36+
user_ids: string[];
3637
}
3738

3839
export interface SessionsAggregateMetrics {
@@ -355,7 +356,7 @@ export class SessionManager {
355356

356357
// Step 1 get all the properties given this filter
357358
const query = `
358-
SELECT
359+
SELECT
359360
min(request_response_rmt.request_created_at) + INTERVAL ${timezoneDifference} MINUTE AS created_at,
360361
max(request_response_rmt.request_created_at) + INTERVAL ${timezoneDifference} MINUTE AS latest_request_created_at,
361362
properties['Helicone-Session-Id'] as session_id,
@@ -365,7 +366,8 @@ export class SessionManager {
365366
count(*) AS total_requests,
366367
sum(request_response_rmt.prompt_tokens) AS prompt_tokens,
367368
sum(request_response_rmt.completion_tokens) AS completion_tokens,
368-
sum(request_response_rmt.prompt_tokens) + sum(request_response_rmt.completion_tokens) AS total_tokens
369+
sum(request_response_rmt.prompt_tokens) + sum(request_response_rmt.completion_tokens) AS total_tokens,
370+
groupUniqArray(request_response_rmt.user_id) AS user_ids
369371
FROM request_response_rmt
370372
WHERE (
371373
has(properties, 'Helicone-Session-Id')

web/components/templates/sessions/initialColumns.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const getColumns = () => {
1010
"total_tokens",
1111
"total_requests",
1212
"avg_latency",
13+
"user_ids",
1314
];
1415
return columns.map((property) => {
1516
return {
@@ -19,7 +20,13 @@ export const getColumns = () => {
1920
return value;
2021
},
2122
cell: ({ row }: any) => {
22-
return row.original.metadata[property];
23+
const value = row.original.metadata[property];
24+
// Format arrays as comma-separated strings
25+
if (Array.isArray(value)) {
26+
const filtered = value.filter((v: string) => v && v.trim() !== "");
27+
return filtered.length > 0 ? filtered.join(", ") : "-";
28+
}
29+
return value;
2330
},
2431
};
2532
});

web/components/templates/sessions/sessionId/Span.tsx

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
useCallback,
1111
useEffect,
1212
useMemo,
13+
useRef,
1314
useState,
1415
} from "react";
1516
import { PiSplitHorizontalBold } from "react-icons/pi";
@@ -21,6 +22,7 @@ import {
2122
LabelList,
2223
Tooltip as RechartsTooltip,
2324
ReferenceArea,
25+
ReferenceLine,
2426
ResponsiveContainer,
2527
XAxis,
2628
YAxis,
@@ -73,6 +75,12 @@ export const TraceSpan = ({
7375
const [dragStartX, setDragStartX] = useState<number | null>(null);
7476
const [initialDragMovement, setInitialDragMovement] = useState(false);
7577

78+
// Crosshair state for showing precise timestamp on hover
79+
const [crosshairX, setCrosshairX] = useState<number | null>(null);
80+
const [crosshairPixelX, setCrosshairPixelX] = useState<number | null>(null);
81+
const [isHoveringChart, setIsHoveringChart] = useState(false);
82+
const chartContainerRef = useRef<HTMLDivElement>(null);
83+
7684
const spanData: BarChartTrace[] = useMemo(() => {
7785
if (!session || !session.traces) return [];
7886
const startTimeMs = session.start_time_unix_timestamp_ms;
@@ -237,9 +245,33 @@ export const TraceSpan = ({
237245
[lastChartDimensions],
238246
);
239247

240-
// Handle mouse move for dragging the highlighter
248+
// Handle mouse move for dragging the highlighter and updating crosshair
241249
const handleMouseMove = useCallback(
242250
(e: any) => {
251+
// Update crosshair using native mouse position for accurate pixel placement
252+
const container = chartContainerRef.current;
253+
if (container && e.chartX !== undefined) {
254+
const chartWidth = e.width || lastChartDimensions?.width || 1000;
255+
const leftMargin = 20;
256+
const rightMargin = 30;
257+
const plotAreaWidth = chartWidth - leftMargin - rightMargin;
258+
259+
// Store pixel position for CSS positioning
260+
setCrosshairPixelX(e.chartX);
261+
262+
// Calculate domain value for the label
263+
const adjustedX = e.chartX - leftMargin;
264+
if (adjustedX >= 0 && adjustedX <= plotAreaWidth) {
265+
const ratio = adjustedX / plotAreaWidth;
266+
const xDomain = domain[0] + ratio * (domain[1] - domain[0]);
267+
setCrosshairX(xDomain);
268+
// Set hovering true when we have valid coordinates (more reliable than onMouseEnter)
269+
setIsHoveringChart(true);
270+
} else {
271+
setCrosshairX(null);
272+
}
273+
}
274+
243275
if (
244276
!isDragging ||
245277
!highlighterActive ||
@@ -250,15 +282,15 @@ export const TraceSpan = ({
250282
)
251283
return;
252284

253-
// Ensure we have a valid chartX value
254-
const chartX = e.chartX;
255-
if (typeof chartX !== "number") return;
285+
// Get chartX for drag handling
286+
const dragChartX = e.chartX;
287+
if (typeof dragChartX !== "number") return;
256288

257289
// Consistently track chart dimensions
258-
const chartWidth = e.width || lastChartDimensions?.width || 1000;
259290
updateChartDimensions(e);
291+
const dragChartWidth = e.width || lastChartDimensions?.width || 1000;
260292

261-
const deltaX = chartX - dragStartX;
293+
const deltaX = dragChartX - dragStartX;
262294

263295
// Skip tiny movements to reduce jitter
264296
if (Math.abs(deltaX) < 2) return;
@@ -270,11 +302,11 @@ export const TraceSpan = ({
270302

271303
// Direct ratio calculation for movement - simpler and more accurate
272304
const domainWidth = domain[1] - domain[0];
273-
const domainDeltaX = (deltaX / chartWidth) * domainWidth;
305+
const domainDeltaX = (deltaX / dragChartWidth) * domainWidth;
274306

275307
// Don't update highlighter during initial movement
276308
if (!initialDragMovement) {
277-
setDragStartX(chartX);
309+
setDragStartX(dragChartX);
278310
return;
279311
}
280312

@@ -319,7 +351,7 @@ export const TraceSpan = ({
319351
setHighlighterEnd(newEnd);
320352

321353
// Always update dragStartX to prevent accumulation of small movements
322-
setDragStartX(chartX);
354+
setDragStartX(dragChartX);
323355

324356
// Prevent default behavior and stop propagation
325357
e.preventDefault?.();
@@ -336,9 +368,21 @@ export const TraceSpan = ({
336368
lastChartDimensions,
337369
initialDragMovement,
338370
updateChartDimensions,
371+
pixelToDomain,
339372
],
340373
);
341374

375+
// Handle mouse enter/leave for crosshair visibility
376+
const handleMouseEnter = useCallback(() => {
377+
setIsHoveringChart(true);
378+
}, []);
379+
380+
const handleMouseLeave = useCallback(() => {
381+
setIsHoveringChart(false);
382+
setCrosshairX(null);
383+
setCrosshairPixelX(null);
384+
}, []);
385+
342386
// Handle mouse down on the chart for dragging the highlighter
343387
const handleMouseDown = useCallback(
344388
(e: any) => {
@@ -500,11 +544,12 @@ export const TraceSpan = ({
500544
className="relative flex h-full select-none flex-col"
501545
id="sessions-trace-span"
502546
>
503-
<ScrollArea>
504-
<ResponsiveContainer
505-
width="100%"
506-
height={Math.max(300, spanData.length * BAR_SIZE)}
507-
>
547+
<div ref={chartContainerRef} className="relative">
548+
<ScrollArea>
549+
<ResponsiveContainer
550+
width="100%"
551+
height={Math.max(300, spanData.length * BAR_SIZE)}
552+
>
508553
<BarChart
509554
data={spanData}
510555
layout="vertical"
@@ -514,7 +559,11 @@ export const TraceSpan = ({
514559
onMouseDown={handleMouseDown}
515560
onMouseMove={handleMouseMove}
516561
onMouseUp={handleMouseUp}
517-
onMouseLeave={handleMouseUp}
562+
onMouseEnter={handleMouseEnter}
563+
onMouseLeave={(e) => {
564+
handleMouseUp();
565+
handleMouseLeave();
566+
}}
518567
>
519568
<CartesianGrid
520569
strokeDasharray="3 3"
@@ -784,9 +833,38 @@ export const TraceSpan = ({
784833
/>
785834
</>
786835
)}
836+
787837
</BarChart>
788-
</ResponsiveContainer>
789-
</ScrollArea>
838+
</ResponsiveContainer>
839+
</ScrollArea>
840+
841+
{/* CSS-positioned crosshair overlay */}
842+
{isHoveringChart && crosshairPixelX !== null && crosshairX !== null && !isDragging && (
843+
<div
844+
className="pointer-events-none absolute top-0 z-10"
845+
style={{
846+
left: crosshairPixelX,
847+
height: "100%",
848+
}}
849+
>
850+
<div
851+
className="h-full border-l border-dashed"
852+
style={{
853+
borderColor: theme === "dark" ? "#94a3b8" : "#64748b",
854+
}}
855+
/>
856+
<div
857+
className="absolute -top-1 left-1/2 -translate-x-1/2 whitespace-nowrap rounded px-1.5 py-0.5 text-xs font-medium"
858+
style={{
859+
backgroundColor: theme === "dark" ? "#334155" : "#f1f5f9",
860+
color: theme === "dark" ? "#e2e8f0" : "#334155",
861+
}}
862+
>
863+
{crosshairX.toFixed(3)}s
864+
</div>
865+
</div>
866+
)}
867+
</div>
790868
<ResponsiveContainer width="100%" height={52}>
791869
<BarChart
792870
data={spanData}

web/components/templates/sessions/sessionsPage.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import FoldedHeader from "@/components/shared/FoldedHeader";
22
import { FreeTierLimitBanner } from "@/components/shared/FreeTierLimitBanner";
33
import { EmptyStateCard } from "@/components/shared/helicone/EmptyStateCard";
4+
import LivePill from "@/components/shared/LivePill";
45
import { Button } from "@/components/ui/button";
56
import {
67
Command,
@@ -83,6 +84,7 @@ export type TSessions = {
8384
completion_tokens: number;
8485
total_tokens: number;
8586
avg_latency: number;
87+
user_ids?: string[];
8688
};
8789
};
8890

@@ -112,9 +114,13 @@ const SessionsPage = (props: SessionsPageProps) => {
112114
);
113115
const [page, setPage] = useState<number>(props.currentPage);
114116

115-
const [timeFilter, setTimeFilter] = useState<TimeFilter>({
116-
start: getTimeIntervalAgo("1m"),
117-
end: new Date(),
117+
const [timeFilter, setTimeFilter] = useState<TimeFilter>(() => {
118+
const now = new Date();
119+
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
120+
return {
121+
start: threeDaysAgo,
122+
end: now,
123+
};
118124
});
119125

120126
const [sessionIdSearch] = useURLParams<string | undefined>(
@@ -126,6 +132,7 @@ const SessionsPage = (props: SessionsPageProps) => {
126132
const [sessionNameSearch, setSessionNameSearch] = useState<
127133
string | undefined
128134
>(undefined);
135+
const [isLive, setIsLive] = useLocalStorage("isLive-SessionsPage", false);
129136

130137
const debouncedSessionNameSearch = useDebounce(sessionNameSearch, 500);
131138

@@ -147,12 +154,13 @@ const SessionsPage = (props: SessionsPageProps) => {
147154
props.selectedName,
148155
);
149156

150-
const { sessions, isLoading, hasSessions } = useSessions({
157+
const { sessions, isLoading, hasSessions, refetch, isRefetching } = useSessions({
151158
timeFilter,
152159
sessionIdSearch: debouncedSessionIdSearch ?? "",
153160
selectedName,
154161
page: page,
155162
pageSize: currentPageSize,
163+
isLive,
156164
});
157165

158166
const { aggregateMetrics, isLoading: isCountLoading } =
@@ -376,15 +384,24 @@ const SessionsPage = (props: SessionsPageProps) => {
376384
timeFilterOptions={[]}
377385
onSelect={onTimeSelectHandler}
378386
isFetching={isSessionsLoading}
379-
defaultValue={"1m"}
387+
defaultValue={"7d"}
380388
custom={true}
389+
isLive={isLive}
381390
/>
382391

383392
<FilterASTButton />
384393
</section>
385394
}
386395
rightSection={
387396
<section className="flex flex-row items-center gap-2">
397+
<LivePill
398+
isLive={isLive}
399+
setIsLive={setIsLive}
400+
isDataLoading={isLoading}
401+
isRefetching={isRefetching}
402+
refetch={refetch}
403+
/>
404+
388405
<div className="flex flex-row items-center gap-2 rounded-lg bg-sky-200">
389406
{selectedIds.length > 0 && (
390407
<Tooltip>

web/lib/freeTierLimits.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,9 @@ export const FREE_TIER_CONFIG: FreeTierConfig = {
122122
},
123123
sessions: {
124124
main: {
125-
getLimit: () => 1,
126-
description: (limit) =>
127-
`You can have up to ${limit} named sessions with the free tier`,
125+
getLimit: () => Infinity, // Unlimited sessions for free tier
126+
description: () => `Unlimited sessions`,
128127
upgradeFeatureName: FEATURE_DISPLAY_NAMES.sessions,
129-
upgradeMessage: (limit, used) =>
130-
`You've used ${used}/${limit} named sessions. Upgrade for unlimited access.`,
131128
},
132129
},
133130
properties: {

web/services/hooks/sessions.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ const useSessions = ({
1313
selectedName,
1414
page = 1,
1515
pageSize = 50,
16+
isLive = false,
1617
}: {
1718
timeFilter: TimeFilter;
1819
sessionIdSearch: string;
1920
selectedName?: string;
2021
page?: number;
2122
pageSize?: number;
23+
isLive?: boolean;
2224
}) => {
2325
const org = useOrg();
2426
const filterStore = useFilterAST();
@@ -65,7 +67,7 @@ const useSessions = ({
6567
refetchOnWindowFocus: false,
6668
retry: 2,
6769
refetchIntervalInBackground: false,
68-
refetchInterval: false,
70+
refetchInterval: isLive ? 2000 : false,
6971
});
7072
const properties = useQuery({
7173
queryKey: ["/v1/property/query", org?.currentOrg?.id],

0 commit comments

Comments
 (0)