From ebd52335bb3fc5c8f9f8ece7800c0566c3e2dbdd Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 21 Nov 2021 22:00:14 -0800 Subject: [PATCH] Referrer filtering. --- assets/arrow-up-right-from-square.svg | 1 + components/common/ErrorMessage.module.css | 2 ++ components/metrics/FilterTags.js | 27 +++++++++++++++ components/metrics/FilterTags.module.css | 14 ++++++++ components/metrics/MetricsBar.js | 5 +-- components/metrics/PagesTable.js | 12 +++---- components/metrics/ReferrersTable.js | 35 ++++++++++++++++---- components/metrics/ReferrersTable.module.css | 31 +++++++++++++++++ components/metrics/WebsiteChart.js | 32 ++++++------------ components/metrics/WebsiteChart.module.css | 10 +++--- components/pages/WebsiteDetails.js | 2 +- lib/filters.js | 4 +-- lib/queries.js | 22 ++++++++++-- pages/api/website/[id]/pageviews.js | 9 +++-- pages/api/website/[id]/stats.js | 6 ++-- 15 files changed, 158 insertions(+), 54 deletions(-) create mode 100644 assets/arrow-up-right-from-square.svg create mode 100644 components/metrics/FilterTags.js create mode 100644 components/metrics/FilterTags.module.css create mode 100644 components/metrics/ReferrersTable.module.css diff --git a/assets/arrow-up-right-from-square.svg b/assets/arrow-up-right-from-square.svg new file mode 100644 index 00000000..90ad457f --- /dev/null +++ b/assets/arrow-up-right-from-square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/common/ErrorMessage.module.css b/components/common/ErrorMessage.module.css index 232b5f84..88769cf5 100644 --- a/components/common/ErrorMessage.module.css +++ b/components/common/ErrorMessage.module.css @@ -6,6 +6,8 @@ margin: auto; display: flex; z-index: 1; + background-color: var(--gray50); + padding: 10px; } .icon { diff --git a/components/metrics/FilterTags.js b/components/metrics/FilterTags.js new file mode 100644 index 00000000..9720525e --- /dev/null +++ b/components/metrics/FilterTags.js @@ -0,0 +1,27 @@ +import React from 'react'; +import classNames from 'classnames'; +import Button from 'components/common/Button'; +import Times from 'assets/times.svg'; +import styles from './FilterTags.module.css'; + +export default function FilterTags({ params, onClick }) { + if (Object.keys(params).filter(key => params[key]).length === 0) { + return null; + } + return ( +
+ {Object.keys(params).map(key => { + if (!params[key]) { + return null; + } + return ( +
+ +
+ ); + })} +
+ ); +} diff --git a/components/metrics/FilterTags.module.css b/components/metrics/FilterTags.module.css new file mode 100644 index 00000000..bb1536e5 --- /dev/null +++ b/components/metrics/FilterTags.module.css @@ -0,0 +1,14 @@ +.filters { + display: flex; + justify-content: flex-start; + align-items: flex-start; +} + +.tag { + text-align: center; + margin-bottom: 10px; +} + +.tag + .tag { + margin-left: 20px; +} diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index 435870a1..7852b96c 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -18,7 +18,7 @@ export default function MetricsBar({ websiteId, className }) { const { startDate, endDate, modified } = dateRange; const [format, setFormat] = useState(true); const { - query: { url }, + query: { url, ref }, } = usePageQuery(); const { data, error, loading } = useFetch( @@ -28,10 +28,11 @@ export default function MetricsBar({ websiteId, className }) { start_at: +startDate, end_at: +endDate, url, + ref, }, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [url, modified], + [modified, url, ref], ); const formatFunc = format diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index b500e270..6fe8c139 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -16,7 +16,7 @@ export default function PagesTable({ websiteId, websiteDomain, showFilters, ...p const [filter, setFilter] = useState(FILTER_COMBINED); const { resolve, - query: { url }, + query: { url: currentUrl }, } = usePageQuery(); const buttons = [ @@ -27,16 +27,16 @@ export default function PagesTable({ websiteId, websiteDomain, showFilters, ...p { label: , value: FILTER_RAW }, ]; - const renderLink = ({ x }) => { + const renderLink = ({ x: url }) => { return ( - + - {safeDecodeURI(x)} + {safeDecodeURI(url)} ); diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 4dad8655..f2fa0215 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -4,6 +4,12 @@ import MetricsTable from './MetricsTable'; import FilterButtons from 'components/common/FilterButtons'; import { refFilter } from 'lib/filters'; import { safeDecodeURI } from 'lib/url'; +import Link from 'next/link'; +import classNames from 'classnames'; +import usePageQuery from 'hooks/usePageQuery'; +import External from 'assets/arrow-up-right-from-square.svg'; +import Icon from '../common/Icon'; +import styles from './ReferrersTable.module.css'; export const FILTER_DOMAIN_ONLY = 0; export const FILTER_COMBINED = 1; @@ -11,6 +17,10 @@ export const FILTER_RAW = 2; export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) { const [filter, setFilter] = useState(FILTER_COMBINED); + const { + resolve, + query: { ref: currentRef }, + } = usePageQuery(); const buttons = [ { @@ -24,13 +34,24 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters, { label: , value: FILTER_RAW }, ]; - const renderLink = ({ w: href, x: url }) => { - return (href || url).startsWith('http') ? ( - - {safeDecodeURI(url)} - - ) : ( - safeDecodeURI(url) + const renderLink = ({ w: link, x: label }) => { + console.log({ link, label }); + return ( +
+ + + {safeDecodeURI(label)} + + + + } className={styles.icon} /> + +
); }; diff --git a/components/metrics/ReferrersTable.module.css b/components/metrics/ReferrersTable.module.css new file mode 100644 index 00000000..238667f3 --- /dev/null +++ b/components/metrics/ReferrersTable.module.css @@ -0,0 +1,31 @@ +body .inactive { + color: var(--gray500); +} + +body .active { + color: var(--gray900); + font-weight: 600; +} + +.row { + display: flex; + justify-content: space-between; +} + +.row .link { + display: none; +} + +.row .label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.row:hover .link { + display: block; +} + +.icon { + cursor: pointer; +} diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index fca6d541..dea86ae6 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -5,17 +5,16 @@ import MetricsBar from './MetricsBar'; import WebsiteHeader from './WebsiteHeader'; import DateFilter from 'components/common/DateFilter'; import StickyHeader from 'components/helpers/StickyHeader'; -import Button from 'components/common/Button'; import useFetch from 'hooks/useFetch'; import useDateRange from 'hooks/useDateRange'; import useTimezone from 'hooks/useTimezone'; import usePageQuery from 'hooks/usePageQuery'; import { getDateArray, getDateLength } from 'lib/date'; -import Times from 'assets/times.svg'; +import ErrorMessage from 'components/common/ErrorMessage'; +import FilterTags from 'components/metrics/FilterTags'; +import useShareToken from 'hooks/useShareToken'; +import { TOKEN_HEADER } from 'lib/constants'; import styles from './WebsiteChart.module.css'; -import ErrorMessage from '../common/ErrorMessage'; -import useShareToken from '../../hooks/useShareToken'; -import { TOKEN_HEADER } from '../../lib/constants'; export default function WebsiteChart({ websiteId, @@ -33,7 +32,7 @@ export default function WebsiteChart({ const { router, resolve, - query: { url }, + query: { url, ref }, } = usePageQuery(); const { data, loading, error } = useFetch( @@ -45,11 +44,12 @@ export default function WebsiteChart({ unit, tz: timezone, url, + ref, }, onDataLoad, headers: { [TOKEN_HEADER]: shareToken?.token }, }, - [url, modified], + [modified, url, ref], ); const chartData = useMemo(() => { @@ -62,8 +62,8 @@ export default function WebsiteChart({ return { pageviews: [], sessions: [] }; }, [data]); - function handleCloseFilter() { - router.push(resolve({ url: undefined })); + function handleCloseFilter(param) { + router.push(resolve({ [param]: undefined })); } return ( @@ -75,7 +75,7 @@ export default function WebsiteChart({ stickyClassName={styles.sticky} enabled={stickyHeader} > - {url && } +
@@ -90,7 +90,7 @@ export default function WebsiteChart({
-
+
{error && } {!hideChart && ( ); } - -const PageFilter = ({ url, onClick }) => { - return ( -
- -
- ); -}; diff --git a/components/metrics/WebsiteChart.module.css b/components/metrics/WebsiteChart.module.css index 0e947aea..d0a8ea68 100644 --- a/components/metrics/WebsiteChart.module.css +++ b/components/metrics/WebsiteChart.module.css @@ -1,9 +1,14 @@ .container { + position: relative; display: flex; flex-direction: column; align-self: stretch; } +.chart { + position: relative; +} + .title { font-size: var(--font-size-large); line-height: 60px; @@ -37,11 +42,6 @@ align-items: center; } -.url { - text-align: center; - margin-bottom: 10px; -} - @media only screen and (max-width: 992px) { .filter { display: block; diff --git a/components/pages/WebsiteDetails.js b/components/pages/WebsiteDetails.js index 0ddc89e2..a8442ea1 100644 --- a/components/pages/WebsiteDetails.js +++ b/components/pages/WebsiteDetails.js @@ -118,9 +118,9 @@ export default function WebsiteDetails({ websiteId }) { showLink={false} stickyHeader /> + {!chartLoaded && }
- {!chartLoaded && } {chartLoaded && !view && ( diff --git a/lib/filters.js b/lib/filters.js index bb8b0c38..af54b965 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -95,9 +95,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { const url = cleanUrl(x); - if (!domainOnly && !raw) { - links[url] = x; - } + links[url] = x; if (url) { if (!obj[url]) { diff --git a/lib/queries.js b/lib/queries.js index ff9d557f..78b5bfdb 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -318,14 +318,20 @@ export async function getEvents(websites, start_at) { export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { const params = [website_id, start_at, end_at]; - const { url } = filters; + const { url, ref } = filters; let urlFilter = ''; + let refFilter = ''; if (url) { urlFilter = `and url=$${params.length + 1}`; params.push(decodeURIComponent(url)); } + if (ref) { + refFilter = `and referrer like $${params.length + 1}`; + params.push(`%${decodeURIComponent(ref)}%`); + } + return rawQuery( ` select sum(t.c) as "pageviews", @@ -341,6 +347,7 @@ export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { where website_id=$1 and created_at between $2 and $3 ${urlFilter} + ${refFilter} group by 1, 2 ) t `, @@ -355,16 +362,24 @@ export function getPageviewStats( timezone = 'utc', unit = 'day', count = '*', - url, + filters = {}, ) { const params = [website_id, start_at, end_at]; + const { url, ref } = filters; + let urlFilter = ''; + let refFilter = ''; if (url) { urlFilter = `and url=$${params.length + 1}`; params.push(decodeURIComponent(url)); } + if (ref) { + refFilter = `and referrer like $${params.length + 1}`; + params.push(`%${decodeURIComponent(ref)}%`); + } + return rawQuery( ` select ${getDateQuery('created_at', unit, timezone)} t, @@ -373,6 +388,7 @@ export function getPageviewStats( where website_id=$1 and created_at between $2 and $3 ${urlFilter} + ${refFilter} group by 1 order by 1 `, @@ -411,7 +427,7 @@ export function getSessionMetrics(website_id, start_at, end_at, field, filters = export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) { const params = [website_id, start_at, end_at]; - const { domain, url } = filters; + const { domain, url, ref } = filters; let domainFilter = ''; let urlFilter = ''; diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 965f28ae..22966ca7 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -11,7 +11,7 @@ export default async (req, res) => { return unauthorized(res); } - const { id, start_at, end_at, unit, tz, url } = req.query; + const { id, start_at, end_at, unit, tz, url, ref } = req.query; const websiteId = +id; const startDate = new Date(+start_at); @@ -22,8 +22,11 @@ export default async (req, res) => { } const [pageviews, sessions] = await Promise.all([ - getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url), - getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), + getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, ref }), + getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', { + url, + ref, + }), ]); return ok(res, { pageviews, sessions }); diff --git a/pages/api/website/[id]/stats.js b/pages/api/website/[id]/stats.js index 80dd22ec..9dd82361 100644 --- a/pages/api/website/[id]/stats.js +++ b/pages/api/website/[id]/stats.js @@ -8,7 +8,7 @@ export default async (req, res) => { return unauthorized(res); } - const { id, start_at, end_at, url } = req.query; + const { id, start_at, end_at, url, ref } = req.query; const websiteId = +id; const startDate = new Date(+start_at); @@ -18,8 +18,8 @@ export default async (req, res) => { const prevStartDate = new Date(+start_at - distance); const prevEndDate = new Date(+end_at - distance); - const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url }); - const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, { url }); + const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url, ref }); + const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, { url, ref }); const stats = Object.keys(metrics[0]).reduce((obj, key) => { obj[key] = {