Merge branch 'master' of https://github.com/umami-software/umami into francis/uc-24-kafka-test

This commit is contained in:
Francis Cao 2022-08-18 16:54:57 -07:00
commit 295ce2487d
20 changed files with 206 additions and 175 deletions

View File

@ -12,15 +12,11 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
ARG DATABASE_URL
ARG DATABASE_TYPE ARG DATABASE_TYPE
ARG BASE_PATH ARG BASE_PATH
ARG DISABLE_LOGIN
ENV DATABASE_URL $DATABASE_URL
ENV DATABASE_TYPE $DATABASE_TYPE ENV DATABASE_TYPE $DATABASE_TYPE
ENV BASE_PATH $BASE_PATH ENV BASE_PATH $BASE_PATH
ENV DISABLE_LOGIN $DISABLE_LOGIN
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
@ -36,12 +32,11 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
RUN yarn global add prisma RUN yarn add npm-run-all dotenv prisma
RUN yarn add npm-run-all dotenv
# You only need to copy next.config.js if you are NOT using the default configuration # You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js . COPY --from=builder /app/next.config.js .
COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/scripts ./scripts COPY --from=builder /app/scripts ./scripts

View File

@ -76,12 +76,12 @@ docker-compose up
Alternatively, to pull just the Umami Docker image with PostgreSQL support: Alternatively, to pull just the Umami Docker image with PostgreSQL support:
```bash ```bash
docker pull docker.umami.is/umami-software/umami:postgresql-latest docker pull docker.umami.dev/umami-software/umami:postgresql-latest
``` ```
Or with MySQL support: Or with MySQL support:
```bash ```bash
docker pull docker.umami.is/umami-software/umami:mysql-latest docker pull docker.umami.dev/umami-software/umami:mysql-latest
``` ```
## Getting updates ## Getting updates

View File

@ -6,15 +6,10 @@ import { setItem } from 'lib/web';
import { REPO_URL, VERSION_CHECK } from 'lib/constants'; import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import Button from './Button'; import Button from './Button';
import styles from './UpdateNotice.module.css'; import styles from './UpdateNotice.module.css';
import useUser from 'hooks/useUser';
import useConfig from 'hooks/useConfig';
export default function UpdateNotice() { export default function UpdateNotice() {
const { user } = useUser();
const { updatesDisabled } = useConfig();
const { latest, checked, hasUpdate, releaseUrl } = useStore(); const { latest, checked, hasUpdate, releaseUrl } = useStore();
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
const allowCheck = user?.is_admin && !updatesDisabled;
const updateCheck = useCallback(() => { const updateCheck = useCallback(() => {
setItem(VERSION_CHECK, { version: latest, time: Date.now() }); setItem(VERSION_CHECK, { version: latest, time: Date.now() });
@ -32,12 +27,12 @@ export default function UpdateNotice() {
} }
useEffect(() => { useEffect(() => {
if (!checked && allowCheck) { if (!checked) {
checkVersion(); checkVersion();
} }
}, [checked]); }, [checked]);
if (!hasUpdate || dismissed || !allowCheck) { if (!hasUpdate || dismissed) {
return null; return null;
} }

View File

@ -4,10 +4,12 @@ import { useRouter } from 'next/router';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout'; import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from 'components/common/CopyButton'; import CopyButton from 'components/common/CopyButton';
import useConfig from 'hooks/useConfig';
export default function TrackingCodeForm({ values, onClose }) { export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef(); const ref = useRef();
const { basePath } = useRouter(); const { basePath } = useRouter();
const { trackerScriptName } = useConfig();
return ( return (
<FormLayout> <FormLayout>
@ -24,7 +26,9 @@ export default function TrackingCodeForm({ values, onClose }) {
rows={3} rows={3}
cols={60} cols={60}
spellCheck={false} spellCheck={false}
defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${document.location.origin}${basePath}/umami.js"></script>`} defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${
document.location.origin
}${basePath}/${trackerScriptName ? `${trackerScriptName}.js` : 'umami.js'}"></script>`}
readOnly readOnly
/> />
</FormRow> </FormRow>

View File

@ -4,11 +4,9 @@ import classNames from 'classnames';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import styles from './Footer.module.css'; import styles from './Footer.module.css';
import useStore from 'store/version'; import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants';
import { HOMEPAGE_URL, REPO_URL } from 'lib/constants';
export default function Footer() { export default function Footer() {
const { current } = useStore();
const { pathname } = useRouter(); const { pathname } = useRouter();
return ( return (
@ -28,9 +26,9 @@ export default function Footer() {
/> />
</div> </div>
<div className={classNames(styles.version, 'col-12 col-md-4')}> <div className={classNames(styles.version, 'col-12 col-md-4')}>
<Link href={REPO_URL}>{`v${current}`}</Link> <Link href={REPO_URL}>{`v${CURRENT_VERSION}`}</Link>
</div> </div>
{!pathname.includes('/share/') && <Script src={`/telemetry.js?v=${current}`} />} {!pathname.includes('/share/') && <Script src={`/telemetry.js`} />}
</footer> </footer>
); );
} }

View File

@ -8,22 +8,26 @@ import ThemeButton from 'components/settings/ThemeButton';
import HamburgerButton from 'components/common/HamburgerButton'; import HamburgerButton from 'components/common/HamburgerButton';
import UpdateNotice from 'components/common/UpdateNotice'; import UpdateNotice from 'components/common/UpdateNotice';
import UserButton from 'components/settings/UserButton'; import UserButton from 'components/settings/UserButton';
import useUser from 'hooks/useUser';
import { HOMEPAGE_URL } from 'lib/constants'; import { HOMEPAGE_URL } from 'lib/constants';
import useConfig from '/hooks/useConfig';
import useUser from 'hooks/useUser';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
import styles from './Header.module.css'; import styles from './Header.module.css';
export default function Header() { export default function Header() {
const { user } = useUser(); const { user } = useUser();
const { pathname } = useRouter(); const { pathname } = useRouter();
const { updatesDisabled } = useConfig();
const isSharePage = pathname.includes('/share/');
const allowUpdate = user?.is_admin && !updatesDisabled && !isSharePage;
return ( return (
<> <>
<UpdateNotice /> {allowUpdate && <UpdateNotice />}
<header className={classNames(styles.header, 'row')}> <header className={classNames(styles.header, 'row')}>
<div className={styles.title}> <div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} /> <Icon icon={<Logo />} size="large" className={styles.logo} />
<Link href={pathname.includes('/share') ? HOMEPAGE_URL : '/'}>umami</Link> <Link href={isSharePage ? HOMEPAGE_URL : '/'}>umami</Link>
</div> </div>
<HamburgerButton /> <HamburgerButton />
{user && ( {user && (

View File

@ -1,4 +1,3 @@
import React, { useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
@ -9,36 +8,37 @@ import DropDown from 'components/common/DropDown';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import EventsChart from 'components/metrics/EventsChart'; import EventsChart from 'components/metrics/EventsChart';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Icon from 'components/common/Icon';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useUser from 'hooks/useUser';
import ChevronDown from 'assets/chevron-down.svg';
import styles from './TestConsole.module.css'; import styles from './TestConsole.module.css';
export default function TestConsole() { export default function TestConsole() {
const { user } = useUser();
const [website, setWebsite] = useState();
const [show, setShow] = useState(true);
const { basePath } = useRouter();
const { data } = useFetch('/websites'); const { data } = useFetch('/websites');
const router = useRouter();
const {
basePath,
query: { id },
} = router;
const websiteId = id?.[0];
if (!data || !user?.is_admin) { if (!data) {
return null; return null;
} }
const options = data.map(({ name, website_id }) => ({ label: name, value: website_id })); const options = data.map(({ name, website_id }) => ({ label: name, value: website_id }));
const website = data.find(({ website_id }) => website_id === +websiteId);
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value; const selectedValue = options.find(({ value }) => value === website?.website_id)?.value;
console.log({ websiteId, data, options, website });
function handleSelect(value) { function handleSelect(value) {
setWebsite(data.find(({ website_id }) => website_id === value)); router.push(`/console/${value}`);
} }
function handleClick() { function handleClick() {
window.umami('event (default)'); window.umami('umami-default');
window.umami.trackView('/page-view', 'https://www.google.com'); window.umami.trackView('/page-view', 'https://www.google.com');
window.umami.trackEvent('event (custom)', null, 'custom-type'); window.umami.trackEvent('track-event-no-data');
window.umami.trackEvent('event (custom)', { test: 'test-data' }, 'custom-data-type'); window.umami.trackEvent('track-event-with-data', { test: 'test-data', time: Date.now() });
} }
return ( return (
@ -62,43 +62,37 @@ export default function TestConsole() {
onChange={handleSelect} onChange={handleSelect}
/> />
</PageHeader> </PageHeader>
{!selectedValue && <EmptyPlaceholder msg="I hope you know what you're doing here" />} {website && (
{selectedValue && (
<> <>
<div>
<Icon
icon={<ChevronDown />}
className={classNames({ [styles.hidden]: !show })}
onClick={() => setShow(!show)}
/>
</div>
{show && (
<div className={classNames(styles.test, 'row')}> <div className={classNames(styles.test, 'row')}>
<div className="col-4"> <div className="col-4">
<PageHeader>Page links</PageHeader> <PageHeader>Page links</PageHeader>
<div> <div>
<Link href={`?page=1`}> <Link href={`/console/${websiteId}?page=1`}>
<a>page one</a> <a>page one</a>
</Link> </Link>
</div> </div>
<div> <div>
<Link href={`?page=2`}> <Link href={`/console/${websiteId}?page=2`}>
<a>page two</a> <a>page two</a>
</Link> </Link>
</div> </div>
<div> <div>
<Link href={`https://www.google.com`}> <Link href={`https://www.google.com`}>
<a className="umami--click--external-link">external link</a> <a className="umami--click--external-link-direct">external link (direct)</a>
</Link>
</div>
<div>
<Link href={`https://www.google.com`}>
<a className="umami--click--external-link-tab" target="_blank">
external link (tab)
</a>
</Link> </Link>
</div> </div>
</div> </div>
<div className="col-4"> <div className="col-4">
<PageHeader>CSS events</PageHeader> <PageHeader>CSS events</PageHeader>
<Button <Button id="primary-button" className="umami--click--button-click" variant="action">
id="primary-button"
className="umami--click--primary-button"
variant="action"
>
Send event Send event
</Button> </Button>
</div> </div>
@ -109,7 +103,6 @@ export default function TestConsole() {
</Button> </Button>
</div> </div>
</div> </div>
)}
<div className="row"> <div className="row">
<div className="col-12"> <div className="col-12">
<WebsiteChart <WebsiteChart

View File

@ -49,14 +49,42 @@ CREATE TABLE `event_data` (
-- AddForeignKey -- AddForeignKey
ALTER TABLE `event_data` ADD CONSTRAINT `event_data_event_id_fkey` FOREIGN KEY (`event_id`) REFERENCES `event`(`event_id`) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `event_data` ADD CONSTRAINT `event_data_event_id_fkey` FOREIGN KEY (`event_id`) REFERENCES `event`(`event_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex -- CreateProcedureRenameIndex
ALTER TABLE `account` RENAME INDEX `username` TO `account_username_key`; CREATE PROCEDURE `UmamiRenameIndexIfExists`(
IN i_table_name VARCHAR(128),
IN i_current_index_name VARCHAR(128),
IN i_new_index_name VARCHAR(128)
)
BEGIN
SET @tableName = i_table_name;
SET @currentIndexName = i_current_index_name;
SET @newIndexName = i_new_index_name;
SET @indexExists = 0;
SELECT
1
INTO @indexExists FROM
INFORMATION_SCHEMA.STATISTICS
WHERE
TABLE_NAME = @tableName
AND INDEX_NAME = @currentIndexName;
SET @query = CONCAT(
'ALTER TABLE `', @tableName, '` RENAME INDEX `', @currentIndexName, '` TO `', @newIndexName, '`;'
);
IF @indexExists THEN
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END;
-- RenameIndex -- RenameIndex
ALTER TABLE `session` RENAME INDEX `session_uuid` TO `session_session_uuid_key`; CALL UmamiRenameIndexIfExists('account', 'username', 'account_username_key');
CALL UmamiRenameIndexIfExists('session', 'session_uuid', 'session_session_uuid_key');
CALL UmamiRenameIndexIfExists('website', 'share_id', 'website_share_id_key');
CALL UmamiRenameIndexIfExists('website', 'website_uuid', 'website_website_uuid_key');
-- RenameIndex -- Drop CreateProcedureRenameIndex
ALTER TABLE `website` RENAME INDEX `share_id` TO `website_share_id_key`; drop procedure `UmamiRenameIndexIfExists`;
-- RenameIndex
ALTER TABLE `website` RENAME INDEX `website_uuid` TO `website_website_uuid_key`;

View File

@ -54,13 +54,13 @@ CREATE UNIQUE INDEX "event_data_event_id_key" ON "event_data"("event_id");
ALTER TABLE "event_data" ADD CONSTRAINT "event_data_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "event"("event_id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "event_data" ADD CONSTRAINT "event_data_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "event"("event_id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex -- RenameIndex
ALTER INDEX "account.username_unique" RENAME TO "account_username_key"; ALTER INDEX IF EXISTS "account.username_unique" RENAME TO "account_username_key";
-- RenameIndex -- RenameIndex
ALTER INDEX "session.session_uuid_unique" RENAME TO "session_session_uuid_key"; ALTER INDEX IF EXISTS "session.session_uuid_unique" RENAME TO "session_session_uuid_key";
-- RenameIndex -- RenameIndex
ALTER INDEX "website.share_id_unique" RENAME TO "website_share_id_key"; ALTER INDEX IF EXISTS "website.share_id_unique" RENAME TO "website_share_id_key";
-- RenameIndex -- RenameIndex
ALTER INDEX "website.website_uuid_unique" RENAME TO "website_website_uuid_key"; ALTER INDEX IF EXISTS "website.website_uuid_unique" RENAME TO "website_website_uuid_key";

View File

@ -67,7 +67,7 @@
"message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?", "message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?",
"message.copied": "In Zwischenablage kopiert!", "message.copied": "In Zwischenablage kopiert!",
"message.delete-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.", "message.delete-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
"message.edit-dashboard": "Edit dashboard", "message.edit-dashboard": "Dashboard bearbeiten",
"message.failure": "Es ist ein Fehler aufgetreten.", "message.failure": "Es ist ein Fehler aufgetreten.",
"message.get-share-url": "Freigabe-URL abrufen", "message.get-share-url": "Freigabe-URL abrufen",
"message.get-tracking-code": "Erstelle Tracking Kennung", "message.get-tracking-code": "Erstelle Tracking Kennung",
@ -103,7 +103,7 @@
"metrics.operating-systems": "Betriebssysteme", "metrics.operating-systems": "Betriebssysteme",
"metrics.page-views": "Seitenaufrufe", "metrics.page-views": "Seitenaufrufe",
"metrics.pages": "Seiten", "metrics.pages": "Seiten",
"metrics.query-parameters": "Query parameters", "metrics.query-parameters": "Abfrageparameter",
"metrics.referrers": "Referrer", "metrics.referrers": "Referrer",
"metrics.screens": "Bildschirmauflösungen", "metrics.screens": "Bildschirmauflösungen",
"metrics.unique-visitors": "Eindeutige Besucher", "metrics.unique-visitors": "Eindeutige Besucher",

View File

@ -67,7 +67,7 @@
"message.confirm-reset": "Êtes-vous sûr de vouloir réinistialiser les statistiques de {target} ?", "message.confirm-reset": "Êtes-vous sûr de vouloir réinistialiser les statistiques de {target} ?",
"message.copied": "Copié !", "message.copied": "Copié !",
"message.delete-warning": "Toutes les données associées seront également supprimées.", "message.delete-warning": "Toutes les données associées seront également supprimées.",
"message.edit-dashboard": "Edit dashboard", "message.edit-dashboard": "Modifier l'ordre des sites",
"message.failure": "Un problème est survenu.", "message.failure": "Un problème est survenu.",
"message.get-share-url": "Obtenir l'URL de partage", "message.get-share-url": "Obtenir l'URL de partage",
"message.get-tracking-code": "Obtenir le code de suivi", "message.get-tracking-code": "Obtenir le code de suivi",

View File

@ -1,3 +1,4 @@
export const CURRENT_VERSION = process.env.currentVersion;
export const AUTH_TOKEN = 'umami.auth'; export const AUTH_TOKEN = 'umami.auth';
export const LOCALE_CONFIG = 'umami.locale'; export const LOCALE_CONFIG = 'umami.locale';
export const TIMEZONE_CONFIG = 'umami.timezone'; export const TIMEZONE_CONFIG = 'umami.timezone';

View File

@ -35,6 +35,7 @@ if (process.env.FORCE_SSL) {
module.exports = { module.exports = {
env: { env: {
currentVersion: pkg.version, currentVersion: pkg.version,
isProduction: process.env.NODE_ENV === 'production',
}, },
basePath: process.env.BASE_PATH, basePath: process.env.BASE_PATH,
output: 'standalone', output: 'standalone',

View File

@ -13,7 +13,7 @@
"dev": "next dev", "dev": "next dev",
"build": "npm-run-all build-tracker build-geo build-db build-app", "build": "npm-run-all build-tracker build-geo build-db build-app",
"start": "npm-run-all check-db start-next", "start": "npm-run-all check-db start-next",
"start-docker": "npm-run-all check-db build-tracker start-server", "start-docker": "npm-run-all check-db update-tracker start-server",
"start-env": "node scripts/start-env.js", "start-env": "node scripts/start-env.js",
"start-server": "node server.js", "start-server": "node server.js",
"start-next": "next start", "start-next": "next start",
@ -24,6 +24,7 @@
"build-geo": "node scripts/build-geo.js", "build-geo": "node scripts/build-geo.js",
"build-db-schema": "prisma db pull", "build-db-schema": "prisma db pull",
"build-db-client": "prisma generate", "build-db-client": "prisma generate",
"update-tracker": "node scripts/update-tracker.js",
"update-db": "prisma migrate deploy", "update-db": "prisma migrate deploy",
"check-db": "node scripts/check-db.js", "check-db": "node scripts/check-db.js",
"copy-db-files": "node scripts/copy-db-files.js", "copy-db-files": "node scripts/copy-db-files.js",

View File

@ -1,18 +1,18 @@
import { TELEMETRY_PIXEL } from 'lib/constants'; import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
export default function handler(req, res) { export default function handler(req, res) {
const { v } = req.query; res.setHeader('content-type', 'text/javascript');
if (process.env.DISABLE_TELEMETRY) {
return res.send('/* telemetry disabled */');
}
const script = ` const script = `
(()=>{const i=document.createElement('img'); (()=>{const i=document.createElement('img');
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${v}'); i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;'); i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
document.body.appendChild(i);})(); document.body.appendChild(i);})();
`; `;
res.setHeader('content-type', 'text/javascript'); return res.send(script.replace(/\s\s+/g, ''));
if (process.env.DISABLE_TELEMETRY) {
res.send('/* telemetry disabled */');
} else {
res.send(script.replace(/\s\s+/g, ''));
}
} }

View File

@ -2,11 +2,13 @@ import React from 'react';
import Layout from 'components/layout/Layout'; import Layout from 'components/layout/Layout';
import TestConsole from 'components/pages/TestConsole'; import TestConsole from 'components/pages/TestConsole';
import useRequireLogin from 'hooks/useRequireLogin'; import useRequireLogin from 'hooks/useRequireLogin';
import useUser from 'hooks/useUser';
export default function TestPage() { export default function ConsolePage({ enabled }) {
const { loading } = useRequireLogin(); const { loading } = useRequireLogin();
const { user } = useUser();
if (loading) { if (loading || !enabled || !user?.is_admin) {
return null; return null;
} }
@ -16,3 +18,9 @@ export default function TestPage() {
</Layout> </Layout>
); );
} }
export async function getServerSideProps() {
return {
props: { enabled: !!process.env.ENABLE_TEST_CONSOLE },
};
}

View File

@ -63,8 +63,11 @@ async function run(cmd, args) {
async function checkMigrations() { async function checkMigrations() {
const output = await run('prisma', ['migrate', 'status']); const output = await run('prisma', ['migrate', 'status']);
console.log(output);
const missingMigrations = output.includes('have not yet been applied'); const missingMigrations = output.includes('have not yet been applied');
const missingInitialMigration = output.includes('01_init'); const missingInitialMigration =
output.includes('01_init') && !output.includes('The last common migration is: 01_init');
const notManaged = output.includes('The current database is not managed'); const notManaged = output.includes('The current database is not managed');
if (notManaged || missingMigrations) { if (notManaged || missingMigrations) {

18
scripts/update-tracker.js Normal file
View File

@ -0,0 +1,18 @@
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const endPoint = process.env.COLLECT_API_ENDPOINT;
if (endPoint) {
const file = path.resolve(__dirname, '../public/umami.js');
const tracker = fs.readFileSync(file);
fs.writeFileSync(
path.resolve(file),
tracker.toString().replace(/"\/api\/collect"/g, `"${endPoint}"`),
);
console.log(`Updated tracker endpoint: ${endPoint}.`);
}

View File

@ -1,11 +1,11 @@
import create from 'zustand'; import create from 'zustand';
import produce from 'immer'; import produce from 'immer';
import semver from 'semver'; import semver from 'semver';
import { VERSION_CHECK, UPDATES_URL } from 'lib/constants'; import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from 'lib/constants';
import { getItem } from 'lib/web'; import { getItem } from 'lib/web';
const initialState = { const initialState = {
current: process.env.currentVersion, current: CURRENT_VERSION,
latest: null, latest: null,
hasUpdate: false, hasUpdate: false,
checked: false, checked: false,

View File

@ -5,23 +5,25 @@ import { removeTrailingSlash } from '../lib/url';
const { const {
screen: { width, height }, screen: { width, height },
navigator: { language }, navigator: { language },
location: { hostname, pathname, search }, location,
localStorage, localStorage,
document, document,
history, history,
} = window; } = window;
const { hostname, pathname, search } = location;
const { currentScript } = document;
const script = document.querySelector('script[data-website-id]'); if (!currentScript) return;
if (!script) return; const _data = 'data-';
const _false = 'false';
const attr = script.getAttribute.bind(script); const attr = currentScript.getAttribute.bind(currentScript);
const website = attr('data-website-id'); const website = attr(_data + 'website-id');
const hostUrl = attr('data-host-url'); const hostUrl = attr(_data + 'host-url');
const autoTrack = attr('data-auto-track') !== 'false'; const autoTrack = attr(_data + 'auto-track') !== _false;
const dnt = attr('data-do-not-track'); const dnt = attr(_data + 'do-not-track');
const cssEvents = attr('data-css-events') !== 'false'; const cssEvents = attr(_data + 'css-events') !== _false;
const domain = attr('data-domains') || ''; const domain = attr(_data + 'domains') || '';
const domains = domain.split(',').map(n => n.trim()); const domains = domain.split(',').map(n => n.trim());
const eventClass = /^umami--([a-z]+)--([\w]+[\w-]*)$/; const eventClass = /^umami--([a-z]+)--([\w]+[\w-]*)$/;
@ -34,7 +36,8 @@ import { removeTrailingSlash } from '../lib/url';
const root = hostUrl const root = hostUrl
? removeTrailingSlash(hostUrl) ? removeTrailingSlash(hostUrl)
: script.src.split('/').slice(0, -1).join('/'); : currentScript.src.split('/').slice(0, -1).join('/');
const endpoint = `${root}/api/collect`;
const screen = `${width}x${height}`; const screen = `${width}x${height}`;
const listeners = {}; const listeners = {};
let currentUrl = `${pathname}${search}`; let currentUrl = `${pathname}${search}`;
@ -43,21 +46,6 @@ import { removeTrailingSlash } from '../lib/url';
/* Collect metrics */ /* Collect metrics */
const post = (url, data, callback) => {
const req = new XMLHttpRequest();
req.open('POST', url, true);
req.setRequestHeader('Content-Type', 'application/json');
if (cache) req.setRequestHeader('x-umami-cache', cache);
req.onreadystatechange = () => {
if (req.readyState === 4) {
callback(req.response);
}
};
req.send(JSON.stringify(data));
};
const getPayload = () => ({ const getPayload = () => ({
website, website,
hostname, hostname,
@ -68,7 +56,7 @@ import { removeTrailingSlash } from '../lib/url';
const assign = (a, b) => { const assign = (a, b) => {
Object.keys(b).forEach(key => { Object.keys(b).forEach(key => {
a[key] = b[key]; if (b[key] !== undefined) a[key] = b[key];
}); });
return a; return a;
}; };
@ -76,17 +64,16 @@ import { removeTrailingSlash } from '../lib/url';
const collect = (type, payload) => { const collect = (type, payload) => {
if (trackingDisabled()) return; if (trackingDisabled()) return;
post( return fetch(endpoint, {
`${root}/api/collect`, method: 'POST',
{ body: JSON.stringify({ type, payload }),
type, headers: assign({ 'Content-Type': 'application/json' }, { ['x-umami-cache']: cache }),
payload, })
}, .then(res => res.text())
res => (cache = res), .then(text => (cache = text));
);
}; };
const trackView = (url = currentUrl, referrer = currentRef, uuid = website) => { const trackView = (url = currentUrl, referrer = currentRef, uuid = website) =>
collect( collect(
'pageview', 'pageview',
assign(getPayload(), { assign(getPayload(), {
@ -95,9 +82,8 @@ import { removeTrailingSlash } from '../lib/url';
referrer, referrer,
}), }),
); );
};
const trackEvent = (event_name = 'custom', event_data, url = currentUrl, uuid = website) => { const trackEvent = (event_name, event_data, url = currentUrl, uuid = website) =>
collect( collect(
'event', 'event',
assign(getPayload(), { assign(getPayload(), {
@ -107,49 +93,45 @@ import { removeTrailingSlash } from '../lib/url';
event_data, event_data,
}), }),
); );
};
/* Handle events */ /* Handle events */
const sendEvent = name => {
const payload = getPayload();
payload.event_name = name;
const data = JSON.stringify({
type: 'event',
payload,
});
fetch(`${root}/api/collect`, {
method: 'POST',
body: data,
keepalive: true,
});
};
const addEvents = node => { const addEvents = node => {
const elements = node.querySelectorAll(eventSelect); const elements = node.querySelectorAll(eventSelect);
Array.prototype.forEach.call(elements, addEvent); Array.prototype.forEach.call(elements, addEvent);
}; };
const addEvent = element => { const addEvent = element => {
(element.getAttribute('class') || '').split(' ').forEach(className => { const get = element.getAttribute.bind(element);
(get('class') || '').split(' ').forEach(className => {
if (!eventClass.test(className)) return; if (!eventClass.test(className)) return;
const [, type, name] = className.split('--'); const [, event, name] = className.split('--');
const listener = listeners[className] const listener = listeners[className]
? listeners[className] ? listeners[className]
: (listeners[className] = () => { : (listeners[className] = e => {
if (element.tagName === 'A') { if (
sendEvent(name); event === 'click' &&
element.tagName === 'A' &&
!(
e.ctrlKey ||
e.shiftKey ||
e.metaKey ||
(e.button && e.button === 1) ||
get('target')
)
) {
e.preventDefault();
trackEvent(name).then(() => {
location.href = get('href');
});
} else { } else {
trackEvent(name); trackEvent(name);
} }
}); });
element.addEventListener(type, listener, true); element.addEventListener(event, listener, true);
}); });
}; };