Skip to content

v12 - #40

Merged
adriaandotcom merged 105 commits into
mainfrom
v12
Oct 22, 2026
Merged

v12#40
adriaandotcom merged 105 commits into
mainfrom
v12

Conversation

@adriaandotcom

@adriaandotcom adriaandotcom commented May 12, 2023 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #97

Prepare public script v12:

Before merging, update beta.js as well or inform customer team 13489e0b-295c-476b-b955-46389fe10895.

Security implications

Has security impact - described as: - [ ] No security impact - [x] Has security impact - described as: - Query parameter values are never collected by this addition. - Name collection is limited to parameters with non-empty values and can be disabled.

Testing

  • Frozen pnpm install passed with Node.js 24.18.0
  • Rebuilt all distribution files
  • Full unit suite passed: 16 tests
  • Focused pageview query suite passed: 4 tests
  • Prettier check passed for the edited source and unit test
  • BrowserStack workflow passed

Checklist

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

Summary by CodeRabbit

  • New Features
    • Added automatic tracking for phone links, custom click and form-submission events, and optional 404 page detection.
    • Manual pageviews support paths, callbacks, and metadata. Query parameters can be collected according to configured rules, and repeated pageviews for the same path and query are suppressed.
    • Eligible requests can use fetch or beacon delivery, with image-request fallback when needed.
  • Bug Fixes
    • Improved phone-link tracking when navigation is cancelled and pageview handling when paths differ only by query string.
    • Script headers now accept both “Privacy friendly” and “Privacy-first” labels.
  • Documentation
    • Removed the Cloudflare app from the listed destinations for compiled scripts.

Open workspace in Conductor

simpleanalytics-agent and others added 3 commits October 20, 2026 00:14
* 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

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>
Comment thread src/default.js Outdated
Comment thread src/default.js
@github-actions

Copy link
Copy Markdown
Contributor

Documentation update

_docs/20_script-settings/06_pings.md

Location: New page in the script-settings category, alongside 05_allow-url-parameters.md

Copy and paste this into the documentation repository:

---
title: Pings
category: script-settings
permalink: /pings
created_at: 2026-10-20
last_modified_at: 2026-10-20
---

We send [time on page](/explained/time-on-page) when a visitor leaves or hides a page. On mobile browsers that page view can be discarded before that moment, and then no time on page is recorded at all.

With the `pings` setting you can send the time on page at fixed moments during a visit, so you always keep a lower bound for that page view. Each ping replaces the previously sent value for the same page view, so your stats never count the same seconds twice.

## Script setting `data-pings`

Specify the thresholds in seconds, separated by a comma:

```html
<script
  data-pings="5,30,60"
  src="https://scripts.simpleanalyticscdn.com/latest.js"
></script>

With the embed above we send the time on page after 5, 30 and 60 seconds of visible time, on top of the request we already send when the visitor leaves or hides the page.

Only visible time counts. When a visitor switches to another tab, the timer pauses and the remaining pings move along with it.

Using the settings object

You can also set it via the settings object:

<script>
  window.sa_settings = { pings: "5,30,60" };
</script>

Defaults

By default there are no pings and we only send the time on page when the page is hidden or the visitor navigates away. Adding pings sends an extra request per threshold, so pick the thresholds that matter to you rather than a long list.

Pings do nothing when timeonpage is disabled via ignore metrics.

Need any help? We love to help you set up your script. Don't hesitate to contact us. Silly questions don't exist.

Comment thread test/deploy.test.mts Outdated
Conductor Adriaan and others added 5 commits October 21, 2026 09:02
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>
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>
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>
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>
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>
Comment thread src/default.js Outdated
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>
@github-actions

Copy link
Copy Markdown
Contributor

Documentation update

_docs/33_explained/05_uniques.md

Location: ## How we detect unique pageviews, replacing the "A pageview is NOT counted as unique when:" list and adding a new subsection after the two example paragraphs that follow 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), unless the referrer is a Cloudflare challenge page (see below)
- The user navigates using browser history (back/forward buttons)
- The user reloads the page
- The referrer's hostname is in your [non-unique hostnames](/non-unique-hostnames) list, for example a payment provider

### Cloudflare challenge pages

If your site runs behind Cloudflare with a managed challenge or "I'm Under Attack" mode, visitors first pass a challenge page on your own domain. The browser then loads your page with that challenge page as the referrer, which would normally make the arrival look like internal navigation.

To avoid losing those visitors, we treat the very first pageview of a page load as unique when the referrer is on your own hostname and contains a Cloudflare challenge marker (`?__cf_` or `&__cf_`). We store no referrer for that pageview, so it appears as direct traffic: the visitor's original referrer was already replaced by the challenge page, and the challenge URL itself is never stored.

Only that first pageview can be counted this way. Later pageviews, page reloads, and back/forward navigation stay non-unique, and a challenge referrer coming from a [non-unique hostname](/non-unique-hostnames) other than your own also stays non-unique.

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>
Comment thread src/default.js Outdated
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>
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>
@adriaandotcom
adriaandotcom merged commit 0dfaca5 into main Oct 22, 2026
5 checks passed
@adriaandotcom
adriaandotcom deleted the v12 branch October 22, 2026 13:21
Comment thread src/auto-events.js
@@ -73,20 +75,9 @@
if (typeof optionsLink === "undefined")
log("options object not found, please specify", "warn");

window.saAutomatedLink = function saAutomatedLink(element, type) {
var saAutomatedLink = function saAutomatedLink(element, type) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? Shouldn't it be on window anymore?

Comment thread src/auto-events.js
}
} catch (error) {
log(error.message, "warn");
}
};

function collectLink(link, onclick) {
window.saAutomatedLink = saAutomatedLink;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically double then, no?

Comment thread src/auto-events.js
Comment on lines +180 to +182
link.addEventListener("click", function () {
saAutomatedLink(this, collect);
});

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense to keep the saAutomatedLink function just internally only? Why expose to window?

Comment thread src/default.js
Comment on lines +12 to +13
// Only initialize in a browser.
if (!window) return;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there no /** if skipnonwindow **/ because it was there before, no?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change: needs review Changes affecting security, data protection, or system stability.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Differentiate paid and organic referral traffic

4 participants