Referrer filtering.

This commit is contained in:
Mike Cao 2021-11-21 22:00:14 -08:00
parent 65d4094095
commit ebd52335bb
15 changed files with 158 additions and 54 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M392 320C378.75 320 368 330.75 368 344V456C368 460.406 364.406 464 360 464H56C51.594 464 48 460.406 48 456V152C48 147.594 51.594 144 56 144H168C181.25 144 192 133.25 192 120S181.25 96 168 96H56C25.125 96 0 121.125 0 152V456C0 486.875 25.125 512 56 512H360C390.875 512 416 486.875 416 456V344C416 330.75 405.25 320 392 320ZM488 0H320C306.75 0 296 10.75 296 24S306.75 48 320 48H430.062L183.031 295.031C173.656 304.406 173.656 319.594 183.031 328.969C187.719 333.656 193.844 336 200 336S212.281 333.656 216.969 328.969L464 81.938V192C464 205.25 474.75 216 488 216S512 205.25 512 192V24C512 10.75 501.25 0 488 0Z"/></svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -6,6 +6,8 @@
margin: auto;
display: flex;
z-index: 1;
background-color: var(--gray50);
padding: 10px;
}
.icon {

View File

@ -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 (
<div className={classNames(styles.filters, 'col-12')}>
{Object.keys(params).map(key => {
if (!params[key]) {
return null;
}
return (
<div className={styles.tag}>
<Button icon={<Times />} onClick={() => onClick(key)} variant="action" iconRight>
{`${key}: ${params[key]}`}
</Button>
</div>
);
})}
</div>
);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
];
const renderLink = ({ x }) => {
const renderLink = ({ x: url }) => {
return (
<Link href={resolve({ url: x })} replace={true}>
<Link href={resolve({ url })} replace={true}>
<a
className={classNames({
[styles.inactive]: url && x !== url,
[styles.active]: x === url,
[styles.inactive]: currentUrl && url !== currentUrl,
[styles.active]: url === currentUrl,
})}
>
{safeDecodeURI(x)}
{safeDecodeURI(url)}
</a>
</Link>
);

View File

@ -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: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
];
const renderLink = ({ w: href, x: url }) => {
return (href || url).startsWith('http') ? (
<a href={href || url} target="_blank" rel="noreferrer">
{safeDecodeURI(url)}
const renderLink = ({ w: link, x: label }) => {
console.log({ link, label });
return (
<div className={styles.row}>
<Link href={resolve({ ref: label })} replace={true}>
<a
className={classNames(styles.label, {
[styles.inactive]: currentRef && label !== currentRef,
[styles.active]: label === currentRef,
})}
>
{safeDecodeURI(label)}
</a>
) : (
safeDecodeURI(url)
</Link>
<a href={link || label} target="_blank" rel="noreferrer noopener" className={styles.link}>
<Icon icon={<External />} className={styles.icon} />
</a>
</div>
);
};

View File

@ -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;
}

View File

@ -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 && <PageFilter url={url} onClick={handleCloseFilter} />}
<FilterTags params={{ url, ref }} onClick={handleCloseFilter} />
<div className="col-12 col-lg-9">
<MetricsBar websiteId={websiteId} />
</div>
@ -90,7 +90,7 @@ export default function WebsiteChart({
</StickyHeader>
</div>
<div className="row">
<div className="col">
<div className={classNames(styles.chart, 'col')}>
{error && <ErrorMessage />}
{!hideChart && (
<PageviewsChart
@ -106,13 +106,3 @@ export default function WebsiteChart({
</div>
);
}
const PageFilter = ({ url, onClick }) => {
return (
<div className={classNames(styles.url, 'col-12')}>
<Button icon={<Times />} onClick={onClick} variant="action" iconRight>
{url}
</Button>
</div>
);
};

View File

@ -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;

View File

@ -118,9 +118,9 @@ export default function WebsiteDetails({ websiteId }) {
showLink={false}
stickyHeader
/>
</div>
</div>
{!chartLoaded && <Loading />}
</div>
</div>
{chartLoaded && !view && (
<GridLayout>
<GridRow>

View File

@ -95,9 +95,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
const url = cleanUrl(x);
if (!domainOnly && !raw) {
links[url] = x;
}
if (url) {
if (!obj[url]) {

View File

@ -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 = '';

View File

@ -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 });

View File

@ -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] = {