From b72a4c001c86475df3988711f33e9c30371400e2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 10 Oct 2020 11:04:07 -0700 Subject: [PATCH] Refactored realtime API. Add dot component and colored dots in log. --- components/common/Dot.js | 15 +++++ components/common/Dot.module.css | 17 ++++++ components/metrics/ActiveUsers.js | 3 +- components/metrics/ActiveUsers.module.css | 8 --- components/metrics/RealtimeHeader.js | 8 ++- components/metrics/RealtimeLog.js | 13 ++++ components/pages/RealtimeDashboard.js | 8 +-- lib/format.js | 16 +++++ lib/queries.js | 27 +++++++++ pages/api/realtime.js | 72 ----------------------- pages/api/realtime/init.js | 26 ++++++++ pages/api/realtime/update.js | 26 ++++++++ 12 files changed, 153 insertions(+), 86 deletions(-) create mode 100644 components/common/Dot.js create mode 100644 components/common/Dot.module.css delete mode 100644 pages/api/realtime.js create mode 100644 pages/api/realtime/init.js create mode 100644 pages/api/realtime/update.js diff --git a/components/common/Dot.js b/components/common/Dot.js new file mode 100644 index 00000000..3f424820 --- /dev/null +++ b/components/common/Dot.js @@ -0,0 +1,15 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './Dot.module.css'; + +export default function Dot({ color, size, className }) { + return ( +
+ ); +} diff --git a/components/common/Dot.module.css b/components/common/Dot.module.css new file mode 100644 index 00000000..9081dc5c --- /dev/null +++ b/components/common/Dot.module.css @@ -0,0 +1,17 @@ +.dot { + background: var(--green400); + width: 10px; + height: 10px; + border-radius: 100%; + margin-right: 10px; +} + +.dot.small { + width: 8px; + height: 8px; +} + +.dot.large { + width: 16px; + height: 16px; +} diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index c099d8f7..3b691c10 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import useFetch from 'hooks/useFetch'; +import Dot from 'components/common/Dot'; import styles from './ActiveUsers.module.css'; export default function ActiveUsers({ websiteId, token, className }) { @@ -19,7 +20,7 @@ export default function ActiveUsers({ websiteId, token, className }) { return (
-
+
, value: 0 }, - ].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id }))); + ].concat( + websites.map(({ name, website_id }, index) => ({ + label: name, + value: website_id, + divider: index === 0, + })), + ); const { pageviews, sessions, events, countries } = data; diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index 80c14e37..7bb37e08 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -11,7 +11,9 @@ import { BROWSERS } from 'lib/constants'; import Bolt from 'assets/bolt.svg'; import Visitor from 'assets/visitor.svg'; import Eye from 'assets/eye.svg'; +import { stringToColor } from 'lib/format'; import styles from './RealtimeLog.module.css'; +import Dot from '../common/Dot'; const TYPE_PAGEVIEW = 0; const TYPE_SESSION = 1; @@ -26,11 +28,19 @@ const TYPE_ICONS = { export default function RealtimeLog({ data, websites }) { const [locale] = useLocale(); const countryNames = useCountryNames(locale); + const logs = useMemo(() => { const { pageviews, sessions, events } = data; return [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); }, [data]); + const uuids = useMemo(() => { + return data.sessions.reduce((obj, { session_id, session_uuid }) => { + obj[session_id] = session_uuid; + return obj; + }, {}); + }, [data]); + function getType({ view_id, session_id, event_id }) { if (event_id) { return TYPE_EVENT; @@ -88,6 +98,9 @@ export default function RealtimeLog({ data, websites }) { const row = logs[index]; return (
+
+ +
{format(new Date(row.created_at), 'h:mm:ss')}
diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 30bf1ee4..d514e348 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -16,7 +16,7 @@ import useCountryNames from 'hooks/useCountryNames'; import { FormattedMessage } from 'react-intl'; const REALTIME_RANGE = 30; -const REALTIME_INTERVAL = 55000; +const REALTIME_INTERVAL = 3000; function mergeData(state, data, time) { const ids = state.map(({ __id }) => __id); @@ -34,9 +34,9 @@ export default function RealtimeDashboard() { const countryNames = useCountryNames(locale); const [data, setData] = useState(); const [websiteId, setWebsiteId] = useState(0); - const { data: init, loading } = useFetch('/api/realtime', { params: { type: 'init' } }); - const { data: updates } = useFetch('/api/realtime', { - params: { type: 'update', start_at: data?.timestamp }, + const { data: init, loading } = useFetch('/api/realtime/init'); + const { data: updates } = useFetch('/api/realtime/update', { + params: { start_at: data?.timestamp }, disabled: !init?.websites?.length || !data, interval: REALTIME_INTERVAL, headers: { 'x-umami-token': init?.token }, diff --git a/lib/format.js b/lib/format.js index b031509b..a336c1c4 100644 --- a/lib/format.js +++ b/lib/format.js @@ -62,3 +62,19 @@ export function formatLongNumber(value) { return formatNumber(n); } + +export function stringToColor(str) { + if (!str) { + return '#ffffff'; + } + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let color = '#'; + for (let i = 0; i < 3; i++) { + let value = (hash >> (i * 8)) & 0xff; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} diff --git a/lib/queries.js b/lib/queries.js index ecf351b1..ed97eb93 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -500,3 +500,30 @@ export function getEventMetrics( params, ); } + +export async function getRealtimeData(websites, time) { + const [pageviews, sessions, events] = await Promise.all([ + getPageviews(websites, time), + getSessions(websites, time), + getEvents(websites, time), + ]); + + return { + pageviews: pageviews.map(({ view_id, ...props }) => ({ + __id: `p${view_id}`, + view_id, + ...props, + })), + sessions: sessions.map(({ session_id, ...props }) => ({ + __id: `s${session_id}`, + session_id, + ...props, + })), + events: events.map(({ event_id, ...props }) => ({ + __id: `e${event_id}`, + event_id, + ...props, + })), + timestamp: Date.now(), + }; +} diff --git a/pages/api/realtime.js b/pages/api/realtime.js deleted file mode 100644 index 538f611a..00000000 --- a/pages/api/realtime.js +++ /dev/null @@ -1,72 +0,0 @@ -import { subMinutes } from 'date-fns'; -import { useAuth } from 'lib/middleware'; -import { ok, methodNotAllowed, badRequest } from 'lib/response'; -import { getEvents, getPageviews, getSessions, getUserWebsites } from 'lib/queries'; -import { createToken, parseToken } from 'lib/crypto'; - -export default async (req, res) => { - await useAuth(req, res); - - async function getData(websites, time) { - const [pageviews, sessions, events] = await Promise.all([ - getPageviews(websites, time), - getSessions(websites, time), - getEvents(websites, time), - ]); - - return { - pageviews: pageviews.map(({ view_id, ...props }) => ({ - __id: `p${view_id}`, - view_id, - ...props, - })), - sessions: sessions.map(({ session_id, ...props }) => ({ - __id: `s${session_id}`, - session_id, - ...props, - })), - events: events.map(({ event_id, ...props }) => ({ - __id: `e${event_id}`, - event_id, - ...props, - })), - timestamp: Date.now(), - }; - } - - if (req.method === 'GET') { - const { type, start_at } = req.query; - const { user_id } = req.auth; - - if (type === 'init') { - const websites = await getUserWebsites(user_id); - const ids = websites.map(({ website_id }) => website_id); - const token = await createToken({ websites: ids }); - const data = await getData(ids, subMinutes(new Date(), 30)); - - return ok(res, { - websites, - token, - data, - }); - } - - if (type === 'update') { - const token = req.headers['x-umami-token']; - - if (!token) { - return badRequest(res); - } - - const { websites } = await parseToken(token); - - const data = await getData(websites, new Date(+start_at)); - - return ok(res, data); - } - - return badRequest(res); - } - - return methodNotAllowed(res); -}; diff --git a/pages/api/realtime/init.js b/pages/api/realtime/init.js new file mode 100644 index 00000000..52e66600 --- /dev/null +++ b/pages/api/realtime/init.js @@ -0,0 +1,26 @@ +import { subMinutes } from 'date-fns'; +import { useAuth } from 'lib/middleware'; +import { ok, methodNotAllowed } from 'lib/response'; +import { getUserWebsites, getRealtimeData } from 'lib/queries'; +import { createToken } from 'lib/crypto'; + +export default async (req, res) => { + await useAuth(req, res); + + if (req.method === 'GET') { + const { user_id } = req.auth; + + const websites = await getUserWebsites(user_id); + const ids = websites.map(({ website_id }) => website_id); + const token = await createToken({ websites: ids }); + const data = await getRealtimeData(ids, subMinutes(new Date(), 30)); + + return ok(res, { + websites, + token, + data, + }); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/realtime/update.js b/pages/api/realtime/update.js new file mode 100644 index 00000000..15582f81 --- /dev/null +++ b/pages/api/realtime/update.js @@ -0,0 +1,26 @@ +import { useAuth } from 'lib/middleware'; +import { ok, methodNotAllowed, badRequest } from 'lib/response'; +import { getRealtimeData } from 'lib/queries'; +import { parseToken } from 'lib/crypto'; + +export default async (req, res) => { + await useAuth(req, res); + + if (req.method === 'GET') { + const { start_at } = req.query; + + const token = req.headers['x-umami-token']; + + if (!token) { + return badRequest(res); + } + + const { websites } = await parseToken(token); + + const data = await getRealtimeData(websites, new Date(+start_at)); + + return ok(res, data); + } + + return methodNotAllowed(res); +};