From 957cfdcb5cc3131e7ae4d006284cda7d104b439b Mon Sep 17 00:00:00 2001 From: kirillgroshkov Date: Tue, 15 Oct 2024 18:45:52 +0200 Subject: [PATCH] feat: botDetectionService.isBot to give BotReason enum instead of plain boolean --- src/bot.test.ts | 34 ++++++++++++++++++++-------------- src/bot.ts | 42 +++++++++++++++++++++++++++--------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/bot.test.ts b/src/bot.test.ts index 4f40dca..f047448 100644 --- a/src/bot.test.ts +++ b/src/bot.test.ts @@ -1,4 +1,4 @@ -import { botDetectionService } from './bot' +import { botDetectionService, BotReason } from './bot' beforeEach(() => { Object.assign(globalThis, { @@ -15,7 +15,7 @@ test('serverSide script is not a bot', () => { Object.assign(globalThis, { window: undefined, }) - expect(botDetectionService.isBot()).toBe(false) + expect(botDetectionService.isBot()).toBeUndefined() expect(botDetectionService.isCDP()).toBe(false) expect(botDetectionService.isBotOrCDP()).toBe(false) }) @@ -27,7 +27,7 @@ test('innocent chrome is not a bot', () => { } as Navigator, chrome: {}, }) - expect(botDetectionService.isBot()).toBe(false) + expect(botDetectionService.isBot()).toBeUndefined() }) test('innocent safari is not a bot', () => { @@ -36,32 +36,36 @@ test('innocent safari is not a bot', () => { userAgent: userAgentSafari, } as Navigator, }) - expect(botDetectionService.isBot()).toBe(false) + expect(botDetectionService.isBot()).toBeUndefined() +}) + +test('no navigator means bot', () => { + expect(botDetectionService.isBot()).toBe(BotReason.NoNavigator) }) test('no userAgent means bot', () => { globalThis.navigator = {} as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.NoUserAgent) }) test('"headless" in userAgent means bot', () => { globalThis.navigator = { userAgent: 'HeadlessChrome' } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.UserAgent) }) test('"electron" in userAgent means bot', () => { globalThis.navigator = { userAgent: 'I am Electron 99' } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.UserAgent) }) test('"phantom" in userAgent means bot', () => { globalThis.navigator = { userAgent: 'Phantom Menace' } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.UserAgent) }) test('"slimer" in userAgent means bot', () => { globalThis.navigator = { userAgent: 'Slimer than Slime' } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.UserAgent) }) test('navigator.webdriver means bot', () => { @@ -69,7 +73,7 @@ test('navigator.webdriver means bot', () => { userAgent: userAgentSafari, webdriver: true, } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.WebDriver) }) test('0 plugins means bot', () => { @@ -77,7 +81,7 @@ test('0 plugins means bot', () => { userAgent: userAgentSafari, plugins: [] as any, } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.ZeroPlugins) }) test('"" languages means bot', () => { @@ -85,14 +89,14 @@ test('"" languages means bot', () => { userAgent: userAgentSafari, languages: '' as any, } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.EmptyLanguages) }) test('Chrome without chrome means bot', () => { globalThis.navigator = { userAgent: userAgentChrome, } as Navigator - expect(botDetectionService.isBot()).toBe(true) + expect(botDetectionService.isBot()).toBe(BotReason.ChromeWithoutChrome) }) // This test helps with coverage, while not really testing anything useful @@ -101,8 +105,10 @@ test('cdp in jest looks like a bot, because it does error serialization', () => window: globalThis, }) + // console.log('isBot', botDetectionService.isBot()) const isCDP = botDetectionService.isCDP() - expect(isCDP).toBe(botDetectionService.isBotOrCDP()) + // expect(isCDP).toBe(botDetectionService.isBotOrCDP()) + expect(isCDP).toBeDefined() // const runInIDE = process.argv.some( // a => a === '--runTestsByPath' || a.includes('IDEA') || a.includes('processChild.js'), diff --git a/src/bot.ts b/src/bot.ts index c24c4b7..89a5f02 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -10,42 +10,44 @@ import { isServerSide } from '@naturalcycles/js-lib' */ class BotDetectionService { isBotOrCDP(): boolean { - return this.isBot() || this.isCDP() + return !!this.isBot() || this.isCDP() } - isBot(): boolean { + /** + * Returns undefined if it's not a Bot, + * otherwise a truthy BotReason. + */ + isBot(): BotReason | undefined { // SSR - not a bot - if (isServerSide()) return false + if (isServerSide()) return const { navigator } = globalThis - if (!navigator) return false + if (!navigator) return BotReason.NoNavigator const { userAgent } = navigator - if (!userAgent) return true + if (!userAgent) return BotReason.NoUserAgent - if (/headless/i.test(userAgent)) return true - if (/electron/i.test(userAgent)) return true - if (/phantom/i.test(userAgent)) return true - if (/slimer/i.test(userAgent)) return true + if (/headless/i.test(userAgent)) return BotReason.UserAgent + if (/electron/i.test(userAgent)) return BotReason.UserAgent + if (/phantom/i.test(userAgent)) return BotReason.UserAgent + if (/slimer/i.test(userAgent)) return BotReason.UserAgent if (navigator.webdriver) { - return true + return BotReason.WebDriver } if (navigator.plugins?.length === 0) { - return true // Headless Chrome + return BotReason.ZeroPlugins // Headless Chrome } if ((navigator.languages as any) === '') { - return true // Headless Chrome + return BotReason.EmptyLanguages // Headless Chrome } // isChrome is true if the browser is Chrome, Chromium or Opera // this is "the chrome test" from https://intoli.com/blog/not-possible-to-block-chrome-headless/ // this property is for some reason not present by default in headless chrome if (userAgent.includes('Chrome') && !(globalThis as any).chrome) { - return true // Headless Chrome + return BotReason.ChromeWithoutChrome // Headless Chrome } - - return false } /** @@ -86,3 +88,13 @@ class BotDetectionService { } export const botDetectionService = new BotDetectionService() + +export enum BotReason { + NoNavigator = 1, + NoUserAgent = 2, + UserAgent = 3, + WebDriver = 4, + ZeroPlugins = 5, + EmptyLanguages = 6, + ChromeWithoutChrome = 7, +}