From 5413ce5d61510e3cd3cfc2008325f766af55aa77 Mon Sep 17 00:00:00 2001 From: Arthur Sepiol Date: Sun, 7 Dec 2025 20:35:38 +0300 Subject: [PATCH] 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. --- .../[websiteId]/sessions/SessionProfile.tsx | 3 ++- .../[websiteId]/sessions/SessionsTable.tsx | 3 ++- src/components/common/Avatar.tsx | 22 ++++++++++------ src/lib/botDetection.ts | 25 +++++++++++++++++++ 4 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 src/lib/botDetection.ts diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx index 6624d439d..1f55ef622 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx @@ -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({ )} - + diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx index 5d3bb374e..60632fedd 100644 --- a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx @@ -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) { {(row: any) => ( - + )} diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 9b198b30c..3277ffcaf 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -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; const avatar = useMemo(() => { - return createAvatar(lib, { - ...props, + return createAvatar(style, { seed, size, backgroundColor: [backgroundColor], }).toDataUri(); - }, []); + }, [seed, isBot]); return Avatar; } diff --git a/src/lib/botDetection.ts b/src/lib/botDetection.ts new file mode 100644 index 000000000..b3a85a8c8 --- /dev/null +++ b/src/lib/botDetection.ts @@ -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); +}