diff --git a/apps/backend/src/app/api/latest/analytics/events/batch/route.tsx b/apps/backend/src/app/api/latest/analytics/events/batch/route.tsx index 23e66b6e3..fca94a65d 100644 --- a/apps/backend/src/app/api/latest/analytics/events/batch/route.tsx +++ b/apps/backend/src/app/api/latest/analytics/events/batch/route.tsx @@ -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) - : {}; - 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; + 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), diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx index cbe64ac4a..7ffc7c9b0 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx @@ -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; } diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx index ab0cf5471..3e408c802 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx @@ -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")) { diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx index d4d07f091..d8c506e3c 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx @@ -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]); diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx index 655b392b0..d6c4ea9c0 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/top-lists.tsx @@ -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;