mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 09:45:04 +01:00
Updated tracker and collect.
This commit is contained in:
parent
43ef6884df
commit
cdacb640c6
@ -36,7 +36,7 @@
|
||||
["store", "./store"],
|
||||
["styles", "./styles"]
|
||||
],
|
||||
"extensions": [".ts", ".js", ".jsx", ".json"]
|
||||
"extensions": [".ts", ".tsx", ".js", ".jsx", ".json"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5,7 +5,7 @@
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
background: var(--base75);
|
||||
border-bottom: 2px solid var(--base300);
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ export default function BarChart({
|
||||
millisecond: 'T',
|
||||
second: 'pp',
|
||||
minute: 'p',
|
||||
hour: 'h aaa',
|
||||
hour: 'h:mm aaa - PP',
|
||||
day: 'PPPP',
|
||||
week: 'PPPP',
|
||||
month: 'LLLL yyyy',
|
||||
@ -135,15 +135,13 @@ export default function BarChart({
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [animationDuration, renderTooltip, stacked, colors]);
|
||||
}, [animationDuration, renderTooltip, stacked, colors, unit]);
|
||||
|
||||
const createChart = () => {
|
||||
Chart.defaults.font.family = 'Inter';
|
||||
|
||||
const options = getOptions();
|
||||
|
||||
onCreate(options);
|
||||
|
||||
chart.current = new Chart(canvas.current, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
@ -151,6 +149,8 @@ export default function BarChart({
|
||||
},
|
||||
options,
|
||||
});
|
||||
|
||||
onCreate(chart.current);
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
@ -158,9 +158,11 @@ export default function BarChart({
|
||||
|
||||
chart.current.options = getOptions();
|
||||
|
||||
onUpdate(chart.current);
|
||||
chart.current.data.datasets = datasets;
|
||||
|
||||
chart.current.update();
|
||||
|
||||
onUpdate(chart.current);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -17,7 +17,7 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||
query: { url, eventName },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, isLoading } = useQuery(['events', { websiteId, modified, eventName }], () =>
|
||||
const { data, isLoading } = useQuery(['events', websiteId, modified, eventName], () =>
|
||||
get(`/websites/${websiteId}/events`, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
@ -33,12 +33,12 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||
if (!data) return [];
|
||||
if (isLoading) return data;
|
||||
|
||||
const map = data.reduce((obj, { x, y }) => {
|
||||
const map = data.reduce((obj, { x, t, y }) => {
|
||||
if (!obj[x]) {
|
||||
obj[x] = [];
|
||||
}
|
||||
|
||||
obj[x].push({ x, y });
|
||||
obj[x].push({ x: t, y });
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
@ -58,22 +58,12 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||
borderWidth: 1,
|
||||
};
|
||||
});
|
||||
}, [data, isLoading]);
|
||||
|
||||
function handleUpdate(chart) {
|
||||
chart.data.datasets = datasets;
|
||||
|
||||
chart.update();
|
||||
}
|
||||
}, [data, isLoading, startDate, endDate, unit]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading icon="dots" />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
className={className}
|
||||
@ -81,7 +71,6 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||
unit={unit}
|
||||
height={300}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
onUpdate={handleUpdate}
|
||||
loading={isLoading}
|
||||
stacked
|
||||
/>
|
||||
|
@ -38,22 +38,10 @@ export default function PageviewsChart({
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
const handleUpdate = chart => {
|
||||
const {
|
||||
data: { datasets },
|
||||
} = chart;
|
||||
const datasets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
datasets[0].data = data.sessions;
|
||||
datasets[0].label = formatMessage(labels.uniqueVisitors);
|
||||
datasets[1].data = data.pageviews;
|
||||
datasets[1].label = formatMessage(labels.pageViews);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const datasets = [
|
||||
return [
|
||||
{
|
||||
label: formatMessage(labels.uniqueVisitors),
|
||||
data: data.sessions,
|
||||
@ -67,6 +55,7 @@ export default function PageviewsChart({
|
||||
...colors.views,
|
||||
},
|
||||
];
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
@ -78,7 +67,6 @@ export default function PageviewsChart({
|
||||
unit={unit}
|
||||
records={records}
|
||||
animationDuration={visible ? animationDuration : 0}
|
||||
onUpdate={handleUpdate}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Button, Column, Row } from 'react-basics';
|
||||
import Script from 'next/script';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import useApi from 'hooks/useApi';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Button, Column, Row } from 'react-basics';
|
||||
import styles from './TestConsole.module.css';
|
||||
|
||||
export default function TestConsole() {
|
||||
@ -24,10 +24,10 @@ export default function TestConsole() {
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
window.umami('umami-default');
|
||||
window.umami.trackView('/page-view', 'https://www.google.com');
|
||||
window.umami.trackEvent('track-event-no-data');
|
||||
window.umami.trackEvent('track-event-with-data', {
|
||||
window.umami.track({ url: '/page-view', referrer: 'https://www.google.com' });
|
||||
window.umami.track('track-event-no-data');
|
||||
window.umami.track('track-event-with-data', {
|
||||
data: {
|
||||
test: 'test-data',
|
||||
time: new Date(),
|
||||
number: 1,
|
||||
@ -40,6 +40,7 @@ export default function TestConsole() {
|
||||
},
|
||||
},
|
||||
array: [1, 2, 3],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -52,22 +53,17 @@ export default function TestConsole() {
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<Head>
|
||||
{typeof window !== 'undefined' && website && (
|
||||
<script
|
||||
async
|
||||
defer
|
||||
data-website-id={website.id}
|
||||
src={`${basePath}/script.js`}
|
||||
data-cache="true"
|
||||
/>
|
||||
)}
|
||||
</Head>
|
||||
<PageHeader title="Test console">
|
||||
<WebsiteSelect websiteId={website?.id} onSelect={handleChange} />
|
||||
</PageHeader>
|
||||
{website && (
|
||||
<>
|
||||
<Script
|
||||
async
|
||||
data-website-id={website.id}
|
||||
src={`${basePath}/script.js`}
|
||||
data-cache="true"
|
||||
/>
|
||||
<Row className={styles.test}>
|
||||
<Column xs="4">
|
||||
<div className={styles.header}>Page links</div>
|
||||
@ -78,14 +74,14 @@ export default function TestConsole() {
|
||||
<Link href={`/console/${websiteId}?page=2`}>page two</Link>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://www.google.com" className="umami--click--external-link-direct">
|
||||
<a href="https://www.google.com" data-umami-event="external-link-direct">
|
||||
external link (direct)
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://www.google.com"
|
||||
className="umami--click--external-link-tab"
|
||||
data-umami-event="external-link-tab"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@ -94,10 +90,20 @@ export default function TestConsole() {
|
||||
</div>
|
||||
</Column>
|
||||
<Column xs="4">
|
||||
<div className={styles.header}>CSS events</div>
|
||||
<Button id="primary-button" className="umami--click--button-click" variant="action">
|
||||
<div className={styles.header}>Click events</div>
|
||||
<Button id="send-event-button" data-umami-event="button-click" variant="action">
|
||||
Send event
|
||||
</Button>
|
||||
<p />
|
||||
<Button
|
||||
id="send-event-data-button"
|
||||
data-umami-event="button-click"
|
||||
data-umami-event-name="bob"
|
||||
data-umami-event-id="123"
|
||||
variant="action"
|
||||
>
|
||||
Send event with data
|
||||
</Button>
|
||||
</Column>
|
||||
<Column xs="4">
|
||||
<div className={styles.header}>Javascript events</div>
|
||||
@ -108,14 +114,12 @@ export default function TestConsole() {
|
||||
</Row>
|
||||
<Row>
|
||||
<Column>
|
||||
<div className={styles.header}>Statistics</div>
|
||||
<WebsiteChart
|
||||
websiteId={website.id}
|
||||
title={website.name}
|
||||
domain={website.domain}
|
||||
showLink
|
||||
/>
|
||||
<div className={styles.header}>Events</div>
|
||||
<EventsChart websiteId={website.id} />
|
||||
</Column>
|
||||
</Row>
|
||||
|
@ -1,5 +1,5 @@
|
||||
.test {
|
||||
border: 1px solid var(--base200);
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: 5px;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
@ -7,9 +7,5 @@
|
||||
.header {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 90px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
transform: rotate(-90deg);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import isbot from 'isbot';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import { createToken, ok, send, badRequest, forbidden } from 'next-basics';
|
||||
import { savePageView, saveEvent } from 'queries';
|
||||
import { saveEvent } from 'queries';
|
||||
import { useCors, useSession } from 'lib/middleware';
|
||||
import { getJsonBody, getIpAddress } from 'lib/detect';
|
||||
import { secret } from 'lib/crypto';
|
||||
@ -34,11 +34,15 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
|
||||
|
||||
const { type, payload } = getJsonBody(req);
|
||||
|
||||
const { url, referrer, eventName, eventData, pageTitle } = payload;
|
||||
if (type !== 'event') {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const { url, referrer, name: eventName, data: eventData, title: pageTitle } = payload;
|
||||
|
||||
// Validate eventData is JSON
|
||||
if (eventData && !(typeof eventData === 'object' && !Array.isArray(eventData))) {
|
||||
return badRequest(res, 'Event Data must be in the form of a JSON Object.');
|
||||
return badRequest(res, 'Invalid event data.');
|
||||
}
|
||||
|
||||
const ignoreIps = process.env.IGNORE_IP;
|
||||
@ -87,48 +91,34 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
|
||||
|
||||
const session = req.session;
|
||||
|
||||
let urlPath = url.split('?')[0];
|
||||
const urlQuery = url.split('?')[1];
|
||||
let referrerPath;
|
||||
let referrerQuery;
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [urlPath, urlQuery] = url?.split('?') || [];
|
||||
let [referrerPath, referrerQuery] = referrer?.split('?') || [];
|
||||
let referrerDomain;
|
||||
|
||||
try {
|
||||
const newRef = new URL(referrer);
|
||||
referrerPath = newRef.pathname;
|
||||
referrerDomain = newRef.hostname;
|
||||
referrerQuery = newRef.search.substring(1);
|
||||
} catch {
|
||||
referrerPath = referrer?.split('?')[0];
|
||||
referrerQuery = referrer?.split('?')[1];
|
||||
if (referrerPath.startsWith('http')) {
|
||||
const refUrl = new URL(referrer);
|
||||
referrerPath = refUrl.pathname;
|
||||
referrerQuery = refUrl.search.substring(1);
|
||||
referrerDomain = refUrl.hostname;
|
||||
}
|
||||
|
||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||
urlPath = urlPath.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
if (type === 'pageview') {
|
||||
await savePageView({
|
||||
...session,
|
||||
await saveEvent({
|
||||
urlPath,
|
||||
urlQuery,
|
||||
referrerPath,
|
||||
referrerQuery,
|
||||
referrerDomain,
|
||||
pageTitle,
|
||||
});
|
||||
} else if (type === 'event') {
|
||||
await saveEvent({
|
||||
...session,
|
||||
urlPath,
|
||||
urlQuery,
|
||||
pageTitle,
|
||||
eventName,
|
||||
eventData,
|
||||
...session,
|
||||
sessionId: session.id,
|
||||
});
|
||||
} else {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const token = createToken(session, secret());
|
||||
|
||||
|
@ -3,14 +3,16 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import kafka from 'lib/kafka';
|
||||
import prisma from 'lib/prisma';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import cache from 'lib/cache';
|
||||
import { saveEventData } from '../eventData/saveEventData';
|
||||
import { saveEventData } from 'queries/analytics/eventData/saveEventData';
|
||||
|
||||
export async function saveEvent(args: {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
websiteId: string;
|
||||
urlPath: string;
|
||||
urlQuery?: string;
|
||||
referrerPath?: string;
|
||||
referrerQuery?: string;
|
||||
referrerDomain?: string;
|
||||
pageTitle?: string;
|
||||
eventName?: string;
|
||||
eventData?: any;
|
||||
@ -32,7 +34,7 @@ export async function saveEvent(args: {
|
||||
}
|
||||
|
||||
async function relationalQuery(data: {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
websiteId: string;
|
||||
urlPath: string;
|
||||
urlQuery?: string;
|
||||
@ -40,8 +42,7 @@ async function relationalQuery(data: {
|
||||
eventName?: string;
|
||||
eventData?: any;
|
||||
}) {
|
||||
const { websiteId, id: sessionId, urlPath, urlQuery, eventName, eventData, pageTitle } = data;
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const { websiteId, sessionId, urlPath, urlQuery, eventName, eventData, pageTitle } = data;
|
||||
const websiteEventId = uuid();
|
||||
|
||||
const websiteEvent = prisma.client.websiteEvent.create({
|
||||
@ -51,9 +52,9 @@ async function relationalQuery(data: {
|
||||
sessionId,
|
||||
urlPath: urlPath?.substring(0, URL_LENGTH),
|
||||
urlQuery: urlQuery?.substring(0, URL_LENGTH),
|
||||
pageTitle: pageTitle,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||
pageTitle,
|
||||
eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
|
||||
},
|
||||
});
|
||||
|
||||
@ -62,7 +63,6 @@ async function relationalQuery(data: {
|
||||
websiteId,
|
||||
sessionId,
|
||||
eventId: websiteEventId,
|
||||
revId: website?.revId,
|
||||
urlPath: urlPath?.substring(0, URL_LENGTH),
|
||||
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||
eventData,
|
||||
@ -73,7 +73,7 @@ async function relationalQuery(data: {
|
||||
}
|
||||
|
||||
async function clickhouseQuery(data: {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
websiteId: string;
|
||||
urlPath: string;
|
||||
urlQuery?: string;
|
||||
@ -93,7 +93,7 @@ async function clickhouseQuery(data: {
|
||||
}) {
|
||||
const {
|
||||
websiteId,
|
||||
id: sessionId,
|
||||
sessionId,
|
||||
urlPath,
|
||||
urlQuery,
|
||||
pageTitle,
|
||||
@ -103,10 +103,8 @@ async function clickhouseQuery(data: {
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
...args
|
||||
} = data;
|
||||
const { getDateFormat, sendMessage } = kafka;
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const eventId = uuid();
|
||||
const createdAt = getDateFormat(new Date());
|
||||
|
||||
@ -121,11 +119,9 @@ async function clickhouseQuery(data: {
|
||||
url_path: urlPath?.substring(0, URL_LENGTH),
|
||||
url_query: urlQuery?.substring(0, URL_LENGTH),
|
||||
page_title: pageTitle,
|
||||
event_type: EVENT_TYPE.customEvent,
|
||||
event_name: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||
rev_id: website?.revId || 0,
|
||||
event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||
event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
|
||||
created_at: createdAt,
|
||||
...args,
|
||||
};
|
||||
|
||||
await sendMessage(message, 'event');
|
||||
@ -135,7 +131,6 @@ async function clickhouseQuery(data: {
|
||||
websiteId,
|
||||
sessionId,
|
||||
eventId,
|
||||
revId: website?.revId,
|
||||
urlPath: urlPath?.substring(0, URL_LENGTH),
|
||||
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||
eventData,
|
||||
|
@ -1,132 +0,0 @@
|
||||
import { URL_LENGTH, EVENT_TYPE } from 'lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import kafka from 'lib/kafka';
|
||||
import prisma from 'lib/prisma';
|
||||
import cache from 'lib/cache';
|
||||
import { uuid } from 'lib/crypto';
|
||||
|
||||
export async function savePageView(args: {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
urlPath: string;
|
||||
urlQuery?: string;
|
||||
referrerPath?: string;
|
||||
referrerQuery?: string;
|
||||
referrerDomain?: string;
|
||||
pageTitle?: string;
|
||||
hostname?: string;
|
||||
browser?: string;
|
||||
os?: string;
|
||||
device?: string;
|
||||
screen?: string;
|
||||
language?: string;
|
||||
country?: string;
|
||||
subdivision1?: string;
|
||||
subdivision2?: string;
|
||||
city?: string;
|
||||
}) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(data: {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
urlPath: string;
|
||||
urlQuery?: string;
|
||||
referrerPath?: string;
|
||||
referrerQuery?: string;
|
||||
referrerDomain?: string;
|
||||
pageTitle?: string;
|
||||
}) {
|
||||
const {
|
||||
websiteId,
|
||||
id: sessionId,
|
||||
urlPath,
|
||||
urlQuery,
|
||||
referrerPath,
|
||||
referrerQuery,
|
||||
referrerDomain,
|
||||
pageTitle,
|
||||
} = data;
|
||||
|
||||
return prisma.client.websiteEvent.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
websiteId,
|
||||
sessionId,
|
||||
urlPath: urlPath?.substring(0, URL_LENGTH),
|
||||
urlQuery: urlQuery?.substring(0, URL_LENGTH),
|
||||
referrerPath: referrerPath?.substring(0, URL_LENGTH),
|
||||
referrerQuery: referrerQuery?.substring(0, URL_LENGTH),
|
||||
referrerDomain: referrerDomain?.substring(0, URL_LENGTH),
|
||||
pageTitle: pageTitle,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery(data: {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
urlPath: string;
|
||||
urlQuery?: string;
|
||||
referrerPath?: string;
|
||||
referrerQuery?: string;
|
||||
referrerDomain?: string;
|
||||
pageTitle?: string;
|
||||
hostname?: string;
|
||||
browser?: string;
|
||||
os?: string;
|
||||
device?: string;
|
||||
screen?: string;
|
||||
language?: string;
|
||||
country?: string;
|
||||
subdivision1?: string;
|
||||
subdivision2?: string;
|
||||
city?: string;
|
||||
}) {
|
||||
const {
|
||||
websiteId,
|
||||
id: sessionId,
|
||||
urlPath,
|
||||
urlQuery,
|
||||
referrerPath,
|
||||
referrerQuery,
|
||||
referrerDomain,
|
||||
pageTitle,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
...args
|
||||
} = data;
|
||||
const { getDateFormat, sendMessage } = kafka;
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
|
||||
const message = {
|
||||
website_id: websiteId,
|
||||
session_id: sessionId,
|
||||
event_id: uuid(),
|
||||
rev_id: website?.revId || 0,
|
||||
country: country ? country : null,
|
||||
subdivision1: subdivision1 ? subdivision1 : null,
|
||||
subdivision2: subdivision2 ? subdivision2 : null,
|
||||
city: city ? city : null,
|
||||
url_path: urlPath?.substring(0, URL_LENGTH),
|
||||
url_query: urlQuery?.substring(0, URL_LENGTH),
|
||||
referrer_path: referrerPath?.substring(0, URL_LENGTH),
|
||||
referrer_query: referrerQuery?.substring(0, URL_LENGTH),
|
||||
referrer_domain: referrerDomain?.substring(0, URL_LENGTH),
|
||||
page_title: pageTitle,
|
||||
event_type: EVENT_TYPE.pageView,
|
||||
created_at: getDateFormat(new Date()),
|
||||
...args,
|
||||
};
|
||||
|
||||
await sendMessage(message, 'event');
|
||||
|
||||
return data;
|
||||
}
|
@ -9,7 +9,6 @@ export * from './analytics/event/saveEvent';
|
||||
export * from './analytics/pageview/getPageviewMetrics';
|
||||
export * from './analytics/pageview/getPageviews';
|
||||
export * from './analytics/pageview/getPageviewStats';
|
||||
export * from './analytics/pageview/savePageView';
|
||||
export * from './analytics/session/createSession';
|
||||
export * from './analytics/session/getSession';
|
||||
export * from './analytics/session/getSessionMetrics';
|
||||
|
310
tracker/index.js
310
tracker/index.js
@ -12,12 +12,24 @@
|
||||
|
||||
if (!currentScript) return;
|
||||
|
||||
const assign = (a, b) => {
|
||||
Object.keys(b).forEach(key => {
|
||||
if (b[key] !== undefined) a[key] = b[key];
|
||||
});
|
||||
return a;
|
||||
};
|
||||
const delayDuration = 300;
|
||||
const _data = 'data-';
|
||||
const _false = 'false';
|
||||
const attr = currentScript.getAttribute.bind(currentScript);
|
||||
const website = attr(_data + 'website-id');
|
||||
const hostUrl = attr(_data + 'host-url');
|
||||
const autoTrack = attr(_data + 'auto-track') !== _false;
|
||||
const dnt = attr(_data + 'do-not-track');
|
||||
const domain = attr(_data + 'domains') || '';
|
||||
const domains = domain.split(',').map(n => n.trim());
|
||||
const root = hostUrl
|
||||
? hostUrl.replace(/\/$/, '')
|
||||
: currentScript.src.split('/').slice(0, -1).join('/');
|
||||
const endpoint = `${root}/api/send`;
|
||||
const screen = `${width}x${height}`;
|
||||
const eventRegex = /data-umami-event-([\w-_]+)/;
|
||||
|
||||
/* Helper functions */
|
||||
|
||||
const hook = (_this, method, callback) => {
|
||||
const orig = _this[method];
|
||||
@ -29,6 +41,25 @@
|
||||
};
|
||||
};
|
||||
|
||||
const getPath = url => {
|
||||
if (url.substring(0, 4) === 'http') {
|
||||
return '/' + url.split('/').splice(3).join('/');
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const getPayload = () => ({
|
||||
website,
|
||||
hostname,
|
||||
screen,
|
||||
language,
|
||||
title,
|
||||
url: currentUrl,
|
||||
referrer: currentRef,
|
||||
});
|
||||
|
||||
/* Tracking functions */
|
||||
|
||||
const doNotTrack = () => {
|
||||
const { doNotTrack, navigator, external } = window;
|
||||
|
||||
@ -47,212 +78,121 @@
|
||||
(dnt && doNotTrack()) ||
|
||||
(domain && !domains.includes(hostname));
|
||||
|
||||
const delayDuration = 300;
|
||||
const _data = 'data-';
|
||||
const _false = 'false';
|
||||
const attr = currentScript.getAttribute.bind(currentScript);
|
||||
const website = attr(_data + 'website-id');
|
||||
const hostUrl = attr(_data + 'host-url');
|
||||
const autoTrack = attr(_data + 'auto-track') !== _false;
|
||||
const dnt = attr(_data + 'do-not-track');
|
||||
const cssEvents = attr(_data + 'css-events') !== _false;
|
||||
const domain = attr(_data + 'domains') || '';
|
||||
const domains = domain.split(',').map(n => n.trim());
|
||||
const root = hostUrl
|
||||
? hostUrl.replace(/\/$/, '')
|
||||
: currentScript.src.split('/').slice(0, -1).join('/');
|
||||
const endpoint = `${root}/api/send`;
|
||||
const screen = `${width}x${height}`;
|
||||
const eventClass = /^umami--([a-z]+)--([\w]+[\w-]*)$/;
|
||||
const eventSelect = "[class*='umami--']";
|
||||
const handlePush = (state, title, url) => {
|
||||
if (!url) return;
|
||||
|
||||
let listeners = {};
|
||||
let currentUrl = `${pathname}${search}`;
|
||||
let currentRef = document.referrer;
|
||||
let currentPageTitle = document.title;
|
||||
let cache;
|
||||
currentRef = currentUrl;
|
||||
currentUrl = getPath(url.toString());
|
||||
|
||||
if (currentRef.substring(0, 4) === 'http') {
|
||||
if (currentRef.split('/')[2].split(':')[0] === hostname) {
|
||||
currentRef = '/' + currentRef.split('/').splice(3).join('/');
|
||||
if (currentUrl !== currentRef) {
|
||||
setTimeout(track, delayDuration);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
const callback = e => {
|
||||
const t = e.target;
|
||||
const attr = t.getAttribute.bind(t);
|
||||
const eventName = attr(_data + 'umami-event');
|
||||
|
||||
if (eventName) {
|
||||
const eventData = {};
|
||||
|
||||
t.getAttributeNames().forEach(name => {
|
||||
const match = name.match(eventRegex);
|
||||
|
||||
if (match) {
|
||||
eventData[match[1]] = attr(name);
|
||||
}
|
||||
|
||||
/* Collect metrics */
|
||||
|
||||
const getPayload = () => ({
|
||||
website,
|
||||
hostname,
|
||||
screen,
|
||||
language,
|
||||
url: currentUrl,
|
||||
});
|
||||
|
||||
const collect = (type, payload) => {
|
||||
if (t.tagName === 'A') {
|
||||
const href = attr('href');
|
||||
const target = attr('target');
|
||||
|
||||
if (
|
||||
href &&
|
||||
target !== '_blank' &&
|
||||
!(e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1))
|
||||
) {
|
||||
e.preventDefault();
|
||||
track(eventName, { data: eventData }).then(() => {
|
||||
location.href = href;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
track(eventName, { data: eventData });
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', callback, true);
|
||||
};
|
||||
|
||||
const observeTitle = () => {
|
||||
const callback = ([entry]) => {
|
||||
title = entry.target.data;
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(callback);
|
||||
|
||||
observer.observe(document.querySelector('head > title'), {
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
};
|
||||
|
||||
const send = payload => {
|
||||
if (trackingDisabled()) return;
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ type, payload }),
|
||||
headers: assign({ 'Content-Type': 'application/json' }, { ['x-umami-cache']: cache }),
|
||||
body: JSON.stringify({ type: 'event', payload }),
|
||||
headers: { 'Content-Type': 'application/json', ['x-umami-cache']: cache },
|
||||
})
|
||||
.then(res => res.text())
|
||||
.then(text => (cache = text));
|
||||
};
|
||||
|
||||
const trackView = (
|
||||
url = currentUrl,
|
||||
referrer = currentRef,
|
||||
websiteId = website,
|
||||
pageTitle = currentPageTitle,
|
||||
) =>
|
||||
collect(
|
||||
'pageview',
|
||||
assign(getPayload(), {
|
||||
website: websiteId,
|
||||
url,
|
||||
referrer,
|
||||
pageTitle,
|
||||
}),
|
||||
);
|
||||
|
||||
const trackEvent = (
|
||||
eventName,
|
||||
eventData,
|
||||
url = currentUrl,
|
||||
websiteId = website,
|
||||
pageTitle = currentPageTitle,
|
||||
) =>
|
||||
collect(
|
||||
'event',
|
||||
assign(getPayload(), {
|
||||
website: websiteId,
|
||||
url,
|
||||
pageTitle,
|
||||
eventName,
|
||||
eventData,
|
||||
}),
|
||||
);
|
||||
|
||||
/* Handle events */
|
||||
|
||||
const addEvents = node => {
|
||||
const elements = node.querySelectorAll(eventSelect);
|
||||
Array.prototype.forEach.call(elements, addEvent);
|
||||
};
|
||||
|
||||
const addEvent = element => {
|
||||
const get = element.getAttribute.bind(element);
|
||||
(get('class') || '').split(' ').forEach(className => {
|
||||
if (!eventClass.test(className)) return;
|
||||
|
||||
const [, event, name] = className.split('--');
|
||||
|
||||
const listener = listeners[className]
|
||||
? listeners[className]
|
||||
: (listeners[className] = e => {
|
||||
if (
|
||||
event === 'click' &&
|
||||
element.tagName === 'A' &&
|
||||
!(
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.metaKey ||
|
||||
(e.button && e.button === 1) ||
|
||||
get('target')
|
||||
)
|
||||
) {
|
||||
e.preventDefault();
|
||||
trackEvent(name).then(() => {
|
||||
const href = get('href');
|
||||
if (href) {
|
||||
location.href = href;
|
||||
const track = (name = {}, data = {}) => {
|
||||
if (typeof name === 'string') {
|
||||
return send({ ...getPayload(), ...data, name });
|
||||
} else if (typeof name === 'object') {
|
||||
return send({ ...getPayload(), ...name });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
trackEvent(name);
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener(event, listener, true);
|
||||
});
|
||||
return Promise.reject();
|
||||
};
|
||||
|
||||
/* Handle history changes */
|
||||
|
||||
const handlePush = (state, title, url) => {
|
||||
if (!url) return;
|
||||
|
||||
observeTitle();
|
||||
currentRef = currentUrl;
|
||||
const newUrl = url.toString();
|
||||
|
||||
if (newUrl.substring(0, 4) === 'http') {
|
||||
currentUrl = '/' + newUrl.split('/').splice(3).join('/');
|
||||
} else {
|
||||
currentUrl = newUrl;
|
||||
}
|
||||
|
||||
if (currentUrl !== currentRef) {
|
||||
setTimeout(() => trackView(), delayDuration);
|
||||
}
|
||||
};
|
||||
|
||||
const observeDocument = () => {
|
||||
const monitorMutate = mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
const element = mutation.target;
|
||||
addEvent(element);
|
||||
addEvents(element);
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(monitorMutate);
|
||||
observer.observe(document, { childList: true, subtree: true });
|
||||
};
|
||||
|
||||
const observeTitle = () => {
|
||||
const monitorMutate = mutations => {
|
||||
currentPageTitle = mutations[0].target.text;
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(monitorMutate);
|
||||
observer.observe(document.querySelector('title'), {
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
childList: true,
|
||||
});
|
||||
};
|
||||
|
||||
/* Global */
|
||||
|
||||
if (!window.umami) {
|
||||
const umami = eventValue => trackEvent(eventValue);
|
||||
umami.trackView = trackView;
|
||||
umami.trackEvent = trackEvent;
|
||||
|
||||
window.umami = umami;
|
||||
}
|
||||
|
||||
/* Start */
|
||||
|
||||
if (!window.umami) {
|
||||
window.umami = {
|
||||
track,
|
||||
};
|
||||
}
|
||||
|
||||
let currentUrl = `${pathname}${search}`;
|
||||
let currentRef = getPath(document.referrer);
|
||||
let title = document.title;
|
||||
let cache;
|
||||
let initialized;
|
||||
|
||||
if (autoTrack && !trackingDisabled()) {
|
||||
history.pushState = hook(history, 'pushState', handlePush);
|
||||
history.replaceState = hook(history, 'replaceState', handlePush);
|
||||
handleClick();
|
||||
observeTitle();
|
||||
|
||||
const update = () => {
|
||||
if (document.readyState === 'complete') {
|
||||
trackView();
|
||||
|
||||
if (cssEvents) {
|
||||
addEvents(document);
|
||||
observeDocument();
|
||||
}
|
||||
const init = () => {
|
||||
if (document.readyState === 'complete' && !initialized) {
|
||||
track();
|
||||
initialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('readystatechange', update, true);
|
||||
document.addEventListener('readystatechange', init, true);
|
||||
|
||||
update();
|
||||
init();
|
||||
}
|
||||
})(window);
|
||||
|
Loading…
Reference in New Issue
Block a user