umami/src/app/api/websites/[websiteId]/metrics/route.ts

178 lines
4.7 KiB
TypeScript

import { z } from 'zod';
import thenby from 'thenby';
import { canViewWebsite } from '@/lib/auth';
import {
SESSION_COLUMNS,
EVENT_COLUMNS,
FILTER_COLUMNS,
OPERATORS,
SEARCH_DOMAINS,
SOCIAL_DOMAINS,
EMAIL_DOMAINS,
SHOPPING_DOMAINS,
VIDEO_DOMAINS,
PAID_AD_PARAMS,
} from '@/lib/constants';
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
import { json, unauthorized, badRequest } from '@/lib/response';
import {
getPageviewMetrics,
getSessionMetrics,
getEventMetrics,
getChannelMetrics,
} from '@/queries';
import { filterParams } from '@/lib/schema';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string }> },
) {
const schema = z.object({
type: z.string(),
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
limit: z.coerce.number().optional(),
offset: z.coerce.number().optional(),
search: z.string().optional(),
...filterParams,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId } = await params;
const { type, limit, offset, search } = query;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const { startDate, endDate } = await getRequestDateRange(query);
const column = FILTER_COLUMNS[type] || type;
const filters = {
...(await getRequestFilters(query)),
startDate,
endDate,
};
if (search) {
filters[type] = {
name: type,
column,
operator: OPERATORS.contains,
value: search,
};
}
if (SESSION_COLUMNS.includes(type)) {
const data = await getSessionMetrics(websiteId, type, filters, limit, offset);
if (type === 'language') {
const combined = {};
for (const { x, y } of data) {
const key = String(x).toLowerCase().split('-')[0];
if (combined[key] === undefined) {
combined[key] = { x: key, y };
} else {
combined[key].y += y;
}
}
return json(Object.values(combined));
}
return json(data);
}
if (EVENT_COLUMNS.includes(type)) {
let data;
if (type === 'event') {
data = await getEventMetrics(websiteId, type, filters, limit, offset);
} else {
data = await getPageviewMetrics(websiteId, type, filters, limit, offset);
}
return json(data);
}
if (type === 'channel') {
const data = await getChannelMetrics(websiteId, filters);
const channels = getChannels(data);
return json(
Object.keys(channels)
.map(key => ({ x: key, y: channels[key] }))
.sort(thenby.firstBy('y', -1)),
);
}
return badRequest();
}
function getChannels(data: { domain: string; query: string; visitors: number }[]) {
const channels = {
direct: 0,
referral: 0,
affiliate: 0,
email: 0,
sms: 0,
organicSearch: 0,
organicSocial: 0,
organicShopping: 0,
organicVideo: 0,
paidAds: 0,
paidSearch: 0,
paidSocial: 0,
paidShopping: 0,
paidVideo: 0,
};
const match = (value: string) => {
return (str: string | RegExp) => {
return typeof str === 'string' ? value?.includes(str) : (str as RegExp).test(value);
};
};
for (const { domain, query, visitors } of data) {
if (!domain && !query) {
channels.direct += Number(visitors);
}
const prefix = /utm_medium=(.*cp.*|ppc|retargeting|paid.*)/.test(query) ? 'paid' : 'organic';
if (PAID_AD_PARAMS.some(match(query))) {
channels.paidAds += Number(visitors);
} else if (/utm_medium=(referral|app|link)/.test(query)) {
channels.referral += Number(visitors);
} else if (/utm_medium=affiliate/.test(query)) {
channels.affiliate += Number(visitors);
} else if (/utm_(source|medium)=sms/.test(query)) {
channels.sms += Number(visitors);
} else if (SEARCH_DOMAINS.some(match(domain)) || /utm_medium=organic/.test(query)) {
channels[`${prefix}Search`] += Number(visitors);
} else if (
SOCIAL_DOMAINS.some(match(domain)) ||
/utm_medium=(social|social-network|social-media|sm|social network|social media)/.test(query)
) {
channels[`${prefix}Social`] += Number(visitors);
} else if (EMAIL_DOMAINS.some(match(domain)) || /utm_medium=(.*e[-_ ]?mail.*)/.test(query)) {
channels.email += Number(visitors);
} else if (
SHOPPING_DOMAINS.some(match(domain)) ||
/utm_campaign=(.*(([^a-df-z]|^)shop|shopping).*)/.test(query)
) {
channels[`${prefix}Shopping`] += Number(visitors);
} else if (VIDEO_DOMAINS.some(match(domain)) || /utm_medium=(.*video.*)/.test(query)) {
channels[`${prefix}Video`] += Number(visitors);
}
}
return channels;
}