mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +01:00
Grouped referrers.
This commit is contained in:
parent
fd4a405779
commit
84193a4ec5
6 changed files with 175 additions and 71 deletions
|
|
@ -25,6 +25,7 @@ const views = {
|
|||
exit: PagesTable,
|
||||
title: PagesTable,
|
||||
referrer: ReferrersTable,
|
||||
grouped: ReferrersTable,
|
||||
host: HostsTable,
|
||||
browser: BrowsersTable,
|
||||
os: OSTable,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
import { z } from 'zod';
|
||||
import thenby from 'thenby';
|
||||
import { canViewWebsite } from '@/lib/auth';
|
||||
import { SESSION_COLUMNS, EVENT_COLUMNS, FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
|
||||
import {
|
||||
SESSION_COLUMNS,
|
||||
EVENT_COLUMNS,
|
||||
FILTER_COLUMNS,
|
||||
OPERATORS,
|
||||
SEARCH_DOMAINS,
|
||||
SOCIAL_DOMAINS,
|
||||
EMAIL_DOMAINS,
|
||||
SHOPPING_DOMAINS,
|
||||
VIDEO_DOMAINS,
|
||||
PAID_AD_PARAMS,
|
||||
} from '@/lib/constants';
|
||||
import { getRequestFilters, getRequestDateRange, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized, badRequest } from '@/lib/response';
|
||||
import { getPageviewMetrics, getSessionMetrics, getChannelMetrics } from '@/queries';
|
||||
|
|
@ -94,64 +105,6 @@ export async function GET(
|
|||
return badRequest();
|
||||
}
|
||||
|
||||
const SOCIAL_DOMAINS = [
|
||||
'facebook.com',
|
||||
'fb.com',
|
||||
'instagram.com',
|
||||
'ig.com',
|
||||
'twitter.com',
|
||||
't.co',
|
||||
'x.com',
|
||||
'linkedin.',
|
||||
'tiktok.',
|
||||
'reddit.',
|
||||
'threads.net',
|
||||
'bsky.app',
|
||||
'news.ycombinator.com',
|
||||
];
|
||||
|
||||
const SEARCH_DOMAINS = [
|
||||
'google.',
|
||||
'bing.com',
|
||||
'msn.com',
|
||||
'duckduckgo.com',
|
||||
'search.brave.com',
|
||||
'yandex.',
|
||||
'baidu.com',
|
||||
'ecosia.org',
|
||||
'chatgpt.com',
|
||||
'perplexity.ai',
|
||||
];
|
||||
|
||||
const SHOPPING_DOMAINS = [
|
||||
'amazon.',
|
||||
'ebay.com',
|
||||
'walmart.com',
|
||||
'alibab.com',
|
||||
'aliexpress.com',
|
||||
'etsy.com',
|
||||
'bestbuy.com',
|
||||
'target.com',
|
||||
'newegg.com',
|
||||
];
|
||||
|
||||
const EMAIL_DOMAINS = ['gmail.', 'mail.yahoo.', 'outlook.', 'hotmail.', 'protonmail.', 'proton.me'];
|
||||
|
||||
const VIDEO_DOMAINS = ['youtube.', 'twitch.'];
|
||||
|
||||
const PAID_AD_PARAMS = [
|
||||
'utm_source=google',
|
||||
'gclid=',
|
||||
'fbclid=',
|
||||
'msclkid=',
|
||||
'dclid=',
|
||||
'twclid=',
|
||||
'li_fat_id=',
|
||||
'epik=',
|
||||
'ttclid=',
|
||||
'scid=',
|
||||
];
|
||||
|
||||
function getChannels(data: { domain: string; query: string; visitors: number }[]) {
|
||||
const channels = {
|
||||
direct: 0,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { GROUPED_DOMAINS } from '@/lib/constants';
|
||||
|
||||
function getHostName(url: string) {
|
||||
const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?([^:/\n?=]+)/im);
|
||||
return match && match.length > 1 ? match[1] : null;
|
||||
|
|
@ -9,16 +11,11 @@ export function Favicon({ domain, ...props }) {
|
|||
}
|
||||
|
||||
const hostName = domain ? getHostName(domain) : null;
|
||||
const src = hostName
|
||||
? `https://icons.duckduckgo.com/ip3/${GROUPED_DOMAINS[hostName]?.domain || hostName}.ico`
|
||||
: null;
|
||||
|
||||
return hostName ? (
|
||||
<img
|
||||
src={`https://icons.duckduckgo.com/ip3/${hostName}.ico`}
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
{...props}
|
||||
/>
|
||||
) : null;
|
||||
return hostName ? <img src={src} width={16} height={16} alt="" {...props} /> : null;
|
||||
}
|
||||
|
||||
export default Favicon;
|
||||
|
|
|
|||
|
|
@ -295,6 +295,8 @@ export const labels = defineMessages({
|
|||
paidSocial: { id: 'label.paid-social', defaultMessage: 'Paid social' },
|
||||
paidShopping: { id: 'label.paid-shopping', defaultMessage: 'Paid shopping' },
|
||||
paidVideo: { id: 'label.paid-video', defaultMessage: 'Paid video' },
|
||||
grouped: { id: 'label.grouped', defaultMessage: 'Grouped' },
|
||||
other: { id: 'label.other', defaultMessage: 'Other' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -1,12 +1,53 @@
|
|||
import FilterLink from '@/components/common/FilterLink';
|
||||
import Favicon from '@/components/common/Favicon';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import FilterButtons from '@/components/common/FilterButtons';
|
||||
import thenby from 'thenby';
|
||||
import { GROUPED_DOMAINS } from '@/lib/constants';
|
||||
import { Flexbox } from 'react-basics';
|
||||
|
||||
export function ReferrersTable(props: MetricsTableProps) {
|
||||
export interface ReferrersTableProps extends MetricsTableProps {
|
||||
allowFilter?: boolean;
|
||||
}
|
||||
|
||||
export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
|
||||
const {
|
||||
router,
|
||||
renderUrl,
|
||||
query: { view = 'referrer' },
|
||||
} = useNavigation();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSelect = (key: any) => {
|
||||
router.push(renderUrl({ view: key }), { scroll: false });
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: formatMessage(labels.domain),
|
||||
key: 'referrer',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.grouped),
|
||||
key: 'grouped',
|
||||
},
|
||||
];
|
||||
|
||||
const renderLink = ({ x: referrer }) => {
|
||||
if (view === 'grouped') {
|
||||
if (referrer === '_other') {
|
||||
return `(${formatMessage(labels.other)})`;
|
||||
} else {
|
||||
return (
|
||||
<Flexbox alignItems="center" gap={10}>
|
||||
<Favicon domain={referrer} />
|
||||
{GROUPED_DOMAINS.find(({ domain }) => domain === referrer)?.name}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterLink
|
||||
id="referrer"
|
||||
|
|
@ -19,6 +60,27 @@ export function ReferrersTable(props: MetricsTableProps) {
|
|||
);
|
||||
};
|
||||
|
||||
const groupedFilter = (data: any[]) => {
|
||||
const groups = { _other: 0 };
|
||||
|
||||
for (const { x, y } of data) {
|
||||
for (const { domain, match } of GROUPED_DOMAINS) {
|
||||
if (Array.isArray(match) ? match.some(str => x.includes(str)) : x.includes(match)) {
|
||||
if (!groups[domain]) {
|
||||
groups[domain] = 0;
|
||||
}
|
||||
groups[domain] += y;
|
||||
} else {
|
||||
groups._other += y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(groups)
|
||||
.map((key: any) => ({ x: key, y: groups[key] }))
|
||||
.sort(thenby.firstBy('y', -1));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricsTable
|
||||
|
|
@ -26,8 +88,13 @@ export function ReferrersTable(props: MetricsTableProps) {
|
|||
title={formatMessage(labels.referrers)}
|
||||
type="referrer"
|
||||
metric={formatMessage(labels.views)}
|
||||
dataFilter={view === 'grouped' ? groupedFilter : undefined}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
>
|
||||
{allowFilter && (
|
||||
<FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />
|
||||
)}
|
||||
</MetricsTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,6 +338,90 @@ export const IP_ADDRESS_HEADERS = [
|
|||
'x-appengine-user-ip',
|
||||
];
|
||||
|
||||
export const SOCIAL_DOMAINS = [
|
||||
'facebook.com',
|
||||
'fb.com',
|
||||
'instagram.com',
|
||||
'ig.com',
|
||||
'twitter.com',
|
||||
't.co',
|
||||
'x.com',
|
||||
'linkedin.',
|
||||
'tiktok.',
|
||||
'reddit.',
|
||||
'threads.net',
|
||||
'bsky.app',
|
||||
'news.ycombinator.com',
|
||||
'snapchat.',
|
||||
'pinterest.',
|
||||
];
|
||||
|
||||
export const SEARCH_DOMAINS = [
|
||||
'google.',
|
||||
'bing.com',
|
||||
'msn.com',
|
||||
'duckduckgo.com',
|
||||
'search.brave.com',
|
||||
'yandex.',
|
||||
'baidu.com',
|
||||
'ecosia.org',
|
||||
'chatgpt.com',
|
||||
'perplexity.ai',
|
||||
];
|
||||
|
||||
export const SHOPPING_DOMAINS = [
|
||||
'amazon.',
|
||||
'ebay.com',
|
||||
'walmart.com',
|
||||
'alibab.com',
|
||||
'aliexpress.com',
|
||||
'etsy.com',
|
||||
'bestbuy.com',
|
||||
'target.com',
|
||||
'newegg.com',
|
||||
];
|
||||
|
||||
export const EMAIL_DOMAINS = [
|
||||
'gmail.',
|
||||
'mail.yahoo.',
|
||||
'outlook.',
|
||||
'hotmail.',
|
||||
'protonmail.',
|
||||
'proton.me',
|
||||
];
|
||||
|
||||
export const VIDEO_DOMAINS = ['youtube.', 'twitch.'];
|
||||
|
||||
export const PAID_AD_PARAMS = [
|
||||
'utm_source=google',
|
||||
'gclid=',
|
||||
'fbclid=',
|
||||
'msclkid=',
|
||||
'dclid=',
|
||||
'twclid=',
|
||||
'li_fat_id=',
|
||||
'epik=',
|
||||
'ttclid=',
|
||||
'scid=',
|
||||
];
|
||||
|
||||
export const GROUPED_DOMAINS = [
|
||||
{ name: 'Google', domain: 'google.com', match: 'google.' },
|
||||
{ name: 'Facebook', domain: 'facebook.com', match: 'facebook.' },
|
||||
{ name: 'Reddit', domain: 'reddit.com', match: 'reddit.' },
|
||||
{ name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' },
|
||||
{ name: 'GitHub', domain: 'github.com', match: 'github.' },
|
||||
{ name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' },
|
||||
{ name: 'Bing', domain: 'bing.com', match: 'bing.' },
|
||||
{ name: 'Brave', domain: 'brave.com', match: 'brave.' },
|
||||
{ name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' },
|
||||
{ name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] },
|
||||
{ name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] },
|
||||
{ name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' },
|
||||
{ name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' },
|
||||
{ name: 'ChatGPT', domain: 'chatgpt.com', match: 'chatgpt.' },
|
||||
];
|
||||
|
||||
export const MAP_FILE = '/datamaps.world.json';
|
||||
|
||||
export const ISO_COUNTRIES = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue