browserx 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -1
- data/README.md +122 -21
- data/bot_categories.yml +1522 -0
- data/bot_ip_ranges.yml +3035 -0
- data/bots.yml +1249 -0
- data/lib/browser/action_controller.rb +2 -1
- data/lib/browser/base.rb +20 -5
- data/lib/browser/bot/ip_ranges.rb +194 -0
- data/lib/browser/bot/known_bots_matcher.rb +1 -1
- data/lib/browser/bot.rb +98 -2
- data/lib/browser/browser.rb +69 -1
- data/lib/browser/cache.rb +92 -0
- data/lib/browser/client_hints.rb +73 -0
- data/lib/browser/cloudflare.rb +65 -0
- data/lib/browser/detection.rb +25 -0
- data/lib/browser/device/psvita.rb +1 -1
- data/lib/browser/device/surface.rb +1 -1
- data/lib/browser/device.rb +31 -7
- data/lib/browser/middleware/context.rb +2 -1
- data/lib/browser/platform.rb +34 -7
- data/lib/browser/version.rb +1 -1
- data/lib/browserx.rb +1 -1
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b7ec2cc79907813c061fbd0fd29de46378a16b55266853496a4bc9ed704a0b3
|
|
4
|
+
data.tar.gz: 624eddba5541547f0e43e4de1d70d12082e40fb4f1f9267423915b7db00c5b1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9109d7b4da2dfc23f155c834b85fa86c8d477739b6afaab08fe4ad7bd3e65acfe24ae837b156d0d32679a3397990407055530221f54c1b0d47f01d916e3547b5
|
|
7
|
+
data.tar.gz: 93be1a0915373089c25935ef3877a7a6c10dd9d853a619480f7272c41161ebe9b25e0653280301f543d434893963949bb65cf901c35a07cc33ddcfe9e65bc2bf
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.0.0] — 2026-11-04
|
|
6
|
+
|
|
7
|
+
A repeated user agent is now answered in 2 µs, about 600 more bots are recognised, and bots, Cloudflare headers and
|
|
8
|
+
Client Hints can be checked against the request. Without the new headers, every answer stays browser 6.2.0's,
|
|
9
|
+
except bot detection, which finds more bots.
|
|
10
|
+
|
|
11
|
+
**Breaking**
|
|
12
|
+
- `bot?` is true for about 600 more user agents: bots from crawler-user-agents and ai.robots.txt, synced weekly,
|
|
13
|
+
add 1,247 entries to `bots.yml`. Of the crawlers added there since 2024, browser misses 353 out of 882 and
|
|
14
|
+
BrowserX 11. Analytics that filter on `bot?` will count fewer humans from the day you switch.
|
|
15
|
+
- The Rails helper and the middleware now pass the request's headers, so Client Hints refine the answers on
|
|
16
|
+
Chromium: Windows 11 is no longer `windows10?`, and Chrome for Android in desktop mode is an Android tablet,
|
|
17
|
+
not a Linux desktop.
|
|
18
|
+
|
|
19
|
+
**New**
|
|
20
|
+
- `bot.category` (16 categories, close to Cloudflare's verified bot categories), `bot.ai?` and
|
|
21
|
+
`bot.user_triggered?`. Test drivers (Playwright, Puppeteer, Selenium, headless Chrome) get the
|
|
22
|
+
`browser_automation` category but are not bots, so system tests keep running as humans.
|
|
23
|
+
- `bot.verified?(ip)` checks a crawler against the IP ranges its owner publishes (Google, Bing, OpenAI, Apple,
|
|
24
|
+
Perplexity, DuckDuckGo), shipped with the gem and refreshable at runtime; reverse DNS is opt-in. `bot.owner`
|
|
25
|
+
names the owner.
|
|
26
|
+
- `Browser.new(ua, headers: request.env)` reads Cloudflare's bot headers (`browser.cloudflare`: verified bot,
|
|
27
|
+
category, score, country; header names configurable) and Chromium's Client Hints (`browser.client_hints`,
|
|
28
|
+
`platform.windows11?`).
|
|
29
|
+
- `Browser.cache_size` (default 1,000, `0` turns it off) bounds the new per-user-agent cache: about 1 KB per
|
|
30
|
+
entry, so about 1 MB per process.
|
|
31
|
+
|
|
32
|
+
**Faster**
|
|
33
|
+
- Answers are cached per user agent, matchers that can't match are skipped, and bots are found through a byte
|
|
34
|
+
trie. Ruby 4.0.9: a repeated user agent takes 1.8 µs instead of 57.5 with YJIT and 3.6 µs instead of 72.5
|
|
35
|
+
without; 2,000 distinct user agents take 24.2 µs instead of 62.4 with YJIT and 43.9 µs instead of 77.3
|
|
36
|
+
without. Objects per request: 9 instead of 116 repeated, 50 instead of 125 distinct.
|
|
37
|
+
|
|
38
|
+
**Fixed**
|
|
39
|
+
- Loading `browser` and `browserx` together raises a clear `LoadError` instead of one silently overriding the
|
|
40
|
+
other.
|
|
41
|
+
|
|
5
42
|
## [1.0.0] — 2026-11-04
|
|
6
43
|
|
|
7
44
|
First release as `browserx`, forked from browser 6.2.0 (`dd88933`). Same API: `gem "browserx"` replaces
|
|
@@ -25,5 +62,6 @@ First release as `browserx`, forked from browser 6.2.0 (`dd88933`). Same API: `g
|
|
|
25
62
|
- A corpus of 20,328 user agents, with snapshot and diff scripts proving answers match upstream, and a
|
|
26
63
|
request benchmark.
|
|
27
64
|
|
|
28
|
-
[Unreleased]: https://github.com/next-gen-gems/browserx/compare/
|
|
65
|
+
[Unreleased]: https://github.com/next-gen-gems/browserx/compare/v2.0.0...HEAD
|
|
66
|
+
[2.0.0]: https://github.com/next-gen-gems/browserx/compare/v1.0.0...v2.0.0
|
|
29
67
|
[1.0.0]: https://github.com/next-gen-gems/browserx/releases/tag/v1.0.0
|
data/README.md
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
# BrowserX
|
|
2
2
|
|
|
3
|
-
Browser detection for Ruby, with ActionController integration
|
|
4
|
-
[browser](https://rubygems.org/gems/browser) gem
|
|
5
|
-
`gem "browser"`, and every call site keeps working — same `Browser` namespace, same
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
Browser detection for Ruby, with ActionController integration: the API of the
|
|
4
|
+
[browser](https://rubygems.org/gems/browser) gem, faster, and a bot list that stays fresh. Put `gem "browserx"` in
|
|
5
|
+
your Gemfile instead of `gem "browser"`, and every call site keeps working — same `Browser` namespace, same
|
|
6
|
+
`require "browser"`, same predicates.
|
|
7
|
+
|
|
8
|
+
- **Faster**: 25 µs per request instead of 126 µs with browser 6.2.0, 2 µs once a user agent was seen (Ruby 4.0.9,
|
|
9
|
+
YJIT, `bench/request.rb`).
|
|
10
|
+
- **Fresher bots**: synced every week from [crawler-user-agents](https://github.com/monperrus/crawler-user-agents)
|
|
11
|
+
and [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt), with categories (`bot.category`, `bot.ai?`).
|
|
12
|
+
Of the 882 crawler user agents added to crawler-user-agents since 2024 (test drivers and scanners left out),
|
|
13
|
+
browser misses 353, BrowserX 11.
|
|
14
|
+
- **Verified signals**: `bot.verified?(ip)` checks a crawler against the IP ranges Google, Bing, OpenAI, Apple,
|
|
15
|
+
Perplexity and DuckDuckGo publish, `browser.cloudflare` reads the headers of a Cloudflare Transform Rule, and
|
|
16
|
+
Chromium's Client Hints tell Windows 11 from Windows 10.
|
|
10
17
|
|
|
11
18
|
Site: https://next-gen-gems.github.io/browserx/
|
|
12
19
|
|
|
@@ -64,6 +71,9 @@ browser.epiphany?
|
|
|
64
71
|
# Get bot info
|
|
65
72
|
browser.bot.name
|
|
66
73
|
browser.bot.search_engine?
|
|
74
|
+
browser.bot.category # :ai_crawler, :search_engine_crawler, :scanner… (nil for a browser)
|
|
75
|
+
browser.bot.ai?
|
|
76
|
+
browser.bot.user_triggered? # an AI agent acting for a person right now (ChatGPT-User, Claude-User)
|
|
67
77
|
browser.bot?
|
|
68
78
|
browser.bot.why? # shows which matcher detected this user agent as a bot.
|
|
69
79
|
Browser::Bot.why?(ua)
|
|
@@ -187,9 +197,17 @@ end
|
|
|
187
197
|
### Migrating from browser
|
|
188
198
|
|
|
189
199
|
Replace `gem "browser"` with `gem "browserx"` in the Gemfile, nothing else: `require "browser"`, the `Browser`
|
|
190
|
-
namespace and every predicate stay.
|
|
200
|
+
namespace and every predicate stay. Both gems define `Browser`, so BrowserX refuses to load next to `browser`,
|
|
201
|
+
with an error saying so, rather than letting one silently override the other. BrowserX requires Ruby 3.3 or later
|
|
191
202
|
and is tested on Rails 8.0 and 8.1.
|
|
192
203
|
|
|
204
|
+
What changes on its own: the bots of the weekly sync are bots from the day you switch (600 user agents of the
|
|
205
|
+
corpus that browser took for humans), so analytics that leave bots out count fewer visitors from that day; 792
|
|
206
|
+
user agents named `Generic Bot` get a name; no browser, platform or device answer moves, and test drivers stay out
|
|
207
|
+
of `bot?`. To roll back, put `gem "browser"` back, after removing the calls to what browser lacks
|
|
208
|
+
(`Browser.cache_size=`, `bot.category`, `bot.ai?`, `bot.user_triggered?`). The
|
|
209
|
+
[migration guide](https://next-gen-gems.github.io/browserx/guide.html#migrating) has the details.
|
|
210
|
+
|
|
193
211
|
### Rails integration
|
|
194
212
|
|
|
195
213
|
Just add it to the Gemfile.
|
|
@@ -310,6 +328,27 @@ following requirements will be considered a bot.
|
|
|
310
328
|
- Any known bot listed under
|
|
311
329
|
[bots.yml](bots.yml)
|
|
312
330
|
|
|
331
|
+
Every week, `script/sync_bots.rb` adds the bots of
|
|
332
|
+
[crawler-user-agents](https://github.com/monperrus/crawler-user-agents) and
|
|
333
|
+
[ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt) (both MIT) to the end of `bots.yml`, and
|
|
334
|
+
categorizes them in `bot_categories.yml`, through a pull request that lists the answers that change. How many of
|
|
335
|
+
those lists' bots browser and BrowserX detect, per category, is on the
|
|
336
|
+
[Bots page](https://next-gen-gems.github.io/browserx/bots.html#coverage).
|
|
337
|
+
|
|
338
|
+
`browser.bot.category` names what the bot declares it is, with the names of Cloudflare's verified bot categories
|
|
339
|
+
where one fits (`search_engine_crawler`, `ai_crawler`, `ai_search`, `ai_assistant`, `page_preview`,
|
|
340
|
+
`feed_fetcher`, `monitoring_and_analytics`, `advertising_and_marketing`, `archiver`, `security`…), plus
|
|
341
|
+
`ai_agent`, `http_library`, `scanner` and `browser_automation`. `bot.ai?` is true for the four AI categories,
|
|
342
|
+
`bot.user_triggered?` for `ai_assistant` and `ai_agent`. The category is `nil` for a browser, a bot exception
|
|
343
|
+
and a bot no list names.
|
|
344
|
+
|
|
345
|
+
HTTP libraries (`curl`, `got`, `python-requests`) and scanners (sqlmap, Nikto, Censys) are bots, as `curl` and
|
|
346
|
+
`python-requests` already were in browser. Test drivers (headless Chrome, Playwright, Puppeteer, Selenium) are
|
|
347
|
+
not: an app's system tests drive a real browser through them, and the headless Chrome of Playwright and Selenium
|
|
348
|
+
sends `HeadlessChrome`. They get the `browser_automation` category instead, so an app can tell them apart in
|
|
349
|
+
production (`browser.bot? || browser.bot.category == :browser_automation`). PhantomJS stays a bot, as in
|
|
350
|
+
browser.
|
|
351
|
+
|
|
313
352
|
To add custom matchers, you can add a callable object to
|
|
314
353
|
`Browser::Bot.matchers`. The following example matches everything that has a
|
|
315
354
|
`externalhit` substring on it. The bot name will always be `General Bot`.
|
|
@@ -339,8 +378,34 @@ To extend the bot list, you can manipulate the methods below:
|
|
|
339
378
|
Browser::Bot.bots.merge!(new_bots_hash)
|
|
340
379
|
Browser::Bot.bot_exceptions += new_exceptions
|
|
341
380
|
Browser::Bot.search_engines.merge!(new_search_engines_hash)
|
|
381
|
+
Browser::Bot.category_by_keyword["faraday"] = :http_library
|
|
342
382
|
```
|
|
343
383
|
|
|
384
|
+
### Verified crawlers
|
|
385
|
+
|
|
386
|
+
Google, Bing, OpenAI, Apple, Perplexity and DuckDuckGo publish their crawlers' IP ranges. They ship in
|
|
387
|
+
`bot_ip_ranges.yml`, rewritten by the weekly sync (`script/sync_ip_ranges.rb`). The lookup is a binary search, no
|
|
388
|
+
network call; an IP that does not parse answers `false`.
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
browser.bot.owner # => :google for a user agent claiming Googlebot
|
|
392
|
+
browser.bot.verified?(request.remote_ip) # => whether the IP is in Google's ranges
|
|
393
|
+
browser.bot.verified?(request.remote_ip, reverse_dns: true) # also Google's DNS check: two DNS queries
|
|
394
|
+
Browser::Bot::IPRanges.refresh # fetches the ranges again, in a long-lived process
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Cloudflare and Client Hints
|
|
398
|
+
|
|
399
|
+
`Browser.new(user_agent, accept_language:, headers: rack_env)` also reads the request's headers; the Rails helper
|
|
400
|
+
and the middleware pass them. `browser.cloudflare` reads the headers a Cloudflare Transform Rule sets
|
|
401
|
+
(`verified_bot?`, `bot_category`, `bot_score`) and `CF-IPCountry` (`country`); their names are configurable with
|
|
402
|
+
`Browser::Cloudflare.header_names = {bot_score: "X-My-Score"}`. Chromium's Client Hints (`Sec-CH-UA`,
|
|
403
|
+
`Sec-CH-UA-Mobile`, `Sec-CH-UA-Platform`, `Sec-CH-UA-Platform-Version`) refine the platform and the device, and
|
|
404
|
+
`browser.platform.windows11?` needs them. Without `headers:`, every answer is the user agent's, as before. The
|
|
405
|
+
rule to create and the details are in the [guide](https://next-gen-gems.github.io/browserx/guide.html#cloudflare).
|
|
406
|
+
|
|
407
|
+
`bench/signals.rb` measures what they cost.
|
|
408
|
+
|
|
344
409
|
### Middleware
|
|
345
410
|
|
|
346
411
|
You can use the `Browser::Middleware` to redirect user agents.
|
|
@@ -383,17 +448,33 @@ Browser.user_agent_size_limit = 4096
|
|
|
383
448
|
Browser.accept_language_size_limit = 4096
|
|
384
449
|
```
|
|
385
450
|
|
|
451
|
+
### Cache
|
|
452
|
+
|
|
453
|
+
Each process keeps what it detected for the last 1,000 distinct user agents (browser, platform, device, bot
|
|
454
|
+
answers) and the last 1,000 Accept-Language headers it parsed, about 1 KB per user agent: a repeated user agent
|
|
455
|
+
costs about 2 µs. The cache empties itself when full, and when `Browser.matchers`, `Browser::Bot.matchers`,
|
|
456
|
+
`Browser::Platform.matchers`, `Browser::Device.matchers`, the bot list or its exceptions change. Setting
|
|
457
|
+
`Browser.cache_size` empties it too, for example after renaming an entry of `Browser::Bot.bots` in place.
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
Browser.cache_size = 5_000 # 0 turns it off
|
|
461
|
+
```
|
|
462
|
+
|
|
386
463
|
## Performance
|
|
387
464
|
|
|
388
465
|
`bench/request.rb` replays what a web request asks (name, version, `bot?`, platform, device, accept-language) over
|
|
389
466
|
2,000 distinct user agents of the corpus and over 20 repeated ones, median of 7. On Ruby 4.0.9 with YJIT, browser
|
|
390
|
-
6.2.0 takes
|
|
467
|
+
6.2.0 takes 126 µs and 245 objects per request, BrowserX 25 µs and 52 objects on a new user agent, 2 µs and 12
|
|
468
|
+
objects on a repeated one. All four runs, with and without YJIT, are on the
|
|
469
|
+
[benchmarks page](https://next-gen-gems.github.io/browserx/benchmarks.html).
|
|
391
470
|
|
|
392
471
|
```bash
|
|
393
|
-
|
|
394
|
-
ruby --yjit -I lib bench/request.rb
|
|
472
|
+
bundle exec rake bench UPSTREAM=path/to/upstream # fnando/browser at dd88933
|
|
395
473
|
```
|
|
396
474
|
|
|
475
|
+
It runs `ruby [--yjit] -I lib bench/request.rb --write` for each gem and JIT, which records the run (gem, commit,
|
|
476
|
+
Ruby, YJIT, machine, date) in `docs/benchmarks.json`, then `rake docs:render` rewrites the site's numbers from it.
|
|
477
|
+
|
|
397
478
|
## Development
|
|
398
479
|
|
|
399
480
|
```bash
|
|
@@ -403,7 +484,7 @@ bundle exec rubocop
|
|
|
403
484
|
```
|
|
404
485
|
|
|
405
486
|
A change that is not meant to change an answer proves it on the corpus of 20,000 user agents, against a plain
|
|
406
|
-
clone of [fnando/browser](https://github.com/fnando/browser) at `dd88933
|
|
487
|
+
clone of [fnando/browser](https://github.com/fnando/browser) at `dd88933`, as CI does on every pull request:
|
|
407
488
|
|
|
408
489
|
```bash
|
|
409
490
|
ruby -I path/to/upstream/lib script/corpus_snapshot.rb > /tmp/upstream.jsonl
|
|
@@ -411,28 +492,48 @@ ruby -I lib script/corpus_snapshot.rb > /tmp/browserx.jsonl
|
|
|
411
492
|
ruby script/corpus_diff.rb /tmp/upstream.jsonl /tmp/browserx.jsonl
|
|
412
493
|
```
|
|
413
494
|
|
|
414
|
-
|
|
415
|
-
|
|
495
|
+
The diff passes when the answers that differ are exactly those of `corpus/expected_differences.jsonl`: a change
|
|
496
|
+
meant to change answers rewrites it with `ruby script/corpus_diff.rb --write …` and shows its diff.
|
|
497
|
+
|
|
498
|
+
The weekly bot sync (`.github/workflows/bot-sync.yml`) runs the same steps by hand:
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
ruby script/sync_bots.rb # bots.yml and bot_categories.yml
|
|
502
|
+
ruby script/bot_coverage.rb path/to/upstream/lib # docs/bot_coverage.json, and its Markdown table
|
|
503
|
+
ruby script/render_docs.rb # the site's numbers, from docs/*.json and the lists
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The numbers of `docs/*.html` sit between `<!-- render:… -->` markers and come from `docs/benchmarks.json`,
|
|
507
|
+
`docs/bot_coverage.json`, the bot lists and `corpus/expected_differences.jsonl`: CI fails when a page differs from
|
|
508
|
+
what `bundle exec rake docs:render` writes.
|
|
509
|
+
|
|
510
|
+
To add a bot, add its user agent to `test/ua_bots.yml` and its readable name to `bots.yml` above the synced
|
|
511
|
+
section, keyed by a lowercased string the user agent contains. A search engine goes in `test/ua_search_engines.yml`,
|
|
416
512
|
`test/ua_bots.yml` and `search_engines.yml`.
|
|
417
513
|
|
|
418
514
|
### Releasing
|
|
419
515
|
|
|
420
|
-
|
|
421
|
-
whose CI is
|
|
516
|
+
Pull requests leave `CHANGELOG.md` alone: a release starts by writing its notes under `## [Unreleased]`, from the
|
|
517
|
+
pull requests merged since the last tag, without committing them. Then, from an up-to-date `main` whose CI is
|
|
518
|
+
green:
|
|
422
519
|
|
|
423
520
|
```sh
|
|
424
521
|
bin/release minor --dry-run # the notes and the changes, nothing written
|
|
425
522
|
bin/release minor # or patch, major, or an exact version such as 1.2.0
|
|
426
523
|
```
|
|
427
524
|
|
|
428
|
-
`bin/release` dates the Unreleased section
|
|
429
|
-
`vX.Y.Z
|
|
430
|
-
|
|
525
|
+
`main` takes no direct push, so `bin/release` dates the Unreleased section and bumps the version on a branch
|
|
526
|
+
`release/vX.Y.Z`, opens its pull request, which merges itself once CI is green, and waits for it. It then pushes a
|
|
527
|
+
signed tag `vX.Y.Z` on the merge commit (`bin/release --tag X.Y.Z` does it if you stopped waiting). The tag starts
|
|
528
|
+
the Release workflow, which publishes the gem to RubyGems through trusted publishing and creates the GitHub release
|
|
529
|
+
with the version's notes and the `.gem`.
|
|
431
530
|
|
|
432
531
|
## License
|
|
433
532
|
|
|
434
533
|
MIT, by Level UP Solutions, see [LICENSE.md](LICENSE.md). Originally written by Nando Vieira as
|
|
435
534
|
[fnando/browser](https://github.com/fnando/browser) (MIT). The corpus gathers user agents from
|
|
436
535
|
[ua-parser/uap-core](https://github.com/ua-parser/uap-core) (Apache 2.0),
|
|
437
|
-
[crawler-user-agents](https://github.com/monperrus/crawler-user-agents) (MIT) and browser's test fixtures
|
|
438
|
-
[
|
|
536
|
+
[crawler-user-agents](https://github.com/monperrus/crawler-user-agents) (MIT) and browser's test fixtures; the bot
|
|
537
|
+
list and its categories come from [crawler-user-agents](https://github.com/monperrus/crawler-user-agents) (MIT)
|
|
538
|
+
and [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt) (MIT); the crawler IP ranges are those their
|
|
539
|
+
owners publish for that purpose: see [corpus/SOURCES.md](corpus/SOURCES.md).
|