analytics: count usage of the hosted site with PostHog - #27
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds product analytics to satisfactorymap.net only. Four hand-written events, autocapture off:
$pageviewsave_loadedsource,size_mb,objects,mstool_openedtool(dock panel id)feature_usedfeature(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.json→frontendDist), so there is no separate web build to hold a snippet.window.__TAURI__gates it off, as dolocalhost/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-corpfor wasm. Under it a cross-origin<script>is a no-cors request needingCross-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'sno-externalbuild, 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_urlto every event — which would have shipped users' save locations to analytics. Asanitize_propertieshook 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:
?url=https://example.com/secret/MyFactory.sav→$current_urlis the bare originNote 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