diff --git a/components/common/EventDataButton.js b/components/common/EventDataButton.js
new file mode 100644
index 00000000..2b895840
--- /dev/null
+++ b/components/common/EventDataButton.js
@@ -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 (
+ <>
+ }
+ tooltip={}
+ tooltipId="button-event"
+ size="small"
+ onClick={handleClick}
+ className={styles.button}
+ >
+ Event Data
+
+ {showEventData && (
+ }>
+
+
+ )}
+ >
+ );
+}
+
+EventDataButton.propTypes = {
+ websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+};
+
+export default EventDataButton;
diff --git a/components/common/EventDataButton.module.css b/components/common/EventDataButton.module.css
new file mode 100644
index 00000000..cd2a2ed6
--- /dev/null
+++ b/components/common/EventDataButton.module.css
@@ -0,0 +1,3 @@
+.button {
+ width: fit-content;
+}
diff --git a/components/forms/EventDataForm.js b/components/forms/EventDataForm.js
new file mode 100644
index 00000000..041fdf93
--- /dev/null
+++ b/components/forms/EventDataForm.js
@@ -0,0 +1,265 @@
+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: , value: '1day' },
+ {
+ label: (
+
+ ),
+ value: '24hour',
+ },
+ {
+ label: ,
+ value: '-1day',
+ },
+ {
+ label: ,
+ value: '1week',
+ divider: true,
+ },
+ {
+ label: (
+
+ ),
+ value: '7day',
+ },
+ {
+ label: ,
+ value: '1month',
+ divider: true,
+ },
+ {
+ label: (
+
+ ),
+ value: '30day',
+ },
+ {
+ label: (
+
+ ),
+ value: '90day',
+ },
+ { label: , value: '1year' },
+ {
+ label: ,
+ value: 'custom',
+ divider: true,
+ },
+];
+
+export default function EventDataForm({ websiteId, onClose, className }) {
+ const { post } = useApi();
+ const [message, setMessage] = useState();
+ const [columns, setColumns] = useState({ url: 'count', inventory: 'sum', price: 'avg' });
+ const [filters, setFilters] = useState({ url: '/about' });
+ 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 { ...rest } = list;
+
+ delete rest[`${value}`];
+
+ setState(rest);
+ };
+
+ 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();
+ setData([]);
+ } else {
+ setData(data);
+
+ if (message) {
+ setMessage(null);
+ }
+ }
+ };
+
+ return (
+ <>
+ {message}
+
+
{Object.keys(params).map(key => {
if (!params[key]) {
return null;
diff --git a/components/metrics/FilterTags.module.css b/components/metrics/FilterTags.module.css
index bb1536e5..50ae60a0 100644
--- a/components/metrics/FilterTags.module.css
+++ b/components/metrics/FilterTags.module.css
@@ -7,8 +7,5 @@
.tag {
text-align: center;
margin-bottom: 10px;
-}
-
-.tag + .tag {
- margin-left: 20px;
+ margin-right: 20px;
}
diff --git a/components/metrics/WebsiteHeader.js b/components/metrics/WebsiteHeader.js
index 1a6bdf15..517ef140 100644
--- a/components/metrics/WebsiteHeader.js
+++ b/components/metrics/WebsiteHeader.js
@@ -1,14 +1,13 @@
-import React from 'react';
+import Arrow from 'assets/arrow-right.svg';
import classNames from 'classnames';
-import { FormattedMessage } from 'react-intl';
+import Favicon from 'components/common/Favicon';
import Link from 'components/common/Link';
import OverflowText from 'components/common/OverflowText';
-import PageHeader from 'components/layout/PageHeader';
import RefreshButton from 'components/common/RefreshButton';
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 Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css';
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
diff --git a/components/pages/WebsiteDetails.js b/components/pages/WebsiteDetails.js
index 3fc234a4..b40bc9ee 100644
--- a/components/pages/WebsiteDetails.js
+++ b/components/pages/WebsiteDetails.js
@@ -24,6 +24,7 @@ import useFetch from 'hooks/useFetch';
import usePageQuery from 'hooks/usePageQuery';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import styles from './WebsiteDetails.module.css';
+import EventDataButton from 'components/common/EventDataButton';
const messages = defineMessages({
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
@@ -183,6 +184,7 @@ export default function WebsiteDetails({ websiteId }) {
+
diff --git a/lang/en-US.json b/lang/en-US.json
index aa5e3ae3..f58a25cd 100644
--- a/lang/en-US.json
+++ b/lang/en-US.json
@@ -1,6 +1,8 @@
{
"label.accounts": "Accounts",
"label.add-account": "Add account",
+ "label.add-column": "Add column",
+ "label.add-filter": "Add filter",
"label.add-website": "Add website",
"label.administrator": "Administrator",
"label.all": "All",
@@ -25,6 +27,8 @@
"label.edit-account": "Edit account",
"label.edit-website": "Edit website",
"label.enable-share-url": "Enable share URL",
+ "label.event-data": "Event Data",
+ "label.field-name": "Field Name",
"label.invalid": "Invalid",
"label.invalid-domain": "Invalid domain",
"label.language": "Language",
@@ -48,6 +52,7 @@
"label.reset": "Reset",
"label.reset-website": "Reset statistics",
"label.save": "Save",
+ "label.search": "Search",
"label.settings": "Settings",
"label.share-url": "Share URL",
"label.single-day": "Single day",
@@ -58,8 +63,10 @@
"label.timezone": "Timezone",
"label.today": "Today",
"label.tracking-code": "Tracking code",
+ "label.type": "Type",
"label.unknown": "Unknown",
"label.username": "Username",
+ "label.value": "Value",
"label.view-details": "View details",
"label.websites": "Websites",
"label.yesterday": "Yesterday",
diff --git a/lib/clickhouse.js b/lib/clickhouse.js
index bf4bc5c5..41b1df5a 100644
--- a/lib/clickhouse.js
+++ b/lib/clickhouse.js
@@ -65,8 +65,7 @@ function getCommaSeparatedStringFormat(data) {
}
function getBetweenDates(field, start_at, end_at) {
- return `${field} between ${getDateFormat(start_at)}
- and ${getDateFormat(end_at)}`;
+ return `${field} between ${getDateFormat(start_at)} and ${getDateFormat(end_at)}`;
}
function getJsonField(column, property) {
@@ -81,7 +80,7 @@ function getEventDataColumnsQuery(column, columns) {
return arr;
}
- arr.push(`${filter}(${getJsonField(column, key)})`);
+ arr.push(`${filter}(${getJsonField(column, key)}) as ${key}_${filter}`);
return arr;
}, []);
diff --git a/lib/prisma.js b/lib/prisma.js
index 1ebf8aa4..ab1e6ebf 100644
--- a/lib/prisma.js
+++ b/lib/prisma.js
@@ -85,13 +85,13 @@ function getTimestampInterval(field) {
}
}
-function getJsonField(column, property, value) {
+function getJsonField(column, property, isNumber) {
const db = getDatabaseType(process.env.DATABASE_URL);
if (db === POSTGRESQL) {
let accessor = `${column} ->> '${property}'`;
- if (value && typeof value === 'number') {
+ if (isNumber) {
accessor = `CAST(${accessor} AS DECIMAL)`;
}
@@ -111,7 +111,9 @@ function getEventDataColumnsQuery(column, columns) {
return arr;
}
- arr.push(`${filter}(${getJsonField(column, key)})`);
+ const isNumber = ['sum', 'avg', 'min', 'max'].some(a => a === filter);
+
+ arr.push(`${filter}(${getJsonField(column, key, isNumber)}) as "${filter}(${key})"`);
return arr;
}, []);
@@ -127,8 +129,10 @@ function getEventDataFilterQuery(column, filters) {
return arr;
}
+ const isNumber = filter && typeof filter === 'number';
+
arr.push(
- `${getJsonField(column, key, filter)} = ${
+ `${getJsonField(column, key, isNumber)} = ${
typeof filter === 'string' ? `'${filter}'` : filter
}`,
);
diff --git a/pages/api/websites/[id]/eventdata.js b/pages/api/websites/[id]/eventdata.js
index 61cd8671..86a17b77 100644
--- a/pages/api/websites/[id]/eventdata.js
+++ b/pages/api/websites/[id]/eventdata.js
@@ -4,8 +4,6 @@ import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
import { allowQuery } from 'lib/auth';
import { useAuth, useCors } from 'lib/middleware';
-const unitTypes = ['year', 'month', 'hour', 'day'];
-
export default async (req, res) => {
await useCors(req, res);
await useAuth(req, res);
@@ -15,18 +13,11 @@ export default async (req, res) => {
return unauthorized(res);
}
- const {
- website_id: websiteId,
- start_at,
- end_at,
- unit,
- timezone,
- event_name: eventName,
- columns,
- filters,
- } = req.body;
+ const { id: websiteId } = req.query;
- if (!moment.tz.zone(timezone) || !unitTypes.includes(unit)) {
+ const { start_at, end_at, timezone, event_name: eventName, columns, filters } = req.body;
+
+ if (!moment.tz.zone(timezone)) {
return badRequest(res);
}
@@ -37,7 +28,6 @@ export default async (req, res) => {
startDate,
endDate,
timezone,
- unit,
eventName,
columns,
filters,
diff --git a/pages/login.js b/pages/login.js
index d46110c8..12f70ac9 100644
--- a/pages/login.js
+++ b/pages/login.js
@@ -16,6 +16,6 @@ export default function LoginPage({ loginDisabled }) {
export async function getServerSideProps() {
return {
- props: { loginDisabled: !!process.env.DISABLE_LOGIN || process.env.isCloudMode },
+ props: { loginDisabled: !!process.env.DISABLE_LOGIN || !!process.env.isCloudMode },
};
}
diff --git a/queries/analytics/event/getEventData.js b/queries/analytics/event/getEventData.js
index 8fa87f2f..91302d30 100644
--- a/queries/analytics/event/getEventData.js
+++ b/queries/analytics/event/getEventData.js
@@ -9,16 +9,12 @@ export async function getEventData(...args) {
});
}
-async function relationalQuery(
- websiteId,
- { startDate, endDate, timezone = 'utc', unit = 'day', event_name, columns, filters },
-) {
- const { rawQuery, getDateQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma;
+async function relationalQuery(websiteId, { startDate, endDate, event_name, columns, filters }) {
+ const { rawQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma;
const params = [startDate, endDate];
return rawQuery(
`select
- ${getDateQuery('event.created_at', unit, timezone)} t,
${getEventDataColumnsQuery('event_data.event_data', columns)}
from event
join website
@@ -28,38 +24,40 @@ async function relationalQuery(
where website_uuid='${websiteId}'
and event.created_at between $1 and $2
${event_name ? `and event_name = ${event_name}` : ''}
- ${filters ? `and ${getEventDataFilterQuery('event_data.event_data', filters)}` : ''}
- group by 1
- order by 2`,
+ ${
+ 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, timezone = 'UTC', unit = 'day', event_name, columns, filters },
-) {
- const {
- rawQuery,
- getDateQuery,
- getBetweenDates,
- getEventDataColumnsQuery,
- getEventDataFilterQuery,
- } = clickhouse;
+async function clickhouseQuery(websiteId, { startDate, endDate, event_name, columns, filters }) {
+ const { rawQuery, getBetweenDates, getEventDataColumnsQuery, getEventDataFilterQuery } =
+ clickhouse;
const params = [websiteId];
return rawQuery(
`select
- event_name x,
- ${getDateQuery('created_at', unit, timezone)} t,
${getEventDataColumnsQuery('event_data', columns)}
from event
where website_id= $1
${event_name ? `and event_name = ${event_name}` : ''}
and ${getBetweenDates('created_at', startDate, endDate)}
- ${filters ? `and ${getEventDataFilterQuery('event_data', filters)}` : ''}
- group by x, t
- order by t`,
+ ${
+ 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}`] };
+ });
+ });
}
diff --git a/queries/analytics/event/getEventMetrics.js b/queries/analytics/event/getEventMetrics.js
index edf5de8c..605bb688 100644
--- a/queries/analytics/event/getEventMetrics.js
+++ b/queries/analytics/event/getEventMetrics.js
@@ -18,7 +18,7 @@ async function relationalQuery(
filters = {},
) {
const { rawQuery, getDateQuery, getFilterQuery } = prisma;
- const params = [websiteId, start_at, end_at];
+ const params = [start_at, end_at];
return rawQuery(
`select
@@ -29,7 +29,7 @@ async function relationalQuery(
join website
on event.website_id = website.website_id
where website_uuid='${websiteId}'
- and event.created_at between $2 and $3
+ and event.created_at between $1 and $2
${getFilterQuery('event', filters, params)}
group by 1, 2
order by 2`,
diff --git a/queries/analytics/pageview/getPageviewMetrics.js b/queries/analytics/pageview/getPageviewMetrics.js
index e1c4d43f..69607d00 100644
--- a/queries/analytics/pageview/getPageviewMetrics.js
+++ b/queries/analytics/pageview/getPageviewMetrics.js
@@ -11,7 +11,7 @@ export async function getPageviewMetrics(...args) {
async function relationalQuery(websiteId, { startDate, endDate, column, table, filters = {} }) {
const { rawQuery, parseFilters } = prisma;
- const params = [websiteId, startDate, endDate];
+ const params = [startDate, endDate];
const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
table,
column,
@@ -25,7 +25,7 @@ async function relationalQuery(websiteId, { startDate, endDate, column, table, f
${` join website on ${table}.website_id = website.website_id`}
${joinSession}
where website.website_uuid='${websiteId}'
- and ${table}.created_at between $2 and $3
+ and ${table}.created_at between $1 and $2
${pageviewQuery}
${joinSession && sessionQuery}
${eventQuery}
diff --git a/queries/analytics/pageview/getPageviewParams.js b/queries/analytics/pageview/getPageviewParams.js
index 8ec26dec..5cdabfa3 100644
--- a/queries/analytics/pageview/getPageviewParams.js
+++ b/queries/analytics/pageview/getPageviewParams.js
@@ -10,7 +10,7 @@ export async function getPageviewParams(...args) {
async function relationalQuery(websiteId, start_at, end_at, column, table, filters = {}) {
const { parseFilters, rawQuery } = prisma;
- const params = [websiteId, start_at, end_at];
+ const params = [start_at, end_at];
const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
table,
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`}
${joinSession}
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 '%?%'
${pageviewQuery}
${joinSession && sessionQuery}
diff --git a/queries/analytics/pageview/getPageviewStats.js b/queries/analytics/pageview/getPageviewStats.js
index ceed4daf..5ec8339f 100644
--- a/queries/analytics/pageview/getPageviewStats.js
+++ b/queries/analytics/pageview/getPageviewStats.js
@@ -22,7 +22,7 @@ async function relationalQuery(
},
) {
const { getDateQuery, parseFilters, rawQuery } = prisma;
- const params = [websiteId, start_at, end_at];
+ const params = [start_at, end_at];
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(
'pageview',
null,
@@ -38,7 +38,7 @@ async function relationalQuery(
on pageview.website_id = website.website_id
${joinSession}
where website.website_uuid='${websiteId}'
- and pageview.created_at between $2 and $3
+ and pageview.created_at between $1 and $2
${pageviewQuery}
${sessionQuery}
group by 1`,
diff --git a/queries/analytics/session/getSessionMetrics.js b/queries/analytics/session/getSessionMetrics.js
index cbf3ed58..020bddfb 100644
--- a/queries/analytics/session/getSessionMetrics.js
+++ b/queries/analytics/session/getSessionMetrics.js
@@ -11,7 +11,7 @@ export async function getSessionMetrics(...args) {
async function relationalQuery(websiteId, { startDate, endDate, field, filters = {} }) {
const { parseFilters, rawQuery } = prisma;
- const params = [websiteId, startDate, endDate];
+ const params = [startDate, endDate];
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(null, filters, params);
return rawQuery(
@@ -24,7 +24,7 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters =
on pageview.website_id = website.website_id
${joinSession}
where website.website_uuid='${websiteId}'
- and pageview.created_at between $2 and $3
+ and pageview.created_at between $1 and $2
${pageviewQuery}
${sessionQuery}
)
diff --git a/queries/analytics/stats/getActiveVisitors.js b/queries/analytics/stats/getActiveVisitors.js
index 9d6b1f09..3a898d94 100644
--- a/queries/analytics/stats/getActiveVisitors.js
+++ b/queries/analytics/stats/getActiveVisitors.js
@@ -12,7 +12,7 @@ export async function getActiveVisitors(...args) {
async function relationalQuery(websiteId) {
const date = subMinutes(new Date(), 5);
- const params = [websiteId, date];
+ const params = [date];
return prisma.rawQuery(
`select count(distinct session_id) x
@@ -20,7 +20,7 @@ async function relationalQuery(websiteId) {
join website
on pageview.website_id = website.website_id
where website.website_uuid = '${websiteId}'
- and pageview.created_at >= $2`,
+ and pageview.created_at >= $1`,
params,
);
}
diff --git a/queries/analytics/stats/getWebsiteStats.js b/queries/analytics/stats/getWebsiteStats.js
index b6bf7b87..134e1c3e 100644
--- a/queries/analytics/stats/getWebsiteStats.js
+++ b/queries/analytics/stats/getWebsiteStats.js
@@ -11,7 +11,7 @@ export async function getWebsiteStats(...args) {
async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) {
const { getDateQuery, getTimestampInterval, parseFilters, rawQuery } = prisma;
- const params = [websiteId, start_at, end_at];
+ const params = [start_at, end_at];
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(
'pageview',
null,
@@ -34,7 +34,7 @@ async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) {
on pageview.website_id = website.website_id
${joinSession}
where website.website_uuid='${websiteId}'
- and pageview.created_at between $2 and $3
+ and pageview.created_at between $1 and $2
${pageviewQuery}
${sessionQuery}
group by 1, 2