feat(telemetry): collections, universal server-fn coverage, beacon, client tracker
- activities indexes + client_events/client_errors/rollups/alerts collections - global function middleware via createStart; reads skipped on success - actor stash, denial dedup, control-flow error guards - api route logging, auth event overrides, beacon endpoint, web vitals - centralized admin role checks, shared skeletons, award-badges fix - extracted deriveRunState with tests
This commit is contained in:
@@ -22,6 +22,7 @@ import { useDisclosure } from '@mantine/hooks'
|
||||
import { useEffect } from 'react'
|
||||
import toast from '@/lib/sonner'
|
||||
import { logger } from '@/lib/logger'
|
||||
import { captureBoundaryError } from '@/lib/telemetry/client'
|
||||
import { XCircleIcon, WarningIcon } from '@phosphor-icons/react'
|
||||
import { Trans, useLingui } from '@lingui/react/macro'
|
||||
import Button from './button'
|
||||
@@ -41,6 +42,7 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
||||
|
||||
useEffect(() => {
|
||||
logger.error('DefaultCatchBoundary | ', error)
|
||||
captureBoundaryError(error)
|
||||
|
||||
if (errorMessage.toLowerCase().includes('unauthenticated')) {
|
||||
toast.error(t`You've been logged out`)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { track } from '@/lib/telemetry/client'
|
||||
import { Box, Paper, Group, Text, ActionIcon } from '@mantine/core'
|
||||
import { DownloadIcon, XIcon } from '@phosphor-icons/react'
|
||||
import { Trans, useLingui } from '@lingui/react/macro'
|
||||
@@ -26,13 +27,17 @@ export function IOSInstallPrompt() {
|
||||
|
||||
if ((isIOS || isAndroid) && !isInStandaloneMode && !hasBeenDismissed) {
|
||||
setPlatform(isIOS ? 'ios' : 'android')
|
||||
const timer = setTimeout(() => setShow(true), 3000)
|
||||
const timer = setTimeout(() => {
|
||||
setShow(true)
|
||||
track('pwa_ios_prompt_shown')
|
||||
}, 3000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleDismiss = () => {
|
||||
localStorage.setItem('pwa-install-prompt-dismissed', 'true')
|
||||
track('pwa_ios_prompt_dismissed')
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Box, Group, Paper, Skeleton } from "@mantine/core";
|
||||
import type { MantineRadius } from "@mantine/core";
|
||||
|
||||
export interface CardSkeletonProps {
|
||||
withAvatar?: boolean;
|
||||
avatarSize?: number;
|
||||
withBadge?: boolean;
|
||||
radius?: MantineRadius;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export const CardSkeleton = ({
|
||||
withAvatar = true,
|
||||
avatarSize = 32,
|
||||
withBadge = true,
|
||||
radius = "md",
|
||||
opacity = 1,
|
||||
}: CardSkeletonProps) => {
|
||||
return (
|
||||
<Paper p="sm" withBorder radius={radius} style={{ opacity }}>
|
||||
<Group gap="sm" align="center" wrap="nowrap">
|
||||
{withAvatar && (
|
||||
<Skeleton height={avatarSize} width={avatarSize} radius="sm" />
|
||||
)}
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Skeleton height={16} width="70%" mb={4} />
|
||||
<Skeleton height={12} width="40%" />
|
||||
</Box>
|
||||
{withBadge && <Skeleton height={20} width={60} radius="xl" />}
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardSkeleton;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./list-skeleton";
|
||||
export * from "./card-skeleton";
|
||||
export * from "./stat-tile-skeleton";
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Box, Divider, Group, Skeleton, Stack } from "@mantine/core";
|
||||
import type { MantineSpacing } from "@mantine/core";
|
||||
|
||||
export interface ListSkeletonProps {
|
||||
rows?: number;
|
||||
withSearch?: boolean;
|
||||
withAvatar?: boolean;
|
||||
avatarSize?: number;
|
||||
withSubtitle?: boolean;
|
||||
withTrailing?: boolean | "double" | "text";
|
||||
titleWidth?: string | number;
|
||||
padding?: MantineSpacing;
|
||||
fadeStep?: number;
|
||||
fadeFloor?: number;
|
||||
}
|
||||
|
||||
export const ListSkeleton = ({
|
||||
rows = 6,
|
||||
withSearch = false,
|
||||
withAvatar = false,
|
||||
avatarSize = 40,
|
||||
withSubtitle = false,
|
||||
withTrailing = false,
|
||||
titleWidth = "45%",
|
||||
padding = "md",
|
||||
fadeStep = 0.1,
|
||||
fadeFloor = 0.35,
|
||||
}: ListSkeletonProps) => {
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
{withSearch && (
|
||||
<Box px={padding} pb="sm">
|
||||
<Skeleton height={42} radius="sm" />
|
||||
</Box>
|
||||
)}
|
||||
{Array.from({ length: rows }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{ opacity: Math.max(1 - index * fadeStep, fadeFloor) }}
|
||||
>
|
||||
<Group
|
||||
p={padding}
|
||||
wrap="nowrap"
|
||||
w="100%"
|
||||
justify={withTrailing === "text" ? "space-between" : undefined}
|
||||
>
|
||||
{withAvatar && (
|
||||
<Skeleton height={avatarSize} width={avatarSize} radius="sm" />
|
||||
)}
|
||||
{withSubtitle ? (
|
||||
<Stack gap={6}>
|
||||
<Skeleton height={14} width={160} radius="sm" />
|
||||
<Skeleton height={10} width={100} radius="sm" />
|
||||
</Stack>
|
||||
) : (
|
||||
<Skeleton height={16} width={titleWidth} radius="sm" />
|
||||
)}
|
||||
{withTrailing === "double" ? (
|
||||
<Stack ml="auto" gap={6} align="flex-end">
|
||||
<Skeleton height={10} width={90} radius="sm" />
|
||||
<Skeleton height={10} width={70} radius="sm" />
|
||||
</Stack>
|
||||
) : withTrailing === "text" ? (
|
||||
<Skeleton height={10} width={60} radius="sm" />
|
||||
) : withTrailing ? (
|
||||
<Skeleton ml="auto" height={20} width={20} radius="sm" />
|
||||
) : null}
|
||||
</Group>
|
||||
<Divider />
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListSkeleton;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Card, Group, Skeleton, Stack } from "@mantine/core";
|
||||
|
||||
export interface StatTileSkeletonProps {
|
||||
withCard?: boolean;
|
||||
align?: "row" | "column";
|
||||
numberWidth?: number;
|
||||
numberHeight?: number;
|
||||
labelWidth?: number;
|
||||
labelHeight?: number;
|
||||
}
|
||||
|
||||
export const StatTileSkeleton = ({
|
||||
withCard = false,
|
||||
align = "column",
|
||||
numberWidth = align === "row" ? 36 : 48,
|
||||
numberHeight = 28,
|
||||
labelWidth = align === "row" ? 110 : 80,
|
||||
labelHeight = align === "row" ? 14 : 12,
|
||||
}: StatTileSkeletonProps) => {
|
||||
const content =
|
||||
align === "row" ? (
|
||||
<Group gap="xs" align="baseline">
|
||||
<Skeleton height={numberHeight} width={numberWidth} radius="sm" />
|
||||
<Skeleton height={labelHeight} width={labelWidth} radius="sm" />
|
||||
</Group>
|
||||
) : (
|
||||
<Stack gap={4} align="center">
|
||||
<Skeleton height={numberHeight} width={numberWidth} radius="sm" />
|
||||
<Skeleton height={labelHeight} width={labelWidth} radius="sm" />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
if (!withCard) return content;
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" p="md">
|
||||
{content}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatTileSkeleton;
|
||||
Reference in New Issue
Block a user