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),
|
getEventMetrics(websiteId, { type: 'event' }, filters),
|
||||||
getPageviewMetrics(websiteId, { type: 'path' }, filters),
|
getPageviewMetrics(websiteId, { type: 'path' }, filters),
|
||||||
getPageviewMetrics(websiteId, { type: 'referrer' }, filters),
|
getPageviewMetrics(websiteId, { type: 'referrer' }, filters),
|
||||||
getSessionMetrics(websiteId, { type: 'browser' }, filters),
|
getSessionMetrics(websiteId, { type: 'browser' }, filters).then(r => r.data),
|
||||||
getSessionMetrics(websiteId, { type: 'os' }, filters),
|
getSessionMetrics(websiteId, { type: 'os' }, filters).then(r => r.data),
|
||||||
getSessionMetrics(websiteId, { type: 'device' }, filters),
|
getSessionMetrics(websiteId, { type: 'device' }, filters).then(r => r.data),
|
||||||
getSessionMetrics(websiteId, { type: 'country' }, filters),
|
getSessionMetrics(websiteId, { type: 'country' }, filters).then(r => r.data),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@ import { useApi } from '../useApi';
|
||||||
import { useDateParameters } from '../useDateParameters';
|
import { useDateParameters } from '../useDateParameters';
|
||||||
import { useFilterParameters } from '../useFilterParameters';
|
import { useFilterParameters } from '../useFilterParameters';
|
||||||
|
|
||||||
export type WebsiteMetricsData = {
|
export type WebsiteMetricsData =
|
||||||
x: string;
|
| { x: string; y: number }[]
|
||||||
y: number;
|
| { data: { x: string; y: number }[]; total: number };
|
||||||
}[];
|
|
||||||
|
|
||||||
export function useWebsiteMetricsQuery(
|
export function useWebsiteMetricsQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
|
|
|
||||||
|
|
@ -40,21 +40,25 @@ export function MetricsTable({
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (data) {
|
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 (dataFilter) {
|
||||||
if (Array.isArray(dataFilter)) {
|
if (Array.isArray(dataFilter)) {
|
||||||
items = dataFilter.reduce((arr, filter) => {
|
filtered = dataFilter.reduce((arr, filter) => {
|
||||||
return filter(arr);
|
return filter(arr);
|
||||||
}, items);
|
}, filtered);
|
||||||
} else {
|
} 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 [];
|
return [];
|
||||||
}, [data, dataFilter, limit, type]);
|
}, [data, dataFilter, limit, type]);
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,16 @@ export function WorldMap({ websiteId, data, ...props }: WorldMapProps) {
|
||||||
type: 'country',
|
type: 'country',
|
||||||
});
|
});
|
||||||
|
|
||||||
const metrics = useMemo(
|
const metrics = useMemo(() => {
|
||||||
() => (data || mapData ? percentFilter((data || mapData) as any[]) : []),
|
const source = data || mapData;
|
||||||
[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) => {
|
const getFillColor = (code: string) => {
|
||||||
if (code === 'AQ') return;
|
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 [];
|
if (!Array.isArray(data)) return [];
|
||||||
const total = data.reduce((n, { y }) => n + y, 0);
|
const sum = total ?? data.reduce((n, { y }) => n + y, 0);
|
||||||
return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props }));
|
return data.map(({ x, y, ...props }) => ({ x, y, z: sum ? (y / sum) * 100 : 0, ...props }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const paramFilter = (data: any[]) => {
|
export const paramFilter = (data: any[]) => {
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,14 @@ export interface SessionMetricsParameters {
|
||||||
offset?: number | string;
|
offset?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionMetricsResult {
|
||||||
|
data: { x: string; y: number; country?: string }[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSessionMetrics(
|
export async function getSessionMetrics(
|
||||||
...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters]
|
...args: [websiteId: string, parameters: SessionMetricsParameters, filters: QueryFilters]
|
||||||
) {
|
): Promise<SessionMetricsResult> {
|
||||||
return runQuery({
|
return runQuery({
|
||||||
[PRISMA]: () => relationalQuery(...args),
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
|
@ -25,7 +30,7 @@ async function relationalQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
parameters: SessionMetricsParameters,
|
parameters: SessionMetricsParameters,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
) {
|
): Promise<SessionMetricsResult> {
|
||||||
const { type, limit = 500, offset = 0 } = parameters;
|
const { type, limit = 500, offset = 0 } = parameters;
|
||||||
let column = FILTER_COLUMNS[type] || type;
|
let column = FILTER_COLUMNS[type] || type;
|
||||||
const { parseFilters, rawQuery } = prisma;
|
const { parseFilters, rawQuery } = prisma;
|
||||||
|
|
@ -45,7 +50,8 @@ async function relationalQuery(
|
||||||
column = `lower(left(${type}, 2))`;
|
column = `lower(left(${type}, 2))`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rawQuery(
|
// Get the data with limit
|
||||||
|
const data = await rawQuery(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${column} x,
|
${column} x,
|
||||||
|
|
@ -67,14 +73,35 @@ async function relationalQuery(
|
||||||
`,
|
`,
|
||||||
{ ...queryParams, ...parameters },
|
{ ...queryParams, ...parameters },
|
||||||
FUNCTION_NAME,
|
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(
|
async function clickhouseQuery(
|
||||||
websiteId: string,
|
websiteId: string,
|
||||||
parameters: SessionMetricsParameters,
|
parameters: SessionMetricsParameters,
|
||||||
filters: QueryFilters,
|
filters: QueryFilters,
|
||||||
): Promise<{ x: string; y: number }[]> {
|
): Promise<SessionMetricsResult> {
|
||||||
const { type, limit = 500, offset = 0 } = parameters;
|
const { type, limit = 500, offset = 0 } = parameters;
|
||||||
let column = FILTER_COLUMNS[type] || type;
|
let column = FILTER_COLUMNS[type] || type;
|
||||||
const { parseFilters, rawQuery } = clickhouse;
|
const { parseFilters, rawQuery } = clickhouse;
|
||||||
|
|
@ -88,10 +115,11 @@ async function clickhouseQuery(
|
||||||
column = `lower(left(${type}, 2))`;
|
column = `lower(left(${type}, 2))`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sql = '';
|
let dataSql = '';
|
||||||
|
let totalSql = '';
|
||||||
|
|
||||||
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
|
if (EVENT_COLUMNS.some(item => Object.keys(filters).includes(item))) {
|
||||||
sql = `
|
dataSql = `
|
||||||
select
|
select
|
||||||
${column} x,
|
${column} x,
|
||||||
count(distinct session_id) y
|
count(distinct session_id) y
|
||||||
|
|
@ -109,8 +137,18 @@ async function clickhouseQuery(
|
||||||
limit ${limit}
|
limit ${limit}
|
||||||
offset ${offset}
|
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 {
|
} else {
|
||||||
sql = `
|
dataSql = `
|
||||||
select
|
select
|
||||||
${column} x,
|
${column} x,
|
||||||
uniq(session_id) y
|
uniq(session_id) y
|
||||||
|
|
@ -128,7 +166,25 @@ async function clickhouseQuery(
|
||||||
limit ${limit}
|
limit ${limit}
|
||||||
offset ${offset}
|
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