Skip to content

analytics: count usage of the hosted site with PostHog - #27

Merged
valentinps merged 1 commit into
mainfrom
feat/analytics-posthog
Sep 6, 2026
Merged

valentinps merged 1 commit into
mainfrom
feat/analytics-posthog

Conversation

@valentinps

Copy link
Copy Markdown
Owner

Adds product analytics to satisfactorymap.net only. Four hand-written events, autocapture off:

event properties
$pageview
save_loaded source, size_mb, objects, ms
tool_opened tool (dock panel id)
feature_used feature (from an explicit allow-list of control ids)

Nothing about the save is sent: no session name, file name, coordinates, or item names.

Three constraints shaped this

"Online only" is a runtime gate, not a build split. The desktop app bundles this very dist/ (tauri.conf.jsonfrontendDist), so there is no separate web build to hold a snippet. window.__TAURI__ gates it off, as do localhost/file:// so development never lands in the same project as real traffic. The desktop CSP would block the request anyway — the gate is what makes the app genuinely phone-home-free rather than merely unable to phone home.

COEP rules out the documented snippet. The site ships Cross-Origin-Embedder-Policy: require-corp for wasm. Under it a cross-origin <script> is a no-cors request needing Cross-Origin-Resource-Policy, which PostHog's CDN does not send — so the standard snippet is blocked outright. posthog-js 1.427.2 is therefore vendored (vendor/posthog.js, upstream's no-external build, which never injects further script tags), leaving the CORS-mode ingest request as the only cross-origin traffic.

One leak found and fixed. The app loads saves via ?url=<remote .sav>, and PostHog attaches $current_url to every event — which would have shipped users' save locations to analytics. A sanitize_properties hook strips query and fragment from every URL-ish property.

Cookieless (persistence: "memory"), so no consent banner is needed and no identifier is stored; the cost is that reloads count as new visitors, so totals read as visits rather than people.

Verification

End-to-end against the dev server (which sends the production COOP/COEP headers), with the ingest endpoint stubbed so nothing reached the real project:

  • all four events leave with the expected properties
  • ?url=https://example.com/secret/MyFactory.sav$current_url is the bare origin
  • localhost gate: 0 events; Tauri gate: 0 events

Note for future automated checks: posthog-js silently drops captures from automated browsers (navigator.webdriver), so driving this with Playwright needs automation masked.

Also updates the README's privacy bullet, which previously claimed the save never leaves your machine without qualification, and adds a third-party-code section to NOTICES.md covering both the new posthog-js and the already-vendored Leaflet.

🤖 Generated with Claude Code

Four events, all written by hand: $pageview, save_loaded (source, size,
object count, wall-clock ms), tool_opened, and feature_used off an explicit
allow-list of control ids. Autocapture is off -- on a canvas UI it would
mostly record "clicked the map" -- and nothing about the save is sent: no
session name, no file name, no coordinates, no item names.

Hosted site only, decided at runtime rather than at build time, because the
desktop app bundles this very dist/ (tauri.conf.json frontendDist) and there
is no separate web build to put a snippet in. window.__TAURI__ gates it off
there, and localhost/file:// runs are gated off too so development does not
land in the same project as real traffic. Its CSP would block the request
anyway; the gate is what makes the desktop genuinely phone-home-free rather
than merely unable to phone home.

The library is vendored rather than loaded from PostHog's CDN. The site
ships COEP require-corp for wasm, under which a cross-origin <script> is a
no-cors request needing Cross-Origin-Resource-Policy -- which their CDN does
not send, so the documented snippet is blocked outright. Serving it
same-origin sidesteps that, and the upstream "no-external" build never
injects further script tags, leaving the CORS-mode ingest request as the
only cross-origin traffic.

Cookieless (persistence: memory), so no consent banner is required and no
identifier is stored; the cost is that reloads count as new visitors, so
read the totals as visits rather than people. URL properties are stripped of
their query string before send: the app takes a save to load as
?url=<remote .sav>, which PostHog would otherwise attach to every event as
$current_url.

Verified end-to-end against the dev server (which sends the production COOP/
COEP headers) with the ingest endpoint stubbed: all four events leave with
the expected properties, ?url= is stripped from $current_url, and both the
localhost and Tauri gates emit nothing at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@valentinps
valentinps merged commit a71f143 into main Sep 6, 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