mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge 1a04856a8b into 0cd63049ed
This commit is contained in:
commit
56d32ddaca
6 changed files with 95 additions and 30 deletions
|
|
@ -34,10 +34,10 @@ export async function GET(
|
|||
getEventMetrics(websiteId, { type: 'event' }, filters),
|
||||
getPageviewMetrics(websiteId, { type: 'path' }, filters),
|
||||
getPageviewMetrics(websiteId, { type: 'referrer' }, filters),
|
||||
getSessionMetrics(websiteId, { type: 'browser' }, filters),
|
||||
getSessionMetrics(websiteId, { type: 'os' }, filters),
|
||||
getSessionMetrics(websiteId, { type: 'device' }, filters),
|
||||
getSessionMetrics(websiteId, { type: 'country' }, filters),
|
||||
getSessionMetrics(websiteId, { type: 'browser' }, filters).then(r => r.data),
|
||||
getSessionMetrics(websiteId, { type: 'os' }, filters).then(r => r.data),
|
||||
getSessionMetrics(websiteId, { type: 'device' }, filters).then(r => r.data),
|
||||
getSessionMetrics(websiteId, { type: 'country' }, filters).then(r => r.data),
|
||||
]);
|
||||
|
||||
const zip = new JSZip();
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ import { useApi } from '../useApi';
|
|||
import { useDateParameters } from '../useDateParameters';
|
||||
import { useFilterParameters } from '../useFilterParameters';
|
||||
|
||||
export type WebsiteMetricsData = {
|
||||
x: string;
|
||||
y: number;
|
||||
}[];
|
||||
export type WebsiteMetricsData =
|
||||
| { x: string; y: number }[]
|
||||
| { data: { x: string; y: number }[]; total: number };
|
||||
|
||||
export function useWebsiteMetricsQuery(
|
||||
websiteId: string,
|
||||
|
|
|
|||
|
|
@ -40,21 +40,25 @@ export function MetricsTable({
|
|||
|
||||
const filteredData = useMemo(() => {
|
||||
if (data) {
|
||||
let items = data as any[];
|
||||
// Handle both old format (array) and new format ({ data, total })
|
||||
const items = Array.isArray(data) ? data : data.data;
|
||||
const total = Array.isArray(data) ? undefined : data.total;
|
||||
|
||||
let filtered = items as any[];
|
||||
|
||||
if (dataFilter) {
|
||||
if (Array.isArray(dataFilter)) {
|
||||
items = dataFilter.reduce((arr, filter) => {
|
||||
filtered = dataFilter.reduce((arr, filter) => {
|
||||
return filter(arr);
|
||||
}, items);
|
||||
}, filtered);
|
||||
} else {
|
||||
items = dataFilter(items);
|
||||
filtered = dataFilter(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
items = percentFilter(items);
|
||||
filtered = percentFilter(filtered, total);
|
||||
|
||||
return items.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props }));
|
||||
return filtered.map(({ x, y, z, ...props }) => ({ label: x, count: y, percent: z, ...props }));
|
||||
}
|
||||
return [];
|
||||
}, [data, dataFilter, limit, type]);
|
||||
|
|
|
|||
|
|
@ -32,10 +32,16 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) {
|
|||
type: 'country',
|
||||
});
|
||||
|
||||
const metrics = useMemo(
|
||||
() => (data || mapData ? percentFilter((data || mapData) as any[]) : []),
|
||||
[data, mapData],
|
||||
);
|
||||
const metrics = useMemo(() => {
|
||||
const source = data || mapData;
|
||||
if (!source) return [];
|
||||
|
||||
// Handle both old format (array) and new format ({ data, total })
|
||||
const items = Array.isArray(source) ? source : source.data;
|
||||
const total = Array.isArray(source) ? undefined : source.total;
|
||||
|
||||
return percentFilter(items, total);
|
||||
}, [data, mapData]);
|
||||
|
||||
const getFillColor = (code: string) => {
|
||||
if (code === 'AQ') return;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export const percentFilter = (data: any[]) => {
|
||||
export const percentFilter = (data: any[], total?: number) => {
|
||||
if (!Array.isArray(data)) return [];
|
||||
const total = data.reduce((n, { y }) => n + y, 0);
|
||||
return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
|
||||
const sum = total ?? data.reduce((n, { y }) => n + y, 0);
|
||||
return data.map(({ x, y, ...props }) => ({ x, y, z: sum ? (y / sum) * 100 : 0, ...props }));
|
||||
};
|
||||
|
||||
export const paramFilter = (data: any[]) => {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,14 @@ export interface SessionMetricsParameters {
|
|||
offset?: number | string;
|
||||
}
|
||||
|
||||
export interface SessionMetricsResult {
|
||||
data: { x: string; y: number; country?: string }[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export async function getSessionMetrics(
|
||||
...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters]
|
||||
) {
|
||||
): Promise<SessionMetricsResult> {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
|
|
@ -25,7 +30,7 @@ async function relationalQuery(
|
|||
websiteId: string,
|
||||
parameters: SessionMetricsParameters,
|
||||
filters: QueryFilters,
|
||||
) {
|
||||
): Promise<SessionMetricsResult> {
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { parseFilters, rawQuery } = prisma;
|
||||
|
|
@ -45,7 +50,8 @@ async function relationalQuery(
|
|||
column = `lower(left(${type}, 2))`;
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
// Get the data with limit
|
||||
const data = await rawQuery(
|
||||
`
|
||||
select
|
||||
${column} x,
|
||||
|
|
@ -67,14 +73,35 @@ async function relationalQuery(
|
|||
`,
|
||||
{ ...queryParams, ...parameters },
|
||||
FUNCTION_NAME,
|
||||
);
|
||||
) as { x: string; y: number; country?: string }[];
|
||||
|
||||
// Get total unique sessions
|
||||
const totalResult = await rawQuery(
|
||||
`
|
||||
select count(distinct website_event.session_id) as total
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
${joinSessionQuery}
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and website_event.event_type != 2
|
||||
${filterQuery}
|
||||
`,
|
||||
queryParams,
|
||||
FUNCTION_NAME,
|
||||
) as { total: number }[];
|
||||
|
||||
return {
|
||||
data,
|
||||
total: Number(totalResult[0]?.total) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
parameters: SessionMetricsParameters,
|
||||
filters: QueryFilters,
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
): Promise<SessionMetricsResult> {
|
||||
const { type, limit = 500, offset = 0 } = parameters;
|
||||
let column = FILTER_COLUMNS[type] || type;
|
||||
const { parseFilters, rawQuery } = clickhouse;
|
||||
|
|
@ -88,10 +115,11 @@ async function clickhouseQuery(
|
|||
column = `lower(left(${type}, 2))`;
|
||||
}
|
||||
|
||||
let sql = '';
|
||||
let dataSql = '';
|
||||
let totalSql = '';
|
||||
|
||||
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
|
||||
sql = `
|
||||
dataSql = `
|
||||
select
|
||||
${column} x,
|
||||
count(distinct session_id) y
|
||||
|
|
@ -109,8 +137,18 @@ async function clickhouseQuery(
|
|||
limit ${limit}
|
||||
offset ${offset}
|
||||
`;
|
||||
|
||||
totalSql = `
|
||||
select count(distinct session_id) as total
|
||||
from website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
${filterQuery}
|
||||
`;
|
||||
} else {
|
||||
sql = `
|
||||
dataSql = `
|
||||
select
|
||||
${column} x,
|
||||
uniq(session_id) y
|
||||
|
|
@ -128,7 +166,25 @@ async function clickhouseQuery(
|
|||
limit ${limit}
|
||||
offset ${offset}
|
||||
`;
|
||||
|
||||
totalSql = `
|
||||
select uniq(session_id) as total
|
||||
from website_event_stats_hourly as website_event
|
||||
${cohortQuery}
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type != 2
|
||||
${filterQuery}
|
||||
`;
|
||||
}
|
||||
|
||||
return rawQuery(sql, { ...queryParams, ...parameters }, FUNCTION_NAME);
|
||||
const [data, totalResult] = await Promise.all([
|
||||
rawQuery(dataSql, { ...queryParams, ...parameters }, FUNCTION_NAME) as Promise<{ x: string; y: number; country?: string }[]>,
|
||||
rawQuery(totalSql, queryParams, FUNCTION_NAME) as Promise<{ total: number }[]>,
|
||||
]);
|
||||
|
||||
return {
|
||||
data,
|
||||
total: Number(totalResult[0]?.total) || 0,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue