diff --git a/components/messages.js b/components/messages.js index 28f5bf18..cc1d0e86 100644 --- a/components/messages.js +++ b/components/messages.js @@ -97,6 +97,7 @@ export const labels = defineMessages({ all: { id: 'label.all', defaultMessage: 'All' }, sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' }, + logs: { id: 'label.activity-log', defaultMessage: 'Activity log' }, }); export const messages = defineMessages({ @@ -168,6 +169,14 @@ export const messages = defineMessages({ id: 'message.team-not-found', defaultMessage: 'Team not found.', }, + visitorLog: { + id: 'message.visitor-log', + defaultMessage: 'Visitor from {country} using {browser} on {os} {device}', + }, + eventLog: { + id: 'message.event-log', + defaultMessage: '{event} on {url}', + }, }); export const devices = defineMessages({ diff --git a/components/metrics/RealtimeChart.js b/components/metrics/RealtimeChart.js index 6ead67bf..00bd0051 100644 --- a/components/metrics/RealtimeChart.js +++ b/components/metrics/RealtimeChart.js @@ -8,7 +8,7 @@ function mapData(data) { let last = 0; const arr = []; - data.reduce((obj, { timestamp }) => { + data?.reduce((obj, { timestamp }) => { const t = startOfMinute(new Date(timestamp)); if (t.getTime() > last) { obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; @@ -33,16 +33,9 @@ export default function RealtimeChart({ data, unit, ...props }) { return { pageviews: [], sessions: [] }; } - const visitors = data.sessions?.reduce((arr, val) => { - if (!arr.find(({ sessionId }) => sessionId === val.sessionId)) { - return arr.concat(val); - } - return arr; - }, []); - return { pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), - sessions: getDateArray(mapData(visitors), startDate, endDate, unit), + sessions: getDateArray(mapData(data.visitors), startDate, endDate, unit), }; }, [data, startDate, endDate, unit]); diff --git a/components/pages/realtime/RealtimeDashboard.js b/components/pages/realtime/RealtimeDashboard.js index a59a6d07..9ac6a863 100644 --- a/components/pages/realtime/RealtimeDashboard.js +++ b/components/pages/realtime/RealtimeDashboard.js @@ -58,7 +58,7 @@ export default function RealtimeDashboard({ websiteId }) { const realtimeData = useMemo(() => { if (!currentData) { - return { pageviews: [], sessions: [], events: [], countries: [] }; + return { pageviews: [], sessions: [], events: [], countries: [], visitors: [] }; } currentData.countries = percentFilter( @@ -84,6 +84,13 @@ export default function RealtimeDashboard({ websiteId }) { .sort(firstBy('y', -1)), ); + currentData.visitors = currentData.sessions.reduce((arr, val) => { + if (!arr.find(({ sessionId }) => sessionId === val.sessionId)) { + return arr.concat(val); + } + return arr; + }, []); + return currentData; }, [currentData]); diff --git a/components/pages/realtime/RealtimeHeader.js b/components/pages/realtime/RealtimeHeader.js index bfcce6e9..15730d5d 100644 --- a/components/pages/realtime/RealtimeHeader.js +++ b/components/pages/realtime/RealtimeHeader.js @@ -5,14 +5,7 @@ import styles from './RealtimeHeader.module.css'; export default function RealtimeHeader({ data = {} }) { const { formatMessage } = useIntl(); - const { pageviews, sessions, events, countries } = data; - - const visitors = sessions?.reduce((arr, { sessionId }) => { - if (sessionId && !arr.includes(sessionId)) { - return arr.concat(sessionId); - } - return arr; - }, []); + const { pageviews, visitors, events, countries } = data; return (
diff --git a/components/pages/realtime/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js index 25fba487..06b7537e 100644 --- a/components/pages/realtime/RealtimeLog.js +++ b/components/pages/realtime/RealtimeLog.js @@ -1,11 +1,11 @@ import { useMemo, useState } from 'react'; -import { StatusLight, Icon } from 'react-basics'; +import { StatusLight, Icon, Text } from 'react-basics'; import { useIntl, FormattedMessage } from 'react-intl'; import { FixedSizeList } from 'react-window'; import firstBy from 'thenby'; import FilterButtons from 'components/common/FilterButtons'; import NoData from 'components/common/NoData'; -import { getDeviceMessage, labels } from 'components/messages'; +import { getDeviceMessage, labels, messages } from 'components/messages'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; @@ -15,12 +15,12 @@ import { safeDecodeURI } from 'next-basics'; import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; -const TYPE_ALL = 'type-all'; -const TYPE_PAGEVIEW = 'type-pageview'; -const TYPE_SESSION = 'type-session'; -const TYPE_EVENT = 'type-event'; +const TYPE_ALL = 'all'; +const TYPE_PAGEVIEW = 'pageview'; +const TYPE_SESSION = 'session'; +const TYPE_EVENT = 'event'; -const TYPE_ICONS = { +const icons = { [TYPE_PAGEVIEW]: , [TYPE_SESSION]: , [TYPE_EVENT]: , @@ -32,30 +32,6 @@ export default function RealtimeLog({ data, websiteDomain }) { const countryNames = useCountryNames(locale); const [filter, setFilter] = useState(TYPE_ALL); - const logs = useMemo(() => { - if (!data) { - return []; - } - - const { pageviews, sessions, events } = data; - const logs = [...pageviews, ...sessions, ...events].sort(firstBy('createdAt', -1)); - if (filter) { - return logs.filter(row => getType(row) === filter); - } - return logs; - }, [data, filter]); - - const uuids = useMemo(() => { - if (!data) { - return []; - } - - return data.sessions.reduce((obj, { sessionId, sessionUuid }) => { - obj[sessionId] = sessionUuid; - return obj; - }, {}); - }, [data]); - const buttons = [ { label: formatMessage(labels.all), @@ -66,7 +42,7 @@ export default function RealtimeLog({ data, websiteDomain }) { key: TYPE_PAGEVIEW, }, { - label: formatMessage(labels.sessions), + label: formatMessage(labels.visitors), key: TYPE_SESSION, }, { @@ -75,42 +51,41 @@ export default function RealtimeLog({ data, websiteDomain }) { }, ]; - function getType({ pageviewId, sessionId, eventId }) { - if (eventId) { - return TYPE_EVENT; - } - if (pageviewId) { - return TYPE_PAGEVIEW; - } - if (sessionId) { - return TYPE_SESSION; - } - return null; - } + const getTime = ({ createdAt }) => dateFormat(new Date(createdAt), 'pp', locale); - function getIcon(row) { - return TYPE_ICONS[getType(row)]; - } + const getColor = ({ sessionId }) => stringToColor(sessionId); - function getDetail({ - eventName, - pageviewId, - sessionId, - url, - browser, - os, - country, - device, - websiteId, - }) { - if (eventName) { - return
{eventName}
; + const getIcon = ({ __type }) => icons[__type]; + + const getDetail = log => { + const { __type, eventName, url, browser, os, country, device } = log; + + if (__type === TYPE_EVENT) { + return ( + {eventName || formatMessage(labels.unknown)}, + url: ( + + {url} + + ), + }} + /> + ); } - if (pageviewId) { + + if (__type === TYPE_PAGEVIEW) { return ( @@ -118,11 +93,11 @@ export default function RealtimeLog({ data, websiteDomain }) { ); } - if (sessionId) { + + if (__type === TYPE_SESSION) { return ( {countryNames[country] || formatMessage(labels.unknown)}, browser: {BROWSERS[browser]}, @@ -132,17 +107,7 @@ export default function RealtimeLog({ data, websiteDomain }) { /> ); } - } - - function getTime({ createdAt }) { - return dateFormat(new Date(createdAt), 'pp', locale); - } - - function getColor(row) { - const { sessionId } = row; - - return stringToColor(uuids[sessionId] || `${sessionId}}`); - } + }; const Row = ({ index, style }) => { const row = logs[index]; @@ -153,19 +118,32 @@ export default function RealtimeLog({ data, websiteDomain }) {
{getTime(row)}
- - {getDetail(row)} + {getIcon(row)} + {getDetail(row)}
); }; + const logs = useMemo(() => { + if (!data) { + return []; + } + + const { pageviews, visitors, events } = data; + const logs = [...pageviews, ...visitors, ...events].sort(firstBy('createdAt', -1)); + + if (filter !== TYPE_ALL) { + return logs.filter(({ __type }) => __type === filter); + } + + return logs; + }, [data, filter]); + return (
-
- -
+
{formatMessage(labels.logs)}
{logs?.length === 0 && } {logs?.length > 0 && ( diff --git a/components/pages/realtime/RealtimeLog.module.css b/components/pages/realtime/RealtimeLog.module.css index 292bbd8c..7e54e34c 100644 --- a/components/pages/realtime/RealtimeLog.module.css +++ b/components/pages/realtime/RealtimeLog.module.css @@ -1,16 +1,14 @@ .table { - font-size: var(--font-size-xs); + font-size: var(--font-size-sm); overflow: hidden; height: 100%; - display: grid; - grid-template-rows: fit-content(100%) fit-content(100%) auto; } .header { display: flex; align-items: center; justify-content: space-between; - font-size: 16px; + font-size: var(--font-size-md); line-height: 40px; font-weight: 600; } @@ -18,6 +16,7 @@ .row { display: flex; align-items: center; + gap: 10px; height: 40px; border-bottom: 1px solid var(--base300); } @@ -44,6 +43,7 @@ .detail { display: flex; flex: 1; + gap: 10px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts index 052259fb..989194bf 100644 --- a/queries/analytics/stats/getRealtimeData.ts +++ b/queries/analytics/stats/getRealtimeData.ts @@ -14,15 +14,15 @@ export async function getRealtimeData(websiteId, time) { return data.map(props => ({ ...props, __id: md5(id, ...Object.values(props)), - timestamp: props.timestamp * 1000, - timestampCompare: new Date(props.createdAt).getTime(), + __type: id, + timestamp: props.timestamp ? props.timestamp * 1000 : new Date(props.createdAt).getTime(), })); }; return { - pageviews: decorate('pageviews', pageviews), - sessions: decorate('sessions', sessions), - events: decorate('events', events), + pageviews: decorate('pageview', pageviews), + sessions: decorate('session', sessions), + events: decorate('event', events), timestamp: Date.now(), }; }