Merge branch 'dev' of https://github.com/umami-software/umami into analytics

This commit is contained in:
Francis Cao committed 2026-04-20 13:01:17 -07:00
commit c7e3fc6820
21 files changed
+3525 -2225

No files matched your search

+44 -44
View File
@@ -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"
}
}
+3349 -2099
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -227,7 +227,7 @@
"TO": "Tonga",
"TT": "Trinidad & Tobago",
"TN": "Tunisia",
"TR": "Turkey",
"TR": "T\u00fcrkiye",
"TM": "Turkmenistan",
"TC": "Turks & Caicos Islands",
"TV": "Tuvalu",
+1 -1
View File
@@ -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} />
+2 -2
View File
@@ -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],
+5 -5
View File
@@ -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 => {
+13 -4
View File
@@ -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>;
}
+29 -22
View File
@@ -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>
+18 -6
View File
@@ -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>
+11 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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(
+23 -10
View File
@@ -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,
);
}
+2 -1
View File
@@ -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}}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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