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