diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 4f901efc..5731fe75 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -16,6 +16,7 @@ import BrowsersTable from './metrics/BrowsersTable'; import OSTable from './metrics/OSTable'; import DevicesTable from './metrics/DevicesTable'; import CountriesTable from './metrics/CountriesTable'; +import EventsTable from './metrics/EventsTable'; export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) { const [data, setData] = useState(); @@ -23,6 +24,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) const [countryData, setCountryData] = useState(); const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange)); const [expand, setExpand] = useState(); + const [showEvents, setShowEvents] = useState(false); const { startDate, endDate } = dateRange; const BackButton = () => ( @@ -50,6 +52,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) value: 'country', component: props => setCountryData(data)} />, }, + { label: 'Events', value: 'event', component: EventsTable }, ]; const tableProps = { @@ -135,6 +138,12 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) setCountryData(data)} /> +
+
+ setShowEvents(data.length > 0)} /> +
+
events
+
)} {expand && ( diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css index ce26f8b1..9e7352a9 100644 --- a/components/WebsiteDetails.module.css +++ b/components/WebsiteDetails.module.css @@ -42,6 +42,10 @@ padding-right: 0; } +.hidden { + display: none; +} + @media only screen and (max-width: 992px) { .row { border: 0; diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js new file mode 100644 index 00000000..9110bfb8 --- /dev/null +++ b/components/metrics/EventsTable.js @@ -0,0 +1,32 @@ +import React from 'react'; +import MetricsTable from './MetricsTable'; +import styles from './EventsTable.module.css'; + +export default function DevicesTable({ + websiteId, + startDate, + endDate, + limit, + onExpand, + onDataLoad, +}) { + return ( + ( + <> + {w} + {x} + + )} + onExpand={onExpand} + onDataLoad={onDataLoad} + /> + ); +} diff --git a/components/metrics/EventsTable.module.css b/components/metrics/EventsTable.module.css new file mode 100644 index 00000000..e6cb3961 --- /dev/null +++ b/components/metrics/EventsTable.module.css @@ -0,0 +1,7 @@ +.type { + font-size: var(--font-size-small); + padding: 2px 4px; + border: 1px solid var(--gray300); + border-radius: 4px; + margin-right: 10px; +} diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 207366fc..bd66cb85 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -21,9 +21,9 @@ export default function MetricsTable({ filterOptions, limit, headerComponent, + renderLabel, onDataLoad = () => {}, onExpand = () => {}, - labelRenderer = e => e, }) { const [data, setData] = useState(); const [format, setFormat] = useState(true); @@ -43,37 +43,34 @@ export default function MetricsTable({ async function loadData() { const data = await get(`/api/website/${websiteId}/rankings`, { + type, start_at: +startDate, end_at: +endDate, - type, }); setData(data); onDataLoad(data); } - function handleSetFormat() { - setFormat(state => !state); - } + const handleSetFormat = () => setFormat(state => !state); - function getRow(x, y, z) { + const getRow = row => { + const { x: label, y: value, z: percent } = row; return ( ); - } + }; const Row = ({ index, style }) => { - const { x, y, z } = rankings[index]; - return
{getRow(x, y, z)}
; + return
{getRow(rankings[index])}
; }; useEffect(() => { @@ -96,13 +93,13 @@ export default function MetricsTable({
- {limit ? ( - rankings.map(({ x, y, z }) => getRow(x, y, z)) - ) : ( - - {Row} - - )} + {limit + ? rankings.map(row => getRow(row)) + : data?.length > 0 && ( + + {Row} + + )}
{limit && data.length > limit && ( @@ -115,7 +112,7 @@ export default function MetricsTable({ ); } -const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labelRenderer }) => { +const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => { const props = useSpring({ width: percent, y: value, @@ -125,7 +122,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labe return (
-
{labelRenderer(decodeURI(label))}
+
{label}
{props.y?.interpolate(format)}
diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index 48ba85c3..aab8b23c 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -25,6 +25,7 @@ export default function PagesTable({ limit={limit} dataFilter={urlFilter} filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }} + renderLabel={({ x }) => decodeURI(x)} onExpand={onExpand} /> ); diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 480a299e..abaa1208 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -13,13 +13,13 @@ export default function Referrers({ }) { const [filter, setFilter] = useState('Combined'); - const renderLink = url => { + const renderLink = ({ x: url }) => { return url.startsWith('http') ? ( - {url} + {decodeURI(url)} ) : ( - url + decodeURI(url) ); }; @@ -40,7 +40,7 @@ export default function Referrers({ raw: filter === 'Raw', }} onExpand={onExpand} - labelRenderer={renderLink} + renderLabel={renderLink} /> ); } diff --git a/lib/db.js b/lib/db.js index e6d69fdb..d32b9f27 100644 --- a/lib/db.js +++ b/lib/db.js @@ -35,5 +35,6 @@ export default prisma; export async function runQuery(query) { return query.catch(e => { console.error(e); + throw e; }); } diff --git a/lib/filters.js b/lib/filters.js index ad72024f..a1bf1b96 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -124,5 +124,5 @@ export const countryFilter = data => export const percentFilter = data => { const total = data.reduce((n, { y }) => n + y, 0); - return data.map(({ x, y }) => ({ x, y, z: total ? (y / total) * 100 : 0 })); + return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props })); }; diff --git a/lib/queries.js b/lib/queries.js index 258d910f..bc5ed7fd 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -400,3 +400,41 @@ export function getActiveVisitors(website_id) { return Promise.resolve([]); } + +export function getEvents(website_id, start_at, end_at) { + const db = getDatabase(); + + if (db === POSTGRESQL) { + return prisma.$queryRaw( + ` + select distinct event_type w, event_value x, count(*) y + from event + where website_id=$1 + and created_at between $2 and $3 + group by 1, 2 + order by 3 desc + `, + website_id, + start_at, + end_at, + ); + } + + if (db === MYSQL) { + return prisma.$queryRaw( + ` + select distinct event_type w, event_value x, count(*) y + from event + where website_id=? + and created_at between ? and ? + group by 1, 2 + order by 3 desc + `, + website_id, + start_at, + end_at, + ); + } + + return Promise.resolve([]); +} diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index a930bca9..d86f3e27 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -1,4 +1,4 @@ -import { getRankings } from 'lib/queries'; +import { getRankings, getEvents } from 'lib/queries'; import { ok, badRequest } from 'lib/response'; const sessionColumns = ['browser', 'os', 'device', 'country']; @@ -6,14 +6,23 @@ const pageviewColumns = ['url', 'referrer']; export default async (req, res) => { const { id, type, start_at, end_at } = req.query; + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); - if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) { + if (type !== 'event' && !sessionColumns.includes(type) && !pageviewColumns.includes(type)) { return badRequest(res); } + if (type === 'event') { + const events = await getEvents(websiteId, startDate, endDate); + + return ok(res, events); + } + const table = sessionColumns.includes(type) ? 'session' : 'pageview'; - const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table); + const rankings = await getRankings(websiteId, startDate, endDate, type, table); return ok(res, rankings); }; diff --git a/sql/schema.mysql.sql b/sql/schema.mysql.sql index 61a16509..106a6748 100644 --- a/sql/schema.mysql.sql +++ b/sql/schema.mysql.sql @@ -71,6 +71,7 @@ create index session_website_id_idx on session(website_id); create index pageview_created_at_idx on pageview(created_at); create index pageview_website_id_idx on pageview(website_id); create index pageview_session_id_idx on pageview(session_id); +create index pageview_website_id_created_at_idx on pageview(website_id, created_at); create index event_created_at_idx on event(created_at); create index event_website_id_idx on event(website_id); diff --git a/sql/schema.postgresql.sql b/sql/schema.postgresql.sql index 5206257d..43533349 100644 --- a/sql/schema.postgresql.sql +++ b/sql/schema.postgresql.sql @@ -64,6 +64,7 @@ create index session_website_id_idx on session(website_id); create index pageview_created_at_idx on pageview(created_at); create index pageview_website_id_idx on pageview(website_id); create index pageview_session_id_idx on pageview(session_id); +create index pageview_website_id_created_at_idx on pageview(website_id, created_at); create index event_created_at_idx on event(created_at); create index event_website_id_idx on event(website_id);