mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 09:45:04 +01:00
Feat/um 49 query builder api (#1573)
* add uuid to event. add indexes * eventdata api * add event data * remove test data * update list
This commit is contained in:
parent
9c36dc485e
commit
ba31f48f1a
48
components/common/EventDataButton.js
Normal file
48
components/common/EventDataButton.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import List from 'assets/list-ul.svg';
|
||||||
|
import Modal from 'components/common/Modal';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from './Button';
|
||||||
|
import EventDataForm from 'components/forms/EventDataForm';
|
||||||
|
import styles from './EventDataButton.module.css';
|
||||||
|
|
||||||
|
function EventDataButton({ websiteId }) {
|
||||||
|
const [showEventData, setShowEventData] = useState(false);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!showEventData) {
|
||||||
|
setShowEventData(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
setShowEventData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
icon={<List />}
|
||||||
|
tooltip={<FormattedMessage id="label.event-data" defaultMessage="Event" />}
|
||||||
|
tooltipId="button-event"
|
||||||
|
size="small"
|
||||||
|
onClick={handleClick}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
Event Data
|
||||||
|
</Button>
|
||||||
|
{showEventData && (
|
||||||
|
<Modal title={<FormattedMessage id="label.event-data" defaultMessage="Query Event Data" />}>
|
||||||
|
<EventDataForm websiteId={websiteId} onClose={handleClose} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EventDataButton.propTypes = {
|
||||||
|
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventDataButton;
|
3
components/common/EventDataButton.module.css
Normal file
3
components/common/EventDataButton.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.button {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
262
components/forms/EventDataForm.js
Normal file
262
components/forms/EventDataForm.js
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import Button from 'components/common/Button';
|
||||||
|
import DateFilter from 'components/common/DateFilter';
|
||||||
|
import DropDown from 'components/common/DropDown';
|
||||||
|
import FormLayout, {
|
||||||
|
FormButtons,
|
||||||
|
FormError,
|
||||||
|
FormMessage,
|
||||||
|
FormRow,
|
||||||
|
} from 'components/layout/FormLayout';
|
||||||
|
import DataTable from 'components/metrics/DataTable';
|
||||||
|
import FilterTags from 'components/metrics/FilterTags';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
import useDateRange from 'hooks/useDateRange';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import styles from './EventDataForm.module.css';
|
||||||
|
import useTimezone from 'hooks/useTimezone';
|
||||||
|
|
||||||
|
export const filterOptions = [
|
||||||
|
{ label: 'Count', value: 'count' },
|
||||||
|
{ label: 'Average', value: 'avg' },
|
||||||
|
{ label: 'Minimum', value: 'min' },
|
||||||
|
{ label: 'Maxmimum', value: 'max' },
|
||||||
|
{ label: 'Sum', value: 'sum' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dateOptions = [
|
||||||
|
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} />
|
||||||
|
),
|
||||||
|
value: '24hour',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.yesterday" defaultMessage="Yesterday" />,
|
||||||
|
value: '-1day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
|
||||||
|
value: '1week',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
|
||||||
|
),
|
||||||
|
value: '7day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
|
||||||
|
value: '1month',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
|
||||||
|
),
|
||||||
|
value: '30day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 90 }} />
|
||||||
|
),
|
||||||
|
value: '90day',
|
||||||
|
},
|
||||||
|
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
|
||||||
|
value: 'custom',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EventDataForm({ websiteId, onClose, className }) {
|
||||||
|
const { post } = useApi();
|
||||||
|
const [message, setMessage] = useState();
|
||||||
|
const [columns, setColumns] = useState();
|
||||||
|
const [filters, setFilters] = useState();
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [dateRange, setDateRange] = useDateRange('report');
|
||||||
|
const { startDate, endDate, value } = dateRange;
|
||||||
|
const [timezone] = useTimezone();
|
||||||
|
const [isValid, setIsValid] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(columns).length > 0) {
|
||||||
|
setIsValid(true);
|
||||||
|
} else {
|
||||||
|
setIsValid(false);
|
||||||
|
}
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
const handleAddTag = (value, list, setState, resetForm) => {
|
||||||
|
setState({ ...list, [`${value.field}`]: value.value });
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveTag = (value, list, setState) => {
|
||||||
|
const next = { ...list };
|
||||||
|
|
||||||
|
delete next[`${value}`];
|
||||||
|
|
||||||
|
setState(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const params = {
|
||||||
|
website_id: websiteId,
|
||||||
|
start_at: +startDate,
|
||||||
|
end_at: +endDate,
|
||||||
|
timezone,
|
||||||
|
columns,
|
||||||
|
filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { ok, data } = await post(`/websites/${websiteId}/eventdata`, params);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
setMessage(<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />);
|
||||||
|
setData([]);
|
||||||
|
} else {
|
||||||
|
setData(data);
|
||||||
|
setMessage(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormMessage>{message}</FormMessage>
|
||||||
|
<div className={classNames(styles.container, className)}>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<FormLayout>
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="date-range">
|
||||||
|
<FormattedMessage id="label.date-range" defaultMessage="Date Range" />
|
||||||
|
</label>
|
||||||
|
<DateFilter
|
||||||
|
value={value}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={setDateRange}
|
||||||
|
options={dateOptions}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ field: '', value: '' }}
|
||||||
|
onSubmit={(value, { resetForm }) =>
|
||||||
|
handleAddTag(value, columns, setColumns, resetForm)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ values, setFieldValue }) => (
|
||||||
|
<Form>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="field">
|
||||||
|
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<Field name="field" type="text" />
|
||||||
|
<FormError name="field" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="value">
|
||||||
|
<FormattedMessage id="label.type" defaultMessage="Type" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<DropDown
|
||||||
|
value={values.value}
|
||||||
|
onChange={value => setFieldValue('value', value)}
|
||||||
|
className={styles.dropdown}
|
||||||
|
name="value"
|
||||||
|
options={filterOptions}
|
||||||
|
/>
|
||||||
|
<FormError name="value" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormButtons className={styles.formButtons}>
|
||||||
|
<Button
|
||||||
|
variant="action"
|
||||||
|
type="submit"
|
||||||
|
disabled={!values.field || !values.value}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="label.add-column" defaultMessage="Add Column" />
|
||||||
|
</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
<FilterTags
|
||||||
|
className={styles.filterTag}
|
||||||
|
params={columns}
|
||||||
|
onClick={value => handleRemoveTag(value, columns, setColumns)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ field: '', value: '' }}
|
||||||
|
onSubmit={(value, { resetForm }) =>
|
||||||
|
handleAddTag(value, filters, setFilters, resetForm)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ values }) => (
|
||||||
|
<Form>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="field">
|
||||||
|
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<Field name="field" type="text" />
|
||||||
|
<FormError name="field" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="value">
|
||||||
|
<FormattedMessage id="label.value" defaultMessage="Value" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<Field name="value" type="text" />
|
||||||
|
<FormError name="value" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormButtons className={styles.formButtons}>
|
||||||
|
<Button
|
||||||
|
variant="action"
|
||||||
|
type="submit"
|
||||||
|
disabled={!values.field || !values.value}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="label.add-filter" defaultMessage="Add Filter" />
|
||||||
|
</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
<FilterTags
|
||||||
|
className={styles.filterTag}
|
||||||
|
params={filters}
|
||||||
|
onClick={value => handleRemoveTag(value, filters, setFilters)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormLayout>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DataTable className={styles.table} data={data} title="Results" showPercentage={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormButtons>
|
||||||
|
<Button variant="action" onClick={handleSubmit} disabled={!isValid}>
|
||||||
|
<FormattedMessage id="label.search" defaultMessage="Search" />
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose}>
|
||||||
|
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||||
|
</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
38
components/forms/EventDataForm.module.css
Normal file
38
components/forms/EventDataForm.module.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
border-right: 1px solid var(--gray300);
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
padding: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters + .filters {
|
||||||
|
border-top: 1px solid var(--gray300);
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 430px;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formButtons {
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
min-height: 39px;
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterTag {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 10px 5px 5px 5px;
|
||||||
|
}
|
@ -16,6 +16,7 @@ export default function DataTable({
|
|||||||
height,
|
height,
|
||||||
animate = true,
|
animate = true,
|
||||||
virtualize = false,
|
virtualize = false,
|
||||||
|
showPercentage = true,
|
||||||
}) {
|
}) {
|
||||||
const [format, setFormat] = useState(true);
|
const [format, setFormat] = useState(true);
|
||||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||||
@ -38,6 +39,7 @@ export default function DataTable({
|
|||||||
animate={animate && !virtualize}
|
animate={animate && !virtualize}
|
||||||
format={formatFunc}
|
format={formatFunc}
|
||||||
onClick={handleSetFormat}
|
onClick={handleSetFormat}
|
||||||
|
showPercentage={showPercentage}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -68,7 +70,15 @@ export default function DataTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
|
const AnimatedRow = ({
|
||||||
|
label,
|
||||||
|
value = 0,
|
||||||
|
percent,
|
||||||
|
animate,
|
||||||
|
format,
|
||||||
|
onClick,
|
||||||
|
showPercentage = true,
|
||||||
|
}) => {
|
||||||
const props = useSpring({
|
const props = useSpring({
|
||||||
width: percent,
|
width: percent,
|
||||||
y: value,
|
y: value,
|
||||||
@ -82,6 +92,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) =>
|
|||||||
<div className={styles.value} onClick={onClick}>
|
<div className={styles.value} onClick={onClick}>
|
||||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||||
</div>
|
</div>
|
||||||
|
{showPercentage && (
|
||||||
<div className={styles.percent}>
|
<div className={styles.percent}>
|
||||||
<animated.div
|
<animated.div
|
||||||
className={styles.bar}
|
className={styles.bar}
|
||||||
@ -91,6 +102,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) =>
|
|||||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||||
</animated.span>
|
</animated.span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,12 +5,12 @@ import Button from 'components/common/Button';
|
|||||||
import Times from 'assets/times.svg';
|
import Times from 'assets/times.svg';
|
||||||
import styles from './FilterTags.module.css';
|
import styles from './FilterTags.module.css';
|
||||||
|
|
||||||
export default function FilterTags({ params, onClick }) {
|
export default function FilterTags({ className, params, onClick }) {
|
||||||
if (Object.keys(params).filter(key => params[key]).length === 0) {
|
if (Object.keys(params).filter(key => params[key]).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.filters, 'col-12')}>
|
<div className={classNames(styles.filters, 'col-12', className)}>
|
||||||
{Object.keys(params).map(key => {
|
{Object.keys(params).map(key => {
|
||||||
if (!params[key]) {
|
if (!params[key]) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -7,8 +7,5 @@
|
|||||||
.tag {
|
.tag {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
margin-right: 20px;
|
||||||
|
|
||||||
.tag + .tag {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import Favicon from 'components/common/Favicon';
|
||||||
import Link from 'components/common/Link';
|
import Link from 'components/common/Link';
|
||||||
import OverflowText from 'components/common/OverflowText';
|
import OverflowText from 'components/common/OverflowText';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
|
||||||
import RefreshButton from 'components/common/RefreshButton';
|
import RefreshButton from 'components/common/RefreshButton';
|
||||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import Favicon from 'components/common/Favicon';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ActiveUsers from './ActiveUsers';
|
import ActiveUsers from './ActiveUsers';
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
|
||||||
import styles from './WebsiteHeader.module.css';
|
import styles from './WebsiteHeader.module.css';
|
||||||
|
|
||||||
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
||||||
|
@ -24,9 +24,9 @@ export default function TestConsole() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = data.map(({ name, websiteId }) => ({ label: name, value: websiteId }));
|
const options = data.map(({ name, websiteUuid }) => ({ label: name, value: websiteUuid }));
|
||||||
const website = data.find(({ websiteId }) => websiteId === +websiteId);
|
const website = data.find(({ websiteUuid }) => websiteId === websiteUuid);
|
||||||
const selectedValue = options.find(({ value }) => value === website?.websiteId)?.value;
|
const selectedValue = options.find(({ value }) => value === website?.websiteUuid)?.value;
|
||||||
|
|
||||||
function handleSelect(value) {
|
function handleSelect(value) {
|
||||||
router.push(`/console/${value}`);
|
router.push(`/console/${value}`);
|
||||||
@ -104,13 +104,13 @@ export default function TestConsole() {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<WebsiteChart
|
<WebsiteChart
|
||||||
websiteId={website.websiteId}
|
websiteId={website.websiteUuid}
|
||||||
title={website.name}
|
title={website.name}
|
||||||
domain={website.domain}
|
domain={website.domain}
|
||||||
showLink
|
showLink
|
||||||
/>
|
/>
|
||||||
<PageHeader>Events</PageHeader>
|
<PageHeader>Events</PageHeader>
|
||||||
<EventsChart websiteId={website.websiteId} />
|
<EventsChart websiteId={website.websiteUuid} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -24,6 +24,7 @@ import useFetch from 'hooks/useFetch';
|
|||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||||
import styles from './WebsiteDetails.module.css';
|
import styles from './WebsiteDetails.module.css';
|
||||||
|
import EventDataButton from 'components/common/EventDataButton';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
|
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
|
||||||
@ -183,6 +184,7 @@ export default function WebsiteDetails({ websiteId }) {
|
|||||||
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
<GridColumn xs={12} md={12} lg={8}>
|
<GridColumn xs={12} md={12} lg={8}>
|
||||||
|
<EventDataButton websiteId={websiteId} />
|
||||||
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE `account` ADD COLUMN `account_uuid` VARCHAR(36);
|
|
||||||
|
|
||||||
-- Backfill UUID
|
|
||||||
UPDATE `account` SET account_uuid=(SELECT uuid());
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE `account` MODIFY `account_uuid` VARCHAR(36) NOT NULL;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX `account_account_uuid_key` ON `account`(`account_uuid`);
|
|
35
db/mysql/migrations/04_add_uuid/migration.sql
Normal file
35
db/mysql/migrations/04_add_uuid/migration.sql
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `account` ADD COLUMN `account_uuid` VARCHAR(36);
|
||||||
|
|
||||||
|
-- Backfill UUID
|
||||||
|
UPDATE `account` SET account_uuid=(SELECT uuid());
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `account` MODIFY `account_uuid` VARCHAR(36) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX `account_account_uuid_key` ON `account`(`account_uuid`);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `event` ADD COLUMN `event_uuid` VARCHAR(36);
|
||||||
|
|
||||||
|
-- Backfill UUID
|
||||||
|
UPDATE `event` SET event_uuid=(SELECT uuid());
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `event` MODIFY `event_uuid` VARCHAR(36) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX `event_event_uuid_key` ON `event`(`event_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `account_account_uuid_idx` ON `account`(`account_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_session_uuid_idx` ON `session`(`session_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_website_uuid_idx` ON `website`(`website_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `event_event_uuid_idx` ON `event`(`event_uuid`);
|
@ -16,6 +16,8 @@ model account {
|
|||||||
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamp(0)
|
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamp(0)
|
||||||
accountUuid String @unique() @map("account_uuid") @db.VarChar(36)
|
accountUuid String @unique() @map("account_uuid") @db.VarChar(36)
|
||||||
website website[]
|
website website[]
|
||||||
|
|
||||||
|
@@index([accountUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model event {
|
model event {
|
||||||
@ -25,6 +27,7 @@ model event {
|
|||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
eventName String @map("event_name") @db.VarChar(50)
|
eventName String @map("event_name") @db.VarChar(50)
|
||||||
|
eventUuid String @unique() @map("event_uuid") @db.VarChar(36)
|
||||||
session session @relation(fields: [sessionId], references: [id])
|
session session @relation(fields: [sessionId], references: [id])
|
||||||
website website @relation(fields: [websiteId], references: [id])
|
website website @relation(fields: [websiteId], references: [id])
|
||||||
eventData eventData?
|
eventData eventData?
|
||||||
@ -32,6 +35,7 @@ model event {
|
|||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
|
@@index([eventUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model eventData {
|
model eventData {
|
||||||
@ -78,6 +82,7 @@ model session {
|
|||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
|
@@index([sessionUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model website {
|
model website {
|
||||||
@ -94,4 +99,5 @@ model website {
|
|||||||
session session[]
|
session session[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@index([websiteUuid])
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,18 @@ ALTER TABLE "account" ALTER COLUMN "account_uuid" SET NOT NULL;
|
|||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "account_account_uuid_key" ON "account"("account_uuid");
|
CREATE UNIQUE INDEX "account_account_uuid_key" ON "account"("account_uuid");
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "event" ADD COLUMN "event_uuid" UUID NULL;
|
||||||
|
|
||||||
|
-- Backfill UUID
|
||||||
|
UPDATE "event" SET event_uuid = gen_random_uuid();
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "event" ALTER COLUMN "event_uuid" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "event_event_uuid_key" ON "event"("event_uuid");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "account_account_uuid_idx" ON "account"("account_uuid");
|
CREATE INDEX "account_account_uuid_idx" ON "account"("account_uuid");
|
||||||
|
|
||||||
@ -19,3 +31,6 @@ CREATE INDEX "session_session_uuid_idx" ON "session"("session_uuid");
|
|||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "website_website_uuid_idx" ON "website"("website_uuid");
|
CREATE INDEX "website_website_uuid_idx" ON "website"("website_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_event_uuid_idx" ON "event"("event_uuid");
|
@ -16,6 +16,8 @@ model account {
|
|||||||
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamptz(6)
|
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamptz(6)
|
||||||
accountUuid String @unique @map("account_uuid") @db.Uuid
|
accountUuid String @unique @map("account_uuid") @db.Uuid
|
||||||
website website[]
|
website website[]
|
||||||
|
|
||||||
|
@@index([accountUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model event {
|
model event {
|
||||||
@ -25,6 +27,7 @@ model event {
|
|||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
eventName String @map("event_name") @db.VarChar(50)
|
eventName String @map("event_name") @db.VarChar(50)
|
||||||
|
eventUuid String @unique @map("event_uuid") @db.Uuid
|
||||||
session session @relation(fields: [sessionId], references: [id])
|
session session @relation(fields: [sessionId], references: [id])
|
||||||
website website @relation(fields: [websiteId], references: [id])
|
website website @relation(fields: [websiteId], references: [id])
|
||||||
eventData eventData?
|
eventData eventData?
|
||||||
@ -32,6 +35,7 @@ model event {
|
|||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
|
@@index([eventUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model eventData {
|
model eventData {
|
||||||
@ -78,6 +82,7 @@ model session {
|
|||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
|
@@index([sessionUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model website {
|
model website {
|
||||||
@ -94,4 +99,5 @@ model website {
|
|||||||
session session[]
|
session session[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@index([websiteUuid])
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"label.accounts": "Accounts",
|
"label.accounts": "Accounts",
|
||||||
"label.add-account": "Add account",
|
"label.add-account": "Add account",
|
||||||
|
"label.add-column": "Add column",
|
||||||
|
"label.add-filter": "Add filter",
|
||||||
"label.add-website": "Add website",
|
"label.add-website": "Add website",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.all": "All",
|
"label.all": "All",
|
||||||
@ -25,6 +27,8 @@
|
|||||||
"label.edit-account": "Edit account",
|
"label.edit-account": "Edit account",
|
||||||
"label.edit-website": "Edit website",
|
"label.edit-website": "Edit website",
|
||||||
"label.enable-share-url": "Enable share URL",
|
"label.enable-share-url": "Enable share URL",
|
||||||
|
"label.event-data": "Event Data",
|
||||||
|
"label.field-name": "Field Name",
|
||||||
"label.invalid": "Invalid",
|
"label.invalid": "Invalid",
|
||||||
"label.invalid-domain": "Invalid domain",
|
"label.invalid-domain": "Invalid domain",
|
||||||
"label.language": "Language",
|
"label.language": "Language",
|
||||||
@ -48,6 +52,7 @@
|
|||||||
"label.reset": "Reset",
|
"label.reset": "Reset",
|
||||||
"label.reset-website": "Reset statistics",
|
"label.reset-website": "Reset statistics",
|
||||||
"label.save": "Save",
|
"label.save": "Save",
|
||||||
|
"label.search": "Search",
|
||||||
"label.settings": "Settings",
|
"label.settings": "Settings",
|
||||||
"label.share-url": "Share URL",
|
"label.share-url": "Share URL",
|
||||||
"label.single-day": "Single day",
|
"label.single-day": "Single day",
|
||||||
@ -58,8 +63,10 @@
|
|||||||
"label.timezone": "Timezone",
|
"label.timezone": "Timezone",
|
||||||
"label.today": "Today",
|
"label.today": "Today",
|
||||||
"label.tracking-code": "Tracking code",
|
"label.tracking-code": "Tracking code",
|
||||||
|
"label.type": "Type",
|
||||||
"label.unknown": "Unknown",
|
"label.unknown": "Unknown",
|
||||||
"label.username": "Username",
|
"label.username": "Username",
|
||||||
|
"label.value": "Value",
|
||||||
"label.view-details": "View details",
|
"label.view-details": "View details",
|
||||||
"label.websites": "Websites",
|
"label.websites": "Websites",
|
||||||
"label.yesterday": "Yesterday",
|
"label.yesterday": "Yesterday",
|
||||||
|
@ -65,8 +65,45 @@ function getCommaSeparatedStringFormat(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBetweenDates(field, start_at, end_at) {
|
function getBetweenDates(field, start_at, end_at) {
|
||||||
return `${field} between ${getDateFormat(start_at)}
|
return `${field} between ${getDateFormat(start_at)} and ${getDateFormat(end_at)}`;
|
||||||
and ${getDateFormat(end_at)}`;
|
}
|
||||||
|
|
||||||
|
function getJsonField(column, property) {
|
||||||
|
return `${column}.${property}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataColumnsQuery(column, columns) {
|
||||||
|
const query = Object.keys(columns).reduce((arr, key) => {
|
||||||
|
const filter = columns[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.push(`${filter}(${getJsonField(column, key)}) as ${key}_${filter}`);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join(',\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataFilterQuery(column, filters) {
|
||||||
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
|
const filter = filters[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.push(
|
||||||
|
`${getJsonField(column, key)} = ${typeof filter === 'string' ? `'${filter}'` : filter}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join('\nand ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilterQuery(column, filters = {}, params = []) {
|
function getFilterQuery(column, filters = {}, params = []) {
|
||||||
@ -186,6 +223,8 @@ export default {
|
|||||||
getDateFormat,
|
getDateFormat,
|
||||||
getCommaSeparatedStringFormat,
|
getCommaSeparatedStringFormat,
|
||||||
getBetweenDates,
|
getBetweenDates,
|
||||||
|
getEventDataColumnsQuery,
|
||||||
|
getEventDataFilterQuery,
|
||||||
getFilterQuery,
|
getFilterQuery,
|
||||||
parseFilters,
|
parseFilters,
|
||||||
findUnique,
|
findUnique,
|
||||||
|
@ -85,6 +85,64 @@ function getTimestampInterval(field) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getJsonField(column, property, isNumber) {
|
||||||
|
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
if (db === POSTGRESQL) {
|
||||||
|
let accessor = `${column} ->> '${property}'`;
|
||||||
|
|
||||||
|
if (isNumber) {
|
||||||
|
accessor = `CAST(${accessor} AS DECIMAL)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db === MYSQL) {
|
||||||
|
return `${column} ->> "$.${property}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataColumnsQuery(column, columns) {
|
||||||
|
const query = Object.keys(columns).reduce((arr, key) => {
|
||||||
|
const filter = columns[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumber = ['sum', 'avg', 'min', 'max'].some(a => a === filter);
|
||||||
|
|
||||||
|
arr.push(`${filter}(${getJsonField(column, key, isNumber)}) as "${filter}(${key})"`);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join(',\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataFilterQuery(column, filters) {
|
||||||
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
|
const filter = filters[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumber = filter && typeof filter === 'number';
|
||||||
|
|
||||||
|
arr.push(
|
||||||
|
`${getJsonField(column, key, isNumber)} = ${
|
||||||
|
typeof filter === 'string' ? `'${filter}'` : filter
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join('\nand ');
|
||||||
|
}
|
||||||
|
|
||||||
function getFilterQuery(table, column, filters = {}, params = []) {
|
function getFilterQuery(table, column, filters = {}, params = []) {
|
||||||
const query = Object.keys(filters).reduce((arr, key) => {
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
const filter = filters[key];
|
const filter = filters[key];
|
||||||
@ -193,6 +251,8 @@ export default {
|
|||||||
getDateQuery,
|
getDateQuery,
|
||||||
getTimestampInterval,
|
getTimestampInterval,
|
||||||
getFilterQuery,
|
getFilterQuery,
|
||||||
|
getEventDataColumnsQuery,
|
||||||
|
getEventDataFilterQuery,
|
||||||
parseFilters,
|
parseFilters,
|
||||||
rawQuery,
|
rawQuery,
|
||||||
transaction,
|
transaction,
|
||||||
|
@ -58,13 +58,11 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
await useSession(req, res);
|
await useSession(req, res);
|
||||||
|
|
||||||
const {
|
const { website, session } = req.session;
|
||||||
session: { website, session },
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
const { type, payload } = getJsonBody(req);
|
const { type, payload } = getJsonBody(req);
|
||||||
|
|
||||||
let { url, referrer, eventName, eventData } = payload;
|
let { url, referrer, event_name: eventName, event_data: eventData } = payload;
|
||||||
|
|
||||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||||
url = url.replace(/\/$/, '');
|
url = url.replace(/\/$/, '');
|
||||||
@ -88,9 +86,8 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const token = createToken(
|
const token = createToken(
|
||||||
{
|
{
|
||||||
websiteId: website.websiteUuid,
|
website,
|
||||||
sessionId: session.sessionId,
|
session,
|
||||||
sessionUuid: session.sessionUuid,
|
|
||||||
},
|
},
|
||||||
secret(),
|
secret(),
|
||||||
);
|
);
|
||||||
|
40
pages/api/websites/[id]/eventdata.js
Normal file
40
pages/api/websites/[id]/eventdata.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { getEventData } from 'queries';
|
||||||
|
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
|
import { allowQuery } from 'lib/auth';
|
||||||
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
if (!(await allowQuery(req))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: websiteId } = req.query;
|
||||||
|
|
||||||
|
const { start_at, end_at, timezone, event_name: eventName, columns, filters } = req.body;
|
||||||
|
|
||||||
|
if (!moment.tz.zone(timezone)) {
|
||||||
|
return badRequest(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(+start_at);
|
||||||
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
|
const events = await getEventData(websiteId, {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
timezone,
|
||||||
|
eventName,
|
||||||
|
columns,
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok(res, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
@ -18,3 +18,9 @@ export default function ConsolePage({ enabled }) {
|
|||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps() {
|
||||||
|
return {
|
||||||
|
props: { enabled: !!process.env.ENABLE_TEST_CONSOLE },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -16,6 +16,6 @@ export default function LoginPage({ loginDisabled }) {
|
|||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
return {
|
return {
|
||||||
props: { loginDisabled: !!process.env.DISABLE_LOGIN || process.env.isCloudMode },
|
props: { loginDisabled: !!process.env.DISABLE_LOGIN || !!process.env.isCloudMode },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
63
queries/analytics/event/getEventData.js
Normal file
63
queries/analytics/event/getEventData.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import clickhouse from 'lib/clickhouse';
|
||||||
|
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||||
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
|
export async function getEventData(...args) {
|
||||||
|
return runQuery({
|
||||||
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relationalQuery(websiteId, { startDate, endDate, event_name, columns, filters }) {
|
||||||
|
const { rawQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma;
|
||||||
|
const params = [startDate, endDate];
|
||||||
|
|
||||||
|
return rawQuery(
|
||||||
|
`select
|
||||||
|
${getEventDataColumnsQuery('event_data.event_data', columns)}
|
||||||
|
from event
|
||||||
|
join website
|
||||||
|
on event.website_id = website.website_id
|
||||||
|
join event_data
|
||||||
|
on event.event_id = event_data.event_id
|
||||||
|
where website_uuid='${websiteId}'
|
||||||
|
and event.created_at between $1 and $2
|
||||||
|
${event_name ? `and event_name = ${event_name}` : ''}
|
||||||
|
${
|
||||||
|
Object.keys(filters).length > 0
|
||||||
|
? `and ${getEventDataFilterQuery('event_data.event_data', filters)}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
params,
|
||||||
|
).then(results => {
|
||||||
|
return Object.keys(results[0]).map(a => {
|
||||||
|
return { x: a, y: results[0][`${a}`] };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickhouseQuery(websiteId, { startDate, endDate, event_name, columns, filters }) {
|
||||||
|
const { rawQuery, getBetweenDates, getEventDataColumnsQuery, getEventDataFilterQuery } =
|
||||||
|
clickhouse;
|
||||||
|
const params = [websiteId];
|
||||||
|
|
||||||
|
return rawQuery(
|
||||||
|
`select
|
||||||
|
${getEventDataColumnsQuery('event_data', columns)}
|
||||||
|
from event
|
||||||
|
where website_id= $1
|
||||||
|
${event_name ? `and event_name = ${event_name}` : ''}
|
||||||
|
and ${getBetweenDates('created_at', startDate, endDate)}
|
||||||
|
${
|
||||||
|
Object.keys(filters).length > 0
|
||||||
|
? `and ${getEventDataFilterQuery('event_data', filters)}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
params,
|
||||||
|
).then(results => {
|
||||||
|
return Object.keys(results[0]).map(a => {
|
||||||
|
return { x: a, y: results[0][`${a}`] };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -18,7 +18,7 @@ async function relationalQuery(
|
|||||||
filters = {},
|
filters = {},
|
||||||
) {
|
) {
|
||||||
const { rawQuery, getDateQuery, getFilterQuery } = prisma;
|
const { rawQuery, getDateQuery, getFilterQuery } = prisma;
|
||||||
const params = [websiteId, start_at, end_at];
|
const params = [start_at, end_at];
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
`select
|
`select
|
||||||
@ -29,7 +29,7 @@ async function relationalQuery(
|
|||||||
join website
|
join website
|
||||||
on event.website_id = website.website_id
|
on event.website_id = website.website_id
|
||||||
where website_uuid='${websiteId}'
|
where website_uuid='${websiteId}'
|
||||||
and event.created_at between $2 and $3
|
and event.created_at between $1 and $2
|
||||||
${getFilterQuery('event', filters, params)}
|
${getFilterQuery('event', filters, params)}
|
||||||
group by 1, 2
|
group by 1, 2
|
||||||
order by 2`,
|
order by 2`,
|
||||||
|
@ -12,13 +12,14 @@ export async function saveEvent(...args) {
|
|||||||
|
|
||||||
async function relationalQuery(
|
async function relationalQuery(
|
||||||
{ websiteId },
|
{ websiteId },
|
||||||
{ session: { id: sessionId }, url, eventName, eventData },
|
{ session: { id: sessionId }, eventUuid, url, eventName, eventData },
|
||||||
) {
|
) {
|
||||||
const data = {
|
const data = {
|
||||||
websiteId,
|
websiteId,
|
||||||
sessionId,
|
sessionId,
|
||||||
url: url?.substring(0, URL_LENGTH),
|
url: url?.substring(0, URL_LENGTH),
|
||||||
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
|
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||||
|
eventUuid,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (eventData) {
|
if (eventData) {
|
||||||
@ -47,7 +48,7 @@ async function clickhouseQuery(
|
|||||||
created_at: getDateFormat(new Date()),
|
created_at: getDateFormat(new Date()),
|
||||||
url: url?.substring(0, URL_LENGTH),
|
url: url?.substring(0, URL_LENGTH),
|
||||||
event_name: eventName?.substring(0, EVENT_NAME_LENGTH),
|
event_name: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||||
event_data: JSON.stringify(eventData),
|
event_data: eventData ? JSON.stringify(eventData) : null,
|
||||||
...sessionArgs,
|
...sessionArgs,
|
||||||
country: country ? country : null,
|
country: country ? country : null,
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ export async function getPageviewMetrics(...args) {
|
|||||||
|
|
||||||
async function relationalQuery(websiteId, { startDate, endDate, column, table, filters = {} }) {
|
async function relationalQuery(websiteId, { startDate, endDate, column, table, filters = {} }) {
|
||||||
const { rawQuery, parseFilters } = prisma;
|
const { rawQuery, parseFilters } = prisma;
|
||||||
const params = [websiteId, startDate, endDate];
|
const params = [startDate, endDate];
|
||||||
const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
|
const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
|
||||||
table,
|
table,
|
||||||
column,
|
column,
|
||||||
@ -25,7 +25,7 @@ async function relationalQuery(websiteId, { startDate, endDate, column, table, f
|
|||||||
${` join website on ${table}.website_id = website.website_id`}
|
${` join website on ${table}.website_id = website.website_id`}
|
||||||
${joinSession}
|
${joinSession}
|
||||||
where website.website_uuid='${websiteId}'
|
where website.website_uuid='${websiteId}'
|
||||||
and ${table}.created_at between $2 and $3
|
and ${table}.created_at between $1 and $2
|
||||||
${pageviewQuery}
|
${pageviewQuery}
|
||||||
${joinSession && sessionQuery}
|
${joinSession && sessionQuery}
|
||||||
${eventQuery}
|
${eventQuery}
|
||||||
|
@ -10,7 +10,7 @@ export async function getPageviewParams(...args) {
|
|||||||
|
|
||||||
async function relationalQuery(websiteId, start_at, end_at, column, table, filters = {}) {
|
async function relationalQuery(websiteId, start_at, end_at, column, table, filters = {}) {
|
||||||
const { parseFilters, rawQuery } = prisma;
|
const { parseFilters, rawQuery } = prisma;
|
||||||
const params = [websiteId, start_at, end_at];
|
const params = [start_at, end_at];
|
||||||
const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
|
const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
|
||||||
table,
|
table,
|
||||||
column,
|
column,
|
||||||
@ -25,7 +25,7 @@ async function relationalQuery(websiteId, start_at, end_at, column, table, filte
|
|||||||
${` join website on ${table}.website_id = website.website_id`}
|
${` join website on ${table}.website_id = website.website_id`}
|
||||||
${joinSession}
|
${joinSession}
|
||||||
where website.website_uuid='${websiteId}'
|
where website.website_uuid='${websiteId}'
|
||||||
and ${table}.created_at between $2 and $3
|
and ${table}.created_at between $1 and $2
|
||||||
and ${table}.url like '%?%'
|
and ${table}.url like '%?%'
|
||||||
${pageviewQuery}
|
${pageviewQuery}
|
||||||
${joinSession && sessionQuery}
|
${joinSession && sessionQuery}
|
||||||
|
@ -22,7 +22,7 @@ async function relationalQuery(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { getDateQuery, parseFilters, rawQuery } = prisma;
|
const { getDateQuery, parseFilters, rawQuery } = prisma;
|
||||||
const params = [websiteId, start_at, end_at];
|
const params = [start_at, end_at];
|
||||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(
|
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(
|
||||||
'pageview',
|
'pageview',
|
||||||
null,
|
null,
|
||||||
@ -38,7 +38,7 @@ async function relationalQuery(
|
|||||||
on pageview.website_id = website.website_id
|
on pageview.website_id = website.website_id
|
||||||
${joinSession}
|
${joinSession}
|
||||||
where website.website_uuid='${websiteId}'
|
where website.website_uuid='${websiteId}'
|
||||||
and pageview.created_at between $2 and $3
|
and pageview.created_at between $1 and $2
|
||||||
${pageviewQuery}
|
${pageviewQuery}
|
||||||
${sessionQuery}
|
${sessionQuery}
|
||||||
group by 1`,
|
group by 1`,
|
||||||
|
@ -11,7 +11,7 @@ export async function getSessionMetrics(...args) {
|
|||||||
|
|
||||||
async function relationalQuery(websiteId, { startDate, endDate, field, filters = {} }) {
|
async function relationalQuery(websiteId, { startDate, endDate, field, filters = {} }) {
|
||||||
const { parseFilters, rawQuery } = prisma;
|
const { parseFilters, rawQuery } = prisma;
|
||||||
const params = [websiteId, startDate, endDate];
|
const params = [startDate, endDate];
|
||||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(null, filters, params);
|
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(null, filters, params);
|
||||||
|
|
||||||
return rawQuery(
|
return rawQuery(
|
||||||
@ -24,7 +24,7 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters =
|
|||||||
on pageview.website_id = website.website_id
|
on pageview.website_id = website.website_id
|
||||||
${joinSession}
|
${joinSession}
|
||||||
where website.website_uuid='${websiteId}'
|
where website.website_uuid='${websiteId}'
|
||||||
and pageview.created_at between $2 and $3
|
and pageview.created_at between $1 and $2
|
||||||
${pageviewQuery}
|
${pageviewQuery}
|
||||||
${sessionQuery}
|
${sessionQuery}
|
||||||
)
|
)
|
||||||
|
@ -12,7 +12,7 @@ export async function getActiveVisitors(...args) {
|
|||||||
|
|
||||||
async function relationalQuery(websiteId) {
|
async function relationalQuery(websiteId) {
|
||||||
const date = subMinutes(new Date(), 5);
|
const date = subMinutes(new Date(), 5);
|
||||||
const params = [websiteId, date];
|
const params = [date];
|
||||||
|
|
||||||
return prisma.rawQuery(
|
return prisma.rawQuery(
|
||||||
`select count(distinct session_id) x
|
`select count(distinct session_id) x
|
||||||
@ -20,7 +20,7 @@ async function relationalQuery(websiteId) {
|
|||||||
join website
|
join website
|
||||||
on pageview.website_id = website.website_id
|
on pageview.website_id = website.website_id
|
||||||
where website.website_uuid = '${websiteId}'
|
where website.website_uuid = '${websiteId}'
|
||||||
and pageview.created_at >= $2`,
|
and pageview.created_at >= $1`,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export async function getWebsiteStats(...args) {
|
|||||||
|
|
||||||
async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) {
|
async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) {
|
||||||
const { getDateQuery, getTimestampInterval, parseFilters, rawQuery } = prisma;
|
const { getDateQuery, getTimestampInterval, parseFilters, rawQuery } = prisma;
|
||||||
const params = [websiteId, start_at, end_at];
|
const params = [start_at, end_at];
|
||||||
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(
|
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(
|
||||||
'pageview',
|
'pageview',
|
||||||
null,
|
null,
|
||||||
@ -34,7 +34,7 @@ async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) {
|
|||||||
on pageview.website_id = website.website_id
|
on pageview.website_id = website.website_id
|
||||||
${joinSession}
|
${joinSession}
|
||||||
where website.website_uuid='${websiteId}'
|
where website.website_uuid='${websiteId}'
|
||||||
and pageview.created_at between $2 and $3
|
and pageview.created_at between $1 and $2
|
||||||
${pageviewQuery}
|
${pageviewQuery}
|
||||||
${sessionQuery}
|
${sessionQuery}
|
||||||
group by 1, 2
|
group by 1, 2
|
||||||
|
@ -17,6 +17,7 @@ export * from './admin/website/resetWebsite';
|
|||||||
export * from './admin/website/updateWebsite';
|
export * from './admin/website/updateWebsite';
|
||||||
export * from './analytics/event/getEventMetrics';
|
export * from './analytics/event/getEventMetrics';
|
||||||
export * from './analytics/event/getEvents';
|
export * from './analytics/event/getEvents';
|
||||||
|
export * from './analytics/event/getEventData';
|
||||||
export * from './analytics/event/saveEvent';
|
export * from './analytics/event/saveEvent';
|
||||||
export * from './analytics/pageview/getPageviewMetrics';
|
export * from './analytics/pageview/getPageviewMetrics';
|
||||||
export * from './analytics/pageview/getPageviewParams';
|
export * from './analytics/pageview/getPageviewParams';
|
||||||
|
Loading…
Reference in New Issue
Block a user