fix(metrics): correct 1d revenue, chart date safety, globe zoom, UA merge, region locale

- metrics-page: derive revenue total from daily revenue series so the 1d view
  no longer shows $0 (hourly composed data has no revenue granularity)
- line-chart: make formatChartXAxisTick resilient to unparseable dates instead
  of throwing (the prior NaN check was unreachable)
- globe: disable OrbitControls zoom in non-interactive mode so the globe no
  longer captures page scroll
- analytics batch route: preserve non-object event.data instead of dropping it;
  only stamp the fallback User-Agent onto object payloads
- top-lists: use a fixed 'en' locale for region names to avoid SSR hydration
  mismatches (dashboard UI is English-only)
This commit is contained in:
mantrakp04 2026-06-01 17:13:49 -07:00
parent abd2e6a3b7
commit 22e17c9dd4
5 changed files with 25 additions and 12 deletions

View File

@ -145,13 +145,17 @@ export const POST = createSmartRouteHandler({
const rows = body.events.map((event) => {
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 === "")
? { ...baseData, user_agent: headerUserAgent }
: baseData;
const isPlainObject = rawData != null && typeof rawData === "object" && !Array.isArray(rawData);
// Only stamp the fallback User-Agent onto object payloads; preserve any other
// (non-object) data as-is instead of dropping it.
let mergedData: unknown = rawData;
if (isPlainObject) {
const baseData = rawData as Record<string, unknown>;
const existingUa = baseData.user_agent;
mergedData = (existingUa == null || existingUa === "")
? { ...baseData, user_agent: headerUserAgent }
: baseData;
}
return ({
event_type: event.event_type,
event_at: new Date(event.event_at_ms),

View File

@ -692,6 +692,7 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
controls.minDistance = cameraDistance;
controls.maxDistance = 600;
} else {
controls.enableZoom = false;
controls.maxDistance = cameraDistance;
controls.minDistance = cameraDistance;
}

View File

@ -116,8 +116,10 @@ function parseChartDate(dateValue: string): Date {
}
function formatChartXAxisTick(value: string): string {
const date = parseChartDate(value);
if (Number.isNaN(date.getTime())) {
let date: Date;
try {
date = parseChartDate(value);
} catch {
return value;
}
if (value.includes("T")) {

View File

@ -1673,7 +1673,10 @@ function MetricsContent({
? undefined
: previousDauPoint.new + previousDauPoint.retained + previousDauPoint.reactivated;
const visitorsTotalInRange = composedData.reduce((sum, row) => sum + row.visitors, 0);
const totalRevenueCentsInRange = composedData.reduce((sum, row) => sum + row.new_cents, 0);
// Revenue is only available at daily granularity, so derive the total from the
// daily revenue series (already filtered by the active range). The hourly composed
// data used in the 1d view has no revenue, which would otherwise zero this out.
const totalRevenueCentsInRange = revenueHoverData.reduce((sum, row) => sum + row.new_cents, 0);
const composedIndexByDate = new Map(allComposedData.map((row, index) => [row.date, index]));
const firstComposedPoint = composedData.at(0);
@ -1701,7 +1704,7 @@ function MetricsContent({
revenueLabel: "Revenue",
revenueDelta: paymentsEnabled && hasFullPreviousComposedWindow ? calculatePeriodDelta(totalRevenueCentsInRange, previousRevenueTotalCents) : undefined,
};
}, [allComposedData, composedData, dauStackedData, paymentsEnabled]);
}, [allComposedData, composedData, dauStackedData, paymentsEnabled, revenueHoverData]);
const bounceByDate = useMemo(() => new Map(dailyBounceRate.map((point) => [point.date, point.activity])), [dailyBounceRate]);
const sessionByDate = useMemo(() => new Map(dailyAvgSession.map((point) => [point.date, point.activity])), [dailyAvgSession]);

View File

@ -117,7 +117,10 @@ export function CountryFlag({ code }: { code: string }) {
export function regionName(code: string): string {
try {
const dn = new Intl.DisplayNames([typeof navigator !== "undefined" ? navigator.language : "en"], { type: "region" });
// Use a fixed locale so server and client render identical region names; the
// dashboard UI is English-only, and navigator.language would cause hydration
// mismatches for non-English users.
const dn = new Intl.DisplayNames(["en"], { type: "region" });
return dn.of(code.toUpperCase()) ?? code;
} catch {
return code;