mirror of
https://github.com/kremalicious/umami.git
synced 2024-06-28 16:57:52 +02:00
commit
8384d6af35
|
@ -13,5 +13,5 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"ignoreFiles": ["**/*.js"]
|
||||
"ignoreFiles": ["**/*.js", "**/*.md"]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# umami
|
||||
|
||||
Umami is a simple, fast, website analytics alternative to Google Analytics.
|
||||
Umami is a simple, fast, privacy-focused alternative to Google Analytics.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
|
34
components/common/FilterLink.js
Normal file
34
components/common/FilterLink.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { safeDecodeURI } from 'lib/url';
|
||||
import Icon from './Icon';
|
||||
import External from 'assets/arrow-up-right-from-square.svg';
|
||||
import styles from './FilterLink.module.css';
|
||||
|
||||
export default function FilterLink({ id, value, label, externalUrl }) {
|
||||
const { resolve, query } = usePageQuery();
|
||||
const active = query[id] !== undefined;
|
||||
const selected = query[id] === value;
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<Link href={resolve({ [id]: value })} replace>
|
||||
<a
|
||||
className={classNames(styles.label, {
|
||||
[styles.inactive]: active && !selected,
|
||||
[styles.active]: active && selected,
|
||||
})}
|
||||
>
|
||||
{safeDecodeURI(label || value)}
|
||||
</a>
|
||||
</Link>
|
||||
{externalUrl && (
|
||||
<a href={externalUrl} target="_blank" rel="noreferrer noopener" className={styles.link}>
|
||||
<Icon icon={<External />} className={styles.icon} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
body .inactive {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row .inactive {
|
||||
color: var(--gray500);
|
||||
}
|
||||
|
||||
body .active {
|
||||
.row .active {
|
||||
color: var(--gray900);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.row .link {
|
||||
display: none;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.row .label {
|
|
@ -9,14 +9,18 @@ import styles from './ActiveUsers.module.css';
|
|||
|
||||
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
|
||||
const shareToken = useShareToken();
|
||||
const url = value !== undefined && websiteId ? `/website/${websiteId}/active` : null;
|
||||
const url = websiteId ? `/website/${websiteId}/active` : null;
|
||||
const { data } = useFetch(url, {
|
||||
interval,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
});
|
||||
const count = useMemo(() => {
|
||||
return value || data?.[0]?.x || 0;
|
||||
}, [data, value]);
|
||||
if (websiteId) {
|
||||
return data?.[0]?.x || 0
|
||||
}
|
||||
|
||||
return value !== undefined ? value : 0;
|
||||
}, [data, value, websiteId]);
|
||||
|
||||
if (count === 0) {
|
||||
return null;
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { browserFilter } from 'lib/filters';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import MetricsTable from 'components/metrics/MetricsTable';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
|
||||
export default function BrowsersTable({ websiteId, ...props }) {
|
||||
function renderLink({ x: browser }) {
|
||||
return <FilterLink id="browser" value={browser} label={BROWSERS[browser] || browser} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
|
@ -11,7 +16,7 @@ export default function BrowsersTable({ websiteId, ...props }) {
|
|||
type="browser"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
dataFilter={browserFilter}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import MetricsTable from './MetricsTable';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
|
@ -9,10 +10,16 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
|||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
|
||||
function renderLabel({ x }) {
|
||||
function renderLink({ x: code }) {
|
||||
return (
|
||||
<div className={locale}>
|
||||
{countryNames[x] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />}
|
||||
<FilterLink
|
||||
id="country"
|
||||
value={code}
|
||||
label={
|
||||
countryNames[code] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -25,7 +32,7 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
|||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
onDataLoad={data => onDataLoad?.(percentFilter(data))}
|
||||
renderLabel={renderLabel}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import React from 'react';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { getDeviceMessage } from 'components/messages';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
|
||||
export default function DevicesTable({ websiteId, ...props }) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
function renderLink({ x: device }) {
|
||||
return (
|
||||
<FilterLink id="device" value={device} label={formatMessage(getDeviceMessage(device))} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
|
@ -11,7 +20,7 @@ export default function DevicesTable({ websiteId, ...props }) {
|
|||
type="device"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
renderLabel={({ x }) => <FormattedMessage {...getDeviceMessage(x)} />}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export default function MetricsBar({ websiteId, className }) {
|
|||
const { startDate, endDate, modified } = dateRange;
|
||||
const [format, setFormat] = useState(true);
|
||||
const {
|
||||
query: { url, ref },
|
||||
query: { url, referrer, os, browser, device, country },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, error, loading } = useFetch(
|
||||
|
@ -28,11 +28,15 @@ export default function MetricsBar({ websiteId, className }) {
|
|||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
url,
|
||||
ref,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
},
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
[modified, url, ref],
|
||||
[modified, url, referrer, os, browser, device, country],
|
||||
);
|
||||
|
||||
const formatFunc = format
|
||||
|
|
|
@ -30,7 +30,7 @@ export default function MetricsTable({
|
|||
const {
|
||||
resolve,
|
||||
router,
|
||||
query: { url, referrer },
|
||||
query: { url, referrer, os, browser, device, country },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, loading, error } = useFetch(
|
||||
|
@ -42,12 +42,16 @@ export default function MetricsTable({
|
|||
end_at: +endDate,
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
},
|
||||
onDataLoad,
|
||||
delay: DEFAULT_ANIMATION_DURATION,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
[modified, url, referrer],
|
||||
[modified, url, referrer, os, browser, device, country],
|
||||
);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import React from 'react';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
|
||||
export default function OSTable({ websiteId, ...props }) {
|
||||
function renderLink({ x: os }) {
|
||||
return <FilterLink id="os" value={os} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
|
||||
type="os"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
renderLabel={renderLink}
|
||||
websiteId={websiteId}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import { urlFilter } from 'lib/filters';
|
||||
import { safeDecodeURI } from 'lib/url';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import styles from './PagesTable.module.css';
|
||||
|
||||
export const FILTER_COMBINED = 0;
|
||||
export const FILTER_RAW = 1;
|
||||
|
||||
export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
const {
|
||||
resolve,
|
||||
query: { url: currentUrl },
|
||||
} = usePageQuery();
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
|
@ -28,18 +20,7 @@ export default function PagesTable({ websiteId, websiteDomain, showFilters, ...p
|
|||
];
|
||||
|
||||
const renderLink = ({ x: url }) => {
|
||||
return (
|
||||
<Link href={resolve({ url })} replace={true}>
|
||||
<a
|
||||
className={classNames({
|
||||
[styles.inactive]: currentUrl && url !== currentUrl,
|
||||
[styles.active]: url === currentUrl,
|
||||
})}
|
||||
>
|
||||
{safeDecodeURI(url)}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
return <FilterLink id="url" value={url} />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
body .inactive {
|
||||
color: var(--gray500);
|
||||
}
|
||||
|
||||
body .active {
|
||||
color: var(--gray900);
|
||||
font-weight: 600;
|
||||
}
|
|
@ -24,7 +24,7 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
|
|||
return sessions.filter(
|
||||
({ created_at }) => differenceInMinutes(new Date(), new Date(created_at)) <= 5,
|
||||
).length;
|
||||
}, [sessions]);
|
||||
}, [sessions, websiteId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -2,14 +2,8 @@ import React, { useState } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import { refFilter } from 'lib/filters';
|
||||
import { safeDecodeURI } from 'lib/url';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import External from 'assets/arrow-up-right-from-square.svg';
|
||||
import Icon from '../common/Icon';
|
||||
import styles from './ReferrersTable.module.css';
|
||||
|
||||
export const FILTER_DOMAIN_ONLY = 0;
|
||||
export const FILTER_COMBINED = 1;
|
||||
|
@ -17,10 +11,6 @@ export const FILTER_RAW = 2;
|
|||
|
||||
export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
const {
|
||||
resolve,
|
||||
query: { referrer: currentRef },
|
||||
} = usePageQuery();
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
|
@ -34,24 +24,8 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters,
|
|||
{ label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
|
||||
];
|
||||
|
||||
const renderLink = ({ w: link, x: label }) => {
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<Link href={resolve({ referrer: label })} replace={true}>
|
||||
<a
|
||||
className={classNames(styles.label, {
|
||||
[styles.inactive]: currentRef && label !== currentRef,
|
||||
[styles.active]: label === currentRef,
|
||||
})}
|
||||
>
|
||||
{safeDecodeURI(label)}
|
||||
</a>
|
||||
</Link>
|
||||
<a href={link || label} target="_blank" rel="noreferrer noopener" className={styles.link}>
|
||||
<Icon icon={<External />} className={styles.icon} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
const renderLink = ({ w: link, x: referrer }) => {
|
||||
return <FilterLink id="referrer" value={referrer} externalUrl={link} />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -33,7 +33,7 @@ export default function WebsiteChart({
|
|||
const {
|
||||
router,
|
||||
resolve,
|
||||
query: { url, referrer },
|
||||
query: { url, referrer, os, browser, device, country },
|
||||
} = usePageQuery();
|
||||
const { get } = useApi();
|
||||
|
||||
|
@ -47,11 +47,15 @@ export default function WebsiteChart({
|
|||
tz: timezone,
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
},
|
||||
onDataLoad,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
[modified, url, referrer],
|
||||
[modified, url, referrer, os, browser, device, country],
|
||||
);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
|
@ -88,7 +92,10 @@ export default function WebsiteChart({
|
|||
stickyClassName={styles.sticky}
|
||||
enabled={stickyHeader}
|
||||
>
|
||||
<FilterTags params={{ url, referrer }} onClick={handleCloseFilter} />
|
||||
<FilterTags
|
||||
params={{ url, referrer, os, browser, device, country }}
|
||||
onClick={handleCloseFilter}
|
||||
/>
|
||||
<div className="col-12 col-lg-9">
|
||||
<MetricsBar websiteId={websiteId} />
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"label.enable-share-url": "Habilitar compartir URL",
|
||||
"label.invalid": "Inválido",
|
||||
"label.invalid-domain": "Dominio inválido",
|
||||
"label.language": "Language",
|
||||
"label.language": "Idioma",
|
||||
"label.last-days": "Últimos {x} días",
|
||||
"label.last-hours": "Últimas {x} horas",
|
||||
"label.logged-in-as": "Sesión iniciada como {username}",
|
||||
|
@ -51,7 +51,7 @@
|
|||
"label.settings": "Configuraciones",
|
||||
"label.share-url": "Compartir URL",
|
||||
"label.single-day": "Dia",
|
||||
"label.theme": "Theme",
|
||||
"label.theme": "Tema",
|
||||
"label.this-month": "Este mes",
|
||||
"label.this-week": "Esta semana",
|
||||
"label.this-year": "Este año",
|
||||
|
@ -64,7 +64,7 @@
|
|||
"label.websites": "Sitios",
|
||||
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
||||
"message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?",
|
||||
"message.confirm-reset": "¿Seguro que deseas restablecer las estadisticas de {target}?",
|
||||
"message.confirm-reset": "¿Seguro que deseas restablecer las estadísticas de {target}?",
|
||||
"message.copied": "¡Copiado!",
|
||||
"message.delete-warning": "Toda la información relacionada será eliminada.",
|
||||
"message.failure": "Algo falló.",
|
||||
|
@ -73,7 +73,7 @@
|
|||
"message.go-to-settings": "Ir a la configuración",
|
||||
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
|
||||
"message.log.visitor": "Visitante desde {country} usando {browser} en {os} {device}",
|
||||
"message.new-version-available": "¡Una nueva versíon de umami {version} esta disponible!",
|
||||
"message.new-version-available": "¡Una nueva versión de umami {version} esta disponible!",
|
||||
"message.no-data-available": "Sin información disponible.",
|
||||
"message.no-websites-configured": "No tienes ningún sitio configurado.",
|
||||
"message.page-not-found": "Página no encontrada",
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
"metrics.filter.combined": "总和",
|
||||
"metrics.filter.domain-only": "只看域名",
|
||||
"metrics.filter.raw": "原始",
|
||||
"metrics.languages": "语言",
|
||||
"metrics.languages": "Languages",
|
||||
"metrics.operating-systems": "操作系统",
|
||||
"metrics.page-views": "页面浏览量",
|
||||
"metrics.pages": "网页",
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
"label.add-website": "增加網站",
|
||||
"label.administrator": "管理員",
|
||||
"label.all": "所有",
|
||||
"label.all-events": "All events",
|
||||
"label.all-time": "All time",
|
||||
"label.all-events": "所有事件",
|
||||
"label.all-time": "所有時間段",
|
||||
"label.all-websites": "全部網站",
|
||||
"label.back": "返回",
|
||||
"label.cancel": "取消",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"label.refresh": "刷新",
|
||||
"label.required": "必填",
|
||||
"label.reset": "重置",
|
||||
"label.reset-website": "Reset statistics",
|
||||
"label.reset-website": "重置統計數據",
|
||||
"label.save": "保存",
|
||||
"label.settings": "設置",
|
||||
"label.share-url": "分享連結",
|
||||
|
@ -64,7 +64,7 @@
|
|||
"label.websites": "網站",
|
||||
"message.active-users": "當前線上 {x} 人",
|
||||
"message.confirm-delete": "你確定要刪除 {target} 嗎?",
|
||||
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
|
||||
"message.confirm-reset": "您確定要重置 {target} 的數據嗎?",
|
||||
"message.copied": "複製成功!",
|
||||
"message.delete-warning": "所有相關數據將會被刪除。",
|
||||
"message.failure": "出現錯誤。",
|
||||
|
@ -78,13 +78,13 @@
|
|||
"message.no-websites-configured": "目前無任何網站設定。",
|
||||
"message.page-not-found": "網頁未找到。",
|
||||
"message.powered-by": "運行 {name}",
|
||||
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
|
||||
"message.reset-warning": "本網站的所有統計數據將被刪除,但您的跟蹤代碼將保持不變。",
|
||||
"message.save-success": "成功保存。",
|
||||
"message.share-url": "這是 {target} 的分享連結。",
|
||||
"message.toggle-charts": "Toggle charts",
|
||||
"message.track-stats": "將以下代碼放入被設定網站的 {head} 部分來收集 {target} 的資料。",
|
||||
"message.type-delete": "在下方空格輸入 {delete} 確認",
|
||||
"message.type-reset": "在下方空格輸入 {reset} 確認",
|
||||
"message.type-delete": "在下方空格輸入 {delete} 確認删除",
|
||||
"message.type-reset": "在下方空格輸入 {reset} 確認删除",
|
||||
"metrics.actions": "用戶行為",
|
||||
"metrics.average-visit-time": "平均訪問時間",
|
||||
"metrics.bounce-rate": "跳出率",
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { BROWSERS } from './constants';
|
||||
import { removeTrailingSlash, removeWWW, getDomainName } from './url';
|
||||
|
||||
export const urlFilter = (data, { raw }) => {
|
||||
|
@ -113,8 +112,6 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
|
|||
return Object.keys(map).map(key => ({ x: key, y: map[key], w: links[key] }));
|
||||
};
|
||||
|
||||
export const browserFilter = data => data.map(({ x, y }) => ({ x: BROWSERS[x] ?? x, y }));
|
||||
|
||||
export const eventTypeFilter = (data, types) => {
|
||||
if (!types || types.length === 0) {
|
||||
return data;
|
||||
|
|
240
lib/queries.js
240
lib/queries.js
|
@ -21,24 +21,6 @@ export function getDatabase() {
|
|||
return type;
|
||||
}
|
||||
|
||||
export async function runQuery(query) {
|
||||
return query.catch(e => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
export async function rawQuery(query, params = []) {
|
||||
const db = getDatabase();
|
||||
|
||||
if (db !== POSTGRESQL && db !== MYSQL) {
|
||||
return Promise.reject(new Error('Unknown database.'));
|
||||
}
|
||||
|
||||
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
|
||||
|
||||
return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]);
|
||||
}
|
||||
|
||||
export function getDateQuery(field, unit, timezone) {
|
||||
const db = getDatabase();
|
||||
|
||||
|
@ -72,6 +54,101 @@ export function getTimestampInterval(field) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getFilterQuery(table, filters = {}, params = []) {
|
||||
const query = Object.keys(filters).reduce((arr, key) => {
|
||||
const value = filters[key];
|
||||
|
||||
if (value === undefined) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'url':
|
||||
if (table === 'session' || table === 'pageview') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'os':
|
||||
case 'browser':
|
||||
case 'device':
|
||||
case 'country':
|
||||
if (table === 'session') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'event_type':
|
||||
if (table === 'event') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'referrer':
|
||||
if (table === 'pageview') {
|
||||
arr.push(`and ${table}.referrer like $${params.length + 1}`);
|
||||
params.push(`%${decodeURIComponent(value)}%`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'domain':
|
||||
if (table === 'pageview') {
|
||||
arr.push(`and ${table}.referrer not like $${params.length + 1}`);
|
||||
arr.push(`and ${table}.referrer not like '/%'`);
|
||||
params.push(`%://${value}/%`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
return query.join('\n');
|
||||
}
|
||||
|
||||
export function parseFilters(table, filters = {}, params = []) {
|
||||
const { domain, url, referrer, os, browser, device, country, event_type } = filters;
|
||||
|
||||
const pageviewFilters = { domain, url, referrer };
|
||||
const sessionFilters = { os, browser, device, country };
|
||||
const eventFilters = { event_type };
|
||||
|
||||
return {
|
||||
pageviewFilters,
|
||||
sessionFilters,
|
||||
eventFilters,
|
||||
event: { event_type },
|
||||
joinSession:
|
||||
os || browser || device || country
|
||||
? `inner join session on ${table}.session_id = session.session_id`
|
||||
: '',
|
||||
pageviewQuery: getFilterQuery('pageview', pageviewFilters, params),
|
||||
sessionQuery: getFilterQuery('session', sessionFilters, params),
|
||||
eventQuery: getFilterQuery('event', eventFilters, params),
|
||||
};
|
||||
}
|
||||
|
||||
export async function runQuery(query) {
|
||||
return query.catch(e => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
export async function rawQuery(query, params = []) {
|
||||
const db = getDatabase();
|
||||
|
||||
if (db !== POSTGRESQL && db !== MYSQL) {
|
||||
return Promise.reject(new Error('Unknown database.'));
|
||||
}
|
||||
|
||||
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
|
||||
|
||||
return runQuery(prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]));
|
||||
}
|
||||
|
||||
export async function getWebsiteById(website_id) {
|
||||
return runQuery(
|
||||
prisma.website.findUnique({
|
||||
|
@ -344,19 +421,7 @@ export async function getEvents(websites, start_at) {
|
|||
|
||||
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
const { url, referrer } = filters;
|
||||
let urlFilter = '';
|
||||
let refFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
if (referrer) {
|
||||
refFilter = `and referrer like $${params.length + 1}`;
|
||||
params.push(`%${decodeURIComponent(referrer)}%`);
|
||||
}
|
||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters('pageview', filters, params);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
@ -365,15 +430,16 @@ export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
|
|||
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
||||
sum(t.time) as "totaltime"
|
||||
from (
|
||||
select session_id,
|
||||
${getDateQuery('created_at', 'hour')},
|
||||
select pageview.session_id,
|
||||
${getDateQuery('pageview.created_at', 'hour')},
|
||||
count(*) c,
|
||||
${getTimestampInterval('created_at')} as "time"
|
||||
${getTimestampInterval('pageview.created_at')} as "time"
|
||||
from pageview
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
${refFilter}
|
||||
${joinSession}
|
||||
where pageview.website_id=$1
|
||||
and pageview.created_at between $2 and $3
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
group by 1, 2
|
||||
) t
|
||||
`,
|
||||
|
@ -391,30 +457,18 @@ export function getPageviewStats(
|
|||
filters = {},
|
||||
) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
const { url, referrer } = filters;
|
||||
|
||||
let urlFilter = '';
|
||||
let refFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
if (referrer) {
|
||||
refFilter = `and referrer like $${params.length + 1}`;
|
||||
params.push(`%${decodeURIComponent(referrer)}%`);
|
||||
}
|
||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters('pageview', filters, params);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select ${getDateQuery('created_at', unit, timezone)} t,
|
||||
select ${getDateQuery('pageview.created_at', unit, timezone)} t,
|
||||
count(${count}) y
|
||||
from pageview
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
${refFilter}
|
||||
${joinSession}
|
||||
where pageview.website_id=$1
|
||||
and pageview.created_at between $2 and $3
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
group by 1
|
||||
order by 1
|
||||
`,
|
||||
|
@ -424,25 +478,20 @@ export function getPageviewStats(
|
|||
|
||||
export function getSessionMetrics(website_id, start_at, end_at, field, filters = {}) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
const { url } = filters;
|
||||
|
||||
let urlFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters('pageview', filters, params);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select ${field} x, count(*) y
|
||||
from session
|
||||
where session_id in (
|
||||
select session_id
|
||||
from session as x
|
||||
where x.session_id in (
|
||||
select pageview.session_id
|
||||
from pageview
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
${joinSession}
|
||||
where pageview.website_id=$1
|
||||
and pageview.created_at between $2 and $3
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
)
|
||||
group by 1
|
||||
order by 2 desc
|
||||
|
@ -453,36 +502,18 @@ export function getSessionMetrics(website_id, start_at, end_at, field, filters =
|
|||
|
||||
export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
const { domain, url, referrer } = filters;
|
||||
|
||||
let domainFilter = '';
|
||||
let urlFilter = '';
|
||||
let refFilter = '';
|
||||
|
||||
if (domain) {
|
||||
domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`;
|
||||
params.push(`%://${domain}/%`);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
if (referrer) {
|
||||
refFilter = `and referrer like $${params.length + 1}`;
|
||||
params.push(`%${decodeURIComponent(referrer)}%`);
|
||||
}
|
||||
console.log({ table, filters });
|
||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(table, filters, params);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select ${field} x, count(*) y
|
||||
from ${table}
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${domainFilter}
|
||||
${urlFilter}
|
||||
${refFilter}
|
||||
${joinSession}
|
||||
where ${table}.website_id=$1
|
||||
and ${table}.created_at between $2 and $3
|
||||
${pageviewQuery}
|
||||
${joinSession && sessionQuery}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
`,
|
||||
|
@ -514,20 +545,6 @@ export function getEventMetrics(
|
|||
filters = {},
|
||||
) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
const { url, event_type } = filters;
|
||||
|
||||
let urlFilter = '';
|
||||
let eventTypeFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
if (event_type) {
|
||||
eventTypeFilter = `and event_type=$${params.length + 1}`;
|
||||
params.push(event_type);
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
@ -538,8 +555,7 @@ export function getEventMetrics(
|
|||
from event
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
${eventTypeFilter}
|
||||
${getFilterQuery('event', filters, params)}
|
||||
group by 1, 2
|
||||
order by 2
|
||||
`,
|
||||
|
|
|
@ -49,6 +49,10 @@ export async function getSession(req) {
|
|||
country,
|
||||
device,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!e.message.includes('Unique constraint')) {
|
||||
throw e;
|
||||
|
|
33
package.json
33
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "umami",
|
||||
"version": "1.29.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics.",
|
||||
"version": "1.30.0",
|
||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
"homepage": "https://umami.is",
|
||||
|
@ -13,7 +13,6 @@
|
|||
"dev": "next dev",
|
||||
"build": "npm-run-all build-tracker build-geo build-db build-app",
|
||||
"start": "next start",
|
||||
"start-app": "next start",
|
||||
"start-env": "node -r dotenv/config scripts/start-env.js",
|
||||
"build-app": "next build",
|
||||
"build-tracker": "rollup -c rollup.tracker.config.js",
|
||||
|
@ -38,8 +37,7 @@
|
|||
"change-password": "node scripts/change-password.js",
|
||||
"lint": "next lint --quiet",
|
||||
"prepare": "husky install",
|
||||
"postbuild": "node scripts/postbuild.js",
|
||||
"init": "node scripts/prestart.js"
|
||||
"postbuild": "node scripts/postbuild.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.js": [
|
||||
|
@ -55,8 +53,8 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "4.5.5",
|
||||
"@prisma/client": "3.11.1",
|
||||
"@fontsource/inter": "4.5.7",
|
||||
"@prisma/client": "3.12.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^2.9.4",
|
||||
|
@ -65,12 +63,16 @@
|
|||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.23.0",
|
||||
"date-fns-tz": "^1.1.4",
|
||||
"del": "^6.0.0",
|
||||
"detect-browser": "^5.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv-cli": "^4.0.0",
|
||||
"formik": "^2.2.9",
|
||||
"fs-extra": "^10.0.1",
|
||||
"immer": "^9.0.12",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"is-ci": "^3.0.1",
|
||||
"is-docker": "^3.0.0",
|
||||
"is-localhost-ip": "^1.4.0",
|
||||
"isbot": "^3.4.5",
|
||||
"jose": "2.0.5",
|
||||
|
@ -89,41 +91,36 @@
|
|||
"react-use-measure": "^2.0.4",
|
||||
"react-window": "^1.8.6",
|
||||
"request-ip": "^2.1.3",
|
||||
"semver": "^7.3.5",
|
||||
"semver": "^7.3.6",
|
||||
"thenby": "^1.3.4",
|
||||
"timezone-support": "^2.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
"zustand": "^3.7.0"
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^4.2.29",
|
||||
"@rollup/plugin-buble": "^0.21.3",
|
||||
"@svgr/webpack": "^6.2.1",
|
||||
"async-retry": "^1.3.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"del": "^6.0.0",
|
||||
"dotenv-cli": "^4.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-next": "^12.0.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"husky": "^7.0.0",
|
||||
"is-ci": "^3.0.1",
|
||||
"is-docker": "^3.0.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"postcss": "^8.4.12",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-preset-env": "^7.4.2",
|
||||
"postcss-rtlcss": "^3.5.3",
|
||||
"prettier": "^2.6.0",
|
||||
"prisma": "3.11.1",
|
||||
"postcss-rtlcss": "^3.6.1",
|
||||
"prettier": "^2.6.2",
|
||||
"prisma": "3.12.0",
|
||||
"prompts": "2.4.2",
|
||||
"rollup": "^2.70.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"stylelint": "^14.5.3",
|
||||
"stylelint-config-css-modules": "^3.0.0",
|
||||
"stylelint-config-css-modules": "^4.1.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-recommended": "^7.0.0",
|
||||
"tar": "^6.1.2"
|
||||
|
|
|
@ -24,6 +24,7 @@ const Intl = ({ children }) => {
|
|||
export default function App({ Component, pageProps }) {
|
||||
const { basePath } = useRouter();
|
||||
const { dir } = useLocale();
|
||||
const version = process.env.VERSION;
|
||||
|
||||
return (
|
||||
<Intl>
|
||||
|
@ -34,6 +35,12 @@ export default function App({ Component, pageProps }) {
|
|||
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
|
||||
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
|
||||
<link
|
||||
rel="preload"
|
||||
href={`https://i.umami.is/icon.png?v=${version}`}
|
||||
as="image"
|
||||
type="image/png"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
function redirectHTTPS(req) {
|
||||
const host = req.headers.get('host');
|
||||
if (
|
||||
process.env.FORCE_SSL &&
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
req.nextUrl.protocol === 'http:'
|
||||
) {
|
||||
return NextResponse.redirect(`https://${host}${req.nextUrl.pathname}`, 301);
|
||||
}
|
||||
}
|
||||
|
||||
function customScriptName(req) {
|
||||
const scriptName = process.env.TRACKER_SCRIPT_NAME;
|
||||
|
||||
if (scriptName) {
|
||||
const url = req.nextUrl.clone();
|
||||
const { pathname } = url;
|
||||
const names = scriptName.split(',').map(name => (name + '.js').trim());
|
||||
|
||||
if (pathname.endsWith(`/${scriptName}.js`)) {
|
||||
if (names.find(name => pathname.endsWith(name))) {
|
||||
url.pathname = '/umami.js';
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
|
@ -31,8 +21,16 @@ function disableLogin(req) {
|
|||
}
|
||||
}
|
||||
|
||||
function forceSSL(req, res) {
|
||||
if (process.env.FORCE_SSL && req.nextUrl.protocol === 'http:') {
|
||||
res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function middleware(req) {
|
||||
const fns = [redirectHTTPS, customScriptName, disableLogin];
|
||||
const fns = [customScriptName, disableLogin];
|
||||
|
||||
for (const fn of fns) {
|
||||
const res = fn(req);
|
||||
|
@ -41,5 +39,5 @@ export function middleware(req) {
|
|||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
return forceSSL(req, NextResponse.next());
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@ export default async (req, res) => {
|
|||
return ok(res);
|
||||
}
|
||||
|
||||
if (process.env.IGNORE_IP) {
|
||||
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
|
||||
const ignoreIps = process.env.IGNORE_IP;
|
||||
if (ignoreIps) {
|
||||
const ips = ignoreIps.split(',').map(n => n.trim());
|
||||
const ip = getIpAddress(req);
|
||||
const blocked = ips.find(i => {
|
||||
if (i === ip) return true;
|
||||
|
|
|
@ -33,22 +33,31 @@ export default async (req, res) => {
|
|||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, type, start_at, end_at, url, referrer } = req.query;
|
||||
const { id, type, start_at, end_at, url, referrer, os, browser, device, country } = req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
if (sessionColumns.includes(type)) {
|
||||
let data = await getSessionMetrics(websiteId, startDate, endDate, type, { url });
|
||||
let data = await getSessionMetrics(websiteId, startDate, endDate, type, {
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
});
|
||||
|
||||
if (type === 'language') {
|
||||
let combined = {};
|
||||
|
||||
for (let { x, y } of data) {
|
||||
x = String(x).toLowerCase().split('-')[0];
|
||||
if (!combined[x]) combined[x] = { x, y };
|
||||
else combined[x].y += y;
|
||||
|
||||
if (!combined[x]) {
|
||||
combined[x] = { x, y };
|
||||
} else {
|
||||
combined[x].y += y;
|
||||
}
|
||||
}
|
||||
|
||||
data = Object.values(combined);
|
||||
|
@ -69,18 +78,18 @@ export default async (req, res) => {
|
|||
domain = website.domain;
|
||||
}
|
||||
|
||||
const data = await getPageviewMetrics(
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
getColumn(type),
|
||||
getTable(type),
|
||||
{
|
||||
domain,
|
||||
url: type !== 'url' && url,
|
||||
referrer,
|
||||
},
|
||||
);
|
||||
const column = getColumn(type);
|
||||
const table = getTable(type);
|
||||
|
||||
const data = await getPageviewMetrics(websiteId, startDate, endDate, column, table, {
|
||||
domain,
|
||||
url: type !== 'url' ? url : undefined,
|
||||
referrer: type !== 'referrer' ? referrer : undefined,
|
||||
os: type !== 'os' ? os : undefined,
|
||||
browser: type !== 'browser' ? browser : undefined,
|
||||
device: type !== 'device' ? device : undefined,
|
||||
country: type !== 'country' ? country : undefined,
|
||||
});
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ export default async (req, res) => {
|
|||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, start_at, end_at, unit, tz, url, referrer } = req.query;
|
||||
const { id, start_at, end_at, unit, tz, url, referrer, os, browser, device, country } =
|
||||
req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
|
@ -25,10 +26,20 @@ export default async (req, res) => {
|
|||
}
|
||||
|
||||
const [pageviews, sessions] = await Promise.all([
|
||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, referrer }),
|
||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', {
|
||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', {
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
}),
|
||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct pageview.session_id', {
|
||||
url,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export default async (req, res) => {
|
|||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, start_at, end_at, url, referrer } = req.query;
|
||||
const { id, start_at, end_at, url, referrer, os, browser, device, country } = req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
|
@ -21,10 +21,21 @@ export default async (req, res) => {
|
|||
const prevStartDate = new Date(+start_at - distance);
|
||||
const prevEndDate = new Date(+end_at - distance);
|
||||
|
||||
const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url, referrer });
|
||||
const metrics = await getWebsiteStats(websiteId, startDate, endDate, {
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
});
|
||||
const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, {
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
});
|
||||
|
||||
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"label.language": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Language"
|
||||
"value": "Idioma"
|
||||
}
|
||||
],
|
||||
"label.last-days": [
|
||||
|
@ -334,7 +334,7 @@
|
|||
"label.theme": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Theme"
|
||||
"value": "Tema"
|
||||
}
|
||||
],
|
||||
"label.this-month": [
|
||||
|
@ -448,7 +448,7 @@
|
|||
"message.confirm-reset": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "¿Seguro que deseas restablecer las estadisticas de "
|
||||
"value": "¿Seguro que deseas restablecer las estadísticas de "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
|
@ -538,7 +538,7 @@
|
|||
"message.new-version-available": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "¡Una nueva versíon de umami "
|
||||
"value": "¡Una nueva versión de umami "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
|
|
|
@ -734,7 +734,7 @@
|
|||
"metrics.languages": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "语言"
|
||||
"value": "Languages"
|
||||
}
|
||||
],
|
||||
"metrics.operating-systems": [
|
||||
|
|
|
@ -32,13 +32,13 @@
|
|||
"label.all-events": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "All events"
|
||||
"value": "所有事件"
|
||||
}
|
||||
],
|
||||
"label.all-time": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "All time"
|
||||
"value": "所有時間段"
|
||||
}
|
||||
],
|
||||
"label.all-websites": [
|
||||
|
@ -304,7 +304,7 @@
|
|||
"label.reset-website": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Reset statistics"
|
||||
"value": "重置統計數據"
|
||||
}
|
||||
],
|
||||
"label.save": [
|
||||
|
@ -428,7 +428,7 @@
|
|||
"message.confirm-reset": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are your sure you want to reset "
|
||||
"value": "您確定要重置 "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
|
@ -436,7 +436,7 @@
|
|||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "'s statistics?"
|
||||
"value": " 的數據嗎?"
|
||||
}
|
||||
],
|
||||
"message.copied": [
|
||||
|
@ -564,7 +564,7 @@
|
|||
"message.reset-warning": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "All statistics for this website will be deleted, but your tracking code will remain intact."
|
||||
"value": "本網站的所有統計數據將被刪除,但您的跟蹤代碼將保持不變。"
|
||||
}
|
||||
],
|
||||
"message.save-success": [
|
||||
|
@ -626,7 +626,7 @@
|
|||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " 確認"
|
||||
"value": " 確認删除"
|
||||
}
|
||||
],
|
||||
"message.type-reset": [
|
||||
|
@ -640,7 +640,7 @@
|
|||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": " 確認"
|
||||
"value": " 確認删除"
|
||||
}
|
||||
],
|
||||
"metrics.actions": [
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
require('dotenv').config();
|
||||
const { sendTelemetry } = require('./telemetry');
|
||||
|
||||
async function run() {
|
||||
if (!process.env.DISABLE_TELEMETRY) {
|
||||
await sendTelemetry('start');
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
|
@ -1,7 +1,6 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const retry = require('async-retry');
|
||||
const isCI = require('is-ci');
|
||||
const pkg = require('../package.json');
|
||||
|
||||
|
@ -9,8 +8,6 @@ const dest = path.resolve(__dirname, '../.next/cache/umami.json');
|
|||
const url = 'https://telemetry.umami.is/api/collect';
|
||||
|
||||
async function sendTelemetry(action) {
|
||||
await fs.ensureFile(dest);
|
||||
|
||||
let json = {};
|
||||
|
||||
try {
|
||||
|
@ -19,7 +16,11 @@ async function sendTelemetry(action) {
|
|||
// Ignore
|
||||
}
|
||||
|
||||
await fs.writeJSON(dest, { version: pkg.version });
|
||||
try {
|
||||
await fs.writeJSON(dest, { version: pkg.version });
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
const { default: isDocker } = await import('is-docker');
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
|
@ -38,19 +39,18 @@ async function sendTelemetry(action) {
|
|||
upgrade,
|
||||
};
|
||||
|
||||
await retry(
|
||||
async () => {
|
||||
await fetch(url, {
|
||||
method: 'post',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
{ minTimeout: 500, retries: 1, factor: 1 },
|
||||
).catch(() => {});
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: 'post',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
THEME_CONFIG,
|
||||
DEFAULT_WEBSITE_LIMIT,
|
||||
} from 'lib/constants';
|
||||
import { getItem } from 'lib/web';
|
||||
import { getItem, setItem } from 'lib/web';
|
||||
|
||||
export const defaultDashboardConfig = {
|
||||
showCharts: true,
|
||||
|
@ -42,6 +42,7 @@ export function setUser(user) {
|
|||
|
||||
export function setDashboard(dashboard) {
|
||||
store.setState({ dashboard });
|
||||
setItem(DASHBOARD_CONFIG, dashboard);
|
||||
}
|
||||
|
||||
export default store;
|
||||
|
|
123
yarn.lock
123
yarn.lock
|
@ -1080,10 +1080,10 @@
|
|||
minimatch "^3.0.4"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@fontsource/inter@4.5.5":
|
||||
version "4.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-4.5.5.tgz#d4e52e8a0c84b93abae46efd4c43bd7a7f23907f"
|
||||
integrity sha512-mWnePEroLfaJQWmynipzOVcH6JwT8Jta3+yLsC5Pm/snHBXnOiAOnjBqYjKnvXwJ4eUPt2AaAhyrtwCgWQRGOg==
|
||||
"@fontsource/inter@4.5.7":
|
||||
version "4.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-4.5.7.tgz#f0657c52105ed51e04ac636a25d6016896c5ee9d"
|
||||
integrity sha512-25k3thupaOEBexuU+jAkGqieKPbuhSuA+sinDwp1iBNhqQPiJ9QHDvsXgoCgCbZ4sGlE8aCwZmSlDJrPdJHNkw==
|
||||
|
||||
"@formatjs/cli@^4.2.29":
|
||||
version "4.8.3"
|
||||
|
@ -1349,22 +1349,22 @@
|
|||
resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6"
|
||||
integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==
|
||||
|
||||
"@prisma/client@3.11.1":
|
||||
version "3.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.11.1.tgz#bde6dec71ae133d04ce1c6658e3d76627a3c6dc7"
|
||||
integrity sha512-B3C7zQG4HbjJzUr2Zg9UVkBJutbqq9/uqkl1S138+keZCubJrwizx3RuIvGwI+s+pm3qbsyNqXiZgL3Ir0fSng==
|
||||
"@prisma/client@3.12.0":
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12"
|
||||
integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw==
|
||||
dependencies:
|
||||
"@prisma/engines-version" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||
"@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
|
||||
|
||||
"@prisma/engines-version@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9":
|
||||
version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#81a1835b495ad287ad7824dbd62f74e9eee90fb9"
|
||||
integrity sha512-HkcsDniA4iNb/gi0iuyOJNAM7nD/LwQ0uJm15v360O5dee3TM4lWdSQiTYBMK6FF68ACUItmzSur7oYuUZ2zkQ==
|
||||
"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980":
|
||||
version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886"
|
||||
integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw==
|
||||
|
||||
"@prisma/engines@3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9":
|
||||
version "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9.tgz#09ac23f8f615a8586d8d44538060ada199fe872c"
|
||||
integrity sha512-MILbsGnvmnhCbFGa2/iSnsyGyazU3afzD7ldjCIeLIGKkNBMSZgA2IvpYsAXl+6qFHKGrS3B2otKfV31dwMSQw==
|
||||
"@prisma/engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980":
|
||||
version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#e52e364084c4d05278f62768047b788665e64a45"
|
||||
integrity sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ==
|
||||
|
||||
"@react-spring/animated@~9.4.4":
|
||||
version "9.4.4"
|
||||
|
@ -2031,13 +2031,6 @@ astral-regex@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
||||
|
||||
async-retry@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280"
|
||||
integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==
|
||||
dependencies:
|
||||
retry "0.13.1"
|
||||
|
||||
at-least-node@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
|
@ -4119,6 +4112,11 @@ lru-cache@^6.0.0:
|
|||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lru-cache@^7.4.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.8.0.tgz#649aaeb294a56297b5cbc5d70f198dcc5ebe5747"
|
||||
integrity sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg==
|
||||
|
||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||
|
@ -4931,10 +4929,10 @@ postcss-resolve-nested-selector@^0.1.1:
|
|||
resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
|
||||
integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=
|
||||
|
||||
postcss-rtlcss@^3.5.3:
|
||||
version "3.5.4"
|
||||
resolved "https://registry.yarnpkg.com/postcss-rtlcss/-/postcss-rtlcss-3.5.4.tgz#b0aac0641cbba1fc566cbdc044239b3da7d8f9f0"
|
||||
integrity sha512-5GpWTmBqeM10rRcFBwaKYCg7M6jiytfSS6pwrphTZnNSioxYNUMZH+88xh1oO4AF7Ix9y2qC76/gQq1HIuUf+g==
|
||||
postcss-rtlcss@^3.6.1:
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-rtlcss/-/postcss-rtlcss-3.6.1.tgz#e530e08bfe36b01a49167e015e5f047a632ca9b4"
|
||||
integrity sha512-eliUNvupb9AYn3JpW+L3aaOWeAPh8n6NPVynAoju2mimhO1dz5PTPQAlnhnzYwpqSusTxgZxYjr+RviE5MmSLg==
|
||||
dependencies:
|
||||
rtlcss "^3.5.0"
|
||||
|
||||
|
@ -4958,7 +4956,15 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector
|
|||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
||||
postcss-selector-parser@^6.0.6:
|
||||
version "6.0.10"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
|
||||
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
@ -4993,17 +4999,17 @@ prettier-linter-helpers@^1.0.0:
|
|||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17"
|
||||
integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==
|
||||
prettier@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
|
||||
integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
|
||||
|
||||
prisma@3.11.1:
|
||||
version "3.11.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.11.1.tgz#fff9c0bcf83cb30c2e1d650882d5eb3c5565e028"
|
||||
integrity sha512-aYn8bQwt1xwR2oSsVNHT4PXU7EhsThIwmpNB/MNUaaMx5OPLTro6VdNJe/sJssXFLxhamfWeMjwmpXjljo6xkg==
|
||||
prisma@3.12.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.12.0.tgz#9675e0e72407122759d3eadcb6d27cdccd3497bd"
|
||||
integrity sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg==
|
||||
dependencies:
|
||||
"@prisma/engines" "3.11.1-1.1a2506facaf1a4727b7c26850735e88ec779dee9"
|
||||
"@prisma/engines" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
|
||||
|
||||
progress@^2.0.0:
|
||||
version "2.0.3"
|
||||
|
@ -5327,11 +5333,6 @@ restore-cursor@^3.1.0:
|
|||
onetime "^5.1.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
retry@0.13.1:
|
||||
version "0.13.1"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
|
||||
integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
|
||||
|
||||
reusify@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
|
@ -5454,6 +5455,13 @@ semver@^7.2.1, semver@^7.3.4, semver@^7.3.5:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.3.6:
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.6.tgz#5d73886fb9c0c6602e79440b97165c29581cbb2b"
|
||||
integrity sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==
|
||||
dependencies:
|
||||
lru-cache "^7.4.0"
|
||||
|
||||
serialize-javascript@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
|
||||
|
@ -5719,10 +5727,12 @@ styled-jsx@5.0.1:
|
|||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.1.tgz#78fecbbad2bf95ce6cd981a08918ce4696f5fc80"
|
||||
integrity sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==
|
||||
|
||||
stylelint-config-css-modules@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-config-css-modules/-/stylelint-config-css-modules-3.0.0.tgz#441bdca77d2ca3cb978524134fdfeb8f51c236ee"
|
||||
integrity sha512-NDvOK4M98r6XGZyYLEwd+1LSqVm8ReXERbdppnhRy25QzuGcjgYvBNA1v/BxeMXq8DCljg0h4uEbQy+av/37JQ==
|
||||
stylelint-config-css-modules@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-config-css-modules/-/stylelint-config-css-modules-4.1.0.tgz#b507bc074ba5bfda9f40f0be79b540db249f0c78"
|
||||
integrity sha512-w6d552NscwvpUEaUcmq8GgWXKRv6lVHLbDj6QIHSM2vCWr83qRqRvXBJCfXDyaG/J3Zojw2inU9VvU99ZlXuUw==
|
||||
optionalDependencies:
|
||||
stylelint-scss "^4.2.0"
|
||||
|
||||
stylelint-config-prettier@^9.0.3:
|
||||
version "9.0.3"
|
||||
|
@ -5734,6 +5744,17 @@ stylelint-config-recommended@^7.0.0:
|
|||
resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-7.0.0.tgz#7497372ae83ab7a6fffc18d7d7b424c6480ae15e"
|
||||
integrity sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==
|
||||
|
||||
stylelint-scss@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.2.0.tgz#e25fd390ee38a7e89fcfaec2a8f9dce2ec6ddee8"
|
||||
integrity sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA==
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
postcss-media-query-parser "^0.2.3"
|
||||
postcss-resolve-nested-selector "^0.1.1"
|
||||
postcss-selector-parser "^6.0.6"
|
||||
postcss-value-parser "^4.1.0"
|
||||
|
||||
stylelint@^14.5.3:
|
||||
version "14.6.1"
|
||||
resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.6.1.tgz#aff137b0254515fc36b91921d88a3eb2edc194bf"
|
||||
|
@ -6236,7 +6257,7 @@ yocto-queue@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zustand@^3.7.0:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.1.tgz#7388f0a7175a6c2fd9a2880b383a4bf6cdf6b7c6"
|
||||
integrity sha512-wHBCZlKj+bg03/hP+Tzv24YhnqqP8MCeN9ECPDXoF01062SIbnfl3j9O0znkDw1lNTY0a8WN3F///a0UhhaEqg==
|
||||
zustand@^3.7.2:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d"
|
||||
integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==
|
||||
|
|
Loading…
Reference in New Issue
Block a user