fix: UTC hour truncation, null event data guard, dedupe animation utils

- metrics/route: use setUTCMinutes for hourly bucket keys to match ClickHouse toStartOfHour (Greptile P2, cubic P1)
- analytics batch route: guard against null event.data before object spread (cubic P1)
- dashboard: extract easeOutCubic/prefersReducedMotion to shared animation-utils (Greptile P2)
This commit is contained in:
mantrakp04 2026-06-01 13:53:22 -07:00
parent 398d4f1a00
commit f77f775917
5 changed files with 13 additions and 19 deletions

View File

@ -144,8 +144,9 @@ export const POST = createSmartRouteHandler({
})();
const rows = body.events.map((event) => {
const baseData = (typeof event.data === "object" && !Array.isArray(event.data))
? (event.data as Record<string, unknown>)
const rawData: unknown = event.data;
const baseData = (rawData != null && typeof rawData === "object" && !Array.isArray(rawData))
? (rawData as Record<string, unknown>)
: {};
const existingUa = baseData.user_agent;
const mergedData = (existingUa == null || existingUa === "")

View File

@ -1685,7 +1685,7 @@ async function loadAnalyticsOverview(
const hourlyActiveUsers: DataPoints = [];
const hourlyVisitors: DataPoints = [];
const latestHour = new Date(now);
latestHour.setMinutes(0, 0, 0);
latestHour.setUTCMinutes(0, 0, 0);
for (let i = 23; i >= 0; i--) {
const hour = new Date(latestHour.getTime() - i * 60 * 60 * 1000);
const key = hour.toISOString().slice(0, 13);

View File

@ -0,0 +1,7 @@
export function easeOutCubic(progress: number): number {
return 1 - Math.pow(1 - progress, 3);
}
export function prefersReducedMotion(): boolean {
return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

View File

@ -71,6 +71,7 @@ import {
} from "./line-chart";
import { MetricsErrorFallback, MetricsLoadingFallback } from "./metrics-loading";
import { ReferrersWithAnalyticsCard, TopNamedListCard, TopRegionsCard } from "./top-lists";
import { easeOutCubic, prefersReducedMotion } from "./animation-utils";
import {
ANALYTICS_CHART_METRIC_MODE_ORDER,
toggleAnalyticsChartMetricMode,
@ -128,10 +129,6 @@ const reducedOverviewHeaderLayoutTransition: Transition = {
const scrollableOverflowValues = new Set(["auto", "scroll", "overlay"]);
function easeOutCubic(progress: number): number {
return 1 - Math.pow(1 - progress, 3);
}
function findScrollContainer(element: HTMLElement): HTMLElement | null {
let current = element.parentElement;
while (current != null) {
@ -207,10 +204,6 @@ function useDelayedTrue(value: boolean, delayMs: number): boolean {
return delayedValue;
}
function prefersReducedMotion(): boolean {
return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function useAnimatedSeriesValues<T extends { value: number }>(series: T[]): T[] {
const [animatedSeries, setAnimatedSeries] = useState(series);
const previousSeriesRef = useRef(series);

View File

@ -9,17 +9,10 @@ import { GlobeIcon } from "@phosphor-icons/react";
import type { Icon } from "@phosphor-icons/react";
import { stringCompare } from "@hexclave/shared/dist/utils/strings";
import { useEffect, useMemo, useRef, useState } from "react";
import { easeOutCubic, prefersReducedMotion } from "./animation-utils";
const TOP_LIST_ANIMATION_MS = 260;
function easeOutCubic(progress: number): number {
return 1 - Math.pow(1 - progress, 3);
}
function prefersReducedMotion(): boolean {
return typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function useAnimatedBarValues(rows: Array<{ id: string, value: number }>): Map<string, number> {
const [animatedValues, setAnimatedValues] = useState(() => new Map(rows.map((row) => [row.id, row.value])));
const previousValuesRef = useRef(animatedValues);