mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Merge branch 'master' of https://github.com/umami-software/umami into francis/uc-24-kafka-test
This commit is contained in:
commit
295ce2487d
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 && (
|
||||||
|
@ -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
|
||||||
|
@ -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`;
|
|
@ -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";
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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';
|
||||||
|
@ -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',
|
||||||
|
@ -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",
|
||||||
|
@ -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, ''));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 },
|
||||||
|
};
|
||||||
|
}
|
@ -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
18
scripts/update-tracker.js
Normal 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}.`);
|
||||||
|
}
|
@ -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,
|
||||||
|
108
tracker/index.js
108
tracker/index.js
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user