Skip to content

Count the audience by what a visit did, not by a fingerprint - #244

Merged
openipc-ai merged 2 commits into
masterfrom
audience-classify
Sep 21, 2026
Merged

openipc-ai merged 2 commits into
masterfrom
audience-classify

Conversation

@openipc-ai

Copy link
Copy Markdown
Collaborator

Two corrections to the visitor report in deploy/audience-report.sh, both mine, both found by running it after #238 emptied the wall.

The warning fired on every run

It cross-checked the crawler fingerprint against Accept-Language and complained when the two disagreed. That held only while one fleet dominated both signals. The morning the snapshot crawler left, it became 773 against 592, then 62 against 1 — a permanent warning, which is how a warning turns into a line people skip.

It now asks something the data can always answer: are more visitors touching only the gallery than reading the site? open wall only is by construction the population the fingerprint did not catch — the classifier takes the impossible ones first — so a crawl that has changed its viewport lands there whatever else it changed, and no guess about the next fingerprint is needed.

And the audience was overstated

Every real browser sends Accept-Language. With the wall crawler gone, what was left on /ru and /zh became visible: 62 of 154 visitors sending none, every one reporting an 800px viewport and no browser token. They reach a page outside the wall, so they were counted as readers — a third of the number this report exists to produce.

They now have a line of their own, subtracted from readers, and their pages are out of the per-reader list. A line rather than a silent drop, because a few privacy setups do strip the header and this is the report where someone can judge that and add them back. Same rule the wall split follows: an ambiguous population gets named, never a share of "people".

The same script over three windows

yesterday, the harvest running          since the deploy
  impossible device      609              impossible device        3
  open wall only         661              open wall only          13
  readers                289              readers                154
  no Accept-Language     106              no Accept-Language      80
  WARNING: 661 visitors touched          (silent)
  only the gallery against 289
  who read the site.

Yesterday re-reads as 289 readers and 106 unattributable, where it used to say 395 readers.

Tests

The drift test is replaced by one that adds four wall-only visitors reporting a viewport a real machine has — so the fingerprint cannot see them, which is the point — and asserts the warning names them. Plus a test that the fixture raises no warning, and one that a reader-shaped visit with no Accept-Language is counted on its own line and keeps its page out of the reader list.

752 runs, 3,331 assertions, 0 failures. No rubocop offences in the changed file.

Two corrections to the visitor report, both mine, both found by running it
after #238 emptied the wall.

THE WARNING FIRED ON EVERY RUN. It cross-checked the crawler fingerprint
against Accept-Language and complained when the two disagreed, which held
only while one fleet dominated both signals. The morning the snapshot
crawler left, the numbers became 773 against 592 and then 62 against 1 --
a permanent warning, which is how a warning turns into a line people skip.

It now asks something the data can always answer: are more visitors
touching only the gallery than reading the site? `open wall only` is by
construction the population the fingerprint did NOT catch -- the
classifier takes the impossible ones first -- so a crawl that has changed
its viewport lands there whatever else it changed, and no guess about the
next fingerprint is needed. Yesterday that reads 661 against 289 and says
so. In the ninety minutes after the deploy, 13 against 154, silent.

AND THE AUDIENCE WAS OVERSTATED. Every real browser sends
Accept-Language. With the wall crawler gone, what was left on /ru and /zh
became visible: 62 of 154 visitors sending none, every one reporting an
800px viewport and no browser token. They reach a page outside the wall,
so they were counted as readers -- a third of the number this whole
report exists to produce.

They now have a line of their own, subtracted from readers, and their
pages are out of the per-reader list. A line rather than a silent drop,
because a few privacy setups do strip the header and this is the report
where someone can judge that and add them back. The same rule the wall
split follows: an ambiguous population gets named, never a share of
"people".

Yesterday re-reads as 289 readers and 106 unattributable, where it used
to say 395 readers.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Classify audience by visit behavior instead of crawler fingerprints

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Excludes reader-shaped visits without Accept-Language from readers and page rankings.
• Warns when wall-only visitors outnumber readers, replacing brittle fingerprint drift checks.
• Adds regression coverage for warning thresholds and ambiguous no-language visits.
Diagram

graph TD
  A["Access log"] --> B["Visitor classifier"] --> C{"Visit class"} --> D["Impossible device"]
  C --> E["Wall only"] --> H["Audience report"]
  C --> F["No language"] --> H
  C --> G["Readers"] --> H
  D --> H
Loading
High-Level Assessment

The behavior-based approach is appropriate because it relies on stable visit outcomes rather than expecting two fleet-specific signals to remain correlated. Maintaining more fingerprints or introducing probabilistic bot scoring would add operational complexity while obscuring ambiguous traffic; reporting that population separately preserves transparency.

Files changed (2) +99 / -28

Bug fix (1) +46 / -17
audience-report.shClassify ambiguous visits and detect gallery collection behavior +46/-17

Classify ambiguous visits and detect gallery collection behavior

• Visits reaching reader pages without Accept-Language are now reported separately instead of inflating reader totals or page rankings. The warning now compares wall-only visitors with readers, avoiding permanent alerts caused by crawler fingerprint and language-signal drift.

deploy/audience-report.sh

Tests (1) +53 / -11
audience_report_test.rbCover behavioral warnings and no-language visitor exclusion +53/-11

Cover behavioral warnings and no-language visitor exclusion

• Replaces the fingerprint-drift test with wall-only collection scenarios using realistic viewports. Adds coverage for warning suppression and verifies that no-language visits remain outside reader totals and page rankings.

test/deploy/audience_report_test.rb

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Old log rows erase real readers ✓ Resolved 🐞 Bug ≡ Correctness
Description
visitors defaults language to - when an old-format row has no al field, and the new quiet
branch then excludes that visitor from readers. When a daily log spans both formats, any
pre-rollout request marks its address and user-agent pair as having no language, removing the
visitor and every page they read from the audience report.
Code

deploy/audience-report.sh[R136-137]

+          if (v in quiet) no_language++
+          else { readers++; person[v] = 1 }
Evidence
The parser initializes every request to language = "-" and aggregates requests by address plus
user-agent, while the added branch excludes any aggregated visitor present in quiet. The nginx
configuration explicitly documents that appended fields must support a day's log containing both old
and new formats, proving that absence of al does not necessarily mean the browser omitted the
header.

deploy/audience-report.sh[68-74]
deploy/audience-report.sh[136-137]
deploy/audience-report.sh[149-152]
deploy/nginx/conf.d/openipc-logformat.conf[9-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Old-format access-log rows have no `al` field, but `visitors` currently treats them as requests that explicitly omitted Accept-Language. Because the log format documentation states that a deployment day's log spans both formats, the new reader exclusion can remove legitimate visitors and their pages.
## Fix Focus Areas
- deploy/audience-report.sh[68-74]
- deploy/audience-report.sh[136-137]
- deploy/nginx/conf.d/openipc-logformat.conf[9-13]
## Recommended Fix
Track whether an `al` field was actually present separately from its value. Mark a visitor as `quiet` only when a present `al` field is empty or `-`; preserve the previous reader treatment for rows where the field is absent because they use the older log format, and add a mixed-format regression test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread deploy/audience-report.sh
visitors() defaulted language to "-" when no al= field was present, and
the new reader split then read that as "this client omitted
Accept-Language" -- taking the visitor, and every page they read, out of
the audience. A day's log spans both formats whenever a log-format
change lands, and rows written before al= existed carry no field at all,
so the report would have quietly shrunk the audience on exactly the day
someone went looking at it.

Absent and empty are now different things: only a field that is present
and empty says anything about the client.

It changes no number today -- the beacon went live after al= did, so
every beacon row in the current logs carries the field, and yesterday
still reads 289 readers and 106 unattributable. It is a guard for the
archived logs and for the next format change.
@openipc-ai

Copy link
Copy Markdown
Collaborator Author

Real, and it would have been invisible in the direction that matters: quietly shrinking the audience, on exactly the day someone goes looking at a log that spans a format change.

visitors() defaulted language to - when no al= field was present, and the new reader split then read that as "this client omitted Accept-Language" — taking the visitor and every page they read out of the count. Absent and empty are now different things: only a field that is present and empty says anything about the client.

Checked against yesterday's log, which does span both formats: 289 readers and 106 unattributable, unchanged. The beacon went live after al= did, so every beacon row in the current logs carries the field — this is a guard for the archived logs and for the next format change rather than a correction to today's numbers, which is worth saying plainly.

The regression test adds a pre-rollout row ending at urt= with no al= at all, and asserts it counts as a reader, that the no-Accept-Language line does not appear, and that its page is still in the per-reader list.

753 runs, 3,336 assertions, 0 failures.

@openipc-ai
openipc-ai merged commit 2061145 into master Sep 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant