Merge branch 'dev' into patch-2

This commit is contained in:
Mike Cao 2023-07-27 14:06:21 -07:00 committed by GitHub
commit dbf3b83598
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
198 changed files with 13850 additions and 1070 deletions

19
.github/stale.yml vendored
View File

@ -1,19 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- enhancement
- bug
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

22
.github/workflows/stale-issues.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Close stale issues
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v8
with:
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'stale'
stale-issue-message: 'This issue is stale because it has been open for 60 days with no activity.'
close-issue-message: 'This issue was closed because it has been inactive for 7 days since being marked as stale.'
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ node_modules
*.iml
*.log
.vscode
.tool-versions
# debug
npm-debug.log*

1
assets/bar-chart.svg Normal file
View File

@ -0,0 +1 @@
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m7 13v9a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1-1v-9a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1zm7-12h-4a1 1 0 0 0 -1 1v20a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-20a1 1 0 0 0 -1-1zm8 5h-4a1 1 0 0 0 -1 1v15a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-15a1 1 0 0 0 -1-1z"/></svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@ -1,5 +1,7 @@
import { Icons } from 'react-basics';
import AddUser from 'assets/add-user.svg';
import Bars from 'assets/bars.svg';
import BarChart from 'assets/bar-chart.svg';
import Bolt from 'assets/bolt.svg';
import Calendar from 'assets/calendar.svg';
import Clock from 'assets/clock.svg';
@ -22,6 +24,8 @@ import Visitor from 'assets/visitor.svg';
const icons = {
...Icons,
AddUser,
Bars,
BarChart,
Bolt,
Calendar,
Clock,

View File

@ -10,11 +10,7 @@ export function RefreshButton({ websiteId, isLoading }) {
function handleClick() {
if (!isLoading && dateRange) {
if (/^\d+/.test(dateRange.value)) {
setWebsiteDateRange(websiteId, dateRange.value);
} else {
setWebsiteDateRange(websiteId, dateRange);
}
setWebsiteDateRange(websiteId, dateRange);
}
}

View File

@ -1,25 +1,13 @@
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import DateFilter from './DateFilter';
import styles from './WebsiteDateFilter.module.css';
export default function WebsiteDateFilter({ websiteId }) {
const { get } = useApi();
const [dateRange, setDateRange] = useDateRange(websiteId);
const { value, startDate, endDate } = dateRange;
const handleChange = async value => {
if (value === 'all' && websiteId) {
const data = await get(`/websites/${websiteId}`);
if (data) {
const start = new Date(data.createdAt).getTime();
const end = Date.now();
setDateRange(`range:${start}:${end}`);
}
} else if (value !== 'all') {
setDateRange(value);
}
setDateRange(value);
};
return (

View File

@ -81,6 +81,7 @@ export const labels = defineMessages({
devices: { id: 'label.devices', defaultMessage: 'Devices' },
countries: { id: 'label.countries', defaultMessage: 'Countries' },
languages: { id: 'label.languages', defaultMessage: 'Languages' },
event: { id: 'label.event', defaultMessage: 'Event' },
events: { id: 'label.events', defaultMessage: 'Events' },
query: { id: 'label.query', defaultMessage: 'Query' },
queryParameters: { id: 'label.query-parameters', defaultMessage: 'Query parameters' },
@ -159,6 +160,7 @@ export const labels = defineMessages({
value: { id: 'labels.value', defaultMessage: 'Value' },
overview: { id: 'labels.overview', defaultMessage: 'Overview' },
totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' },
insights: { id: 'label.insights', defaultMessage: 'Insights' },
});
export const messages = defineMessages({
@ -270,4 +272,8 @@ export const messages = defineMessages({
id: 'message.no-event-data',
defaultMessage: 'No event data is available.',
},
newVersionAvailable: {
id: 'new-version-available',
defaultMessage: 'A new version of Umami {version} is available!',
},
});

View File

@ -29,7 +29,7 @@ export function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
}
return (
<StatusLight variant="success">
<StatusLight className={styles.container} variant="success">
<div className={styles.text}>{formatMessage(messages.activeUsers, { x: count })}</div>
</StatusLight>
);

View File

@ -1,10 +1,14 @@
.container {
display: flex;
align-items: center;
margin-left: 20px;
}
.text {
display: flex;
white-space: nowrap;
font-size: var(--font-size-md);
font-weight: 400;
}
.value {

View File

@ -18,7 +18,9 @@ export function Dashboard({ userId }) {
const { showCharts, limit, editing } = dashboard;
const [max, setMax] = useState(limit);
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites'], () => get('/websites', { userId }));
const { data, isLoading, error } = useQuery(['websites'], () =>
get('/websites', { userId, includeTeams: 1 }),
);
const hasData = data && data.length !== 0;
const { dir } = useLocale();

View File

@ -1,4 +1,4 @@
import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics';
import { TooltipPopup, Icon, Text, Flexbox, Popup, Item, Button } from 'react-basics';
import Icons from 'components/icons';
import { saveDashboard } from 'store/dashboard';
import useMessages from 'hooks/useMessages';
@ -6,40 +6,30 @@ import useMessages from 'hooks/useMessages';
export function DashboardSettingsButton() {
const { formatMessage, labels } = useMessages();
const menuOptions = [
{
label: formatMessage(labels.toggleCharts),
value: 'charts',
},
{
label: formatMessage(labels.editDashboard),
value: 'order',
},
];
const handleToggleCharts = () => {
saveDashboard(state => ({ showCharts: !state.showCharts }));
};
function handleSelect(value) {
if (value === 'charts') {
saveDashboard(state => ({ showCharts: !state.showCharts }));
}
if (value === 'order') {
saveDashboard({ editing: true });
}
}
const handleEdit = () => {
saveDashboard({ editing: true });
};
return (
<PopupTrigger>
<Button>
<Flexbox gap={10}>
<TooltipPopup label={formatMessage(labels.toggleCharts)} position="bottom">
<Button onClick={handleToggleCharts}>
<Icon>
<Icons.BarChart />
</Icon>
</Button>
</TooltipPopup>
<Button onClick={handleEdit}>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
<Popup alignment="end">
<Menu variant="popup" items={menuOptions} onSelect={handleSelect}>
{({ label, value }) => <Item key={value}>{label}</Item>}
</Menu>
</Popup>
</PopupTrigger>
</Flexbox>
);
}

View File

@ -13,14 +13,15 @@ export function EventDataTable({ data = [] }) {
return (
<GridTable data={data}>
<GridColumn name="event" label={formatMessage(labels.event)}>
{row => (
<Link href={resolveUrl({ event: row.event })} shallow={true}>
{row.event}
</Link>
)}
</GridColumn>
<GridColumn name="field" label={formatMessage(labels.field)}>
{row => {
return (
<Link href={resolveUrl({ view: row.field })} shallow={true}>
{row.field}
</Link>
);
}}
{row => row.field}
</GridColumn>
<GridColumn name="total" label={formatMessage(labels.totalRecords)}>
{({ total }) => total.toLocaleString()}

View File

@ -5,14 +5,14 @@ import Icons from 'components/icons';
import PageHeader from 'components/layout/PageHeader';
import Empty from 'components/common/Empty';
export function EventDataTable({ data = [], field }) {
export function EventDataValueTable({ data = [], event }) {
const { formatMessage, labels } = useMessages();
const { resolveUrl } = usePageQuery();
const Title = () => {
return (
<>
<Link href={resolveUrl({ view: undefined })}>
<Link href={resolveUrl({ event: undefined })}>
<Button>
<Icon rotate={180}>
<Icons.ArrowRight />
@ -20,7 +20,7 @@ export function EventDataTable({ data = [], field }) {
<Text>{formatMessage(labels.back)}</Text>
</Button>
</Link>
<Text>{field}</Text>
<Text>{event}</Text>
</>
);
};
@ -31,6 +31,7 @@ export function EventDataTable({ data = [], field }) {
{data.length <= 0 && <Empty />}
{data.length > 0 && (
<GridTable data={data}>
<GridColumn name="field" label={formatMessage(labels.field)} />
<GridColumn name="value" label={formatMessage(labels.value)} />
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
{({ total }) => total.toLocaleString()}
@ -41,4 +42,4 @@ export function EventDataTable({ data = [], field }) {
);
}
export default EventDataTable;
export default EventDataValueTable;

View File

@ -1,17 +1,26 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import DataTable from 'components/metrics/DataTable';
import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames';
import useMessages from 'hooks/useMessages';
import classNames from 'classnames';
import styles from './RealtimeCountries.module.css';
export function RealtimeCountries({ data }) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const countryNames = useCountryNames(locale);
const { basePath } = useRouter();
const renderCountryName = useCallback(
({ x }) => <span className={locale}>{countryNames[x]}</span>,
[countryNames, locale],
({ x: code }) => (
<span className={classNames(locale, styles.row)}>
<img src={`${basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
{countryNames[code]}
</span>
),
[countryNames, locale, basePath],
);
return (

View File

@ -0,0 +1,5 @@
.row {
display: flex;
align-items: center;
gap: 10px;
}

View File

@ -94,7 +94,7 @@ export function RealtimePage({ websiteId }) {
<WebsiteHeader websiteId={websiteId} />
<RealtimeHeader websiteId={websiteId} data={currentData} />
<div className={styles.chart}>
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
<RealtimeChart data={realtimeData} unit="minute" />
</div>
<GridRow>
<GridColumn xs={12} sm={12} md={12} lg={4} xl={4}>

View File

@ -1,10 +1,10 @@
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { REPORT_PARAMETERS } from 'lib/constants';
import PopupForm from '../PopupForm';
import FieldSelectForm from '../FieldSelectForm';
import FieldAggregateForm from '../FieldAggregateForm';
import FieldFilterForm from '../FieldFilterForm';
import PopupForm from './PopupForm';
import FieldSelectForm from './FieldSelectForm';
import FieldAggregateForm from './FieldAggregateForm';
import FieldFilterForm from './FieldFilterForm';
import styles from './FieldAddForm.module.css';
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {

View File

@ -19,6 +19,10 @@ export default function FieldAggregateForm({ name, type, onSelect }) {
{ label: formatMessage(labels.total), value: 'total' },
{ label: formatMessage(labels.unique), value: 'unique' },
],
uuid: [
{ label: formatMessage(labels.total), value: 'total' },
{ label: formatMessage(labels.unique), value: 'unique' },
],
};
const items = options[type];

View File

@ -9,10 +9,10 @@ export default function FieldSelectForm({ fields, onSelect }) {
<Form>
<FormRow label={formatMessage(labels.fields)}>
<Menu className={styles.menu} onSelect={key => onSelect(fields[key])}>
{fields.map(({ name, type }, index) => {
{fields.map(({ label, name, type }, index) => {
return (
<Item key={index} className={styles.item}>
<div>{name}</div>
<div>{label || name}</div>
<div className={styles.type}>{type}</div>
</Item>
);

View File

@ -3,20 +3,10 @@ import { Button, Icons, Text, Icon } from 'react-basics';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import Funnel from 'assets/funnel.svg';
import Nodes from 'assets/nodes.svg';
import Lightbulb from 'assets/lightbulb.svg';
import styles from './ReportTemplates.module.css';
import { useMessages } from 'hooks';
const reports = [
{
title: 'Funnel',
description: 'Understand the conversion and drop-off rate of users.',
url: '/reports/funnel',
icon: <Funnel />,
},
];
function ReportItem({ title, description, url, icon }) {
return (
<div className={styles.report}>
@ -42,6 +32,21 @@ function ReportItem({ title, description, url, icon }) {
export function ReportTemplates() {
const { formatMessage, labels } = useMessages();
const reports = [
{
title: formatMessage(labels.insights),
description: 'Dive deeper into your data by using segments and filters.',
url: '/reports/insights',
icon: <Lightbulb />,
},
{
title: formatMessage(labels.funnel),
description: 'Understand the conversion and drop-off rate of users.',
url: '/reports/funnel',
icon: <Funnel />,
},
];
return (
<Page>
<PageHeader title={formatMessage(labels.reports)} />

View File

@ -5,7 +5,7 @@ import { ReportContext } from 'components/pages/reports/Report';
import Empty from 'components/common/Empty';
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
import Icons from 'components/icons';
import FieldAddForm from './FieldAddForm';
import FieldAddForm from '../FieldAddForm';
import BaseParameters from '../BaseParameters';
import ParameterList from '../ParameterList';
import styles from './EventDataParameters.module.css';
@ -54,9 +54,11 @@ export function EventDataParameters() {
};
const handleAdd = (group, value) => {
const data = parameterData[group].filter(({ name }) => name !== value.name);
const data = parameterData[group];
updateReport({ parameters: { [group]: data.concat(value) } });
if (!data.find(({ name }) => name === value.name)) {
updateReport({ parameters: { [group]: data.concat(value) } });
}
};
const handleRemove = (group, index) => {

View File

@ -1,44 +0,0 @@
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { REPORT_PARAMETERS } from 'lib/constants';
import PopupForm from '../PopupForm';
import FieldSelectForm from '../FieldSelectForm';
import FieldAggregateForm from '../FieldAggregateForm';
import FieldFilterForm from '../FieldFilterForm';
import styles from './FieldAddForm.module.css';
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
const [selected, setSelected] = useState();
const handleSelect = value => {
const { type } = value;
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
value.value = group === REPORT_PARAMETERS.groups ? '' : 'total';
handleSave(value);
return;
}
setSelected(value);
};
const handleSave = value => {
onAdd(group, value);
onClose();
};
return createPortal(
<PopupForm className={styles.popup} element={element} onClose={onClose}>
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
{selected && group === REPORT_PARAMETERS.fields && (
<FieldAggregateForm {...selected} onSelect={handleSave} />
)}
{selected && group === REPORT_PARAMETERS.filters && (
<FieldFilterForm {...selected} onSelect={handleSave} />
)}
</PopupForm>,
document.body,
);
}
export default FieldAddForm;

View File

@ -1,38 +0,0 @@
.menu {
width: 360px;
max-height: 300px;
overflow: auto;
}
.item {
display: flex;
flex-direction: row;
justify-content: space-between;
border-radius: var(--border-radius);
}
.item:hover {
background: var(--base75);
}
.type {
color: var(--font-color300);
}
.selected {
font-weight: bold;
}
.popup {
display: flex;
}
.filter {
display: flex;
flex-direction: column;
gap: 20px;
}
.dropdown {
min-width: 60px;
}

View File

@ -1,42 +1,22 @@
import { useContext, useRef } from 'react';
import { useApi, useMessages } from 'hooks';
import { useMessages } from 'hooks';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
import { ReportContext } from 'components/pages/reports/Report';
import Empty from 'components/common/Empty';
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
import { REPORT_PARAMETERS, WEBSITE_EVENT_FIELDS } from 'lib/constants';
import Icons from 'components/icons';
import FieldAddForm from './FieldAddForm';
import BaseParameters from '../BaseParameters';
import FieldAddForm from '../FieldAddForm';
import ParameterList from '../ParameterList';
import styles from './InsightsParameters.module.css';
function useFields(websiteId, startDate, endDate) {
const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery(
['fields', websiteId, startDate, endDate],
() =>
get('/reports/event-data', {
websiteId,
startAt: +startDate,
endAt: +endDate,
}),
{ enabled: !!(websiteId && startDate && endDate) },
);
return { data, error, isLoading };
}
export function InsightsParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels, messages } = useMessages();
const { formatMessage, labels } = useMessages();
const ref = useRef(null);
const { parameters } = report || {};
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
const { startDate, endDate } = dateRange || {};
const queryEnabled = websiteId && dateRange && fields?.length;
const { data, error } = useFields(websiteId, startDate, endDate);
const parametersSelected = websiteId && startDate && endDate;
const hasData = data?.length !== 0;
const fieldOptions = Object.keys(WEBSITE_EVENT_FIELDS).map(key => WEBSITE_EVENT_FIELDS[key]);
const parameterGroups = [
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
@ -78,10 +58,7 @@ export function InsightsParameters() {
{(close, element) => {
return (
<FieldAddForm
fields={data.map(({ eventKey, InsightsType }) => ({
name: eventKey,
type: DATA_TYPES[InsightsType],
}))}
fields={fieldOptions}
group={group}
element={element}
onAdd={handleAdd}
@ -95,50 +72,43 @@ export function InsightsParameters() {
};
return (
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}>
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
<BaseParameters />
{!hasData && <Empty message={formatMessage(messages.noInsights)} />}
{parametersSelected &&
hasData &&
parameterGroups.map(({ label, group }) => {
return (
<FormRow
key={label}
label={label}
action={<AddButton group={group} onAdd={handleAdd} />}
{parameterGroups.map(({ label, group }) => {
return (
<FormRow key={label} label={label} action={<AddButton group={group} onAdd={handleAdd} />}>
<ParameterList
items={parameterData[group]}
onRemove={index => handleRemove(group, index)}
>
<ParameterList
items={parameterData[group]}
onRemove={index => handleRemove(group, index)}
>
{({ name, value }) => {
return (
<div className={styles.parameter}>
{group === REPORT_PARAMETERS.fields && (
<>
<div>{name}</div>
<div className={styles.op}>{value}</div>
</>
)}
{group === REPORT_PARAMETERS.filters && (
<>
<div>{name}</div>
<div className={styles.op}>{value[0]}</div>
<div>{value[1]}</div>
</>
)}
{group === REPORT_PARAMETERS.groups && (
<>
<div>{name}</div>
</>
)}
</div>
);
}}
</ParameterList>
</FormRow>
);
})}
{({ name, value }) => {
return (
<div className={styles.parameter}>
{group === REPORT_PARAMETERS.fields && (
<>
<div>{name}</div>
<div className={styles.op}>{value}</div>
</>
)}
{group === REPORT_PARAMETERS.filters && (
<>
<div>{name}</div>
<div className={styles.op}>{value[0]}</div>
<div>{value[1]}</div>
</>
)}
{group === REPORT_PARAMETERS.groups && (
<>
<div>{name}</div>
</>
)}
</div>
);
}}
</ParameterList>
</FormRow>
);
})}
<FormButtons>
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
{formatMessage(labels.runQuery)}

View File

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import PageviewsChart from 'components/metrics/PageviewsChart';
import { useApi, useDateRange, useTimezone, usePageQuery } from 'hooks';
import { getDateArray, getDateLength } from 'lib/date';
import { getDateArray } from 'lib/date';
export function WebsiteChart({ websiteId }) {
const [dateRange] = useDateRange(websiteId);
@ -43,17 +43,9 @@ export function WebsiteChart({ websiteId }) {
};
}
return { pageviews: [], sessions: [] };
}, [data, startDate, endDate, unit, modified]);
}, [data, startDate, endDate, unit]);
return (
<PageviewsChart
websiteId={websiteId}
data={chartData}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
loading={isLoading}
/>
);
return <PageviewsChart websiteId={websiteId} data={chartData} unit={unit} loading={isLoading} />;
}
export default WebsiteChart;

View File

@ -41,7 +41,7 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
</Link>
</WebsiteHeader>
<WebsiteMetricsBar websiteId={id} />
<WebsiteChart websiteId={id} showChart={showCharts} />
{showCharts && <WebsiteChart websiteId={id} />}
</div>
) : null;
})}

View File

@ -5,18 +5,18 @@ import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetric
import { useDateRange, useApi, usePageQuery } from 'hooks';
import styles from './WebsiteEventData.module.css';
function useFields(websiteId, field) {
function useData(websiteId, event) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery(
['event-data:fields', { websiteId, startDate, endDate, field }],
['event-data:events', { websiteId, startDate, endDate, event }],
() =>
get('/event-data/fields', {
get('/event-data/events', {
websiteId,
startAt: +startDate,
endAt: +endDate,
field,
event,
}),
{ enabled: !!(websiteId && startDate && endDate) },
);
@ -26,15 +26,15 @@ function useFields(websiteId, field) {
export default function WebsiteEventData({ websiteId }) {
const {
query: { view },
query: { event },
} = usePageQuery();
const { data } = useFields(websiteId, view);
const { data } = useData(websiteId, event);
return (
<Flexbox className={styles.container} direction="column" gap={20}>
<EventDataMetricsBar websiteId={websiteId} />
{!view && <EventDataTable data={data} />}
{view && <EventDataValueTable field={view} data={data} />}
{!event && <EventDataTable data={data} />}
{event && <EventDataValueTable event={event} data={data} />}
</Flexbox>
);
}

View File

@ -42,9 +42,9 @@ export function WebsiteHeader({ websiteId, showLinks = true, children }) {
<Column className={styles.title} variant="two">
<Favicon domain={domain} />
<Text>{name}</Text>
<ActiveUsers websiteId={websiteId} />
</Column>
<Column className={styles.actions} variant="two">
<ActiveUsers websiteId={websiteId} />
{showLinks && (
<Flexbox alignItems="center">
{links.map(({ label, icon, path }) => {

View File

@ -6,7 +6,7 @@ CREATE TABLE umami.website_event
website_id UUID,
session_id UUID,
event_id UUID,
--session
--sessions
hostname LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
@ -17,14 +17,14 @@ CREATE TABLE umami.website_event
subdivision1 LowCardinality(String),
subdivision2 LowCardinality(String),
city String,
--pageview
--pageviews
url_path String,
url_query String,
referrer_path String,
referrer_query String,
referrer_domain String,
page_title String,
--event
--events
event_type UInt32,
event_name String,
created_at DateTime('UTC'),
@ -38,7 +38,7 @@ CREATE TABLE umami.website_event_queue (
website_id UUID,
session_id UUID,
event_id UUID,
--session
--sessions
hostname LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
@ -49,14 +49,14 @@ CREATE TABLE umami.website_event_queue (
subdivision1 LowCardinality(String),
subdivision2 LowCardinality(String),
city String,
--pageview
--pageviews
url_path String,
url_query String,
referrer_path String,
referrer_query String,
referrer_domain String,
page_title String,
--event
--events
event_type UInt32,
event_name String,
created_at DateTime('UTC'),
@ -66,11 +66,11 @@ CREATE TABLE umami.website_event_queue (
)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list
kafka_topic_list = 'event',
kafka_topic_list = 'events',
kafka_group_name = 'event_consumer_group',
kafka_format = 'JSONEachRow',
kafka_max_block_size = 1048576,
kafka_handle_error_mode = 'stream'
kafka_handle_error_mode = 'stream';
CREATE MATERIALIZED VIEW umami.website_event_queue_mv TO umami.website_event AS
SELECT website_id,
@ -108,7 +108,7 @@ SETTINGS index_granularity = 8192 AS
SELECT _error AS error,
_raw_message AS raw
FROM umami.website_event_queue
WHERE length(_error) > 0
WHERE length(_error) > 0;
CREATE TABLE umami.event_data
(
@ -151,7 +151,7 @@ SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input bro
kafka_group_name = 'event_data_consumer_group',
kafka_format = 'JSONEachRow',
kafka_max_block_size = 1048576,
kafka_handle_error_mode = 'stream'
kafka_handle_error_mode = 'stream';
CREATE MATERIALIZED VIEW umami.event_data_queue_mv TO umami.event_data AS
SELECT website_id,
@ -178,4 +178,4 @@ SETTINGS index_granularity = 8192 AS
SELECT _error AS error,
_raw_message AS raw
FROM umami.event_data_queue
WHERE length(_error) > 0
WHERE length(_error) > 0;

View File

@ -97,8 +97,8 @@ model WebsiteEvent {
model EventData {
id String @id() @map("event_data_id") @db.VarChar(36)
websiteEventId String @map("website_event_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
websiteEventId String @map("website_event_id") @db.VarChar(36)
eventKey String @map("event_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)

View File

@ -1,20 +1,43 @@
import { parseDateRange } from 'lib/date';
import { getMinimumUnit, parseDateRange } from 'lib/date';
import { setItem } from 'next-basics';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
import useLocale from './useLocale';
import websiteStore, { setWebsiteDateRange } from 'store/websites';
import appStore, { setDateRange } from 'store/app';
import useApi from './useApi';
export function useDateRange(websiteId) {
const { get } = useApi();
const { locale } = useLocale();
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
const defaultConfig = DEFAULT_DATE_RANGE;
const globalConfig = appStore(state => state.dateRange);
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
const saveDateRange = value => {
const saveDateRange = async value => {
if (websiteId) {
setWebsiteDateRange(websiteId, value);
let dateRange = value;
if (typeof value === 'string') {
if (value === 'all') {
const result = await get(`/websites/${websiteId}/daterange`);
const { mindate, maxdate } = result;
const startDate = new Date(mindate);
const endDate = new Date(maxdate);
dateRange = {
startDate,
endDate,
unit: getMinimumUnit(startDate, endDate),
value,
};
} else {
dateRange = parseDateRange(value, locale);
}
}
setWebsiteDateRange(websiteId, dateRange);
} else {
setItem(DATE_RANGE_CONFIG, value);
setDateRange(value);

View File

@ -24,6 +24,7 @@ export function useFilters() {
boolean: ['t', 'f'],
number: ['eq', 'neq', 'gt', 'lt', 'gte', 'lte'],
date: ['be', 'af'],
uuid: ['eq'],
};
return { filters, types };

View File

@ -42,6 +42,7 @@
"label.edit": "Edit",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Enable share URL",
"label.event": "Event",
"label.event-data": "Event Data",
"label.events": "Events",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "تعديل",
"label.edit-dashboard": "تعديل لوحة التحكم",
"label.enable-share-url": "تفعيل مشاركة الرابط",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "الأحداث",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Змяніць",
"label.edit-dashboard": "Змяніць інфармацыйную панэль",
"label.enable-share-url": "Дазволіць дзяліцца спасылкай",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Падзеі",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "সম্পাদনা করুন",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "শেয়ার ইউআরএল শেয়ার করুন",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "ঘটনা",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Edita",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Activa l'enllaç per compartir",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Esdeveniments",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Upravit",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Povolit sdílení URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Události",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Rediger",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Aktivér delings-URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Hændelser",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Bearbeite",
"label.edit-dashboard": "Dashboard bearbeite",
"label.enable-share-url": "Freigab-URL aktiviere",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Ereigniss",
"label.field": "Field",

View File

@ -42,7 +42,8 @@
"label.edit": "Bearbeiten",
"label.edit-dashboard": "Dashboard bearbeiten",
"label.enable-share-url": "Freigabe-URL aktivieren",
"label.event-data": "Event Daten",
"label.event": "Event",
"label.event-data": "Event daten",
"label.events": "Ereignisse",
"label.field": "Field",
"label.fields": "Fields",

View File

@ -42,6 +42,7 @@
"label.edit": "Επεξεργασία",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Γεγονότα",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Edit",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Enable share URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Events",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Edit",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Enable share URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Events",
"label.field": "Field",

192
lang/es-ES.json Normal file
View File

@ -0,0 +1,192 @@
{
"label.access-code": "Código de acceso",
"label.actions": "Acciones",
"label.activity-log": "Registro de actividad",
"label.add": "Añadir",
"label.add-description": "Añadir descripción",
"label.add-website": "Nuevo sitio web",
"label.admin": "Administrador",
"label.all": "Todos",
"label.all-time": "Todos los tiempos",
"label.analytics": "Analíticas",
"label.average-visit-time": "Tiempo promedio de visita",
"label.back": "Atrás",
"label.bounce-rate": "Porcentaje de rebote",
"label.browsers": "Navegadores",
"label.cancel": "Cancelar",
"label.change-password": "Cambiar contraseña",
"label.cities": "Ciudades",
"label.clear-all": "Limpiar todo",
"label.confirm": "Confirmar",
"label.confirm-password": "Confirmar contraseña",
"label.continue": "Continuar",
"label.countries": "Países",
"label.create-team": "Crear equipo",
"label.create-user": "Crear usuario",
"label.created": "Creado",
"label.current-password": "Contraseña actual",
"label.custom-range": "Intervalo personalizado",
"label.dashboard": "Panel de control",
"label.data": "Datos",
"label.date-range": "Intervalo de fechas",
"label.default-date-range": "Intervalo por defecto",
"label.delete": "Eliminar",
"label.delete-team": "Eliminar equipo",
"label.delete-user": "Eliminar usuario",
"label.delete-website": "Eliminar sitio",
"label.desktop": "Escritorio",
"label.details": "Detalles",
"label.devices": "Dispositivos",
"label.dismiss": "Ignorar",
"label.domain": "Dominio",
"label.edit": "Editar",
"label.edit-dashboard": "Editar panel",
"label.enable-share-url": "Habilitar compartir URL",
"label.event": "Evento",
"label.event-data": "Datos de evento",
"label.events": "Eventos",
"label.field": "Campo",
"label.fields": "Campos",
"label.filter-combined": "Combinado",
"label.filter-raw": "En crudo",
"label.funnel": "Funnel",
"label.join": "Unir",
"label.join-team": "Unirse al equipo",
"label.language": "Idioma",
"label.languages": "Idiomas",
"label.laptop": "Portátil",
"label.last-days": "Últimos {x} días",
"label.last-hours": "Últimas {x} horas",
"label.leave": "Abandonar",
"label.leave-team": "Abandonar equipo",
"label.login": "Iniciar sesión",
"label.logout": "Cerrar sesión",
"label.members": "Miembros",
"label.mobile": "Móvil",
"label.more": "Más",
"label.name": "Nombre",
"label.new-password": "Nueva contraseña",
"label.none": "Ninguno",
"label.operating-systems": "Sistemas operativos",
"label.owner": "Propietario",
"label.page-views": "Vistas",
"label.pages": "Páginas",
"label.password": "Contraseña",
"label.powered-by": "Con la ayuda de {name}",
"label.profile": "Perfil",
"label.queries": "Consultas",
"label.query": "Query",
"label.query-parameters": "Parámetros de petición",
"label.realtime": "Tiempo real",
"label.referrers": "Referido desde",
"label.refresh": "Actualizar",
"label.regenerate": "Regenerar",
"label.regions": "Regiones",
"label.remove": "Quitar",
"label.reports": "Reportes",
"label.required": "Obligatorio",
"label.reset": "Reiniciar",
"label.reset-website": "Reiniciar estadísticas",
"label.role": "Rol",
"label.run-query": "Ejecutar consulta",
"label.save": "Guardar",
"label.screens": "Pantallas",
"label.select-date": "Seleccionar fecha",
"label.select-website": "Seleccionar sitio web",
"label.sessions": "Sesiones",
"label.settings": "Configuraciones",
"label.share-url": "Compartir URL",
"label.single-day": "Un solo día",
"label.tablet": "Tableta",
"label.team": "Equipo",
"label.team-guest": "Invitado al equipo",
"label.team-id": "ID de equipo",
"label.team-member": "Miembro del equipo",
"label.team-owner": "Admin. del equipo",
"label.teams": "Equipos",
"label.theme": "Tema",
"label.this-month": "Este mes",
"label.this-week": "Esta semana",
"label.this-year": "Este año",
"label.timezone": "Zona horaria",
"label.title": "Título",
"label.today": "Hoy",
"label.toggle-charts": "Alternar gráficas",
"label.tracking-code": "Código de rastreo",
"label.unique-visitors": "Visitantes únicos",
"label.unknown": "Desconocida",
"label.url": "URL",
"label.urls": "URLs",
"label.user": "Usuario",
"label.username": "Nombre de usuario",
"label.users": "Usuarios",
"label.view": "Visualizar",
"label.view-details": "Ver detalles",
"label.view-only": "Ver sólo",
"label.views": "Vistas",
"label.visitors": "Visitantes",
"label.website": "Sitio web",
"label.website-id": "ID del sitio web",
"label.websites": "Sitios web",
"label.window": "Ventana",
"label.yesterday": "Ayer",
"labels.after": "Después",
"labels.average": "Media",
"labels.before": "Antes",
"labels.breakdown": "Desglose",
"labels.contains": "Contiene",
"labels.create-report": "Crear reporte",
"labels.description": "Descripciones",
"labels.does-not-contain": "No contiene",
"labels.does-not-equal": "No es igual a",
"labels.equals": "Es igual a",
"labels.false": "False",
"labels.filters": "Filtros",
"labels.greater-than": "Mayor que",
"labels.greater-than-equals": "Mayor que o igual a",
"labels.less-than": "Menor que",
"labels.less-than-equals": "Menor que o igual a",
"labels.max": "Máx",
"labels.min": "Mín",
"labels.overview": "Resumen",
"labels.sum": "Suma",
"labels.total": "Total",
"labels.total-records": "Total de registros",
"labels.true": "Verdadero",
"labels.type": "Tipo",
"labels.unique": "Único",
"labels.untitled": "Sin título",
"labels.value": "Valor",
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
"message.confirm-delete": "¿Seguro que quieres eliminar {target}?",
"message.confirm-leave": "¿Seguro que quieres abandonar {target}?",
"message.confirm-reset": "¿Seguro que quieres BORRAR las analíticas de {target}?",
"message.delete-account": "Para borrar esta cuenta, escribe {confirmation} a continuación para confirmar.",
"message.delete-website": "Para borrar este sitio web, escribe {confirmation} a continuación para confirmar.",
"message.delete-website-warning": "Toda la información relacionada será eliminada.",
"message.error": "Algo falló.",
"message.event-log": "{event} en {url}",
"message.go-to-settings": "Ir a la configuración",
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
"message.invalid-domain": "Dominio inválido",
"message.min-password-length": "Longitud mínima de {n} caracteres",
"message.no-data-available": "No hay información disponible.",
"message.no-event-data": "No hay datos de eventos disponibles.",
"message.no-match-password": "Las contraseñas no coinciden",
"message.no-teams": "No has creado ningún equipo.",
"message.no-users": "No hay usuarios.",
"message.page-not-found": "Página no encontrada",
"message.reset-website": "Para reiniciar este sitio web, escribe {confirmation} a continuación para confirmar.",
"message.reset-website-warning": "Todas las estadísticas de esta página serán eliminadas, pero el código de rastreo permanecerá intacto.",
"message.saved": "Guardado.",
"message.share-url": "Esta es la URL pública para {target}.",
"message.team-already-member": "Ya eres miembro de este equipo.",
"message.team-not-found": "Equipo no encontrado.",
"message.tracking-code": "Código de rastreo",
"message.user-deleted": "Usuario eliminado.",
"message.visitor-log": "Visitante desde {country} usando {browser} en {os} {device}",
"messages.no-results-found": "No se encontraron resultados.",
"messages.no-team-websites": "Este equipo no tiene ningún sitio web configurado.",
"messages.no-websites-configured": "No tienes ningún sitio web configurado.",
"messages.team-websites-info": "Las analíticas de tus sitios web pueden ser vistas por cualquier miembro del equipo."
}

View File

@ -42,6 +42,7 @@
"label.edit": "Editar",
"label.edit-dashboard": "Editar panel",
"label.enable-share-url": "Habilitar compartir URL",
"label.event": "Evento",
"label.event-data": "Event data",
"label.events": "Eventos",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "ویرایش",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "فعال کردن اشتراک گذاری URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "رویدادها",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Muokkaa",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Ota jakamisen URL-osoite käyttöön",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Tapahtumat",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Ger broyting",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Virkja deili leinki",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Hendingar/tiltøk",
"label.field": "Field",

View File

@ -2,8 +2,8 @@
"label.access-code": "Code d'accès",
"label.actions": "Actions",
"label.activity-log": "Journal d'activité",
"label.add": "Add",
"label.add-description": "Add description",
"label.add": "Ajouter",
"label.add-description": "Ajouter une description",
"label.add-website": "Ajouter un site",
"label.admin": "Administrateur",
"label.all": "Tout",
@ -42,13 +42,14 @@
"label.edit": "Modifier",
"label.edit-dashboard": "Modifier le tableau de bord",
"label.enable-share-url": "Activer l'URL de partage",
"label.event-data": "Event data",
"label.event": "Event",
"label.event-data": "Données d'événements",
"label.events": "Événements",
"label.field": "Field",
"label.fields": "Fields",
"label.field": "Champ",
"label.fields": "Champs",
"label.filter-combined": "Combiné",
"label.filter-raw": "Brut",
"label.funnel": "Funnel",
"label.funnel": "Entonnoir",
"label.join": "Rejoindre",
"label.join-team": "Rejoindre une équipe",
"label.language": "Langue",
@ -73,24 +74,24 @@
"label.password": "Mot de passe",
"label.powered-by": "Propulsé par {name}",
"label.profile": "Profil",
"label.queries": "Queries",
"label.query": "Query",
"label.queries": "Requêtes",
"label.query": "Requête",
"label.query-parameters": "Paramètres d'URL",
"label.realtime": "Temps réel",
"label.referrers": "Sources",
"label.referrers": "Sites référents",
"label.refresh": "Rafraîchir",
"label.regenerate": "Régénérer",
"label.regions": "Régions",
"label.remove": "Retirer",
"label.reports": "Reports",
"label.reports": "Rapports",
"label.required": "Requis",
"label.reset": "Réinitialiser",
"label.reset-website": "Réinitialiser les statistiques",
"label.role": "Rôle",
"label.run-query": "Run query",
"label.run-query": "Éxécuter la requête",
"label.save": "Enregistrer",
"label.screens": "Résolutions d'écran",
"label.select-date": "Select date",
"label.select-date": "Choisir une période",
"label.select-website": "Choisir un site",
"label.sessions": "Sessions",
"label.settings": "Paramètres",
@ -121,46 +122,46 @@
"label.users": "Utilisateurs",
"label.view": "Voir",
"label.view-details": "Voir les détails",
"label.view-only": "View only",
"label.view-only": "Consultation",
"label.views": "Vues",
"label.visitors": "Visiteurs",
"label.website": "Website",
"label.website-id": "ID de site",
"label.websites": "Sites",
"label.window": "Window",
"label.window": "Fenêtre",
"label.yesterday": "Hier",
"labels.after": "After",
"labels.average": "Average",
"labels.before": "Before",
"labels.breakdown": "Breakdown",
"labels.contains": "Contains",
"labels.create-report": "Create report",
"labels.after": "Après",
"labels.average": "Moyenne",
"labels.before": "Avant",
"labels.breakdown": "Répartition",
"labels.contains": "Contient",
"labels.create-report": "Créer un rapport",
"labels.description": "Description",
"labels.does-not-contain": "Does not contain",
"labels.does-not-equal": "Does not equal",
"labels.equals": "Equals",
"labels.false": "False",
"labels.filters": "Filters",
"labels.greater-than": "Greater than",
"labels.greater-than-equals": "Greater than or equals",
"labels.less-than": "Less than",
"labels.less-than-equals": "Less than or equals",
"labels.does-not-contain": "Ne contient pas",
"labels.does-not-equal": "N'est pas égal",
"labels.equals": "Est égal",
"labels.false": "Faux",
"labels.filters": "Filtres",
"labels.greater-than": "Supérieur à",
"labels.greater-than-equals": "Supérieur ou égal à",
"labels.less-than": "Inférieur à",
"labels.less-than-equals": "Inférieur ou égal à",
"labels.max": "Max",
"labels.min": "Min",
"labels.overview": "Overview",
"labels.sum": "Sum",
"labels.overview": "Vue d'ensemble",
"labels.sum": "Somme",
"labels.total": "Total",
"labels.total-records": "Total records",
"labels.true": "True",
"labels.total-records": "Nombre d'enregistrements",
"labels.true": "Vrai",
"labels.type": "Type",
"labels.unique": "Unique",
"labels.untitled": "Untitled",
"labels.value": "Value",
"labels.untitled": "Sans titre",
"labels.value": "Valeur",
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
"message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?",
"message.confirm-leave": "Êtes-vous sûr de vouloir quitter {target} ?",
"message.confirm-reset": "Êtes-vous sûr de vouloir réinitialiser les statistiques de {target} ?",
"message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.",
"message.delete-account": "Pour supprimer ce compte, taper {confirmation} ci-dessous pour confirmer.",
"message.delete-website": "Pour supprimer ce site, taper {confirmation} ci-dessous pour confirmer.",
"message.delete-website-warning": "Toutes les données associées seront supprimées.",
"message.error": "Un problème est survenu.",
@ -170,12 +171,12 @@
"message.invalid-domain": "Domaine invalide",
"message.min-password-length": "Taille minimale de {n} caractères",
"message.no-data-available": "Aucune donnée disponible.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "Aucune donnée d'événement disponible.",
"message.no-match-password": "Les mots de passe ne correspondent pas",
"message.no-teams": "Vous n'avez créé aucune équipe.",
"message.no-users": "Il n'y aucun utilisateur.",
"message.no-teams": "Vous n'avez pas créé d'équipe.",
"message.no-users": "Aucun utilisateur.",
"message.page-not-found": "Page non trouvée.",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website": "Pour réinitialiser ce site, taper {confirmation} ci-dessous pour confirmer.",
"message.reset-website-warning": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact.",
"message.saved": "Enregistré avec succès.",
"message.share-url": "Les statistiques de votre site sont accessibles publiquement sur cette URL :",
@ -184,8 +185,8 @@
"message.tracking-code": "Code de suivi",
"message.user-deleted": "Utilisateur supprimé.",
"message.visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}",
"messages.no-results-found": "No results were found.",
"messages.no-results-found": "Aucun résultat n'a été trouvé.",
"messages.no-team-websites": "Cette équipe n'a aucun site.",
"messages.no-websites-configured": "Vous n'avez configuré aucun site.",
"messages.no-websites-configured": "Vous n'avez pas configuré de site.",
"messages.team-websites-info": "Les sites peuvent être vus par tout utilisateur dans l'équipe."
}

View File

@ -42,6 +42,7 @@
"label.edit": "Editar",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Activar URL de compartición",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Eventos",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "עריכה",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "הפעלת URL שיתוף",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "אירועים",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "संपादित करें",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "शेयर URL सक्षम करें",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "स्पर्धाएँ",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Uredi",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Omogući dijeljenje poveznice",
"label.event": "Event",
"label.event-data": "Podaci događaja",
"label.events": "Events",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Módosítás",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "URL-megosztás engedélyezése",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Események",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Sunting",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Aktifkan URL berbagi",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Perihal",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Modifica",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Abilita URL di condivisione",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Eventi",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "編集",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "共有リンクを有効にする",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "イベント",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "កែប្រែ",
"label.edit-dashboard": "កែផ្ទាំងគ្រប់គ្រង",
"label.enable-share-url": "បើកការចែករំលែក URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "ព្រឹត្តិការណ៍",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "편집",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "URL 공유 활성화",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "이벤트",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Redaguoti",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Įjungti bendrinimą su nuoroda",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Įvykiai",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Засах",
"label.edit-dashboard": "Хянах самбар засах",
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Үйлдэл",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Edit",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Aktifkan url berkongsi",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Peristiwa",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Rediger",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Aktiver delings-URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Arrangementer",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Bewerken",
"label.edit-dashboard": "Dashboard aanpassen",
"label.enable-share-url": "Sta delen via openbare URL toe",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Gebeurtenissen",
"label.field": "Field",

View File

@ -2,7 +2,7 @@
"label.access-code": "Kod dostępu",
"label.actions": "Działania",
"label.activity-log": "Dziennik aktywności",
"label.add": "Add",
"label.add": "Dodaj",
"label.add-description": "Add description",
"label.add-website": "Dodaj witrynę",
"label.admin": "Administrator",
@ -42,13 +42,14 @@
"label.edit": "Edytuj",
"label.edit-dashboard": "Edytuj panel",
"label.enable-share-url": "Włącz udostępnianie adresu URL",
"label.event-data": "Event data",
"label.event": "Event",
"label.event-data": "Dane zdarzenia",
"label.events": "Zdarzenia",
"label.field": "Field",
"label.fields": "Fields",
"label.field": "Pole",
"label.fields": "Pola",
"label.filter-combined": "Połączone",
"label.filter-raw": "Surowe dane",
"label.funnel": "Funnel",
"label.funnel": "Lejek",
"label.join": "Dołącz",
"label.join-team": "Dołącz do zespołu",
"label.language": "Język",
@ -74,7 +75,7 @@
"label.powered-by": "Obsługiwane przez {name}",
"label.profile": "Profil",
"label.queries": "Zapytania",
"label.query": "Query",
"label.query": "Zapytanie",
"label.query-parameters": "Parametry query",
"label.realtime": "Czas rzeczywisty",
"label.referrers": "Źródła odsyłające",
@ -82,15 +83,15 @@
"label.regenerate": "Wygeneruj ponownie",
"label.regions": "Regiony",
"label.remove": "Usuń",
"label.reports": "Reports",
"label.reports": "Raporty",
"label.required": "Wymagany",
"label.reset": "Zresetuj",
"label.reset-website": "Zresetuj statystyki",
"label.role": "Role",
"label.run-query": "Run query",
"label.role": "Rola",
"label.run-query": "Uruchom zapytanie",
"label.save": "Zapisz",
"label.screens": "Ekrany",
"label.select-date": "Select date",
"label.select-date": "Wybierz datę",
"label.select-website": "Wybierz witrynę",
"label.sessions": "Sesje",
"label.settings": "Ustawienia",
@ -114,54 +115,54 @@
"label.tracking-code": "Kod śledzenia",
"label.unique-visitors": "Unikalni odwiedzający",
"label.unknown": "Nieznany",
"label.url": "URL",
"label.urls": "URLs",
"label.url": "Link",
"label.urls": "Linki",
"label.user": "Użytkownik",
"label.username": "Nazwa użytkownika",
"label.users": "Użytkownicy",
"label.view": "Zobacz",
"label.view-details": "Pokaż szczegóły",
"label.view-only": "View only",
"label.view-only": "Wyświetl tylko",
"label.views": "Wyświetlenia",
"label.visitors": "Odwiedzający",
"label.website": "Website",
"label.website": "Witryna",
"label.website-id": "ID witryny",
"label.websites": "Witryny",
"label.window": "Window",
"label.window": "Okno",
"label.yesterday": "Wczoraj",
"labels.after": "After",
"labels.average": "Average",
"labels.before": "Before",
"labels.breakdown": "Breakdown",
"labels.contains": "Contains",
"labels.create-report": "Create report",
"labels.description": "Description",
"labels.does-not-contain": "Does not contain",
"labels.does-not-equal": "Does not equal",
"labels.equals": "Equals",
"labels.false": "False",
"labels.filters": "Filters",
"labels.greater-than": "Greater than",
"labels.greater-than-equals": "Greater than or equals",
"labels.less-than": "Less than",
"labels.less-than-equals": "Less than or equals",
"labels.after": "Po",
"labels.average": "Średnia",
"labels.before": "Przed",
"labels.breakdown": "Podział",
"labels.contains": "Zawiera",
"labels.create-report": "Utwórz raport",
"labels.description": "Opis",
"labels.does-not-contain": "Nie zawiera",
"labels.does-not-equal": "Nie równa się",
"labels.equals": "Równa się",
"labels.false": "Fałsz",
"labels.filters": "Filtry",
"labels.greater-than": "Wiekszy niż",
"labels.greater-than-equals": "Większy lub równy",
"labels.less-than": "Mniejszy niż",
"labels.less-than-equals": "Mniejszy lub równy",
"labels.max": "Max",
"labels.min": "Min",
"labels.overview": "Overview",
"labels.sum": "Sum",
"labels.total": "Total",
"labels.total-records": "Total records",
"labels.true": "True",
"labels.type": "Type",
"labels.unique": "Unique",
"labels.untitled": "Untitled",
"labels.value": "Value",
"labels.overview": "Przegląd",
"labels.sum": "Suma",
"labels.total": "Łącznie",
"labels.total-records": "Łączna liczba rekordów",
"labels.true": "Prawda",
"labels.type": "Typ",
"labels.unique": "Unikalne",
"labels.untitled": "Bez tytułu",
"labels.value": "Wartość",
"message.active-users": "{x} aktualnie {x, plural, one {odwiedzający} other {odwiedzających}}",
"message.confirm-delete": "Czy na pewno chcesz usunąć {target}?",
"message.confirm-leave": "Czy na pewno chcesz opuścić {target}?",
"message.confirm-reset": "Czy na pewno chcesz zresetować statystyki {target}?",
"message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.",
"message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.",
"message.delete-account": "Aby usunąć to konto, wpisz {confirmation} w polu poniżej w celu potwierdzenia.",
"message.delete-website": "Aby usunąć tę stronę, wpisz {confirmation} w polu poniżej w celu potwierdzenia.",
"message.delete-website-warning": "Wszystkie powiązane dane również zostaną usunięte.",
"message.error": "Coś poszło nie tak.",
"message.event-log": "{event} na {url}",
@ -170,7 +171,7 @@
"message.invalid-domain": "Nieprawidłowa witryna",
"message.min-password-length": "Minimalna długość {n} znaków",
"message.no-data-available": "Brak dostępnych danych.",
"message.no-event-data": "No event data is available.",
"message.no-event-data": "Dane dotyczące zdarzeń nie są dostępne.",
"message.no-match-password": "Hasła się nie zgadzają",
"message.no-teams": "Nie stworzyłeś żadnych zespołów.",
"message.no-users": "Nie ma żadnych użytkowników.",
@ -184,7 +185,7 @@
"message.tracking-code": "Kod śledzenia",
"message.user-deleted": "Użytkownik usunięty.",
"message.visitor-log": "Odwiedzający z {country} używa {browser} na {os} {device}",
"messages.no-results-found": "No results were found.",
"messages.no-results-found": "Nie znaleziono żadnych wyników.",
"messages.no-team-websites": "Ten zespół nie ma żadnych witryn internetowych.",
"messages.no-websites-configured": "Nie masz skonfigurowanych żadnych witryn internetowych.",
"messages.team-websites-info": "Strony internetowe mogą być przeglądane przez każdego członka zespołu."

View File

@ -42,6 +42,7 @@
"label.edit": "Editar",
"label.edit-dashboard": "Editar painel",
"label.enable-share-url": "Ativar link de compartilhamento",
"label.event": "Evento",
"label.event-data": "Event data",
"label.events": "Eventos",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Editar",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Ativar link de partilha",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Eventos",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Editare",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Activare adresă URL de distribuire",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Evenimente",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Изменить",
"label.edit-dashboard": "Редактировать дашборд",
"label.enable-share-url": "Разрешить делиться ссылкой",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "События",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "සංස්කරණය කරන්න",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න",
"label.event": "Event",
"label.event-data": "සිදුවීම් දත්ත",
"label.events": "Events",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Upraviť",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Povoliť zdielanie URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Udalosti",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Uredi",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Omogoči URL za skupno rabo",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Dogodki",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Redigera",
"label.edit-dashboard": "Redigera översikt",
"label.enable-share-url": "Aktivera delnings-URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Händelser",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "திருத்துதல்",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "கள முகவரியை பகிரலாம்",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "நிகழ்வுகள்",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "แก้ไข",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "เปิดใช้งานการแชร์ลิงก์",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "เหตุการณ์",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Düzenle",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Olaylar",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Редагувати",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Дозволити ділитися посиланням",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Події",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "ترمیم",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "شیئر یو آر ایل کو فعال کریں",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "واقعات",
"label.field": "Field",

View File

@ -42,6 +42,7 @@
"label.edit": "Chỉnh sửa",
"label.edit-dashboard": "Edit dashboard",
"label.enable-share-url": "Bật khả năng chia sẻ URL",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "Sự kiện",
"label.field": "Field",

View File

@ -2,8 +2,8 @@
"label.access-code": "访问代码",
"label.actions": "用户行为",
"label.activity-log": "活动日志",
"label.add": "Add",
"label.add-description": "Add description",
"label.add": "添加",
"label.add-description": "添加描述",
"label.add-website": "添加网站",
"label.admin": "管理员",
"label.all": "所有",
@ -42,6 +42,7 @@
"label.edit": "编辑",
"label.edit-dashboard": "编辑仪表板",
"label.enable-share-url": "启用共享链接",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "行为类别",
"label.field": "Field",
@ -127,15 +128,15 @@
"label.website": "Website",
"label.website-id": "网站 ID",
"label.websites": "网站",
"label.window": "Window",
"label.window": "窗口",
"label.yesterday": "昨天",
"labels.after": "After",
"labels.average": "Average",
"labels.before": "Before",
"labels.breakdown": "Breakdown",
"labels.contains": "Contains",
"labels.create-report": "Create report",
"labels.description": "Description",
"labels.create-report": "创建报告",
"labels.description": "描述",
"labels.does-not-contain": "Does not contain",
"labels.does-not-equal": "Does not equal",
"labels.equals": "Equals",
@ -154,14 +155,14 @@
"labels.true": "True",
"labels.type": "Type",
"labels.unique": "Unique",
"labels.untitled": "Untitled",
"labels.untitled": "未命名",
"labels.value": "Value",
"message.active-users": "当前在线 {x} 人",
"message.confirm-delete": "你确定要删除 {target} 吗?",
"message.confirm-leave": "你确定要离开 {target} 吗?",
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
"message.delete-account": "To delete this account, type {confirmation} in the box below to confirm.",
"message.delete-website": "To delete this website, type {confirmation} in the box below to confirm.",
"message.delete-account": "确定删除该账户, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
"message.delete-website": "确定删除该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
"message.delete-website-warning": "所有相关数据将会被删除。",
"message.error": "出现错误。",
"message.event-log": "{event} on {url}",
@ -175,7 +176,7 @@
"message.no-teams": "你还没有创建任何团队。",
"message.no-users": "没有任何用户。",
"message.page-not-found": "网页未找到。",
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
"message.reset-website": "确定重置该网站, 请在下面的输入框中输入 {confirmation} 进行二次确认。",
"message.reset-website-warning": "本网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
"message.saved": "保存成功。",
"message.share-url": "这是 {target} 的共享链接。",
@ -184,7 +185,7 @@
"message.tracking-code": "跟踪代码",
"message.user-deleted": "User detected.",
"message.visitor-log": "来自{country}的访客在搭载 {os} 的{device}上使用 {browser} 浏览器进行访问。",
"messages.no-results-found": "No results were found.",
"messages.no-results-found": "没有找到任何结果。",
"messages.no-team-websites": "这个团队没有任何网站。",
"messages.no-websites-configured": "你还没有设置任何网站。",
"messages.team-websites-info": "团队中的任何人都可查看网站。"

View File

@ -42,6 +42,7 @@
"label.edit": "編輯",
"label.edit-dashboard": "編輯管理面板",
"label.enable-share-url": "啟用分享連結",
"label.event": "Event",
"label.event-data": "Event data",
"label.events": "行為類別",
"label.field": "Field",

View File

@ -13,7 +13,7 @@ import {
import { getTeamUser } from 'queries';
import { getTeamWebsite, getTeamWebsiteByTeamMemberId } from 'queries/admin/teamWebsite';
import { validate } from 'uuid';
import { loadWebsite } from './query';
import { loadWebsite } from './load';
import { Auth } from './types';
const log = debug('umami:auth');

View File

@ -2,7 +2,6 @@ import { ClickHouse } from 'clickhouse';
import dateFormat from 'dateformat';
import debug from 'debug';
import { CLICKHOUSE } from 'lib/db';
import { getDynamicDataType } from './dynamicData';
import { WebsiteMetricFilter } from './types';
import { FILTER_COLUMNS } from './constants';
@ -62,49 +61,6 @@ function getDateFormat(date) {
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
}
function getBetweenDates(field, startAt, endAt) {
return `${field} between ${getDateFormat(startAt)} and ${getDateFormat(endAt)}`;
}
function getEventDataFilterQuery(
filters: {
eventKey?: string;
eventValue?: string | number | boolean | Date;
}[] = [],
params: any,
) {
const query = filters.reduce((ac, cv, i) => {
const type = getDynamicDataType(cv.eventValue);
let value = cv.eventValue;
ac.push(`and (event_key = {eventKey${i}:String}`);
switch (type) {
case 'number':
ac.push(`and number_value = {eventValue${i}:UInt64})`);
break;
case 'string':
ac.push(`and string_value = {eventValue${i}:String})`);
break;
case 'boolean':
ac.push(`and string_value = {eventValue${i}:String})`);
value = cv ? 'true' : 'false';
break;
case 'date':
ac.push(`and date_value = {eventValue${i}:DateTime('UTC')})`);
break;
}
params[`eventKey${i}`] = cv.eventKey;
params[`eventValue${i}`] = value;
return ac;
}, []);
return query.join('\n');
}
function getFilterQuery(filters = {}, params = {}) {
const query = Object.keys(filters).reduce((arr, key) => {
const filter = filters[key];
@ -150,7 +106,7 @@ function parseFilters(filters: WebsiteMetricFilter = {}, params: any = {}) {
};
}
async function rawQuery<T>(query, params = {}): Promise<T> {
async function rawQuery<T>(query: string, params: object = {}): Promise<T> {
if (process.env.LOG_QUERY) {
log('QUERY:\n', query);
log('PARAMETERS:\n', params);
@ -166,7 +122,7 @@ async function findUnique(data) {
throw `${data.length} records found when expecting 1.`;
}
return data[0] ?? null;
return findFirst(data);
}
async function findFirst(data) {
@ -189,10 +145,8 @@ export default {
getDateStringQuery,
getDateQuery,
getDateFormat,
getBetweenDates,
getFilterQuery,
getFunnelQuery,
getEventDataFilterQuery,
parseFilters,
findUnique,
findFirst,

View File

@ -18,7 +18,7 @@ export const DEFAULT_THEME = 'light';
export const DEFAULT_ANIMATION_DURATION = 300;
export const DEFAULT_DATE_RANGE = '24hour';
export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_CREATED_AT = '2000-01-01';
export const DEFAULT_RESET_DATE = '2000-01-01';
export const REALTIME_RANGE = 30;
export const REALTIME_INTERVAL = 5000;
@ -120,6 +120,37 @@ export const ROLE_PERMISSIONS = {
[ROLES.teamMember]: [],
} as const;
export const WEBSITE_EVENT_FIELDS = {
eventId: { name: 'event_id', type: 'uuid', label: 'Event ID' },
websiteId: { name: 'website_id', type: 'uuid', label: 'Website ID' },
sessionId: { name: 'session_id', type: 'uuid', label: 'Session ID' },
createdAt: { name: 'created_at', type: 'date', label: 'Created date' },
urlPath: { name: 'url_path', type: 'string', label: 'URL path' },
urlQuery: { name: 'url_query', type: 'string', label: 'URL query' },
referrerPath: { name: 'referrer_path', type: 'string', label: 'Referrer path' },
referrerQuery: { name: 'referrer_query', type: 'string', label: 'Referrer query' },
referrerDomain: { name: 'referrer_domain', type: 'string', label: 'Referrer domain' },
pageTitle: { name: 'page_title', type: 'string', label: 'Page title' },
eventType: { name: 'event_type', type: 'string', label: 'Event type' },
eventName: { name: 'event_name', type: 'string', label: 'Event name' },
};
export const SESSION_FIELDS = {
sessionId: { name: 'session_id', type: 'uuid' },
websiteId: { name: 'website_id', type: 'uuid' },
hostname: { name: 'hostname', type: 'string' },
browser: { name: 'browser', type: 'string' },
os: { name: 'os', type: 'string' },
device: { name: 'device', type: 'string' },
screen: { name: 'screen', type: 'string' },
language: { name: 'language', type: 'string' },
country: { name: 'country', type: 'string' },
subdivision1: { name: 'subdivision1', type: 'string' },
subdivision2: { name: 'subdivision2', type: 'string' },
city: { name: 'city', type: 'string' },
createdAt: { name: 'created_at', type: 'date' },
};
export const THEME_COLORS = {
light: {
primary: '#2680eb',
@ -166,10 +197,9 @@ export const EVENT_COLORS = [
'#ffec16',
];
export const DOMAIN_REGEX =
export const DOMAIN_REGEX =
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9-]+(-[a-z0-9-]+)*\.)+(xn--)?[a-z0-9-]{2,63})$/;
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{16}$/;
export const DESKTOP_SCREEN_WIDTH = 1920;

View File

@ -1,5 +1,3 @@
import crypto from 'crypto';
import { v4, v5 } from 'uuid';
import { startOfMonth } from 'date-fns';
import { hash } from 'next-basics';
@ -12,13 +10,3 @@ export function salt() {
return hash(secret(), ROTATING_SALT);
}
export function uuid(...args) {
if (!args.length) return v4();
return v5(hash(...args, salt()), v5.DNS);
}
export function md5(...args) {
return crypto.createHash('md5').update(args.join('')).digest('hex');
}

View File

@ -26,10 +26,20 @@ import {
differenceInCalendarMonths,
differenceInCalendarYears,
format,
parseISO,
max,
min,
isDate,
} from 'date-fns';
import { getDateLocale } from 'lib/lang';
const dateFuncs = {
minute: [differenceInMinutes, addMinutes, startOfMinute],
hour: [differenceInHours, addHours, startOfHour],
day: [differenceInCalendarDays, addDays, startOfDay],
month: [differenceInCalendarMonths, addMonths, startOfMonth],
year: [differenceInCalendarYears, addYears, startOfYear],
};
export function getTimezone() {
return moment.tz.guess();
}
@ -43,11 +53,19 @@ export function parseDateRange(value, locale = 'en-US') {
return value;
}
if (value?.startsWith?.('range')) {
const [, startAt, endAt] = value.split(':');
if (value === 'all') {
return {
startDate: new Date(0),
endDate: new Date(1),
value,
};
}
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
if (value?.startsWith?.('range')) {
const [, startTime, endTime] = value.split(':');
const startDate = new Date(+startTime);
const endDate = new Date(+endTime);
return {
...getDateRangeValues(startDate, endDate),
@ -148,17 +166,34 @@ export function parseDateRange(value, locale = 'en-US') {
}
}
export function getDateRangeValues(startDate, endDate) {
let unit = 'year';
if (differenceInHours(endDate, startDate) <= 48) {
unit = 'hour';
export function getAllowedUnits(startDate, endDate) {
const units = ['minute', 'hour', 'day', 'month', 'year'];
const minUnit = getMinimumUnit(startDate, endDate);
const index = units.indexOf(minUnit);
return index >= 0 ? units.splice(index) : [];
}
export function getMinimumUnit(startDate, endDate) {
if (differenceInMinutes(endDate, startDate) <= 60) {
return 'minute';
} else if (differenceInHours(endDate, startDate) <= 48) {
return 'hour';
} else if (differenceInCalendarDays(endDate, startDate) <= 90) {
unit = 'day';
return 'day';
} else if (differenceInCalendarMonths(endDate, startDate) <= 24) {
unit = 'month';
return 'month';
}
return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit };
return 'year';
}
export function getDateRangeValues(startDate, endDate) {
return {
startDate: startOfDay(startDate),
endDate: endOfDay(endDate),
unit: getMinimumUnit(startDate, endDate),
};
}
export function getDateFromString(str) {
@ -174,14 +209,6 @@ export function getDateFromString(str) {
return new Date(year, month - 1, day);
}
const dateFuncs = {
minute: [differenceInMinutes, addMinutes, startOfMinute],
hour: [differenceInHours, addHours, startOfHour],
day: [differenceInCalendarDays, addDays, startOfDay],
month: [differenceInCalendarMonths, addMonths, startOfMonth],
year: [differenceInCalendarYears, addYears, startOfYear],
};
export function getDateArray(data, startDate, endDate, unit) {
const arr = [];
const [diff, add, normalize] = dateFuncs[unit];
@ -227,3 +254,11 @@ export function dateFormat(date, str, locale = 'en-US') {
locale: getDateLocale(locale),
});
}
export function maxDate(...args) {
return max(args.filter(n => isDate(n)));
}
export function minDate(...args) {
return min(args.filter(n => isDate(n)));
}

View File

@ -35,3 +35,7 @@ export async function runQuery(queries) {
return queries[CLICKHOUSE]();
}
}
export function notImplemented() {
throw new Error('Not implemented.');
}

View File

@ -1,5 +1,5 @@
import path from 'path';
import requestIp from 'request-ip';
import { getClientIp } from 'request-ip';
import { browserName, detectOS } from 'detect-browser';
import isLocalhost from 'is-localhost-ip';
import maxmind from 'maxmind';
@ -25,7 +25,7 @@ export function getIpAddress(req) {
return req.headers['cf-connecting-ip'];
}
return requestIp.getClientIp(req);
return getClientIp(req);
}
export function getDevice(screen, os) {
@ -62,6 +62,14 @@ export async function getLocation(ip, req) {
return;
}
// Cloudflare headers
if (req.headers['cf-ipcountry']) {
return {
country: req.headers['cf-ipcountry'],
};
}
// Vercel headers
if (req.headers['x-vercel-ip-country']) {
const country = req.headers['x-vercel-ip-country'];
const region = req.headers['x-vercel-ip-country-region'];

View File

@ -61,7 +61,7 @@ async function getProducer(): Promise<Producer> {
return producer;
}
function getDateFormat(date, format?): string {
function getDateFormat(date: Date, format?: string): string {
return dateFormat(date, format ? format : 'UTC:yyyy-mm-dd HH:MM:ss');
}

51
lib/load.ts Normal file
View File

@ -0,0 +1,51 @@
import cache from 'lib/cache';
import { getWebsite, getSession, getUser } from 'queries';
import { User, Website, Session } from '@prisma/client';
export async function loadWebsite(websiteId: string): Promise<Website> {
let website;
if (cache.enabled) {
website = await cache.fetchWebsite(websiteId);
} else {
website = await getWebsite({ id: websiteId });
}
if (!website || website.deletedAt) {
return null;
}
return website;
}
export async function loadSession(sessionId: string): Promise<Session> {
let session;
if (cache.enabled) {
session = await cache.fetchSession(sessionId);
} else {
session = await getSession({ id: sessionId });
}
if (!session) {
return null;
}
return session;
}
export async function loadUser(userId: string): Promise<User> {
let user;
if (cache.enabled) {
user = await cache.fetchUser(userId);
} else {
user = await getUser({ id: userId });
}
if (!user || user.deletedAt) {
return null;
}
return user;
}

View File

@ -73,5 +73,6 @@ export const useAuth = createMiddleware(async (req, res, next) => {
}
(req as any).auth = { user, token, shareToken, authKey };
next();
});

View File

@ -1,7 +1,6 @@
import prisma from '@umami/prisma-client';
import moment from 'moment-timezone';
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { getDynamicDataType } from './dynamicData';
import { FILTER_COLUMNS } from './constants';
const MYSQL_DATE_FORMATS = {
@ -20,20 +19,8 @@ const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01',
};
function toUuid(): string {
const db = getDatabaseType(process.env.DATABASE_URL);
if (db === POSTGRESQL) {
return '::uuid';
}
if (db === MYSQL) {
return '';
}
}
function getAddMinutesQuery(field: string, minutes: number) {
const db = getDatabaseType(process.env.DATABASE_URL);
const db = getDatabaseType();
if (db === POSTGRESQL) {
return `${field} + interval '${minutes} minute'`;
@ -45,7 +32,7 @@ function getAddMinutesQuery(field: string, minutes: number) {
}
function getDateQuery(field: string, unit: string, timezone?: string): string {
const db = getDatabaseType(process.env.DATABASE_URL);
const db = getDatabaseType();
if (db === POSTGRESQL) {
if (timezone) {
@ -65,8 +52,8 @@ function getDateQuery(field: string, unit: string, timezone?: string): string {
}
}
function getTimestampInterval(field: string): string {
const db = getDatabaseType(process.env.DATABASE_URL);
function getTimestampIntervalQuery(field: string): string {
const db = getDatabaseType();
if (db === POSTGRESQL) {
return `floor(extract(epoch from max(${field}) - min(${field})))`;
@ -77,47 +64,6 @@ function getTimestampInterval(field: string): string {
}
}
function getEventDataFilterQuery(
filters: {
eventKey?: string;
eventValue?: string | number | boolean | Date;
}[],
params: any[],
) {
const query = filters.reduce((ac, cv) => {
const type = getDynamicDataType(cv.eventValue);
let value = cv.eventValue;
ac.push(`and (event_key = $${params.length + 1}`);
params.push(cv.eventKey);
switch (type) {
case 'number':
ac.push(`and number_value = $${params.length + 1})`);
params.push(value);
break;
case 'string':
ac.push(`and string_value = $${params.length + 1})`);
params.push(decodeURIComponent(cv.eventValue as string));
break;
case 'boolean':
ac.push(`and string_value = $${params.length + 1})`);
params.push(decodeURIComponent(cv.eventValue as string));
value = cv ? 'true' : 'false';
break;
case 'date':
ac.push(`and date_value = $${params.length + 1})`);
params.push(cv.eventValue);
break;
}
return ac;
}, []);
return query.join('\n');
}
function getFilterQuery(filters = {}, params = []): string {
const query = Object.keys(filters).reduce((arr, key) => {
const filter = filters[key];
@ -163,7 +109,7 @@ function getFunnelQuery(
and l0.referrer_path = $${i + initParamLength}
and l0.url_path = $${levelNumber + initParamLength}
and created_at between $2 and $3
and website_id = $1${toUuid()}
and website_id = $1
)`;
}
@ -197,27 +143,32 @@ function parseFilters(
};
}
async function rawQuery(query: string, params: never[] = []): Promise<any> {
const db = getDatabaseType(process.env.DATABASE_URL);
async function rawQuery(sql: string, data: object): Promise<any> {
const db = getDatabaseType();
const params = [];
if (db !== POSTGRESQL && db !== MYSQL) {
return Promise.reject(new Error('Unknown database.'));
}
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
const query = sql?.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*}}/g, (...args) => {
const [, name, type] = args;
return prisma.rawQuery(sql, params);
params.push(data[name]);
return db === MYSQL ? '?' : `$${params.length}${type ?? ''}`;
});
return prisma.rawQuery(query, params);
}
export default {
...prisma,
getAddMinutesQuery,
getDateQuery,
getTimestampInterval,
getTimestampIntervalQuery,
getFilterQuery,
getFunnelQuery,
getEventDataFilterQuery,
toUuid,
parseFilters,
rawQuery,
};

View File

@ -1,51 +1,31 @@
import cache from 'lib/cache';
import { getWebsite, getSession, getUser } from 'queries';
import { User, Website, Session } from '@prisma/client';
import { NextApiRequest } from 'next';
import { getAllowedUnits, getMinimumUnit } from './date';
import { getWebsiteDateRange } from '../queries';
export async function loadWebsite(websiteId: string): Promise<Website> {
let website;
export async function parseDateRangeQuery(req: NextApiRequest) {
const { id: websiteId, startAt, endAt, unit } = req.query;
if (cache.enabled) {
website = await cache.fetchWebsite(websiteId);
} else {
website = await getWebsite({ id: websiteId });
// All-time
if (+startAt === 0 && +endAt === 1) {
const result = await getWebsiteDateRange(websiteId as string);
const { min, max } = result[0];
const startDate = new Date(min);
const endDate = new Date(max);
return {
startDate,
endDate,
unit: getMinimumUnit(startDate, endDate),
};
}
if (!website || website.deletedAt) {
return null;
}
const startDate = new Date(+startAt);
const endDate = new Date(+endAt);
const minUnit = getMinimumUnit(startDate, endDate);
return website;
}
export async function loadSession(sessionId: string): Promise<Session> {
let session;
if (cache.enabled) {
session = await cache.fetchSession(sessionId);
} else {
session = await getSession({ id: sessionId });
}
if (!session) {
return null;
}
return session;
}
export async function loadUser(userId: string): Promise<User> {
let user;
if (cache.enabled) {
user = await cache.fetchUser(userId);
} else {
user = await getUser({ id: userId });
}
if (!user || user.deletedAt) {
return null;
}
return user;
return {
startDate,
endDate,
unit: (getAllowedUnits(startDate, endDate).includes(unit as string) ? unit : minUnit) as string,
};
}

View File

@ -1,11 +1,11 @@
import { secret, uuid } from 'lib/crypto';
import { secret } from 'lib/crypto';
import { getClientInfo, getJsonBody } from 'lib/detect';
import { parseToken } from 'next-basics';
import { parseToken, uuid } from 'next-basics';
import { CollectRequestBody, NextApiRequestCollect } from 'pages/api/send';
import { createSession } from 'queries';
import { validate } from 'uuid';
import cache from './cache';
import { loadSession, loadWebsite } from './query';
import { loadSession, loadWebsite } from './load';
export async function findSession(req: NextApiRequestCollect) {
const { payload } = getJsonBody<CollectRequestBody>(req);
@ -30,6 +30,12 @@ export async function findSession(req: NextApiRequestCollect) {
// Verify payload
const { website: websiteId, hostname, screen, language } = payload;
// Check the hostname value for legality to eliminate dirty data
const validHostnameRegex = /^[\w-.]+$/;
if (!validHostnameRegex.test(hostname)) {
throw new Error('Invalid hostname.');
}
if (!validate(websiteId)) {
throw new Error('Invalid website ID.');
}

0
lib/sql.ts Normal file
View File

View File

@ -137,3 +137,10 @@ export interface RealtimeUpdate {
events: any[];
timestamp: number;
}
export interface DateRange {
startDate: Date;
endDate: Date;
unit: string;
value: string;
}

View File

@ -90,7 +90,7 @@
"maxmind": "^4.3.6",
"moment-timezone": "^0.5.35",
"next": "13.3.1",
"next-basics": "^0.31.0",
"next-basics": "^0.33.0",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"react": "^18.2.0",

Some files were not shown because too many files have changed in this diff Show More