| 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.
|
|---|
| 5 | import { test } from 'node:test';
|
|---|
| 6 | import assert from 'node:assert/strict';
|
|---|
| 7 |
|
|---|
| 8 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 9 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 10 |
|
|---|
| 11 | const dbMod = await import('../src/config/database.js');
|
|---|
| 12 | const db = dbMod.default;
|
|---|
| 13 | dbMod.initializeDatabase();
|
|---|
| 14 | const Stats = await import('../src/services/StatsService.js');
|
|---|
| 15 |
|
|---|
| 16 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 17 | .run('u1', 'robin', 'r@t', 'x', 'god');
|
|---|
| 18 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'me', 'Me', 'u1');
|
|---|
| 19 |
|
|---|
| 20 | const pv = () => db.prepare('SELECT COALESCE(SUM(pageviews),0) AS n FROM stat_daily').get().n;
|
|---|
| 21 | const fakeReq = (headers = {}, ip = '203.0.113.7') => ({ ip, headers, session: {} });
|
|---|
| 22 |
|
|---|
| 23 | test('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 |
|
|---|
| 29 | test('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 |
|
|---|
| 37 | test("a Klonkt's own embed-fetcher does not count on another Klonkt", () => {
|
|---|
| 38 | const before = pv();
|
|---|
| 39 | Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0 (compatible; Klonkt/1.0; +https://klonkt.com)' }));
|
|---|
| 40 | assert.equal(pv(), before);
|
|---|
| 41 | });
|
|---|
| 42 |
|
|---|
| 43 | test('a SIGNED request never counts, whatever the UA claims', () => {
|
|---|
| 44 | const before = pv();
|
|---|
| 45 | Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0 totally-a-browser', signature: 'keyId="x",signature="y"' }));
|
|---|
| 46 | assert.equal(pv(), before);
|
|---|
| 47 | });
|
|---|
| 48 |
|
|---|
| 49 | test('ActivityPub content-negotiation never counts either', () => {
|
|---|
| 50 | const before = pv();
|
|---|
| 51 | Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0 totally-a-browser', accept: 'application/activity+json' }));
|
|---|
| 52 | Stats.recordPageview('s1', fakeReq({ 'user-agent': 'Mozilla/5.0', accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }));
|
|---|
| 53 | assert.equal(pv(), before);
|
|---|
| 54 | });
|
|---|