Merge branch 'dev' into jajaja

# Conflicts:
#	package.json
#	pnpm-lock.yaml
This commit is contained in:
Mike Cao 2025-05-09 21:56:07 -07:00
commit 94b4b66a3d
122 changed files with 41347 additions and 2747 deletions

View file

@ -187,26 +187,19 @@ export async function POST(request: Request) {
websiteId,
sessionId,
visitId,
createdAt,
// Page
pageTitle: safeDecodeURIComponent(title),
hostname: hostname || urlDomain,
urlPath: safeDecodeURI(urlPath),
urlQuery,
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
referrerPath: safeDecodeURI(referrerPath),
referrerQuery,
referrerDomain,
pageTitle: safeDecodeURIComponent(title),
gclid,
fbclid,
msclkid,
ttclid,
lifatid,
twclid,
eventName: name,
eventData: data,
hostname: hostname || urlDomain,
// Session
distinctId: id,
browser,
os,
device,
@ -215,24 +208,39 @@ export async function POST(request: Request) {
country,
region,
city,
// Events
eventName: name,
eventData: data,
tag,
distinctId: id,
createdAt,
// UTM
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
// Click IDs
gclid,
fbclid,
msclkid,
ttclid,
lifatid,
twclid,
});
}
if (type === COLLECTION_TYPE.identify) {
if (!data) {
return badRequest('Data required.');
if (data) {
await saveSessionData({
websiteId,
sessionId,
sessionData: data,
distinctId: id,
createdAt,
});
}
await saveSessionData({
websiteId,
sessionId,
sessionData: data,
distinctId: id,
createdAt,
});
}
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());

View file

@ -11,7 +11,7 @@ export function renderDateLabels(unit: string, locale: string) {
switch (unit) {
case 'minute':
return formatDate(d, 'p', locale).split(' ')[0];
return formatDate(d, 'h:mm', locale);
case 'hour':
return formatDate(d, 'p', locale);
case 'day':

View file

@ -6,30 +6,23 @@ import prisma from '@/lib/prisma';
import { uuid } from '@/lib/crypto';
import { saveEventData } from './saveEventData';
export async function saveEvent(args: {
export interface SaveEventArgs {
websiteId: string;
sessionId: string;
visitId: string;
createdAt?: Date;
// Page
pageTitle?: string;
hostname?: string;
urlPath: string;
urlQuery?: string;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
utmContent?: string;
utmTerm?: string;
referrerPath?: string;
referrerQuery?: string;
referrerDomain?: string;
pageTitle?: string;
gclid?: string;
fbclid?: string;
msclkid?: string;
ttclid?: string;
lifatid?: string;
twclid?: string;
eventName?: string;
eventData?: any;
hostname?: string;
// Session
distinctId?: string;
browser?: string;
os?: string;
device?: string;
@ -38,73 +31,65 @@ export async function saveEvent(args: {
country?: string;
region?: string;
city?: string;
tag?: string;
distinctId?: string;
createdAt?: Date;
}) {
return runQuery({
[PRISMA]: () => relationalQuery(args),
[CLICKHOUSE]: () => clickhouseQuery(args),
});
}
async function relationalQuery(data: {
websiteId: string;
sessionId: string;
visitId: string;
urlPath: string;
urlQuery?: string;
// Events
eventName?: string;
eventData?: any;
tag?: string;
// UTM
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
utmContent?: string;
utmTerm?: string;
referrerPath?: string;
referrerQuery?: string;
referrerDomain?: string;
// Click IDs
gclid?: string;
fbclid?: string;
msclkid?: string;
ttclid?: string;
lifatid?: string;
twclid?: string;
pageTitle?: string;
eventName?: string;
eventData?: any;
tag?: string;
hostname?: string;
createdAt?: Date;
}) {
const {
websiteId,
sessionId,
visitId,
urlPath,
urlQuery,
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
referrerPath,
referrerQuery,
referrerDomain,
eventName,
eventData,
pageTitle,
gclid,
fbclid,
msclkid,
ttclid,
lifatid,
twclid,
tag,
hostname,
createdAt,
} = data;
}
export async function saveEvent(args: SaveEventArgs) {
return runQuery({
[PRISMA]: () => relationalQuery(args),
[CLICKHOUSE]: () => clickhouseQuery(args),
});
}
async function relationalQuery({
websiteId,
sessionId,
visitId,
createdAt,
pageTitle,
tag,
hostname,
urlPath,
urlQuery,
referrerPath,
referrerQuery,
referrerDomain,
eventName,
eventData,
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
gclid,
fbclid,
msclkid,
ttclid,
lifatid,
twclid,
}: SaveEventArgs) {
const websiteEventId = uuid();
const websiteEvent = prisma.client.websiteEvent.create({
await prisma.client.websiteEvent.create({
data: {
id: websiteEventId,
websiteId,
@ -146,83 +131,49 @@ async function relationalQuery(data: {
createdAt,
});
}
return websiteEvent;
}
async function clickhouseQuery(data: {
websiteId: string;
sessionId: string;
visitId: string;
urlPath: string;
urlQuery?: string;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
utmContent?: string;
utmTerm?: string;
referrerPath?: string;
referrerQuery?: string;
referrerDomain?: string;
pageTitle?: string;
gclid?: string;
fbclid?: string;
msclkid?: string;
ttclid?: string;
lifatid?: string;
twclid?: string;
eventName?: string;
eventData?: any;
hostname?: string;
browser?: string;
os?: string;
device?: string;
screen?: string;
language?: string;
country?: string;
region?: string;
city?: string;
tag?: string;
distinctId?: string;
createdAt?: Date;
}) {
const {
websiteId,
sessionId,
visitId,
urlPath,
urlQuery,
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
referrerPath,
referrerQuery,
referrerDomain,
gclid,
fbclid,
msclkid,
ttclid,
lifatid,
twclid,
pageTitle,
eventName,
eventData,
country,
region,
city,
tag,
distinctId,
createdAt,
...args
} = data;
async function clickhouseQuery({
websiteId,
sessionId,
visitId,
distinctId,
createdAt,
pageTitle,
browser,
os,
device,
screen,
language,
country,
region,
city,
tag,
hostname,
urlPath,
urlQuery,
referrerPath,
referrerQuery,
referrerDomain,
eventName,
eventData,
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
gclid,
fbclid,
msclkid,
ttclid,
lifatid,
twclid,
}: SaveEventArgs) {
const { insert, getUTCString } = clickhouse;
const { sendMessage } = kafka;
const eventId = uuid();
const message = {
...args,
website_id: websiteId,
session_id: sessionId,
visit_id: visitId,
@ -252,6 +203,12 @@ async function clickhouseQuery(data: {
tag: tag,
distinct_id: distinctId,
created_at: getUTCString(createdAt),
browser,
os,
device,
screen,
language,
hostname,
};
if (kafka.enabled) {
@ -271,6 +228,4 @@ async function clickhouseQuery(data: {
createdAt,
});
}
return data;
}

View file

@ -1,4 +1,3 @@
import { Prisma } from '@prisma/client';
import { DATA_TYPE } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
@ -8,7 +7,7 @@ import kafka from '@/lib/kafka';
import prisma from '@/lib/prisma';
import { DynamicData } from '@/lib/types';
export async function saveEventData(data: {
export interface SaveEventDataArgs {
websiteId: string;
eventId: string;
sessionId?: string;
@ -16,19 +15,16 @@ export async function saveEventData(data: {
eventName?: string;
eventData: DynamicData;
createdAt?: Date;
}) {
}
export async function saveEventData(data: SaveEventDataArgs) {
return runQuery({
[PRISMA]: () => relationalQuery(data),
[CLICKHOUSE]: () => clickhouseQuery(data),
});
}
async function relationalQuery(data: {
websiteId: string;
eventId: string;
eventData: DynamicData;
createdAt?: Date;
}): Promise<Prisma.BatchPayload> {
async function relationalQuery(data: SaveEventDataArgs) {
const { websiteId, eventId, eventData, createdAt } = data;
const jsonKeys = flattenJSON(eventData);
@ -46,20 +42,12 @@ async function relationalQuery(data: {
createdAt,
}));
return prisma.client.eventData.createMany({
await prisma.client.eventData.createMany({
data: flattenedData,
});
}
async function clickhouseQuery(data: {
websiteId: string;
eventId: string;
sessionId?: string;
urlPath?: string;
eventName?: string;
eventData: DynamicData;
createdAt?: Date;
}) {
async function clickhouseQuery(data: SaveEventDataArgs) {
const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data;
const { insert, getUTCString } = clickhouse;
@ -88,6 +76,4 @@ async function clickhouseQuery(data: {
} else {
await insert('event_data', messages);
}
return data;
}

View file

@ -18,7 +18,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
`
select
referrer_domain as domain,
referrer_query as query,
url_query as query,
count(distinct session_id) as visitors
from website_event
where website_id = {{websiteId::uuid}}
@ -41,7 +41,7 @@ async function clickhouseQuery(
const sql = `
select
referrer_domain as domain,
referrer_query as query,
url_query as query,
uniq(session_id) as visitors
from website_event
where website_id = {websiteId:UUID}

View file

@ -32,7 +32,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
where website_event.website_id = {{websiteId::uuid}}
${filterQuery}
${dateQuery}
order by website_event.created_at asc
order by website_event.created_at desc
limit 100
`,
params,
@ -59,7 +59,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters): Promis
where website_id = {websiteId:UUID}
${filterQuery}
${dateQuery}
order by createdAt asc
order by createdAt desc
limit 100
`,
{ ...filters, ...params },

View file

@ -24,7 +24,7 @@ export async function getRealtimeData(
const uniques = new Set();
const { countries, urls, referrers, events } = activity.reduce(
const { countries, urls, referrers, events } = activity.reverse().reduce(
(
obj: { countries: any; urls: any; referrers: any; events: any },
event: {

View file

@ -84,7 +84,7 @@ async function relationalQuery(
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) value
from event_data ed
join website_event we
on we.event_id = ed.website_event_Id
on we.event_id = ed.website_event_id
and we.website_id = ed.website_id
join (select website_event_id
from event_data
@ -395,7 +395,7 @@ async function clickhouseQuery(
fbclid != '', 'Facebook / Meta',
msclkid != '', 'Microsoft Ads',
ttclid != '', 'TikTok Ads',
li_fat_id != '', ' LinkedIn Ads',
li_fat_id != '', 'LinkedIn Ads',
twclid != '', 'Twitter Ads (X)','') name,
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
from model m

View file

@ -7,28 +7,29 @@ import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import kafka from '@/lib/kafka';
import clickhouse from '@/lib/clickhouse';
export async function saveSessionData(data: {
export interface SaveSessionDataArgs {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
distinctId?: string;
createdAt?: Date;
}) {
}
export async function saveSessionData(data: SaveSessionDataArgs) {
return runQuery({
[PRISMA]: () => relationalQuery(data),
[CLICKHOUSE]: () => clickhouseQuery(data),
});
}
export async function relationalQuery(data: {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
distinctId?: string;
createdAt?: Date;
}) {
export async function relationalQuery({
websiteId,
sessionId,
sessionData,
distinctId,
createdAt,
}: SaveSessionDataArgs) {
const { client } = prisma;
const { websiteId, sessionId, sessionData, distinctId, createdAt } = data;
const jsonKeys = flattenJSON(sessionData);
@ -75,19 +76,15 @@ export async function relationalQuery(data: {
});
}
}
return flattenedData;
}
async function clickhouseQuery(data: {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
distinctId?: string;
createdAt?: Date;
}) {
const { websiteId, sessionId, sessionData, distinctId, createdAt } = data;
async function clickhouseQuery({
websiteId,
sessionId,
sessionData,
distinctId,
createdAt,
}: SaveSessionDataArgs) {
const { insert, getUTCString } = clickhouse;
const { sendMessage } = kafka;
@ -112,6 +109,4 @@ async function clickhouseQuery(data: {
} else {
await insert('session_data', messages);
}
return data;
}

View file

@ -8,19 +8,20 @@
top,
doNotTrack,
} = window;
const { hostname, href, origin } = location;
const { currentScript, referrer } = document;
const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
if (!currentScript) return;
const { hostname, href, origin } = location;
const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
const _data = 'data-';
const _false = 'false';
const _true = 'true';
const attr = currentScript.getAttribute.bind(currentScript);
const website = attr(_data + 'website-id');
const hostUrl = attr(_data + 'host-url');
const tag = attr(_data + 'tag');
const beforeSend = attr(_data + 'before-send');
const tag = attr(_data + 'tag') || undefined;
const autoTrack = attr(_data + 'auto-track') !== _false;
const dnt = attr(_data + 'do-not-track') === _true;
const excludeSearch = attr(_data + 'exclude-search') === _true;
@ -41,11 +42,11 @@
website,
screen,
language,
title,
title: document.title,
hostname,
url: currentUrl,
referrer: currentRef,
tag: tag ? tag : undefined,
tag,
id: identity ? identity : undefined,
});
@ -56,20 +57,14 @@
/* Event handlers */
const handlePush = (state, title, url) => {
const handlePush = (_state, _title, url) => {
if (!url) return;
currentRef = currentUrl;
currentUrl = new URL(url, location.href);
if (excludeSearch) {
currentUrl.search = '';
}
if (excludeHash) {
currentUrl.hash = '';
}
if (excludeSearch) currentUrl.search = '';
if (excludeHash) currentUrl.hash = '';
currentUrl = currentUrl.toString();
if (currentUrl !== currentRef) {
@ -80,10 +75,8 @@
const handlePathChanges = () => {
const hook = (_this, method, callback) => {
const orig = _this[method];
return (...args) => {
callback.apply(null, args);
return orig.apply(_this, args);
};
};
@ -92,96 +85,47 @@
history.replaceState = hook(history, 'replaceState', handlePush);
};
const handleTitleChanges = () => {
const observer = new MutationObserver(([entry]) => {
title = entry && entry.target ? entry.target.text : undefined;
});
const node = document.querySelector('head > title');
if (node) {
observer.observe(node, {
subtree: true,
characterData: true,
childList: true,
});
}
};
const handleClicks = () => {
document.addEventListener(
'click',
async e => {
const isSpecialTag = tagName => ['BUTTON', 'A'].includes(tagName);
const trackElement = async el => {
const eventName = el.getAttribute(eventNameAttribute);
if (eventName) {
const eventData = {};
const trackElement = async el => {
const attr = el.getAttribute.bind(el);
const eventName = attr(eventNameAttribute);
el.getAttributeNames().forEach(name => {
const match = name.match(eventRegex);
if (match) eventData[match[1]] = el.getAttribute(name);
});
if (eventName) {
const eventData = {};
return track(eventName, eventData);
}
};
const onClick = async e => {
const el = e.target;
const parentElement = el.closest('a,button');
if (!parentElement) return trackElement(el);
el.getAttributeNames().forEach(name => {
const match = name.match(eventRegex);
const { href, target } = parentElement;
if (!parentElement.getAttribute(eventNameAttribute)) return;
if (match) {
eventData[match[1]] = attr(name);
}
});
return track(eventName, eventData);
if (parentElement.tagName === 'BUTTON') {
return trackElement(parentElement);
}
if (parentElement.tagName === 'A' && href) {
const external =
target === '_blank' ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
(e.button && e.button === 1);
if (!external) e.preventDefault();
return trackElement(parentElement).then(() => {
if (!external) {
(target === '_top' ? top.location : location).href = href;
}
};
const findParentTag = (rootElem, maxSearchDepth) => {
let currentElement = rootElem;
for (let i = 0; i < maxSearchDepth; i++) {
if (isSpecialTag(currentElement.tagName)) {
return currentElement;
}
currentElement = currentElement.parentElement;
if (!currentElement) {
return null;
}
}
};
const el = e.target;
const parentElement = isSpecialTag(el.tagName) ? el : findParentTag(el, 10);
if (parentElement) {
const { href, target } = parentElement;
const eventName = parentElement.getAttribute(eventNameAttribute);
if (eventName) {
if (parentElement.tagName === 'A') {
const external =
target === '_blank' ||
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
(e.button && e.button === 1);
if (eventName && href) {
if (!external) {
e.preventDefault();
}
return trackElement(parentElement).then(() => {
if (!external) {
(target === '_top' ? top.location : location).href = href;
}
});
}
} else if (parentElement.tagName === 'BUTTON') {
return trackElement(parentElement);
}
}
} else {
return trackElement(el);
}
},
true,
);
});
}
};
document.addEventListener('click', onClick, true);
};
/* Tracking functions */
@ -196,56 +140,49 @@
const send = async (payload, type = 'event') => {
if (trackingDisabled()) return;
const headers = {
'Content-Type': 'application/json',
};
const callback = window[beforeSend];
if (typeof cache !== 'undefined') {
headers['x-umami-cache'] = cache;
if (typeof callback === 'function') {
payload = callback(type, payload);
}
if (!payload) return;
try {
const res = await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({ type, payload }),
headers,
headers: {
'Content-Type': 'application/json',
...(typeof cache !== 'undefined' && { 'x-umami-cache': cache }),
},
credentials: 'omit',
});
const data = await res.json();
if (data) {
disabled = !!data.disabled;
cache = data.cache;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
/* empty */
/* no-op */
}
};
const init = () => {
if (!initialized) {
initialized = true;
track();
handlePathChanges();
handleTitleChanges();
handleClicks();
initialized = true;
}
};
const track = (name, data) => {
if (typeof name === 'string') {
return send({
...getPayload(),
name,
data,
});
} else if (typeof name === 'object') {
return send({ ...name });
} else if (typeof name === 'function') {
return send(name(getPayload()));
}
if (typeof name === 'string') return send({ ...getPayload(), name, data });
if (typeof name === 'object') return send({ ...name });
if (typeof name === 'function') return send(name(getPayload()));
return send(getPayload());
};
@ -275,10 +212,9 @@
let currentUrl = href;
let currentRef = referrer.startsWith(origin) ? '' : referrer;
let title = document.title;
let cache;
let initialized;
let initialized = false;
let disabled = false;
let cache;
let identity;
if (autoTrack && !trackingDisabled()) {