mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
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:
parent
abd2e6a3b7
commit
22e17c9dd4
@ -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),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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")) {
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user