Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
012b154
fix: API tooltip / chart fixes
perkinsjr Sep 3, 2025
f1d51fc
Fix keys
perkinsjr Sep 3, 2025
d3fbe99
Ratelimits
perkinsjr Sep 3, 2025
bc45baf
formatting
perkinsjr Sep 4, 2025
fe8061f
Merge branch 'main' into eng-2053-tooltip-times-are-wrong-when-lookin…
perkinsjr Sep 4, 2025
4244b92
Minor fixes
perkinsjr Sep 4, 2025
a1add6d
More fixes and code improvements
perkinsjr Sep 4, 2025
f9e4616
replaced the duplicated local `Granularity` union type with a proper …
perkinsjr Sep 8, 2025
1343ec4
optimize tooltip hover handler with O(1) timestamp lookup
perkinsjr Sep 8, 2025
d039e99
Add a constant for fallback
perkinsjr Sep 8, 2025
ff8fbb0
improve timezone accuracy in overview chart tooltips
perkinsjr Sep 8, 2025
27299c8
improve timestamp handling and type safety in logs utils
perkinsjr Sep 8, 2025
4860cb0
remove the aria from tooltip not needed
perkinsjr Sep 8, 2025
c0dbd09
increase upper boundary for edge cases
perkinsjr Sep 8, 2025
038238c
Remove redundancy for Granularity
perkinsjr Sep 8, 2025
f09ae5b
formatting
perkinsjr Sep 8, 2025
105c8a4
export ChartMouseEvent type and replace any types with proper typing
perkinsjr Sep 8, 2025
a611f0c
optimize tooltip lookups with precomputed timestamp map
perkinsjr Sep 8, 2025
ec30287
standardize time buffer constants and error handling across chart com…
perkinsjr Sep 8, 2025
bd59fff
refactor: extract tooltip formatter to shared utility
perkinsjr Sep 8, 2025
519e83d
refactor: reuse precomputed timezone abbreviation to avoid redundant …
perkinsjr Sep 8, 2025
cb0425d
extract shared parseTimestamp helper to handle microsecond timestamps
perkinsjr Sep 8, 2025
379a093
fix: improve timestamp tooltip handling for numeric strings
perkinsjr Sep 8, 2025
0f2d2d0
fix(dashboard): ensure numeric timestamps in calculateTimePoints call
perkinsjr Sep 8, 2025
138d1ed
fix: Update to 12H instead of default 24h
perkinsjr Sep 8, 2025
d703d6b
fix: normalize timestamp comparison in findIndex for reliable interva…
perkinsjr Sep 8, 2025
38d4239
fix: removed unreachable conditonal code
perkinsjr Sep 8, 2025
d02bab9
formatting
perkinsjr Sep 8, 2025
4bb804e
Merge branch 'main' into eng-2053-tooltip-times-are-wrong-when-lookin…
perkinsjr Sep 8, 2025
d2db157
feat: add memoization to timestamp formatting for better chart perfor…
perkinsjr Sep 8, 2025
e035f3b
fix(logs): skip invalid timestamps in overview chart timestamp→index map
perkinsjr Sep 8, 2025
eca153c
fix(dashboard): validate timestamps before adding to index map
perkinsjr Sep 8, 2025
3e1bb17
refactor: replace fragile digit-count logic with numeric magnitude th…
perkinsjr Sep 8, 2025
a48344d
Remove redundancy
perkinsjr Sep 8, 2025
e654219
null check to make sure we don't drop valid epoch
perkinsjr Sep 8, 2025
a8fcd5b
perf(logs): optimize timezone formatting by hoisting DateTimeFormat t…
perkinsjr Sep 8, 2025
84088f5
Merge branch 'eng-2053-tooltip-times-are-wrong-when-looking-at-differ…
perkinsjr Sep 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ export const KeysOverviewLogsCharts = ({
timeseries: verificationTimeseries,
isLoading: verificationIsLoading,
isError: verificationIsError,
granularity: verificationGranularity,
} = useFetchVerificationTimeseries(apiId);

const {
timeseries: activeKeysTimeseries,
isLoading: activeKeysIsLoading,
isError: activeKeysIsError,
granularity,
granularity: activeKeysGranularity,
} = useFetchActiveKeysTimeseries(apiId);

const handleSelectionChange = ({
Expand All @@ -40,8 +41,8 @@ export const KeysOverviewLogsCharts = ({
);

let adjustedEnd = end;
if (start === end && granularity) {
adjustedEnd = end + getTimeBufferForGranularity(granularity);
if (start === end && verificationGranularity) {
adjustedEnd = end + getTimeBufferForGranularity(verificationGranularity);
}
updateFilters([
...activeFilters,
Expand Down Expand Up @@ -100,6 +101,7 @@ export const KeysOverviewLogsCharts = ({
secondaryKey: "error",
}}
tooltipItems={[{ label: "Invalid", dataKey: "error" }]}
granularity={verificationGranularity}
/>
</div>
<div className="w-full md:w-1/2 max-md:h-72">
Expand All @@ -111,6 +113,7 @@ export const KeysOverviewLogsCharts = ({
onSelectionChange={handleSelectionChange}
config={keysChartConfig}
labels={keysChartLabels}
granularity={activeKeysGranularity}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const KeyDetailsLogsChart = ({
onMount={onMount}
onSelectionChange={handleSelectionChange}
config={createOutcomeChartConfig()}
granularity={granularity}
labels={{
title: "REQUESTS",
primaryLabel: "VALID",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const RatelimitOverviewLogsCharts = ({
isError={isError}
enableSelection
onSelectionChange={handleSelectionChange}
granularity={granularity}
config={{
success: {
label: "Passed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function RatelimitLogsChart({
isLoading={isLoading}
isError={isError}
enableSelection
granularity={granularity}
/>
);
}
41 changes: 27 additions & 14 deletions apps/dashboard/components/logs/chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ChartTooltipContent,
} from "@/components/ui/chart";
import { formatNumber } from "@/lib/fmt";
import type { CompoundTimeseriesGranularity } from "@/lib/trpc/routers/utils/granularity";
import { Grid } from "@unkey/icons";
import { useEffect, useRef, useState } from "react";
import { Bar, BarChart, ReferenceArea, ResponsiveContainer, YAxis } from "recharts";
Expand All @@ -18,8 +19,8 @@ import { calculateTimePoints } from "./utils/calculate-timepoints";
import { formatTimestampLabel } from "./utils/format-timestamp";

type Selection = {
start: string | number;
end: string | number;
start: string | number | undefined;
end: string | number | undefined;
startTimestamp?: number;
endTimestamp?: number;
};
Comment on lines 21 to 26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Narrow Selection types to numbers to avoid coercions.

Store numeric x values and drop string union; fewer Number() casts downstream.

 type Selection = {
-  start: string | number | undefined;
-  end: string | number | undefined;
+  start: number | undefined;
+  end: number | undefined;
   startTimestamp?: number;
   endTimestamp?: number;
 };

And coerce on mouse down:

-    setSelection({
-      start: e.activeLabel,
-      end: e.activeLabel,
+    const labelNum = Number(e.activeLabel);
+    setSelection({
+      start: labelNum,
+      end: labelNum,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type Selection = {
start: string | number;
end: string | number;
start: string | number | undefined;
end: string | number | undefined;
startTimestamp?: number;
endTimestamp?: number;
};
// apps/dashboard/components/logs/chart/index.tsx
// ─── Narrow Selection to numbers ───────────────────────────────────────────────
type Selection = {
start: number | undefined;
end: number | undefined;
startTimestamp?: number;
endTimestamp?: number;
};
// ─── In your mouse-down (or similar) handler ────────────────────────────────────
const labelNum = Number(e.activeLabel);
setSelection({
start: labelNum,
end: labelNum,
// …other selection fields unchanged
});
🤖 Prompt for AI Agents
In apps/dashboard/components/logs/chart/index.tsx around lines 21 to 26, the
Selection type uses string | number | undefined which forces downstream
coercions; change start and end to number | undefined (keep
startTimestamp/endTimestamp as-is or remove if redundant), update any code paths
that set Selection (notably the mouse down handler) to coerce incoming x values
to numbers at the point of assignment (e.g., parse/Number on the event
coordinate) so all stored x values are numeric and downstream Number() casts can
be removed.

Expand All @@ -30,6 +31,13 @@ type TimeseriesData = {
[key: string]: unknown;
};

type ChartMouseEvent = {
activeLabel?: string | number;
activePayload?: Array<{
payload: TimeseriesData;
}>;
};

type LogsTimeseriesBarChartProps = {
data?: TimeseriesData[];
config: ChartConfig;
Expand All @@ -39,6 +47,7 @@ type LogsTimeseriesBarChartProps = {
isLoading?: boolean;
isError?: boolean;
enableSelection?: boolean;
granularity?: CompoundTimeseriesGranularity;
};

export function LogsTimeseriesBarChart({
Expand All @@ -50,9 +59,13 @@ export function LogsTimeseriesBarChart({
isLoading,
isError,
enableSelection = false,
granularity,
}: LogsTimeseriesBarChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
const [selection, setSelection] = useState<Selection>({ start: "", end: "" });
const [selection, setSelection] = useState<Selection>({
start: undefined,
end: undefined,
});

// biome-ignore lint/correctness/useExhaustiveDependencies: We need this to re-trigger distanceToTop calculation
useEffect(() => {
Expand All @@ -62,8 +75,7 @@ export function LogsTimeseriesBarChart({
}
}, [onMount, isLoading, isError]);

// biome-ignore lint/suspicious/noExplicitAny: those are safe to leave
const handleMouseDown = (e: any) => {
const handleMouseDown = (e: ChartMouseEvent) => {
if (!enableSelection) {
return;
}
Expand All @@ -76,8 +88,7 @@ export function LogsTimeseriesBarChart({
});
};

// biome-ignore lint/suspicious/noExplicitAny: those are safe to leave
const handleMouseMove = (e: any) => {
const handleMouseMove = (e: ChartMouseEvent) => {
if (!enableSelection) {
return;
}
Expand All @@ -104,8 +115,8 @@ export function LogsTimeseriesBarChart({
onSelectionChange({ start, end });
}
setSelection({
start: "",
end: "",
start: undefined,
end: undefined,
startTimestamp: undefined,
endTimestamp: undefined,
});
Expand All @@ -126,9 +137,8 @@ export function LogsTimeseriesBarChart({
? calculateTimePoints(
data[0]?.originalTimestamp ?? Date.now(),
data.at(-1)?.originalTimestamp ?? Date.now(),
).map((time, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<div key={i} className="z-10">
).map((time) => (
<div key={time.getTime()} className="z-10">
{formatTimestampLabel(time)}
</div>
))
Expand Down Expand Up @@ -187,8 +197,11 @@ export function LogsTimeseriesBarChart({
}
className="rounded-lg shadow-lg border border-gray-4"
labelFormatter={(_, payload) =>
//@ts-expect-error This is okay to ignore
createTimeIntervalFormatter(data, "HH:mm")(payload)
createTimeIntervalFormatter(
data,
"HH:mm",
granularity,
)(payload as TimeseriesData[])
}
/>
);
Expand Down
12 changes: 6 additions & 6 deletions apps/dashboard/components/logs/chart/utils/format-timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { format, fromUnixTime } from "date-fns";

export const formatTimestampLabel = (timestamp: string | number | Date) => {
const date = new Date(timestamp);
return format(date, "MMM dd, h:mma").toUpperCase();
return format(date, "MMM dd, h:mm a").toUpperCase();
};

export const formatTimestampForChart = (
Expand All @@ -14,21 +14,21 @@ export const formatTimestampForChart = (

switch (granularity) {
case "perMinute":
return format(localDate, "HH:mm:ss");
return format(localDate, "h:mm:ss a");
case "per5Minutes":
case "per15Minutes":
case "per30Minutes":
return format(localDate, "HH:mm");
return format(localDate, "h:mm a");
case "perHour":
case "per2Hours":
case "per4Hours":
case "per6Hours":
return format(localDate, "MMM d, HH:mm");
return format(localDate, "MMM d, h:mm a");
case "perDay":
return format(localDate, "MMM d");

case "per12Hours":
return format(localDate, "MMM d, HH:mm");
return format(localDate, "MMM d, h:mm a");
case "per3Days":
return format(localDate, "MMM d");
case "perWeek":
Expand All @@ -52,5 +52,5 @@ const isUnixMicro = (unix: string | number): boolean => {

export const formatTimestampTooltip = (value: string | number) => {
const date = isUnixMicro(value) ? unixMicroToDate(value) : new Date(value);
return format(date, "MMM dd HH:mm:ss");
return format(date, "MMM dd h:mm:ss a");
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";

import { calculateTimePoints } from "@/components/logs/chart/utils/calculate-timepoints";
import { formatTimestampLabel } from "@/components/logs/chart/utils/format-timestamp";
import {
Expand All @@ -8,7 +9,8 @@ import {
ChartTooltipContent,
} from "@/components/ui/chart";
import { formatNumber } from "@/lib/fmt";
import { cn } from "@unkey/ui/src/lib/utils";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { useState } from "react";
import {
Area,
Expand All @@ -18,10 +20,54 @@ import {
ResponsiveContainer,
YAxis,
} from "recharts";

import { OverviewAreaChartError } from "./overview-area-chart-error";
import { OverviewAreaChartLoader } from "./overview-area-chart-loader";
import type { Selection, TimeseriesData } from "./types";
import { createTimeIntervalFormatter } from "./utils";

// Helper function to format tooltip timestamps based on granularity and data span
const formatTooltipTimestamp = (
timestamp: number | string,
granularity?: string,
data?: TimeseriesData[],
) => {
const date = new Date(timestamp);

// If we have data, check if it spans multiple days
if (data && data.length > 1) {
const firstDay = new Date(data[0].originalTimestamp);
const lastDay = new Date(data[data.length - 1].originalTimestamp);

// Check if the data spans multiple calendar days
const firstDayStr = firstDay.toDateString();
const lastDayStr = lastDay.toDateString();

if (firstDayStr !== lastDayStr) {
// Data spans multiple days, always show date + time
return format(date, "MMM dd, h:mm a");
}
}

// For granularities less than 12 hours on same day, show only time
if (
granularity &&
[
"perMinute",
"per5Minutes",
"per15Minutes",
"per30Minutes",
"perHour",
"per2Hours",
"per4Hours",
"per6Hours",
].includes(granularity)
) {
return format(date, "h:mm a");
}

// For granularities 12 hours or more, show date + time
return format(date, "MMM dd, h:mm a");
};

export type ChartMetric = {
key: string;
Expand All @@ -46,6 +92,7 @@ export interface TimeseriesAreaChartProps {
isError?: boolean;
enableSelection?: boolean;
labels: TimeseriesChartLabels;
granularity?: string;
}

export const OverviewAreaChart = ({
Expand All @@ -56,6 +103,7 @@ export const OverviewAreaChart = ({
isError,
enableSelection = false,
labels,
granularity,
}: TimeseriesAreaChartProps) => {
const [selection, setSelection] = useState<Selection>({ start: "", end: "" });

Expand Down Expand Up @@ -241,10 +289,81 @@ export const OverviewAreaChart = ({
label={label}
active={active}
className="rounded-lg shadow-lg border border-gray-4"
labelFormatter={(_, tooltipPayload) =>
//@ts-expect-error safe to leave as is for now
createTimeIntervalFormatter(data, "HH:mm")(tooltipPayload)
}
labelFormatter={(_, tooltipPayload) => {
if (!tooltipPayload?.[0]?.payload?.originalTimestamp) {
return "";
}

const currentPayload = tooltipPayload[0].payload;
const currentTimestamp = currentPayload.originalTimestamp;

if (!data?.length) {
return (
<div className="px-4">
<span className="font-mono text-accent-9 text-xs whitespace-nowrap">
{formatTooltipTimestamp(currentTimestamp, granularity, data)}
</span>
</div>
);
}

// Find position in the data array
const currentIndex = data.findIndex(
(item) => item?.originalTimestamp === currentTimestamp,
);

// If this is the last item or not found, just show current timestamp
if (currentIndex === -1 || currentIndex >= data.length - 1) {
return (
<div className="px-4">
<span className="font-mono text-accent-9 text-xs whitespace-nowrap">
{formatTooltipTimestamp(currentTimestamp, granularity, data)}
</span>
</div>
);
}

// Get the next point in the sequence for interval display
const nextPoint = data[currentIndex + 1];
if (!nextPoint) {
return (
<div className="px-4">
<span className="font-mono text-accent-9 text-xs whitespace-nowrap">
{formatTooltipTimestamp(currentTimestamp, granularity, data)}
</span>
</div>
);
}

// Format both timestamps - considers both granularity and data span
const formattedCurrentTimestamp = formatTooltipTimestamp(
currentTimestamp,
granularity,
data,
);
const formattedNextTimestamp = formatTooltipTimestamp(
nextPoint.originalTimestamp,
granularity,
data,
);

// Get timezone abbreviation
const timezone =
new Intl.DateTimeFormat("en-US", {
timeZoneName: "short",
})
.formatToParts(new Date())
.find((part) => part.type === "timeZoneName")?.value || "";

// Return formatted interval with timezone info
return (
<div className="px-4">
<span className="font-mono text-accent-9 text-xs whitespace-nowrap">
{formattedCurrentTimestamp} - {formattedNextTimestamp} ({timezone})
</span>
</div>
);
}}
/>
);
}}
Expand Down
Loading