PostHog #
PostHog is additive product analytics. It does not replace the existing operational or traffic systems.
| System | Ownership |
|---|---|
| Lith + Silo/Mongo | Boot health, piece-run logs, errors, bundle performance, piece-hit counters, access logs, and storage operations |
| Google Analytics | Existing broad site traffic |
| PostHog | Minimized journeys, funnels, cohorts, and experiments |
The browser integration is inert until Lith receives POSTHOG_PROJECT_TOKEN.
POSTHOG_API_HOST may be https://us.i.posthog.com (default) or
https://eu.i.posthog.com. The project token is public browser configuration;
personal API keys and OAuth tokens never belong in the repository or HTML.
Set POSTHOG_SERVER_ENDPOINT_EVENTS=true separately to enable anonymized,
batched endpoint-volume events. This second switch makes the higher-volume
server stream an explicit rollout decision. POSTHOG_OSKIEWAR_EVENTS=true
separately enables category-only Oskiewar server milestones.
Initial capture is deliberately narrow:
| Event | Properties |
|---|---|
$pageview |
ac_route, with query, hash, published handles, and source removed |
ac_piece_opened |
built-in piece or null, piece_kind, minimized route |
ac_prompt_succeeded |
the safe destination fields from ac_piece_opened; never prompt text |
ac_piece_interacted |
safe piece fields plus input_kind (pointer, touch, or keyboard) |
ac_handle_created |
no properties; emitted only after the first successful handle claim |
ac_media_created |
media_kind and anonymous account_state; never filenames, codes, or content |
$identify |
Auth0 sub; public handle when available; never email |
ac endpoint completed |
endpoint, method/status/latency buckets, analytics class, aggregate count |
Autocapture, session replay, exception capture, performance capture, heatmaps, surveys, and product tours are off. Embedded, packed, local, and preview/icon renders do not initialize PostHog. Do Not Track is respected.
Start with ac_piece_opened to ac_piece_interacted, and use
ac_prompt_succeeded as the successful prompt-navigation milestone. Break down
by piece_kind and built-in piece. Validate that published pieces have
piece = null before adding any dashboard or experiment. Use
ac_handle_created and ac_media_created as activation milestones; neither
event contains handles, filenames, codes, source, prompts, or media content.
The server event is an anonymous aggregate, not a person event. It uses one
Lith-level distinct ID, disables person profiles and GeoIP, and combines equal
dimensions into ten-second batches. Never use it for unique-user counts or
person funnels; sum its count property for request volume.
Oskiewar #
Oskiewar uses the browser client plus anonymous aggregates from Lith and the session server:
| Event | Meaning |
|---|---|
ac_oskiewar_match_started |
Browser play began after character selection |
ac_oskiewar_live_started |
First valid state reached a live room |
ac_oskiewar_spectator_joined |
A viewer entered a live or waiting room |
ac_oskiewar_round_stored |
Lith stored a new validated demo |
ac_oskiewar_match_completed |
A stored round completed the match |
ac_oskiewar_live_viewed |
Browser received its first live state |
ac_oskiewar_replay_viewed |
Browser received a stored demo |
ac_oskiewar_round_followed |
Browser followed the next-round transition |
Properties are allowlisted categories: source_system, surface,
input_family, opponent_type, phase, viewer_state, round_position,
duration_bucket, and result. Not every event uses every property. Custom
properties never include round or series IDs, raw URLs, fighter handles, replay
content, commands, scores, IPs, user agents, or raw errors. Oskiewar routes
collapse to /oskiewar or /oskiewar/round before browser capture.
Server milestones use fixed ac-oskiewar-*-aggregate distinct IDs, disable
person profiles and GeoIP, and combine equal categories into ten-second
batches. Sum properties.count; do not use these aggregates for unique-person
analysis. Browser milestones remain ordinary person events.
Endpoint map #
Run the inventory from the repository root:
node toolchain/analytics/posthog-inventory.mjs > /tmp/ac-posthog-inventory.json
The current tree contains 166 function source files, resolving to 165 unique
names: 161 statically detected handlers and four helpers or scripts. This is
not the same as the number of public routes. Lith also provides aliases, media
routes, host rewrites, operational routes, static files, and piece/index
fallbacks. lithRouteFamilies in the generated JSON maps those surfaces.
Every function is classified by shared/posthog-policy.mjs:
| Policy | PostHog treatment |
|---|---|
minimized-browser-or-aggregate |
Browser journey and/or bounded server count |
aggregate-status-only |
Bounded server count only |
inventory-only |
Mapped for context; emits no event |
existing-lith-silo-only |
Remains in Lith/Silo/Mongo |
disabled / review-required |
Fails the inventory test until reviewed |
The server count contains only function name, HTTP method, status class, duration bucket, analytics class, and count. It never reads or sends path, query, request or response body, IP, user agent, user ID, authorization header, raw error, or stack. Messaging, MCP, local-machine, admin, and existing operational telemetry classes do not emit endpoint events.
Adding a function source requires an analytics classification. Run:
node --test \
system/tests/product-analytics.test.mjs \
system/tests/posthog-server.test.mjs \
system/tests/posthog-inventory.test.mjs
The inventory test fails when the source/handler counts change or any function
lands in review-required. Update the reviewed policy and counts together.
Validation #
Before enabling the server switch, configure only the browser token and verify:
- Production
$pageviewevents contain minimizedac_routevalues. /@handle/...,/$code, and prompt-source routes become/@published,/$code, and/prompt; no query or hash survives.- Published pieces have
piece = null. - Identified profiles contain Auth0
suband optional public handle, never email. - Replay, autocapture, exception, performance, survey, and tour data remain absent.
For Oskiewar, verify that both a random round URL and a short legacy round URL
produce ac_route = /oskiewar/round, then confirm every Oskiewar event's custom
properties use only the categories above. Enable POSTHOG_OSKIEWAR_EVENTS=true
on Lith and the session server only after this browser check.
Then enable POSTHOG_SERVER_ENDPOINT_EVENTS=true and verify ac endpoint completed has only the documented properties. A useful HogQL request-volume
check is:
select
properties.endpoint as endpoint,
sum(toInt64(properties.count)) as requests
from events
where event = 'ac endpoint completed'
group by endpoint
order by requests desc
For product behavior, create a funnel from ac_piece_opened to
ac_piece_interacted and break it down by piece_kind. Endpoint aggregates
answer system-usage volume; Lith/Silo answer failures and diagnostics; neither
should be substituted for the other.
Rollback requires no code or data migration: unset POSTHOG_PROJECT_TOKEN to
disable browser and server analytics, or unset only
POSTHOG_SERVER_ENDPOINT_EVENTS to retain browser journeys. Unset
POSTHOG_OSKIEWAR_EVENTS to stop only Oskiewar server milestones.
Product context #
- The front door is a prompt-driven creative computer.
imnewregisters, verification establishes the account,handleclaims a public identity, and a piece name opens a program. - The browser runtime lives in
system/public/aesthetic.computer; pieces are indisks/; Lith serves the site and API; Silo is the data and storage console. - Built-in, published, and KidLisp pieces are different content classes. Raw source, prompts, paintings, chat, and private account fields are content, not analytics properties.
- Local tools include Slab/host tooling and the prox, fleet, paper, frame, chat, DM, mail, and calendar MCP surfaces. Their existence and public product role are useful context. Prompt/session content, contacts, messages, mail, calendars, fleet host details, secrets, and local files are not analytics inputs.
Any local-tool analytics must be a separate opt-in proposal with a minimized
schema such as { tool_family, operation_class, outcome, duration_bucket }.
Never include arguments, file paths, hostnames, handles, recipients, message
content, artifact contents, or raw errors by default.
Self-driving #
PostHog's setup command is:
npx @posthog/wizard self-driving
Do not run it unattended. It can connect GitHub, enable replay and error tracking, configure signal sources, and schedule scouts. Before activation:
- Verify production events and the privacy contract above.
- Decide whether AI data processing is acceptable.
- Resolve repository routing: Tangled is authoritative and GitHub is currently documented as a read-only mirror, while PostHog requires a writable GitHub repository to open PRs.
- Grant only the selected GitHub repository, require human review and merge, and leave deployments outside PostHog.
- Review every proposed signal source. Replay, error capture, support, Slack, and local MCP content stay off until each has its own privacy review.
Self-driving creates a branch and PR for an actionable report; a human still reviews and merges it. Start with one manually reviewed report, not broad autonomous production mutation.