Skip to content

Count Cloudflare challenge arrivals as unique - #112

Merged
adriaandotcom merged 8 commits into
v12from
fix/v12-cloudflare-query-markers
Oct 15, 2026
Merged

adriaandotcom merged 8 commits into
v12from
fix/v12-cloudflare-query-markers

Conversation

@simple-analytics-ai

@simple-analytics-ai simple-analytics-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Targets v12 (#40). Merge into v12 before releasing #40 to main.

Summary

Fixes #107 and implements the Cloudflare arrival behavior in #48.

Use the owner's requested simple marker check on the raw referrer:

!pages && /[?&]__cf_/.test(getReferrer(trueVar))

This replaces the anchored parameter-name expression with a broad marker-presence check. It accepts the marker at the start of the query or appended after existing campaign parameters. The extra same-URL guard was explicitly declined and is omitted. Existing reload/history and later-pageview handling remain in place. Referrer overrides keep working, and outgoing referrers remain stripped of query strings.

Merged current v12 at 58f6679, including the modified-click fix from #78 and the previously merged manual-pageview, namespace, and phone-click fixes. Source, regression tests, and matching generated bundles are included together. Unrelated pinned auto-events artifacts and older versions are preserved.

Security implications

  • No security impact
  • Has security impact - raw referrers are inspected in memory to classify challenge arrivals as unique; referrer query values are not transmitted.

Testing

  • Node 24.18.0 / pnpm 11.17.0: build with ES5 validation and all 53 unit tests pass.
  • 36 payload checks pass across development, minified CDN, and SRI bundles, covering the simple marker check, existing campaign parameters, reloads, token omission, manual-query overrides, browser-query fallback, filtering, events, and duplicate suppression.
  • Changed JavaScript passes Prettier; git diff --check passes.
  • Against the same-build current v12, excluding headers: latest.js +21 minified bytes / +16 gzip bytes at level 9. Light code is unchanged.
  • Latest main was refreshed through the signed broker and is included. Remote CI is tracked separately from local validation.

Merge Danger

Door: two-way.

Blast Radius: analytics. Challenge arrivals can contribute to unique visitor counts as requested; pageview totals and event names are unchanged.

Checklist

  • Linked to an issue
  • Tested
  • Asked for a review

Requested in the compatibility review: avoid path false positives and inspect raw query-only markers. Reuse the referrer helper without sending raw query values or adding bytes to light builds.

Refs #107
@github-actions

Copy link
Copy Markdown
Contributor

Documentation update

_docs/33_explained/05_uniques.md

Location: ## How we detect unique pageviews, replacing the existing **A pageview is NOT counted as unique when:** list

Copy and paste this into the documentation repository:

**A pageview is NOT counted as unique when:**

- The referrer's hostname matches your website's hostname (internal navigation)
- The user navigates using browser history (back/forward buttons)
- The user reloads the page
- The visitor returns from a Cloudflare security challenge

That last case happens when your site sits behind Cloudflare and a visitor is shown a security challenge (for example a managed challenge or a CAPTCHA). After passing it, Cloudflare sends the visitor back to your page with a challenge parameter such as `__cf_chl_tk` or `__cf_chl_rt_tk` in the URL. We recognise those parameter names in the referrer and count that pageview as non-unique, so a single visitor who passed a challenge is not counted twice. We only look at the parameter names; the referrer we store is still stripped of its query string.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
Claude review checkpoint

Reviewed commit 14b75fc883bb3d749a183eaf6d9b8a2aabecaf6e. This is used to keep later automated reviews focused on changes Claude has not checked yet.

@github-actions github-actions Bot added the change: routine Changes to internal tools or low-risk changes (e.g., design updates or content modifications) label Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

SDLC label: change: routine because Bug fix to the client-side unique-pageview heuristic; the Cloudflare marker is matched in-memory only and the outgoing payload still strips the referrer query, with no change to data collection, storage, retention, authorization, billing, or infrastructure

@simple-analytics-ai

simple-analytics-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

In response to #112 (comment) and #112 (comment), superseding #112 (comment):

I corrected the pending docs patch to match 14b75fc and the owner's simplification in #112 (comment). Cloudflare referrers containing ?__cf_ or &__cf_ can make the first arrival unique; reloads and history navigation remain non-unique, and later pageviews do not use that exception. The copy describes the broad marker check without promising strict parameter-name matching or a same-URL guard, and explains that referrer queries are stripped before transmission.

The revised four-page patch applies cleanly to current docs/master and passes formatting checks. It remains local pending the existing request for approval of a supporting docs PR, with publication dependent on this change and the v12 release.

Simple Analytics Codex and others added 4 commits October 15, 2026 17:01
Requested: make issue #107 mergeable while preserving manual-query overrides, browser-query fallback, and existing filtering. Resolve the older Cloudflare branch against current v12 source and regenerate its bundles, preserving inline events and the merged phone-click fix.

All 45 unit tests and 24 additional query payload checks pass.

Refs #107
Requested: make issue #107 mergeable. Preserve all current v12 changes and the reviewed Cloudflare source fix; rebuild and validate the generated bundles with 45 passing unit tests and 24 payload checks.

Refs #107
User requested correcting PR #112 to match issue #48. Challenge query parameter names now allow the first tracked arrival to be unique, including same-site referrers. Reloads, history navigation, and later manual or SPA pageviews remain non-unique. Keep challenge tokens out of payloads, preserve current v12 changes, and regenerate distributions. Validated with 53 offline unit tests and minified payload checks.
@simple-analytics-ai simple-analytics-ai Bot changed the title Recognize Cloudflare challenge markers only in query names Count Cloudflare challenge arrivals as unique Oct 15, 2026
Comment thread src/default.js Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Documentation update

_docs/33_explained/05_uniques.md

Location: ## How we detect unique pageviews, replacing the existing **A pageview is NOT counted as unique when:** list and the paragraph that follows it

Copy and paste this into the documentation repository:

**A pageview is NOT counted as unique when:**

- The referrer's hostname matches your website's hostname (internal navigation)
- The user navigates using browser history (back/forward buttons)
- The user reloads the page

### Cloudflare security challenges

If your website runs behind Cloudflare and a visitor has to pass a security challenge (for example with "I'm Under Attack" mode enabled), the browser reports the challenge URL as the referrer. That URL is on your own hostname, so without special handling the arrival would look like internal navigation and would not be counted as a visitor.

We recognize these arrivals by looking for a Cloudflare query parameter name such as `__cf_chl_tk` or `__cf_chl_rt_tk` in the referrer, and we count that arrival as unique, just like any other fresh visit. Only the first pageview of the page load can use this exception: reloads, back/forward navigation, and later manual or single-page-application pageviews stay non-unique.

The marker only counts as a query parameter name. A Cloudflare-looking string in the referrer's path, in a query parameter value, or after the `#` is ignored. The challenge token itself never reaches us, because we always strip the query string from the referrer before sending it.

Comment thread src/default.js Outdated
Simple Analytics Codex added 2 commits October 15, 2026 17:21
User asked to replace the parameter-name regex with a literal ?__cf_ check, omit the extra same-URL guard, and merge current v12 into #112. Preserve the latest modified-click, namespace, manual-query, and phone-click fixes. Rebuild matching tracker bundles while retaining unrelated pinned auto-events artifacts.

All 53 unit tests, 33 payload checks, changed JavaScript formatting, and diff checks pass. Manual-query overrides and location.search fallback remain intact.

Refs #107
User requested a literal ?__cf_ check without the extra same-URL guard and asked to merge v12 into #112. Include v12 at 58f6679, retaining its modified-click, namespace, manual-pageview, and phone-click fixes. Use the reviewed, broker-pushed tree with source, tests, and matching distributions.

All 53 unit tests and 33 payload checks pass. Refs #107
Comment thread src/default.js Outdated
Comment thread test/unit/pageview.test.js Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Documentation update

_docs/33_explained/05_uniques.md

Location: ## How we detect unique pageviews, after the "A pageview is NOT counted as unique when" list and before the randomwebsite.com example

Copy and paste this into the documentation repository:

### Cloudflare challenges

When Cloudflare shows a security challenge (for example a managed challenge or a Turnstile interstitial), your visitor is sent back to your own website after passing it. The referrer of that first pageview is then your own hostname, which would normally make the visit non-unique even though the visitor came from somewhere else.

To avoid losing those visitors, we make one exception: if the referrer of the **first** pageview on a page load contains a Cloudflare challenge parameter (a query parameter starting with `__cf_`, such as `__cf_chl_tk` or `__cf_chl_rt_tk`), we count that pageview as unique.

This exception only applies to the first pageview after a page load. Later pageviews, reloads, back and forward navigation, and single-page application navigation remain non-unique as described above. As always, we only read the referrer in the browser to make this decision; the challenge token itself is never sent to us, because we strip query strings from referrers.

User requested a simple marker check and current v12 merge. Address review by accepting both ?__cf_ and appended &__cf_ with the short /[?&]__cf_/ expression, retaining the requested broad matching and omitting the declined same-URL guard. Update the existing regression and matching bundles.

All 53 unit tests and 36 payload checks pass. Current v12 remains included.

Refs #107
@adriaandotcom
adriaandotcom merged commit 6af229d into v12 Oct 15, 2026
11 of 12 checks passed
@adriaandotcom
adriaandotcom deleted the fix/v12-cloudflare-query-markers branch October 15, 2026 22:00
adriaandotcom added a commit that referenced this pull request Oct 22, 2026
* Fix problem with solana extension

It seems Solana from Phantom has this namespace for their browser extension. I added a check for prevent them being detected as bots.

* Move from _phantom to phantom

* Run prettier via GitHub Action

* Fix bug in checking phantom.solana

* Always expose sa_pageview

* Check for Cloudflare under attack mode param

* Run prettier via GitHub Action

* Test for all Cloudflare params

* Clean subdomain hostname, fix auto events for links with child elements, run prettier

* Add acron and remove trailing comma

* Run prettier via GitHub Action

* Save local changes [no ci]

* Add jsdom unit tests

* Fix default test without Browserstack

* Add unit test to job

* Make sure all node js version are the same

* Allow overwriting referrer

* Rename testing file

* Run prettier in dry mode [no ci]

* Add AGENTS guidance

* Improve AGENTS.md

* Add .cursorrules [no ci]

* Add modular unit tests and helpers

* Run browserstack not every time, and add linting

* Run browserstack not every time, for real

* Require prettier for brwoserstack too

* Rename prettier to linting

* chore: fix eslint errors

* feat(auto-events): skip automation when data-simple-event present

* Add manual build step

* Rename to Testing Suite

* fix metadata reload in appendMetadata

* Bind sendBeacon calls

* Add callback support to sa_pageview

* Checkout dist after test:unit

* Reuse query param filter for manual pageviews

* test: add metadata collector attr test

* Run npm run build

* Add tel link tracking to auto events

* Add unit tests for auto-events

* Warn when metadata collector function is not found

* Run npm run build

* Fix delayed phone link events

* fix allowParams regex to be case insensitive

* Fix merged BrowserStack test setup

* Collect compact query parameter names via ignore metrics

* Run BrowserStack tests on Node.js 22.16

* Use numeric BrowserStack test exit codes

* Sync v12 conflict files with current main

* Use querykeys for query parameter name collection

* Fix v12 CI after main sync

* Document tracker compatibility and size priorities for agents (#115)

* Document tracker compatibility and size priorities for agents

* Update AGENTS.md

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>
Co-authored-by: Adriaan <1079135+adriaandotcom@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Support inline events in the main v12 script (#106)

* Support inline event attributes in the main v12 tracker

Requested: integrate documented inline events without overengineering, keep the script small and website behavior intact, and open a separate PR based on and targeting v12.

Use delegated click/submit handlers and keepalive GETs with the existing image fallback. Preserve installed inline helpers and make confirmed 404 detection opt-in. Add focused tests and patch the vulnerable Handlebars build dependency.

* Simplify inline events after review

Requested: remove compatibility checks for the beta helper, measure repeated attribute strings, clarify the DOM alias, and shorten submit-button metadata extraction.

Keep literals because sharing them increases gzip size. Prefer trimmed, capped submitter labels and scan title/body text for 404s. Address the related bot feedback by running the complete unit suite in CI.

* Normalize inline submit button label whitespace

Requested: address scripts PR #106 review comment 4282258759 so formatting a button across multiple lines does not fragment button_text metadata.

Collapse whitespace before trimming and applying the existing 200-character limit. Add a regression for equivalent compact and indented button markup, and regenerate the affected tracker bundles.

Verification: regression fails on the previous compiled tracker; all 33 unit tests pass after the fix. Build, formatting, and diff checks pass.

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Preserve distinct manual pageviews in v12 (#108)

* Preserve distinct manual pageviews in v12

Requested in the compatibility review: fix query-only manual pageview loss and preserve full path-overwriter input. Keep the filtered outgoing path and review campaign attribution separately.

Refs #107

* Preserve ignore-pages and hash routes for manual query paths

Address automated review of item 1: normalize the manual query after the path-overwriter callback and before ignore-pages/hash handling, while keeping its query in the duplicate key.

Refs #107

* Keep non-string manual paths compatible during query normalization

Address review of item 1: only split string path values, preserving numeric manual paths and numeric path-overwriter results without introducing an exception into the customer page.

Refs #107

* Preserve pathname-only input for path overwriters

Requested during review: keep the existing path-overwriter behavior without query strings, while retaining distinct manual-query pageviews. Restore query splitting before getPath and update the callback and duplicate checks.

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Leave phone click navigation to the website and browser (#111)

Requested in item 5: prevent unwanted telephone navigation from analytics callbacks while retaining default outbound, email, download, and phone collection when data-collect is omitted.

Refs #107

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Respect the configured namespace for manual pageviews (#110)

* Respect the configured namespace for manual pageviews

Requested in item 3: keep namespaced trackers from replacing another tracker manual pageview function. Reuse existing namespace and pageview text variables to reduce minified bytes.

Refs #107

* Retain the free legacy manual pageview alias

Address review of item 3: older namespaced installations still call sa_pageview. Keep that alias when free, without overwriting another tracker, and cover both function names in the regression test.

Refs #107

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Fix cmd-click for auto events (#78)

* feat(auto-events): allow cmd-click to open in new tab

* Preserve browser navigation for automatic link events

User request: recheck whether PR #78 remains relevant, fix the current issue, and add the commits to that PR. Current v12 still cancels modified clicks and redirects from tracking callbacks. Use listeners without replacing onclick and reuse the existing keepalive transport; retain event names and metadata. Rebuild current bundles while preserving existing SRI files.

Validated with 41 unit tests, 12 deployment tests, typecheck, and 48 local click/transport scenarios.

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Count Cloudflare challenge arrivals as unique (#112)

* Recognize Cloudflare challenge markers only in query names

Requested in the compatibility review: avoid path false positives and inspect raw query-only markers. Reuse the referrer helper without sending raw query values or adding bytes to light builds.

Refs #107

* Refresh the Cloudflare compatibility fix on current v12

Requested: make issue #107 mergeable while preserving manual-query overrides, browser-query fallback, and existing filtering. Resolve the older Cloudflare branch against current v12 source and regenerate its bundles, preserving inline events and the merged phone-click fix.

All 45 unit tests and 24 additional query payload checks pass.

Refs #107

* Use the requested simple Cloudflare marker check on latest v12

User asked to replace the parameter-name regex with a literal ?__cf_ check, omit the extra same-URL guard, and merge current v12 into #112. Preserve the latest modified-click, namespace, manual-query, and phone-click fixes. Rebuild matching tracker bundles while retaining unrelated pinned auto-events artifacts.

All 53 unit tests, 33 payload checks, changed JavaScript formatting, and diff checks pass. Manual-query overrides and location.search fallback remain intact.

Refs #107

* Keep the simple Cloudflare check working after campaign parameters

User requested a simple marker check and current v12 merge. Address review by accepting both ?__cf_ and appended &__cf_ with the short /[?&]__cf_/ expression, retaining the requested broad matching and omitting the declined same-URL guard. Update the existing regression and matching bundles.

All 53 unit tests and 36 payload checks pass. Current v12 remains included.

Refs #107

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>
Co-authored-by: simple-analytics-ai[bot] <313929673+simple-analytics-ai[bot]@users.noreply.github.com>

* Accept current and legacy script deployment headers (#117)

* Accept current and legacy script deployment headers

Requested: make issue #107 ready to merge. Fix the proven mismatch between compile.js output and deployment header validation while retaining older pinned header support. Add tests against the real deployment manifest, legacy and malformed headers, and run the lightweight deployment suite in CI.

All 15 deployment tests, type checking, and diff checks pass. Visitor-facing scripts are unchanged.

Refs #107

* Check headers for the current SRI version in deployment tests

Requested: make issue #107 mergeable and address review on #117. Reuse latestVersionFromNames with the dist directory so the manifest regression follows the current SRI version rather than only v12. All 15 deployment tests and type checking pass.

Refs #107

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Remove retired Cloudflare app build from v12 (#121)

Requested: open a PR into v12 removing Cloudflare app options while keeping GTM settings and a working client-side guard. Preserve sa_settings and data-attribute behavior, guard browser access, and rebuild current tracker artifacts.

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* fix: retain phone metadata with titled events (#122)

User requested checking unresolved comments with a focus on CodeRabbit, separate reviewable changes, and minimal shipped bytes. Extract phone metadata before applying the existing title-derived event name and cover the regression.

Refs #107

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* test: require current script version in SRI header checks (#123)

User requested verifying unresolved CodeRabbit feedback with separate minimal PRs. Require the discovered SRI version to match latest.js so an obsolete version cannot make deployment validation pass.

Refs #107

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Stop ignoring screen size when scrolled is ignored (#127)

The screen size check matched the "sc" prefix, which also matches
"scrolled". Match "scre" so each metric can be ignored on its own.

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Set the loaded flag for custom event globals (#129)

Automated events wait for window[saGlobal + "_loaded"], but the main
script only set sa_event_loaded and the namespace flag. With a custom
namespace or sa-global (for example ba_event), automated events never
fired. Set the flag for the configured event function once it replaces
the queue stub, and cover both setups in the unit tests.

Rebuild the affected event-enabled distributions (+18 bytes minified,
+4 to +9 bytes gzip). light.js and auto-events.js are unchanged. The
rebuilt files also include the src change from #127, which had not
been rebuilt into dist yet.

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Count label clicks once in inline events (#133)

* Run the pageview callback when the pageview is skipped (#132)

* Send visible time on page when the page gets hidden (#134)

* Send visible time on page when the page gets hidden

Count only visible time and send the cumulative value on every hide and
on pagehide, so durations survive mobile app switches, screen locks and
browser kills, back/forward cache restores no longer send increments, and
Firefox tab closes are no longer lost. Every page view, including manual
ones, closes the previous one. Optional data-pings sends the same value
at visible-time thresholds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep invalid scroll readings out of later appends

Don't write position() back into the scroll maximum, so a NaN reading on a
zero-height layout can't stick for the rest of the page view, and drop the
unobservable push branch of the sent watermark.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Send time on page only when the page gets hidden

Leaving a page also makes it hidden, so the pagehide listener, the
duplicate-send guard, the back/forward cache fallback and the beacon
try/catch are no longer needed. Old browsers that don't fire
visibilitychange on navigation lose those durations; this keeps the
script small. Appends without values are not sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Ignore huge ping values and hides before the first page view

A ping value too large for setTimeout fired immediately and rescheduled
itself in a loop, and ignored pages or manual mode sent an append for a
page view that was never sent on every hide.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Ask agents to weigh script size against edge cases (#136)

* Ask agents to weigh script size against edge cases

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Stop asking agents to commit generated distributions

The Dist section says feature work never commits dist/, which item 6
contradicted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Send the embedded script URL with pageviews (#138)

* Use the caddy destination in the deploy header test

The merge from main renamed the custom destination to caddy, so the
header test matched no custom files and typecheck failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Test that automatic link clicks are not cancelled

Runs auto-events.js on a same-tab outbound link and checks that it binds
a click listener instead of an onclick attribute, leaves the click
alone, and asks sa_event for keepalive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop the placeholder callback in automatic link events

sendEvent already uses a noop for a missing callback, so 0 is enough
to reach the keepalive argument. Saves 11 bytes minified, 1 gzip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Fall back to a pixel when sendBeacon throws

Other scripts can replace navigator.sendBeacon with a wrapper that
throws "Illegal invocation" (#42). Without the fallback no time on page
arrives, the remaining pings stop and every hide throws an error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Limit the Cloudflare unique check to this site's referrers

Cloudflare challenges run on the site itself, so a challenge marker in
a referrer from another non-unique hostname no longer makes the first
pageview unique.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop the referrer on Cloudflare challenge arrivals

The challenge page replaced the original referrer, so the pageview only
sent our own hostname. The queue turns pageviews with our own hostname
as referrer non-unique, which undid the unique arrival.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Detect Cloudflare arrivals from the raw referrer host

The cleaned referrer has no www. and can carry an overwritten hostname,
so www sites kept sending their own hostname as referrer (which the
queue turns non-unique) and sites with a matching non-unique hostname
sent unique=false. Check the raw referrer first, before sameSite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Compare real hostnames to detect same-site referrers

The cleaned referrer has no www. and uses the defined hostname, so on
www sites and sites with data-hostname internal page loads and manual
pageviews were sent as unique, and SPA pageviews and events on
data-hostname sites lost their referrer. Compare the raw referrer host
with the location host, and treat later pageviews as same-site.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Mark appends sent by pings

Appends from data-pings now include ping: true, so the backend can block
them for plans without pings. Appends sent on hide or navigation are
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ironbrands16 <103500025+ironbrands16@users.noreply.github.com>
Co-authored-by: Adriaan <1079135+adriaanvanrossum@users.noreply.github.com>
Co-authored-by: adriaanvanrossum <adriaanvanrossum@users.noreply.github.com>
Co-authored-by: adriaandotcom <adriaandotcom@users.noreply.github.com>
Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>
Co-authored-by: simple-analytics-ai[bot] <313929673+simple-analytics-ai[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Simple Analytics Agent <github-agent@adriaan.simpleanalytics.com>
Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
adriaandotcom added a commit that referenced this pull request Oct 22, 2026
* Fix problem with solana extension

It seems Solana from Phantom has this namespace for their browser extension. I added a check for prevent them being detected as bots.

* Move from _phantom to phantom

* Run prettier via GitHub Action

* Fix bug in checking phantom.solana

* Always expose sa_pageview

* Check for Cloudflare under attack mode param

* Run prettier via GitHub Action

* Test for all Cloudflare params

* Clean subdomain hostname, fix auto events for links with child elements, run prettier

* Add acron and remove trailing comma

* Run prettier via GitHub Action

* Save local changes [no ci]

* Add jsdom unit tests

* Fix default test without Browserstack

* Add unit test to job

* Make sure all node js version are the same

* Allow overwriting referrer

* Rename testing file

* Run prettier in dry mode [no ci]

* Add AGENTS guidance

* Improve AGENTS.md

* Add .cursorrules [no ci]

* Add modular unit tests and helpers

* Run browserstack not every time, and add linting

* Run browserstack not every time, for real

* Require prettier for brwoserstack too

* Rename prettier to linting

* chore: fix eslint errors

* feat(auto-events): skip automation when data-simple-event present

* Add manual build step

* Rename to Testing Suite

* fix metadata reload in appendMetadata

* Bind sendBeacon calls

* Add callback support to sa_pageview

* Checkout dist after test:unit

* Reuse query param filter for manual pageviews

* test: add metadata collector attr test

* Run npm run build

* Add tel link tracking to auto events

* Add unit tests for auto-events

* Warn when metadata collector function is not found

* Run npm run build

* Fix delayed phone link events

* fix allowParams regex to be case insensitive

* Fix merged BrowserStack test setup

* Collect compact query parameter names via ignore metrics

* Run BrowserStack tests on Node.js 22.16

* Use numeric BrowserStack test exit codes

* Sync v12 conflict files with current main

* Use querykeys for query parameter name collection

* Fix v12 CI after main sync

* Document tracker compatibility and size priorities for agents (#115)

* Document tracker compatibility and size priorities for agents

* Update AGENTS.md

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>
Co-authored-by: Adriaan <1079135+adriaandotcom@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Support inline events in the main v12 script (#106)

* Support inline event attributes in the main v12 tracker

Requested: integrate documented inline events without overengineering, keep the script small and website behavior intact, and open a separate PR based on and targeting v12.

Use delegated click/submit handlers and keepalive GETs with the existing image fallback. Preserve installed inline helpers and make confirmed 404 detection opt-in. Add focused tests and patch the vulnerable Handlebars build dependency.

* Simplify inline events after review

Requested: remove compatibility checks for the beta helper, measure repeated attribute strings, clarify the DOM alias, and shorten submit-button metadata extraction.

Keep literals because sharing them increases gzip size. Prefer trimmed, capped submitter labels and scan title/body text for 404s. Address the related bot feedback by running the complete unit suite in CI.

* Normalize inline submit button label whitespace

Requested: address scripts PR #106 review comment 4282258759 so formatting a button across multiple lines does not fragment button_text metadata.

Collapse whitespace before trimming and applying the existing 200-character limit. Add a regression for equivalent compact and indented button markup, and regenerate the affected tracker bundles.

Verification: regression fails on the previous compiled tracker; all 33 unit tests pass after the fix. Build, formatting, and diff checks pass.

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Preserve distinct manual pageviews in v12 (#108)

* Preserve distinct manual pageviews in v12

Requested in the compatibility review: fix query-only manual pageview loss and preserve full path-overwriter input. Keep the filtered outgoing path and review campaign attribution separately.

Refs #107

* Preserve ignore-pages and hash routes for manual query paths

Address automated review of item 1: normalize the manual query after the path-overwriter callback and before ignore-pages/hash handling, while keeping its query in the duplicate key.

Refs #107

* Keep non-string manual paths compatible during query normalization

Address review of item 1: only split string path values, preserving numeric manual paths and numeric path-overwriter results without introducing an exception into the customer page.

Refs #107

* Preserve pathname-only input for path overwriters

Requested during review: keep the existing path-overwriter behavior without query strings, while retaining distinct manual-query pageviews. Restore query splitting before getPath and update the callback and duplicate checks.

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Leave phone click navigation to the website and browser (#111)

Requested in item 5: prevent unwanted telephone navigation from analytics callbacks while retaining default outbound, email, download, and phone collection when data-collect is omitted.

Refs #107

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Respect the configured namespace for manual pageviews (#110)

* Respect the configured namespace for manual pageviews

Requested in item 3: keep namespaced trackers from replacing another tracker manual pageview function. Reuse existing namespace and pageview text variables to reduce minified bytes.

Refs #107

* Retain the free legacy manual pageview alias

Address review of item 3: older namespaced installations still call sa_pageview. Keep that alias when free, without overwriting another tracker, and cover both function names in the regression test.

Refs #107

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Fix cmd-click for auto events (#78)

* feat(auto-events): allow cmd-click to open in new tab

* Preserve browser navigation for automatic link events

User request: recheck whether PR #78 remains relevant, fix the current issue, and add the commits to that PR. Current v12 still cancels modified clicks and redirects from tracking callbacks. Use listeners without replacing onclick and reuse the existing keepalive transport; retain event names and metadata. Rebuild current bundles while preserving existing SRI files.

Validated with 41 unit tests, 12 deployment tests, typecheck, and 48 local click/transport scenarios.

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Count Cloudflare challenge arrivals as unique (#112)

* Recognize Cloudflare challenge markers only in query names

Requested in the compatibility review: avoid path false positives and inspect raw query-only markers. Reuse the referrer helper without sending raw query values or adding bytes to light builds.

Refs #107

* Refresh the Cloudflare compatibility fix on current v12

Requested: make issue #107 mergeable while preserving manual-query overrides, browser-query fallback, and existing filtering. Resolve the older Cloudflare branch against current v12 source and regenerate its bundles, preserving inline events and the merged phone-click fix.

All 45 unit tests and 24 additional query payload checks pass.

Refs #107

* Use the requested simple Cloudflare marker check on latest v12

User asked to replace the parameter-name regex with a literal ?__cf_ check, omit the extra same-URL guard, and merge current v12 into #112. Preserve the latest modified-click, namespace, manual-query, and phone-click fixes. Rebuild matching tracker bundles while retaining unrelated pinned auto-events artifacts.

All 53 unit tests, 33 payload checks, changed JavaScript formatting, and diff checks pass. Manual-query overrides and location.search fallback remain intact.

Refs #107

* Keep the simple Cloudflare check working after campaign parameters

User requested a simple marker check and current v12 merge. Address review by accepting both ?__cf_ and appended &__cf_ with the short /[?&]__cf_/ expression, retaining the requested broad matching and omitting the declined same-URL guard. Update the existing regression and matching bundles.

All 53 unit tests and 36 payload checks pass. Current v12 remains included.

Refs #107

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>
Co-authored-by: simple-analytics-ai[bot] <313929673+simple-analytics-ai[bot]@users.noreply.github.com>

* Accept current and legacy script deployment headers (#117)

* Accept current and legacy script deployment headers

Requested: make issue #107 ready to merge. Fix the proven mismatch between compile.js output and deployment header validation while retaining older pinned header support. Add tests against the real deployment manifest, legacy and malformed headers, and run the lightweight deployment suite in CI.

All 15 deployment tests, type checking, and diff checks pass. Visitor-facing scripts are unchanged.

Refs #107

* Check headers for the current SRI version in deployment tests

Requested: make issue #107 mergeable and address review on #117. Reuse latestVersionFromNames with the dist directory so the manifest regression follows the current SRI version rather than only v12. All 15 deployment tests and type checking pass.

Refs #107

---------

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Remove retired Cloudflare app build from v12 (#121)

Requested: open a PR into v12 removing Cloudflare app options while keeping GTM settings and a working client-side guard. Preserve sa_settings and data-attribute behavior, guard browser access, and rebuild current tracker artifacts.

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* fix: retain phone metadata with titled events (#122)

User requested checking unresolved comments with a focus on CodeRabbit, separate reviewable changes, and minimal shipped bytes. Extract phone metadata before applying the existing title-derived event name and cover the regression.

Refs #107

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* test: require current script version in SRI header checks (#123)

User requested verifying unresolved CodeRabbit feedback with separate minimal PRs. Require the discovered SRI version to match latest.js so an obsolete version cannot make deployment validation pass.

Refs #107

Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>

* Stop ignoring screen size when scrolled is ignored (#127)

The screen size check matched the "sc" prefix, which also matches
"scrolled". Match "scre" so each metric can be ignored on its own.

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Set the loaded flag for custom event globals (#129)

Automated events wait for window[saGlobal + "_loaded"], but the main
script only set sa_event_loaded and the namespace flag. With a custom
namespace or sa-global (for example ba_event), automated events never
fired. Set the flag for the configured event function once it replaces
the queue stub, and cover both setups in the unit tests.

Rebuild the affected event-enabled distributions (+18 bytes minified,
+4 to +9 bytes gzip). light.js and auto-events.js are unchanged. The
rebuilt files also include the src change from #127, which had not
been rebuilt into dist yet.

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Count label clicks once in inline events (#133)

* Run the pageview callback when the pageview is skipped (#132)

* Send visible time on page when the page gets hidden (#134)

* Send visible time on page when the page gets hidden

Count only visible time and send the cumulative value on every hide and
on pagehide, so durations survive mobile app switches, screen locks and
browser kills, back/forward cache restores no longer send increments, and
Firefox tab closes are no longer lost. Every page view, including manual
ones, closes the previous one. Optional data-pings sends the same value
at visible-time thresholds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep invalid scroll readings out of later appends

Don't write position() back into the scroll maximum, so a NaN reading on a
zero-height layout can't stick for the rest of the page view, and drop the
unobservable push branch of the sent watermark.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Send time on page only when the page gets hidden

Leaving a page also makes it hidden, so the pagehide listener, the
duplicate-send guard, the back/forward cache fallback and the beacon
try/catch are no longer needed. Old browsers that don't fire
visibilitychange on navigation lose those durations; this keeps the
script small. Appends without values are not sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Ignore huge ping values and hides before the first page view

A ping value too large for setTimeout fired immediately and rescheduled
itself in a loop, and ignored pages or manual mode sent an append for a
page view that was never sent on every hide.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Ask agents to weigh script size against edge cases (#136)

* Ask agents to weigh script size against edge cases

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Stop asking agents to commit generated distributions

The Dist section says feature work never commits dist/, which item 6
contradicted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Send the embedded script URL with pageviews (#138)

* Use the caddy destination in the deploy header test

The merge from main renamed the custom destination to caddy, so the
header test matched no custom files and typecheck failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Test that automatic link clicks are not cancelled

Runs auto-events.js on a same-tab outbound link and checks that it binds
a click listener instead of an onclick attribute, leaves the click
alone, and asks sa_event for keepalive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop the placeholder callback in automatic link events

sendEvent already uses a noop for a missing callback, so 0 is enough
to reach the keepalive argument. Saves 11 bytes minified, 1 gzip.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Fall back to a pixel when sendBeacon throws

Other scripts can replace navigator.sendBeacon with a wrapper that
throws "Illegal invocation" (#42). Without the fallback no time on page
arrives, the remaining pings stop and every hide throws an error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Limit the Cloudflare unique check to this site's referrers

Cloudflare challenges run on the site itself, so a challenge marker in
a referrer from another non-unique hostname no longer makes the first
pageview unique.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop the referrer on Cloudflare challenge arrivals

The challenge page replaced the original referrer, so the pageview only
sent our own hostname. The queue turns pageviews with our own hostname
as referrer non-unique, which undid the unique arrival.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Detect Cloudflare arrivals from the raw referrer host

The cleaned referrer has no www. and can carry an overwritten hostname,
so www sites kept sending their own hostname as referrer (which the
queue turns non-unique) and sites with a matching non-unique hostname
sent unique=false. Check the raw referrer first, before sameSite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Compare real hostnames to detect same-site referrers

The cleaned referrer has no www. and uses the defined hostname, so on
www sites and sites with data-hostname internal page loads and manual
pageviews were sent as unique, and SPA pageviews and events on
data-hostname sites lost their referrer. Compare the raw referrer host
with the location host, and treat later pageviews as same-site.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Release tested script builds and record deployments

Stop committing dist/. Bumping VERSION in compile.js on main now publishes
GitHub Release vN with the BrowserStack-tested build as scripts-vN.tar.gz.
pnpm run deploy picks a release, verifies its digest, shows what each
destination runs now, and records a GitHub deployment per destination.

Fixes #139

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Record deployments per destination and release only from main

- Deploy and record one destination at a time, so a failure on Caddy no
  longer records the CDN as failed (and the other way around).
- Record a release only when latest.js is part of the deployment.
- Show the newest successful deployment as what a destination runs, and
  list newer failed attempts.
- Run the release workflow's version job only on main.
- Build before the unit and deploy tests, now that dist/ is not
  committed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Address release and deployment review feedback

- Only skip a release when a published release has its archive, so a
  failed publish after the draft can be retried.
- List up to 100 releases, and only those with a recorded digest.
- Say how many deployments were searched when none succeeded.
- README: only deployments with latest.js are recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Reuse an existing draft when publishing a release

If an earlier run created the draft but failed to publish it, upload the
archive to that draft and publish it at the tested commit, instead of
creating a second draft for the same tag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Treat any published release as released

Only drafts are retried, so a retry never attaches a new archive to a
published release whose tag points at another commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ironbrands16 <103500025+ironbrands16@users.noreply.github.com>
Co-authored-by: Adriaan <1079135+adriaanvanrossum@users.noreply.github.com>
Co-authored-by: adriaanvanrossum <adriaanvanrossum@users.noreply.github.com>
Co-authored-by: adriaandotcom <adriaandotcom@users.noreply.github.com>
Co-authored-by: Adriaan <1079135+adriaandotcom@users.noreply.github.com>
Co-authored-by: Simple Analytics Codex <codex@simpleanalytics.invalid>
Co-authored-by: simple-analytics-ai[bot] <313929673+simple-analytics-ai[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Conductor Adriaan <conductor@adriaan.simpleanalytics.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change: routine Changes to internal tools or low-risk changes (e.g., design updates or content modifications)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant