From f71e76e8990ca68b21ae78e38d23595716cf8b36 Mon Sep 17 00:00:00 2001 From: Dev Talan <84081651+devchaudhary24k@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:43:30 +0530 Subject: [PATCH 1/2] docs(deploy): note the PostHog cookieless project setting Co-Authored-By: Claude Opus 5.5 (1M context) --- DEPLOY.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/DEPLOY.md b/DEPLOY.md index 3f026c4..c715730 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -49,6 +49,13 @@ runs with analytics and error reports off. The `VITE_*` values are public: they reach every visitor's browser. Never put a secret in a variable that starts with `VITE_`. A change takes effect on the next deploy or restart. +Hexlode sends PostHog cookieless events, so in the PostHog project turn on **cookieless server +hash mode** and **Discard client IP data**. Without the first, PostHog accepts the events and then +drops them. The app sends only its own named events, such as `page_viewed`, and no `$pageview`, so +look for them under **Activity → Events**; the Web analytics dashboard stays empty. PostHog's +onboarding snippet `posthog.capture(…)` does not work in the console, because the app does not put +PostHog on `window`. + ### 4. Add the domain On the **Domains** tab, add your host with path `/`, container port `3000`, and HTTPS on with a From ae6686d258f949a35a1caf8f995ccf2a77f6a1f8 Mon Sep 17 00:00:00 2001 From: Dev Talan <84081651+devchaudhary24k@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:50:36 +0530 Subject: [PATCH 2/2] fix(usage): keep $ip so PostHog can hash cookieless events PostHog computes the cookieless distinct ID from the IP, user agent and host, and drops cookieless events without $ip (cookieless_missing_ip). before_send set $ip to null on every event, so PostHog accepted them and then discarded all of them. The project's "Discard client IP data" setting removes the IP after hashing instead. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/adr/0005-cookieless-explicit-analytics.md | 6 ++++-- implementation.md | 7 ++++--- src/features/usage/__tests__/usage.test.ts | 10 ++++++---- src/features/usage/usage.ts | 8 -------- src/routes/privacy.tsx | 5 +++-- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/adr/0005-cookieless-explicit-analytics.md b/docs/adr/0005-cookieless-explicit-analytics.md index 058a7df..a4e05ee 100644 --- a/docs/adr/0005-cookieless-explicit-analytics.md +++ b/docs/adr/0005-cookieless-explicit-analytics.md @@ -1,7 +1,9 @@ # Cookieless analytics with explicit events PostHog runs with `cookieless_mode: 'always'` and `person_profiles: 'never'`, so it stores nothing -in the browser and the app needs no consent banner. IP capture, session replay and autocapture are -off because they would record file names shown on screen. The app sends its own detailed events +in the browser and the app needs no consent banner. PostHog hashes each visitor's IP address, user +agent and host into an anonymous ID that changes daily, and the project discards the IP afterwards. +The client leaves `$ip` alone: PostHog drops cookieless events that arrive without one. Session +replay and autocapture are off because they would record file names shown on screen. The app sends its own detailed events from one analytics module instead. Events never contain file names, paths, pixels, image metadata or text the user types. diff --git a/implementation.md b/implementation.md index 15b3a58..d26fc64 100644 --- a/implementation.md +++ b/implementation.md @@ -155,13 +155,14 @@ removed. ## Analytics -- PostHog uses `cookieless_mode: 'always'` and `person_profiles: 'never'`, with IP capture, session - replay and autocapture turned off. The app sends its own events from one analytics module +- PostHog uses `cookieless_mode: 'always'` and `person_profiles: 'never'`, with session replay and + autocapture turned off. The app sends its own events from one analytics module ([ADR 0005](./docs/adr/0005-cookieless-explicit-analytics.md)). - Sentry sends errors with `sendDefaultPii: false` and no replay. File names are removed from error messages before sending. - The PostHog project must have cookieless mode enabled and "Discard client IP data" turned on; - without the first, PostHog ignores cookieless events. The client also clears `$ip` on every event. + without the first, PostHog ignores cookieless events. The client must not clear `$ip`: PostHog + hashes it into the daily anonymous ID and drops cookieless events without it. - The event catalogue lives in `src/features/usage/events.ts`, and the privacy page lists it. ## Deployment diff --git a/src/features/usage/__tests__/usage.test.ts b/src/features/usage/__tests__/usage.test.ts index c57c245..d5bc5f2 100644 --- a/src/features/usage/__tests__/usage.test.ts +++ b/src/features/usage/__tests__/usage.test.ts @@ -36,10 +36,12 @@ describe('analytics', () => { }) }) - it('removes the IP address from every event', () => { - const beforeSend = POSTHOG_OPTIONS.before_send - const event = beforeSend({ event: 'x', properties: { $ip: '203.0.113.9', itemCount: 2 } }) - expect(event?.properties).toEqual({ $ip: null, itemCount: 2 }) + // PostHog hashes the IP into the daily cookieless ID and drops cookieless events without one. + it('leaves the IP for PostHog to hash into the cookieless ID', () => { + const options: Record = POSTHOG_OPTIONS + const beforeSend = options.before_send as ((event: unknown) => unknown) | undefined + const event = { event: 'x', properties: { itemCount: 2 } } + expect(beforeSend ? beforeSend(event) : event).toEqual(event) }) it('does nothing without a key', () => { diff --git a/src/features/usage/usage.ts b/src/features/usage/usage.ts index adb6e97..326f6c6 100644 --- a/src/features/usage/usage.ts +++ b/src/features/usage/usage.ts @@ -5,11 +5,6 @@ import { type AnalyticsEventName, EVENTS, type EventProperties } from '#/features/usage/events' import type { PublicConfig } from '#/features/usage/types' -interface CaptureEvent { - event: string - properties: Record -} - export interface PostHogLike { init(key: string, options: Record): unknown capture(event: string, properties: Record): unknown @@ -35,9 +30,6 @@ export const POSTHOG_OPTIONS = { disable_external_dependency_loading: true, advanced_disable_flags: true, mask_personal_data_properties: true, - /** Clears the IP address. The PostHog project also discards client IP data. */ - before_send: (event: CaptureEvent | null) => - event ? { ...event, properties: { ...event.properties, $ip: null } } : null, } as const export interface AnalyticsOptions { diff --git a/src/routes/privacy.tsx b/src/routes/privacy.tsx index 53e79ff..b992c08 100644 --- a/src/routes/privacy.tsx +++ b/src/routes/privacy.tsx @@ -57,8 +57,9 @@ function Privacy() { What Hexlode measures Product analytics run without cookies and without identifying you. Your IP address is - discarded. Events contain only counts, timings, node types, settings and error codes. - They never contain file names, paths, pixels, image metadata or text you type. + turned into an anonymous ID that changes daily, then discarded. Events contain only + counts, timings, node types, settings and error codes. They never contain file names, + paths, pixels, image metadata or text you type. {Object.entries(EVENTS).map(([name, event]) => (