Merge branch 'dev' of https://github.com/umami-software/umami into analytics
This commit is contained in:
commit
c7e3fc6820
21 files changed
+3525
-2225
No files matched your search
+44
-44
@@ -14,7 +14,7 @@
|
||||
"dev": "dotenv next dev --turbo",
|
||||
"build": "npm-run-all check-env build-db check-db build-tracker build-recorder build-geo build-app",
|
||||
"start": "next start",
|
||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||
"build-docker": "npm-run-all build-db build-tracker build-recorder build-geo build-app",
|
||||
"start-docker": "npm-run-all check-db update-tracker start-server",
|
||||
"start-env": "node scripts/start-env.js",
|
||||
"start-server": "node server.js",
|
||||
@@ -52,16 +52,15 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^1.18.2",
|
||||
"@date-fns/utc": "^1.2.0",
|
||||
"@date-fns/utc": "^2.1.1",
|
||||
"@dicebear/collection": "^9.4.2",
|
||||
"@dicebear/core": "^9.4.2",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@prisma/adapter-pg": "^7.6.0",
|
||||
"@prisma/client": "^7.6.0",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@prisma/adapter-pg": "^7.7.0",
|
||||
"@prisma/client": "^7.7.0",
|
||||
"@prisma/extension-read-replicas": "^0.5.0",
|
||||
"@react-spring/web": "^10.0.3",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@tanstack/react-query": "^5.96.0",
|
||||
"@tanstack/react-query": "^5.99.0",
|
||||
"@umami/react-zen": "^0.245.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"chalk": "^5.6.2",
|
||||
@@ -71,88 +70,89 @@
|
||||
"colord": "^2.9.2",
|
||||
"cors": "^2.8.6",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"date-fns": "^2.23.0",
|
||||
"date-fns-tz": "^1.1.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"debug": "^4.4.3",
|
||||
"del": "^6.0.0",
|
||||
"del": "^8.0.1",
|
||||
"detect-browser": "^5.2.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"esbuild": "^0.27.4",
|
||||
"dotenv": "^17.4.2",
|
||||
"esbuild": "^0.28.0",
|
||||
"immer": "^11.1.4",
|
||||
"ipaddr.js": "^2.3.0",
|
||||
"is-ci": "^3.0.1",
|
||||
"is-docker": "^3.0.0",
|
||||
"is-localhost-ip": "^2.0.0",
|
||||
"isbot": "^5.1.37",
|
||||
"is-ci": "^4.1.0",
|
||||
"is-docker": "^4.0.0",
|
||||
"is-localhost-ip": "^3.0.1",
|
||||
"isbot": "^5.1.39",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"kafkajs": "^2.1.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"maxmind": "^5.0.5",
|
||||
"lucide-react": "^1.8.0",
|
||||
"maxmind": "^5.0.6",
|
||||
"motion": "^12.38.0",
|
||||
"next": "16.2.4",
|
||||
"next-intl": "4.8.3",
|
||||
"next-intl": "4.9.1",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"papaparse": "^5.5.3",
|
||||
"pg": "^8.20.0",
|
||||
"prisma": "^7.6.0",
|
||||
"pure-rand": "^7.0.1",
|
||||
"prisma": "^7.7.0",
|
||||
"pure-rand": "^8.4.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
"react-resizable-panels": "^4.8.0",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-error-boundary": "^6.1.1",
|
||||
"react-resizable-panels": "^4.10.0",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"react-use-measure": "^2.0.4",
|
||||
"react-window": "^1.8.6",
|
||||
"redis": "^4.5.1",
|
||||
"react-window": "^2.2.7",
|
||||
"redis": "^5.12.1",
|
||||
"request-ip": "^3.3.0",
|
||||
"rrweb": "2.0.0-alpha.4",
|
||||
"rrweb-player": "1.0.0-alpha.4",
|
||||
"semver": "^7.7.4",
|
||||
"serialize-error": "^12.0.0",
|
||||
"thenby": "^1.3.4",
|
||||
"serialize-error": "^13.0.1",
|
||||
"thenby": "^1.4.0",
|
||||
"ua-parser-js": "^2.0.9",
|
||||
"uuid": "^13.0.0",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@biomejs/biome": "^2.4.12",
|
||||
"@netlify/plugin-nextjs": "^5.15.9",
|
||||
"@rollup/plugin-alias": "^5.0.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.4",
|
||||
"@rollup/plugin-alias": "^6.0.0",
|
||||
"@rollup/plugin-commonjs": "^29.0.2",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.0",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@rollup/plugin-terser": "^1.0.0",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"cypress": "^13.6.6",
|
||||
"cypress": "^15.14.0",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"postcss": "^8.5.8",
|
||||
"jest": "^30.3.0",
|
||||
"postcss": "^8.5.10",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-preset-env": "7.8.3",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-preset-env": "11.2.1",
|
||||
"prompts": "2.4.2",
|
||||
"rollup": "^4.60.1",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-delete": "^3.0.2",
|
||||
"rollup-plugin-dts": "^6.4.1",
|
||||
"rollup-plugin-node-externals": "^8.1.1",
|
||||
"rollup-plugin-node-externals": "^9.0.1",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"tar": "^7.5.13",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-morph": "^27.0.2",
|
||||
"ts-jest": "^29.4.9",
|
||||
"ts-morph": "^28.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
Generated
+3349
-2099
File diff suppressed because it is too large.
Load diff
@@ -227,7 +227,7 @@
|
||||
"TO": "Tonga",
|
||||
"TT": "Trinidad & Tobago",
|
||||
"TN": "Tunisia",
|
||||
"TR": "Turkey",
|
||||
"TR": "T\u00fcrkiye",
|
||||
"TM": "Turkmenistan",
|
||||
"TC": "Turks & Caicos Islands",
|
||||
"TV": "Tuvalu",
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
"TO": "Tonga",
|
||||
"TT": "Trinidad & Tobago",
|
||||
"TN": "Tunisia",
|
||||
"TR": "Turkey",
|
||||
"TR": "T\u00fcrkiye",
|
||||
"TM": "Turkmenistan",
|
||||
"TC": "Turks & Caicos Islands",
|
||||
"TV": "Tuvalu",
|
||||
|
||||
@@ -110,8 +110,8 @@ export function BoardEditBody({ requiresBoardWebsite = true }: { requiresBoardWe
|
||||
const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
|
||||
|
||||
return (
|
||||
<Box minHeight={`${minHeight}px`}>
|
||||
<Group groupRef={rowGroupRef} orientation="vertical">
|
||||
<Box>
|
||||
<Group style={{ minHeight: `${minHeight}px` }} groupRef={rowGroupRef} orientation="vertical">
|
||||
{rows.map((row, index) => (
|
||||
<Fragment key={`${row.id}:${row.size ?? 'auto'}`}>
|
||||
<Panel
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Column, Grid, Icon, Row, Text } from '@umami/react-zen';
|
||||
import type { ReactNode } from 'react';
|
||||
import { differenceInCalendarDays } from 'date-fns';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useLocale, useMessages, useResultQuery } from '@/components/hooks';
|
||||
@@ -45,8 +46,6 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
|
||||
return arr;
|
||||
}, []) || [];
|
||||
|
||||
const totalDays = rows.length;
|
||||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
@@ -84,6 +83,7 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
|
||||
))}
|
||||
</Grid>
|
||||
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
|
||||
const maxDay = differenceInCalendarDays(endDate, new Date(date));
|
||||
return (
|
||||
<Grid
|
||||
key={rowIndex}
|
||||
@@ -103,10 +103,8 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
|
||||
</Row>
|
||||
</Column>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
return null;
|
||||
}
|
||||
const percentage = records.filter(a => a.day === day)[0]?.percentage;
|
||||
if (day > maxDay) return null;
|
||||
const percentage = records.find(a => a.day === day)?.percentage;
|
||||
return (
|
||||
<Cell key={day}>
|
||||
{percentage ? `${Number(percentage).toFixed(2)}%` : ''}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function WebsiteHeader({
|
||||
titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
|
||||
>
|
||||
<Row alignItems="center" gap="6" wrap="wrap">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
<ActiveUsers websiteId={website.id} allowLink={allowLink} />
|
||||
|
||||
{showActions && (
|
||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Column, Heading, Row, SearchField, Text } from '@umami/react-zen';
|
||||
import Link from '@/components/common/Link';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { List, type RowComponentProps } from 'react-window';
|
||||
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
|
||||
import { useFormat } from '@/components//hooks/useFormat';
|
||||
import { Avatar } from '@/components/common/Avatar';
|
||||
@@ -121,7 +121,7 @@ export function RealtimeLog({ data }: { data: any }) {
|
||||
}
|
||||
};
|
||||
|
||||
const TableRow = ({ index, style }) => {
|
||||
const TableRow = ({ index, style, logs }: RowComponentProps<{ logs: any[] }>) => {
|
||||
const row = logs[index];
|
||||
return (
|
||||
<Row alignItems="center" style={style} gap>
|
||||
@@ -195,9 +195,14 @@ export function RealtimeLog({ data }: { data: any }) {
|
||||
<Column>
|
||||
{logs?.length === 0 && <Empty />}
|
||||
{logs?.length > 0 && (
|
||||
<FixedSizeList width="100%" height={500} itemCount={logs.length} itemSize={50}>
|
||||
{TableRow}
|
||||
</FixedSizeList>
|
||||
<List
|
||||
rowComponent={TableRow}
|
||||
rowCount={logs.length}
|
||||
rowHeight={50}
|
||||
rowProps={{ logs }}
|
||||
defaultHeight={500}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
<SessionModal websiteId={website.id} />
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { type AnimatedComponent, animated } from '@react-spring/web';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
export const AnimatedDiv: AnimatedComponent<any> = animated.div;
|
||||
export const AnimatedDiv = motion.div;
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactQueryOptions } from '@/lib/types';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useActyiveUsersQuery(websiteId: string, options?: ReactQueryOptions) {
|
||||
export function useActiveUsersQuery(websiteId: string, options?: ReactQueryOptions) {
|
||||
const { get, useQuery } = useApi();
|
||||
return useQuery<any>({
|
||||
queryKey: ['websites:active', websiteId],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatInTimeZone, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
|
||||
import { formatInTimeZone, fromZonedTime, toZonedTime } from 'date-fns-tz';
|
||||
import { TIMEZONE_CONFIG, TIMEZONE_LEGACY } from '@/lib/constants';
|
||||
import { getTimezone } from '@/lib/date';
|
||||
import { setItem } from '@/lib/storage';
|
||||
@@ -61,19 +61,19 @@ export function useTimezone() {
|
||||
};
|
||||
|
||||
const toUtc = (date: Date | string | number) => {
|
||||
return zonedTimeToUtc(date, timezone);
|
||||
return fromZonedTime(date, timezone);
|
||||
};
|
||||
|
||||
const fromUtc = (date: Date | string | number) => {
|
||||
return utcToZonedTime(date, timezone);
|
||||
return toZonedTime(date, timezone);
|
||||
};
|
||||
|
||||
const localToUtc = (date: Date | string | number) => {
|
||||
return zonedTimeToUtc(date, localTimeZone);
|
||||
return fromZonedTime(date, localTimeZone);
|
||||
};
|
||||
|
||||
const localFromUtc = (date: Date | string | number) => {
|
||||
return utcToZonedTime(date, localTimeZone);
|
||||
return toZonedTime(date, localTimeZone);
|
||||
};
|
||||
|
||||
const canonicalizeTimezone = (timezone: string): string => {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { StatusLight, Text } from '@umami/react-zen';
|
||||
import { useMemo } from 'react';
|
||||
import { useActyiveUsersQuery, useMessages } from '@/components/hooks';
|
||||
import Link from '@/components/common/Link';
|
||||
import { useActiveUsersQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function ActiveUsers({
|
||||
websiteId,
|
||||
value,
|
||||
refetchInterval = 60000,
|
||||
allowLink = true,
|
||||
}: {
|
||||
websiteId: string;
|
||||
value?: number;
|
||||
refetchInterval?: number;
|
||||
allowLink?: boolean;
|
||||
}) {
|
||||
const { t, labels } = useMessages();
|
||||
const { data } = useActyiveUsersQuery(websiteId, { refetchInterval });
|
||||
const { data } = useActiveUsersQuery(websiteId, { refetchInterval });
|
||||
|
||||
const count = useMemo(() => {
|
||||
if (websiteId) {
|
||||
@@ -26,11 +29,17 @@ export function ActiveUsers({
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
const content = (
|
||||
<StatusLight variant="success">
|
||||
<Text size="sm" weight="medium">
|
||||
{count} {t(labels.online)}
|
||||
</Text>
|
||||
</StatusLight>
|
||||
);
|
||||
}
|
||||
|
||||
if (!allowLink) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return <Link href={`/websites/${websiteId}/realtime`}>{content}</Link>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { config, useSpring } from '@react-spring/web';
|
||||
import { Column, Grid, Row, Text } from '@umami/react-zen';
|
||||
import type { ReactNode } from 'react';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { useSpring, useTransform } from 'motion/react';
|
||||
import { type ReactNode, useEffect } from 'react';
|
||||
import { List, type RowComponentProps } from 'react-window';
|
||||
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { useMessages, useMobile } from '@/components/hooks';
|
||||
@@ -62,7 +62,7 @@ export function ListTable({
|
||||
);
|
||||
};
|
||||
|
||||
const ListTableRow = ({ index, style }) => {
|
||||
const ListTableRow = ({ index, style }: RowComponentProps) => {
|
||||
return <div style={style}>{getRow(data[index], index)}</div>;
|
||||
};
|
||||
|
||||
@@ -82,14 +82,14 @@ export function ListTable({
|
||||
<Column gap="1">
|
||||
{data?.length === 0 && <Empty />}
|
||||
{virtualize && data.length > 0 ? (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={itemCount * ITEM_SIZE}
|
||||
itemCount={data.length}
|
||||
itemSize={ITEM_SIZE}
|
||||
>
|
||||
{ListTableRow}
|
||||
</FixedSizeList>
|
||||
<List
|
||||
style={{ width: '100%', height: itemCount * ITEM_SIZE }}
|
||||
defaultHeight={itemCount * ITEM_SIZE}
|
||||
rowCount={data.length}
|
||||
rowHeight={ITEM_SIZE}
|
||||
rowComponent={ListTableRow}
|
||||
rowProps={{}}
|
||||
/>
|
||||
) : (
|
||||
data.map(getRow)
|
||||
)}
|
||||
@@ -108,12 +108,21 @@ const AnimatedRow = ({
|
||||
formatCount,
|
||||
isPhone,
|
||||
}) => {
|
||||
const props = useSpring({
|
||||
width: percent,
|
||||
y: !Number.isNaN(value) ? value : 0,
|
||||
from: { width: 0, y: 0 },
|
||||
config: animate ? config.default : { duration: 0 },
|
||||
});
|
||||
const y = !Number.isNaN(value) ? value : 0;
|
||||
const ySpring = useSpring(0, { stiffness: 170, damping: 26 });
|
||||
const widthSpring = useSpring(0, { stiffness: 170, damping: 26 });
|
||||
const yText = useTransform(ySpring, n => (formatCount ? formatCount(n) : formatLongNumber(n)));
|
||||
const widthText = useTransform(widthSpring, n => `${n?.toFixed?.(0)}%`);
|
||||
|
||||
useEffect(() => {
|
||||
if (animate) {
|
||||
ySpring.set(y);
|
||||
widthSpring.set(percent);
|
||||
} else {
|
||||
ySpring.jump(y);
|
||||
widthSpring.jump(percent);
|
||||
}
|
||||
}, [y, percent, animate, ySpring, widthSpring]);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
@@ -136,9 +145,7 @@ const AnimatedRow = ({
|
||||
>
|
||||
{change}
|
||||
<Text weight="bold">
|
||||
<AnimatedDiv title={props?.y as any}>
|
||||
{formatCount ? props.y?.to(formatCount) : props.y?.to(formatLongNumber)}
|
||||
</AnimatedDiv>
|
||||
<AnimatedDiv title={String(value)}>{yText}</AnimatedDiv>
|
||||
</Text>
|
||||
</Row>
|
||||
{showPercentage && (
|
||||
@@ -151,7 +158,7 @@ const AnimatedRow = ({
|
||||
color="muted"
|
||||
paddingLeft="3"
|
||||
>
|
||||
<AnimatedDiv>{props.width.to(n => `${n?.toFixed?.(0)}%`)}</AnimatedDiv>
|
||||
<AnimatedDiv>{widthText}</AnimatedDiv>
|
||||
</Row>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSpring } from '@react-spring/web';
|
||||
import { Button, Column, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useSpring, useTransform } from 'motion/react';
|
||||
import { type ReactNode, useEffect } from 'react';
|
||||
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
||||
import { Info } from '@/components/icons';
|
||||
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
||||
@@ -30,8 +30,20 @@ export const MetricCard = ({
|
||||
}: MetricCardProps) => {
|
||||
const diff = value - change;
|
||||
const pct = diff !== 0 ? ((value - diff) / diff) * 100 : value !== 0 ? 100 : 0;
|
||||
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
|
||||
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
|
||||
const x = Number(value) || 0;
|
||||
const p = Number(pct) || 0;
|
||||
const xSpring = useSpring(0, { stiffness: 170, damping: 26 });
|
||||
const pctSpring = useSpring(0, { stiffness: 170, damping: 26 });
|
||||
const valueText = useTransform(xSpring, n => formatValue(n));
|
||||
const pctText = useTransform(pctSpring, n => `${Math.abs(~~n)}%`);
|
||||
|
||||
useEffect(() => {
|
||||
xSpring.set(x);
|
||||
}, [x, xSpring]);
|
||||
|
||||
useEffect(() => {
|
||||
pctSpring.set(p);
|
||||
}, [p, pctSpring]);
|
||||
|
||||
return (
|
||||
<Column
|
||||
@@ -61,11 +73,11 @@ export const MetricCard = ({
|
||||
</Row>
|
||||
)}
|
||||
<Text size="4xl" weight="bold" wrap="nowrap">
|
||||
<AnimatedDiv title={value?.toString()}>{props?.x?.to(x => formatValue(x))}</AnimatedDiv>
|
||||
<AnimatedDiv title={value?.toString()}>{valueText}</AnimatedDiv>
|
||||
</Text>
|
||||
{showChange && (
|
||||
<ChangeLabel value={change} title={formatValue(change)} reverseColors={reverseColors}>
|
||||
<AnimatedDiv>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</AnimatedDiv>
|
||||
<AnimatedDiv>{pctText}</AnimatedDiv>
|
||||
</ChangeLabel>
|
||||
)}
|
||||
</Column>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { config, useSpring } from '@react-spring/web';
|
||||
import { Column, Text } from '@umami/react-zen';
|
||||
import { useRef } from 'react';
|
||||
import { useSpring, useTransform } from 'motion/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
|
||||
import { Badge } from '@/components/common/Badge';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
@@ -45,10 +45,14 @@ export const PerformanceCard = ({
|
||||
const metricChanged = prevMetricRef.current !== metric;
|
||||
if (metricChanged) prevMetricRef.current = metric;
|
||||
|
||||
const spring = useSpring({
|
||||
value: !Number.isNaN(value) ? value : 0,
|
||||
config: metricChanged ? { duration: 0 } : config.default,
|
||||
});
|
||||
const target = !Number.isNaN(value) ? value : 0;
|
||||
const spring = useSpring(target, { stiffness: 170, damping: 26 });
|
||||
const display = useTransform(spring, n => formatValue(n));
|
||||
|
||||
useEffect(() => {
|
||||
if (metricChanged) spring.jump(target);
|
||||
else spring.set(target);
|
||||
}, [target, metricChanged, spring]);
|
||||
|
||||
return (
|
||||
<Column
|
||||
@@ -67,7 +71,7 @@ export const PerformanceCard = ({
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="4xl" weight="bold" wrap="nowrap">
|
||||
<AnimatedDiv>{spring.value.to(n => formatValue(n))}</AnimatedDiv>
|
||||
<AnimatedDiv>{display}</AnimatedDiv>
|
||||
</Text>
|
||||
<Badge variant={RATING_VARIANTS[rating]}>
|
||||
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@ import {
|
||||
subWeeks,
|
||||
subYears,
|
||||
} from 'date-fns';
|
||||
import { utcToZonedTime } from 'date-fns-tz';
|
||||
import { toZonedTime } from 'date-fns-tz';
|
||||
import { getDateLocale } from '@/lib/lang';
|
||||
import type { DateRange } from '@/lib/types';
|
||||
|
||||
@@ -164,7 +164,7 @@ export function parseDateRange(
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
const now = timezone ? utcToZonedTime(date, timezone) : date;
|
||||
const now = timezone ? toZonedTime(date, timezone) : date;
|
||||
const dateLocale = getDateLocale(locale);
|
||||
const { num = 1, unit } = parseDateValue(value);
|
||||
|
||||
|
||||
+4
-3
@@ -1,10 +1,11 @@
|
||||
import prand from 'pure-rand';
|
||||
import { uniformInt } from 'pure-rand/distribution/uniformInt';
|
||||
import { xoroshiro128plus } from 'pure-rand/generator/xoroshiro128plus';
|
||||
|
||||
const seed = Date.now() ^ (Math.random() * 0x100000000);
|
||||
const rng = prand.xoroshiro128plus(seed);
|
||||
const rng = xoroshiro128plus(seed);
|
||||
|
||||
export function random(min: number, max: number) {
|
||||
return prand.unsafeUniformIntDistribution(min, max, rng);
|
||||
return uniformInt(rng, min, max);
|
||||
}
|
||||
|
||||
export function getRandomChars(
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import clickhouse from '@/lib/clickhouse';
|
||||
import { FILTER_COLUMNS, SESSION_COLUMNS } from '@/lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { QueryFilters } from '@/lib/types';
|
||||
|
||||
const SESSION_DB_COLUMNS = new Set(
|
||||
SESSION_COLUMNS.map(col => FILTER_COLUMNS[col as keyof typeof FILTER_COLUMNS]).filter(Boolean),
|
||||
);
|
||||
|
||||
const FUNCTION_NAME = 'getValues';
|
||||
|
||||
export async function getValues(
|
||||
@@ -45,13 +50,27 @@ async function relationalQuery(websiteId: string, column: string, filters: Query
|
||||
}
|
||||
}
|
||||
|
||||
if (SESSION_DB_COLUMNS.has(column)) {
|
||||
return rawQuery(
|
||||
`
|
||||
select ${column} as "value", count(*) as "count"
|
||||
from session
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
${searchQuery}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
limit 10
|
||||
`,
|
||||
{ websiteId, startDate, endDate, search: `%${search}%`, ...params },
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select ${column} as "value", count(*) as "count"
|
||||
from website_event
|
||||
inner join session
|
||||
on session.session_id = website_event.session_id
|
||||
and session.website_id = website_event.website_id
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
${searchQuery}
|
||||
@@ -60,13 +79,7 @@ async function relationalQuery(websiteId: string, column: string, filters: Query
|
||||
order by 2 desc
|
||||
limit 10
|
||||
`,
|
||||
{
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
search: `%${search}%`,
|
||||
...params,
|
||||
},
|
||||
{ websiteId, startDate, endDate, search: `%${search}%`, ...params },
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function getSessionReplays(
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters, sessionId?: string) {
|
||||
const { pagedRawQuery, parseFilters } = prisma;
|
||||
const { search } = filters;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({
|
||||
const { filterQuery, cohortQuery, queryParams, joinSessionQuery } = parseFilters({
|
||||
...filters,
|
||||
websiteId,
|
||||
search: search ? `%${search}%` : undefined,
|
||||
@@ -27,6 +27,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, session
|
||||
filterQuery || cohortQuery
|
||||
? `join (select distinct website_event.website_id, website_event.session_id, website_event.visit_id
|
||||
from website_event
|
||||
${joinSessionQuery}
|
||||
${cohortQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
|
||||
@@ -292,8 +292,8 @@ async function clickhouseQuery(
|
||||
select distinct y.session_id as session_id,
|
||||
y.url_path as url_path,
|
||||
y.referrer_path as referrer_path,
|
||||
y.event_name,
|
||||
y.event_id,
|
||||
y.event_name as event_name,
|
||||
y.event_id as event_id,
|
||||
y.created_at as created_at
|
||||
from level${i} x
|
||||
join level0 y
|
||||
|
||||
@@ -47,7 +47,7 @@ async function relationalQuery(
|
||||
const chart = await rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} t,
|
||||
percentile_cont(0.5) within group (order by ${metric}) as p50,
|
||||
percentile_cont(0.75) within group (order by ${metric}) as p75,
|
||||
percentile_cont(0.95) within group (order by ${metric}) as p95
|
||||
|
||||
Reference in new issue
Block a user