Merge dev.

This commit is contained in:
Mike Cao 2025-04-28 20:09:58 -07:00
commit be1b2fc272
88 changed files with 4120 additions and 21010 deletions

View file

@ -1,4 +1,3 @@
/* eslint-disable no-unused-vars */
export const CURRENT_VERSION = process.env.currentVersion;
export const AUTH_TOKEN = 'umami.auth';
export const LOCALE_CONFIG = 'umami.locale';
@ -12,6 +11,7 @@ export const HOMEPAGE_URL = 'https://umami.is';
export const REPO_URL = 'https://github.com/umami-software/umami';
export const UPDATES_URL = 'https://api.umami.is/v1/updates';
export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png';
export const FAVICON_URL = 'https://icons.duckduckgo.com/ip3/{{domain}}.ico';
export const DEFAULT_LOCALE = process.env.defaultLocale || 'en-US';
export const DEFAULT_THEME = 'light';
@ -33,7 +33,17 @@ export const FILTER_REFERRERS = 'filter-referrers';
export const FILTER_PAGES = 'filter-pages';
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
export const EVENT_COLUMNS = ['url', 'entry', 'exit', 'referrer', 'title', 'query', 'event', 'tag'];
export const EVENT_COLUMNS = [
'url',
'entry',
'exit',
'referrer',
'title',
'query',
'event',
'tag',
'region',
];
export const SESSION_COLUMNS = [
'browser',
@ -42,7 +52,6 @@ export const SESSION_COLUMNS = [
'screen',
'language',
'country',
'region',
'city',
'host',
];
@ -59,7 +68,7 @@ export const FILTER_COLUMNS = {
browser: 'browser',
device: 'device',
country: 'country',
region: 'subdivision1',
region: 'region',
city: 'city',
language: 'language',
event: 'event_name',
@ -117,6 +126,7 @@ export const REPORT_TYPES = {
utm: 'utm',
journey: 'journey',
revenue: 'revenue',
attribution: 'attribution',
} as const;
export const REPORT_PARAMETERS = {

View file

@ -96,12 +96,12 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI
// Cloudflare headers
if (headers.get('cf-ipcountry')) {
const country = decodeHeader(headers.get('cf-ipcountry'));
const subdivision1 = decodeHeader(headers.get('cf-region-code'));
const region = decodeHeader(headers.get('cf-region-code'));
const city = decodeHeader(headers.get('cf-ipcity'));
return {
country,
subdivision1: getRegionCode(country, subdivision1),
region: getRegionCode(country, region),
city,
};
}
@ -109,12 +109,12 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI
// Vercel headers
if (headers.get('x-vercel-ip-country')) {
const country = decodeHeader(headers.get('x-vercel-ip-country'));
const subdivision1 = decodeHeader(headers.get('x-vercel-ip-country-region'));
const region = decodeHeader(headers.get('x-vercel-ip-country-region'));
const city = decodeHeader(headers.get('x-vercel-ip-city'));
return {
country,
subdivision1: getRegionCode(country, subdivision1),
region: getRegionCode(country, region),
city,
};
}
@ -131,14 +131,12 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI
if (result) {
const country = result.country?.iso_code ?? result?.registered_country?.iso_code;
const subdivision1 = result.subdivisions?.[0]?.iso_code;
const subdivision2 = result.subdivisions?.[1]?.names?.en;
const region = result.subdivisions?.[0]?.iso_code;
const city = result.city?.names?.en;
return {
country,
subdivision1: getRegionCode(country, subdivision1),
subdivision2,
region: getRegionCode(country, region),
city,
};
}
@ -149,14 +147,13 @@ export async function getClientInfo(request: Request, payload: Record<string, an
const ip = payload?.ip || getIpAddress(request.headers);
const location = await getLocation(ip, request.headers, !!payload?.ip);
const country = location?.country;
const subdivision1 = location?.subdivision1;
const subdivision2 = location?.subdivision2;
const region = location?.region;
const city = location?.city;
const browser = browserName(userAgent);
const os = detectOS(userAgent) as string;
const device = getDevice(payload?.screen, os);
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
return { userAgent, browser, os, ip, country, region, city, device };
}
export function hasBlockedIp(clientIp: string) {

View file

@ -1,5 +1,6 @@
import debug from 'debug';
import prisma from '@umami/prisma-client';
import { PrismaClient } from '@prisma/client';
import { readReplicas } from '@prisma/extension-read-replicas';
import { formatInTimeZone } from 'date-fns-tz';
import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
@ -10,6 +11,16 @@ import { filtersToArray } from './params';
const log = debug('umami:prisma');
const PRISMA = 'prisma';
const PRISMA_LOG_OPTIONS = {
log: [
{
emit: 'event',
level: 'query',
},
],
};
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%dT%H:%i:00',
hour: '%Y-%m-%d %H:00:00',
@ -151,7 +162,7 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
if (name === 'referrer') {
arr.push(
`and (website_event.referrer_domain != session.hostname or website_event.referrer_domain is null)`,
`and (website_event.referrer_domain != website_event.hostname or website_event.referrer_domain is null)`,
);
}
}
@ -234,14 +245,16 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return db === MYSQL ? '?' : `$${params.length}${type ?? ''}`;
});
return prisma.rawQuery(query, params);
return process.env.DATABASE_REPLICA_URL
? client.$replica().$queryRawUnsafe(query, ...params)
: client.$queryRawUnsafe(query, ...params);
}
async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {};
const size = +pageSize || DEFAULT_PAGE_SIZE;
const data = await prisma.client[model].findMany({
const data = await client[model].findMany({
...criteria,
...{
...(size > 0 && { take: +size, skip: +size * (+page - 1) }),
@ -255,7 +268,7 @@ async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams)
},
});
const count = await prisma.client[model].count({ where: (criteria as any).where });
const count = await client[model].count({ where: (criteria as any).where });
return { data, count, page: +page, pageSize: size, orderBy };
}
@ -323,8 +336,55 @@ function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
};
}
function transaction(input: any, options?: any) {
return client.$transaction(input, options);
}
function getClient(params?: {
logQuery?: boolean;
queryLogger?: () => void;
replicaUrl?: string;
options?: any;
}): PrismaClient {
const {
logQuery = !!process.env.LOG_QUERY,
queryLogger,
replicaUrl = process.env.DATABASE_REPLICA_URL,
options,
} = params || {};
const prisma = new PrismaClient({
errorFormat: 'pretty',
...(logQuery && PRISMA_LOG_OPTIONS),
...options,
});
if (replicaUrl) {
prisma.$extends(
readReplicas({
url: replicaUrl,
}),
);
}
if (logQuery) {
prisma.$on('query' as never, queryLogger || log);
}
if (process.env.NODE_ENV !== 'production') {
global[PRISMA] = prisma;
}
log('Prisma initialized');
return prisma;
}
const client = global[PRISMA] || getClient();
export default {
...prisma,
client,
transaction,
getAddIntervalQuery,
getCastColumnQuery,
getDayDiffQuery,

View file

@ -4,13 +4,13 @@ const REDIS = 'redis';
const enabled = !!process.env.REDIS_URL;
function getClient() {
const client = new UmamiRedisClient(process.env.REDIS_URL);
const redis = new UmamiRedisClient(process.env.REDIS_URL);
if (process.env.NODE_ENV !== 'production') {
global[REDIS] = client;
global[REDIS] = redis;
}
return client;
return redis;
}
const client = global[REDIS] || getClient();

View file

@ -62,6 +62,7 @@ export const reportTypeParam = z.enum([
'goals',
'journey',
'revenue',
'attribution',
]);
export const reportParms = {

View file

@ -196,8 +196,7 @@ export interface SessionData {
screen: string;
language: string;
country: string;
subdivision1: string;
subdivision2: string;
region: string;
city: string;
ip?: string;
userAgent?: string;