mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Mark obvious bot sessions with robot avatars
Detect automated sessions from known data center cities (Council Bluffs, Santa Clara, Ashburn, etc.) with zero session duration, and display them with robot avatars instead of human faces. This provides an instant visual indicator in the Sessions UI without needing to inspect city/duration for each session. Uses both conditions (city match AND zero duration) to minimize false positives while reliably catching obvious bots.
This commit is contained in:
parent
81e27fc18c
commit
5413ce5d61
4 changed files with 43 additions and 10 deletions
|
|
@ -13,6 +13,7 @@ import { X } from 'lucide-react';
|
|||
import { Avatar } from '@/components/common/Avatar';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { useMessages, useWebsiteSessionQuery } from '@/components/hooks';
|
||||
import { isLikelyBot } from '@/lib/botDetection';
|
||||
import { SessionActivity } from './SessionActivity';
|
||||
import { SessionData } from './SessionData';
|
||||
import { SessionInfo } from './SessionInfo';
|
||||
|
|
@ -51,7 +52,7 @@ export function SessionProfile({
|
|||
)}
|
||||
<Column gap="6">
|
||||
<Row justifyContent="center" alignItems="center" gap="6">
|
||||
<Avatar seed={data?.id} size={128} />
|
||||
<Avatar seed={data?.id} size={128} isBot={isLikelyBot(data)} />
|
||||
<Column width="360px">
|
||||
<TextField label="ID" value={data?.id} allowCopy />
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Avatar } from '@/components/common/Avatar';
|
|||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { TypeIcon } from '@/components/common/TypeIcon';
|
||||
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { isLikelyBot } from '@/lib/botDetection';
|
||||
|
||||
export function SessionsTable(props: DataTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -15,7 +16,7 @@ export function SessionsTable(props: DataTableProps) {
|
|||
<DataColumn id="id" label={formatMessage(labels.session)} width="100px">
|
||||
{(row: any) => (
|
||||
<Link href={updateParams({ session: row.id })}>
|
||||
<Avatar seed={row.id} size={32} />
|
||||
<Avatar seed={row.id} size={32} isBot={isLikelyBot(row)} />
|
||||
</Link>
|
||||
)}
|
||||
</DataColumn>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
import { lorelei } from '@dicebear/collection';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { bottts, lorelei } from '@dicebear/collection';
|
||||
import { createAvatar, type Style } from '@dicebear/core';
|
||||
import { useMemo } from 'react';
|
||||
import { getColor, getPastel } from '@/lib/colors';
|
||||
|
||||
const lib = lorelei;
|
||||
|
||||
export function Avatar({ seed, size = 128, ...props }: { seed: string; size?: number }) {
|
||||
export function Avatar({
|
||||
seed,
|
||||
size = 128,
|
||||
isBot = false,
|
||||
}: {
|
||||
seed: string;
|
||||
size?: number;
|
||||
isBot?: boolean;
|
||||
}) {
|
||||
const backgroundColor = getPastel(getColor(seed), 4);
|
||||
const style = (isBot ? bottts : lorelei) as Style<object>;
|
||||
|
||||
const avatar = useMemo(() => {
|
||||
return createAvatar(lib, {
|
||||
...props,
|
||||
return createAvatar(style, {
|
||||
seed,
|
||||
size,
|
||||
backgroundColor: [backgroundColor],
|
||||
}).toDataUri();
|
||||
}, []);
|
||||
}, [seed, isBot]);
|
||||
|
||||
return <img src={avatar} alt="Avatar" style={{ borderRadius: '100%', width: size }} />;
|
||||
}
|
||||
|
|
|
|||
25
src/lib/botDetection.ts
Normal file
25
src/lib/botDetection.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const BOT_CITIES = [
|
||||
'Council Bluffs',
|
||||
'North Richland Hills',
|
||||
'Santa Clara',
|
||||
'Ashburn',
|
||||
'The Dalles',
|
||||
'Boardman',
|
||||
'Quincy',
|
||||
];
|
||||
|
||||
export function isLikelyBot(session: {
|
||||
city?: string;
|
||||
firstAt?: string | Date;
|
||||
lastAt?: string | Date;
|
||||
}): boolean {
|
||||
const cityMatch =
|
||||
session.city && BOT_CITIES.some(botCity => session.city?.toLowerCase() === botCity.toLowerCase());
|
||||
|
||||
const zeroDuration =
|
||||
session.firstAt &&
|
||||
session.lastAt &&
|
||||
new Date(session.firstAt).getTime() === new Date(session.lastAt).getTime();
|
||||
|
||||
return !!(cityMatch && zeroDuration);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue