source: Klonkt/test/stats-bots.test.js@ 0f48411

main
Last change on this file since 0f48411 was 0f48411, checked in by Robin <roboburr@…>, 6 weeks ago

Statistieken tellen lezers, geen servers

Robins vraag (31-7) legde een lek bloot: bij een boost halen alle
fediverse-instances de human-URL op voor hun linkkaart (http.rb/
Mastodon, Pleroma, Misskey, ...), en die UA's stonden niet in de
bot-regex: elke instance telde als post-view, pageview EN unieke
bezoeker. Twee lagen dicht: de fediverse-server-UA's in de regex, en
principieel: elk verzoek met een Signature-header of een
activity/ld+json-Accept is per definitie een server en telt nooit,
wat de UA ook beweert.

Changed files:
src/services/StatsService.js

  • BOT_RE + mastodon/pleroma/akkoma/misskey/firefish/friendica/ hubzilla/lemmy/pixelfed/peertube/gotosocial/honk/http.rb/activitypub
  • isBot: Signature-header of AP-Accept = bot

New file:
test/stats-bots.test.js

  • browser telt; preview-fetchers niet; signed nooit; AP-Accept nooit

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 2.3 KB
RevLine 
[0f48411]1// Statistics count READERS, not servers (Robins vraag, 31-7). Fediverse
2// link-preview fetchers (Mastodon's http.rb and friends) hit the human post
3// URL when a post gets boosted; without these guards every instance counted
4// as a pageview AND a unique visitor.
5import { test } from 'node:test';
6import assert from 'node:assert/strict';
7
8process.env.DATABASE_PATH = ':memory:';
9process.env.PUBLIC_BASE_URL = 'https://test.example';
10
11const dbMod = await import('../src/config/database.js');
12const db = dbMod.default;
13dbMod.initializeDatabase();
14const Stats = await import('../src/services/StatsService.js');
15
16db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
17 .run('u1', 'robin', 'r@t', 'x', 'god');
18db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'me', 'Me', 'u1');
19
20const pv = () => db.prepare('SELECT COALESCE(SUM(pageviews),0) AS n FROM stat_daily').get().n;
21const fakeReq = (headers = {}, ip = '203.0.113.7') => ({ ip, headers, session: {} });
22
23test('a human browser counts', () => {
24 const before = pv();
25 Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0 (iPhone; like Mac OS X) Safari/605.1' }));
26 assert.equal(pv(), before + 1);
27});
28
29test('a fediverse preview fetcher does not count', () => {
30 const before = pv();
31 Stats.recordPageview('s1', fakeReq({ 'user-agent': 'http.rb/5.1.1 (Mastodon/4.2.10; +https://koffieengaar.nl/)' }));
32 Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Pleroma 2.6.2; https://p.example <admin@p.example>' }));
33 Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Misskey/2024.10 (https://m.example)' }));
34 assert.equal(pv(), before, 'server fetches are not readers');
35});
36
37test('a SIGNED request never counts, whatever the UA claims', () => {
38 const before = pv();
39 Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0 totally-a-browser', signature: 'keyId="x",signature="y"' }));
40 assert.equal(pv(), before);
41});
42
43test('ActivityPub content-negotiation never counts either', () => {
44 const before = pv();
45 Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0 totally-a-browser', accept: 'application/activity+json' }));
46 Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0', accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }));
47 assert.equal(pv(), before);
48});
Note: See TracBrowser for help on using the repository browser.