UN-3543 [FIX] Use per-user distinct_id and deployment tagging for PostHog#2049
UN-3543 [FIX] Use per-user distinct_id and deployment tagging for PostHog#2049chandrasekharan-zipstack wants to merge 5 commits into
Conversation
…tHog - distinct_id is now the user's email (fallback orgId:userId) instead of the org, enabling per-user analytics (DAU/WAU, funnels, retention) - org_id/org_name/software registered as super-properties on every event so org-level breakdowns work without the group analytics add-on - deployment super-property derived from hostname (us-prod / eu-prod / saas-staging / dev / self-hosted) to segment the shared PostHog project - init hygiene: person_profiles=identified_only, respect_dnt=true Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Summary by CodeRabbit
WalkthroughAdds PostHog deployment detection helpers, runtime enable flag from container config, privacy-aware initialization with error handling, and a deployment-conditional identity refactoring that gates PII and registers org-level super-properties. ChangesPostHog Configuration and Identity
🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…coded staging host - enablePosthog exposed through window.RUNTIME_CONFIG so containerized envs (staging) can disable PostHog at startup without a rebuild - drop saas-staging/dev hostname mapping; non-prod envs are expected to disable PostHog via VITE_ENABLE_POSTHOG Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| frontend/generate-runtime-config.sh | Adds enablePosthog field to the runtime-config.js output, correctly using the shell default-substitution pattern consistent with the existing logoUrl field. |
| frontend/src/helpers/PostHogDeployment.js | New helper that maps SaaS hostnames to deployment tags and exports isSaasProdDeployment(); the hostname map is a closed list that will need manual updates for new SaaS domains, and isSaasProdDeployment() is re-used as the PII-gating signal in usePostHogEvents, creating a dual role that could diverge. |
| frontend/src/hooks/usePostHogEvents.js | Rewrites setPostHogIdentity to use email as distinct_id for SaaS, fallback to orgId:userId, and registers org super-properties; isOpenSource still relies on the mock_org name check, which misclassifies self-hosted commercial installs as software: SAAS on every event. |
| frontend/src/index.jsx | Adds runtime-config toggle for PostHog, wraps init in try/catch, and registers the deployment super-property immediately after init; logic and fallback semantics are correct. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Container startup\ngenerate-runtime-config.sh] -->|writes enablePosthog| B[window.RUNTIME_CONFIG]
B --> C{index.jsx\nenablePosthog !== 'false'?}
C -- No --> D[PostHog disabled\nno init]
C -- Yes --> E[posthog.init\nperson_profiles: identified_only\nrespect_dnt: true]
E --> F[posthog.register\ndeployment: us-prod / eu-prod / self-hosted]
F --> G[App renders\nPostHogProvider]
G --> H{User logs in\nsetPostHogIdentity called}
H --> I{isSaasProdDeployment\n&& !isOpenSource?}
I -- Yes / canSendPii --> J[distinctId = email\npersonProps += name, email]
I -- No --> K[distinctId = orgId:userId\nno PII in personProps]
J --> L[posthog.identify distinctId, personProperties]
K --> L
L --> M[posthog.register\norg_id, org_name, software]
M --> N[All subsequent events carry\ndeployment + org_id + org_name + software]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Container startup\ngenerate-runtime-config.sh] -->|writes enablePosthog| B[window.RUNTIME_CONFIG]
B --> C{index.jsx\nenablePosthog !== 'false'?}
C -- No --> D[PostHog disabled\nno init]
C -- Yes --> E[posthog.init\nperson_profiles: identified_only\nrespect_dnt: true]
E --> F[posthog.register\ndeployment: us-prod / eu-prod / self-hosted]
F --> G[App renders\nPostHogProvider]
G --> H{User logs in\nsetPostHogIdentity called}
H --> I{isSaasProdDeployment\n&& !isOpenSource?}
I -- Yes / canSendPii --> J[distinctId = email\npersonProps += name, email]
I -- No --> K[distinctId = orgId:userId\nno PII in personProps]
J --> L[posthog.identify distinctId, personProperties]
K --> L
L --> M[posthog.register\norg_id, org_name, software]
M --> N[All subsequent events carry\ndeployment + org_id + org_name + software]
Reviews (4): Last reviewed commit: "Merge branch 'main' into UN-3543-posthog..." | Re-trigger Greptile
There was a problem hiding this comment.
🧹 Nitpick comments (3)
frontend/generate-runtime-config.sh (1)
20-20: 💤 Low valueConsider logging the
enablePosthogvalue for debugging parity.The echo statement logs
logoUrlbut not the newenablePosthogsetting. Adding it would help operators verify PostHog state at container startup.🔧 Suggested enhancement
-echo "Runtime configuration generated successfully with logo URL: ${VITE_CUSTOM_LOGO_URL:-${REACT_APP_CUSTOM_LOGO_URL:-<not set>}}" +echo "Runtime configuration generated successfully with logo URL: ${VITE_CUSTOM_LOGO_URL:-${REACT_APP_CUSTOM_LOGO_URL:-<not set>}}, enablePosthog: ${VITE_ENABLE_POSTHOG:-${REACT_APP_ENABLE_POSTHOG:-<not set>}}"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/generate-runtime-config.sh` at line 20, Update the runtime config echo to also print the PostHog enable flag for startup debugging: read the enablePosthog value from the same env precedence used elsewhere (e.g., VITE_ENABLE_POSTHOG with fallback to REACT_APP_ENABLE_POSTHOG) and append it to the existing log message that currently prints the logo URL (the echo line). Ensure the message clearly labels the flag (e.g., "enablePosthog: <value>") so operators can verify PostHog state at container startup.frontend/src/hooks/usePostHogEvents.js (2)
67-69: ⚡ Quick winAdd a defensive guard when both email and IDs are unavailable.
If
orgIdoruserIdisundefined, the distinctId becomes malformed (e.g.,"undefined:undefined"), which would merge all such edge-case users into one PostHog person and corrupt analytics.Consider early-returning or logging if no usable identifier exists:
Suggested guard
+ if (!email && (!orgId || !userId)) { + // Insufficient identity data; skip PostHog identification + return; + } const distinctId = email || `${orgId}:${userId}`;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/hooks/usePostHogEvents.js` around lines 67 - 69, The distinctId logic in usePostHogEvents.js can produce "undefined:undefined" when email is falsy and orgId/userId are missing; update the hook (usePostHogEvents) to defensively check before building distinctId: require either a truthy email or both a non-empty orgId and userId, and if neither condition holds, early-return (skip sending/posting events) and log a warning via the same logger used in this hook; ensure all places that reference distinctId use the validated value so malformed identifiers are never sent to PostHog.
85-93: ⚡ Quick winWrap PostHog calls in try-catch for consistency.
setPostHogCustomEventalready guardsposthog.capturewith a try-catch (lines 97-101), butidentifyandregisterare unguarded. If PostHog is disabled at runtime or the SDK is in an unexpected state, these calls could throw. Apply the same defensive pattern here.Suggested change
- posthog.identify(distinctId, personProperties); - - // Super-properties ride on every captured event, enabling org-level - // breakdowns without the group analytics add-on - posthog.register({ - org_id: orgId, - org_name: orgName, - software, - }); + try { + posthog.identify(distinctId, personProperties); + + // Super-properties ride on every captured event, enabling org-level + // breakdowns without the group analytics add-on + posthog.register({ + org_id: orgId, + org_name: orgName, + software, + }); + } catch { + // PostHog tracking is non-critical, safe to ignore + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/hooks/usePostHogEvents.js` around lines 85 - 93, Wrap the unguarded posthog.identify(distinctId, personProperties) and posthog.register({...}) calls in the same defensive try-catch pattern used by setPostHogCustomEvent so SDK errors don't bubble up; locate the calls in usePostHogEvents.js (the posthog.identify and posthog.register lines), surround each (or both together) with try { ... } catch (err) { /* log the error similarly to setPostHogCustomEvent — e.g., console.error or existing logger with context */ } to mirror existing error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@frontend/generate-runtime-config.sh`:
- Line 20: Update the runtime config echo to also print the PostHog enable flag
for startup debugging: read the enablePosthog value from the same env precedence
used elsewhere (e.g., VITE_ENABLE_POSTHOG with fallback to
REACT_APP_ENABLE_POSTHOG) and append it to the existing log message that
currently prints the logo URL (the echo line). Ensure the message clearly labels
the flag (e.g., "enablePosthog: <value>") so operators can verify PostHog state
at container startup.
In `@frontend/src/hooks/usePostHogEvents.js`:
- Around line 67-69: The distinctId logic in usePostHogEvents.js can produce
"undefined:undefined" when email is falsy and orgId/userId are missing; update
the hook (usePostHogEvents) to defensively check before building distinctId:
require either a truthy email or both a non-empty orgId and userId, and if
neither condition holds, early-return (skip sending/posting events) and log a
warning via the same logger used in this hook; ensure all places that reference
distinctId use the validated value so malformed identifiers are never sent to
PostHog.
- Around line 85-93: Wrap the unguarded posthog.identify(distinctId,
personProperties) and posthog.register({...}) calls in the same defensive
try-catch pattern used by setPostHogCustomEvent so SDK errors don't bubble up;
locate the calls in usePostHogEvents.js (the posthog.identify and
posthog.register lines), surround each (or both together) with try { ... } catch
(err) { /* log the error similarly to setPostHogCustomEvent — e.g.,
console.error or existing logger with context */ } to mirror existing error
handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 119e73f1-d4d7-4da6-85bf-083e8848f00b
📒 Files selected for processing (3)
frontend/generate-runtime-config.shfrontend/src/hooks/usePostHogEvents.jsfrontend/src/index.jsx
jaseemjaskp
left a comment
There was a problem hiding this comment.
Automated review (PR Review Toolkit: Code Reviewer, Silent Failure Hunter, Type Design Analyzer, PR Test Analyzer, Comment Analyzer, Code Simplifier).
Scope: the 3 files changed by this PR. I've omitted findings that duplicate the existing greptile comments (degenerate distinctId at usePostHogEvents.js:69, reset() on logout at :93, and ||/empty-string semantics at index.jsx:17 — all still worth addressing). The items below are new.
- email/name sent to PostHog only from Unstract-managed SaaS deployments; self-hosted/OSS identify with an anonymous org-scoped id (tightens the pre-existing behaviour of sending email person props from everywhere) - deployment detection extracted to helpers/PostHogDeployment.js, shared by init and identity - init/register and identify wrapped in try/catch so analytics failures (blocked storage, ad-blockers, CSP) can't break bootstrap or the app - guard degenerate "undefined:<id>" fallback distinct_id when orgId absent - log enablePosthog in runtime-config startup echo; dot notation nits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/generate-runtime-config.sh (1)
8-14:⚠️ Potential issue | 🟠 Major | ⚡ Quick winEscape runtime env values before embedding into JavaScript.
Line 8–14 currently inject raw environment values into quoted JS strings. If any value contains
"or\,runtime-config.jsbecomes invalid and can break bootstrap.Suggested fix
#!/bin/sh @@ +# Escape values for safe inclusion in JS string literals +js_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +FAVICON_PATH_RAW="${VITE_FAVICON_PATH:-${REACT_APP_FAVICON_PATH}}" +LOGO_URL_RAW="${VITE_CUSTOM_LOGO_URL:-${REACT_APP_CUSTOM_LOGO_URL}}" +ENABLE_POSTHOG_RAW="${VITE_ENABLE_POSTHOG:-${REACT_APP_ENABLE_POSTHOG}}" + +FAVICON_PATH_ESCAPED="$(js_escape "$FAVICON_PATH_RAW")" +LOGO_URL_ESCAPED="$(js_escape "$LOGO_URL_RAW")" +ENABLE_POSTHOG_ESCAPED="$(js_escape "$ENABLE_POSTHOG_RAW")" + cat > /usr/share/nginx/html/config/runtime-config.js << EOF // This file is auto-generated at runtime. Do not modify manually. window.RUNTIME_CONFIG = { - faviconPath: "${VITE_FAVICON_PATH:-${REACT_APP_FAVICON_PATH}}", - logoUrl: "${VITE_CUSTOM_LOGO_URL:-${REACT_APP_CUSTOM_LOGO_URL}}", - enablePosthog: "${VITE_ENABLE_POSTHOG:-${REACT_APP_ENABLE_POSTHOG}}" + faviconPath: "${FAVICON_PATH_ESCAPED}", + logoUrl: "${LOGO_URL_ESCAPED}", + enablePosthog: "${ENABLE_POSTHOG_ESCAPED}" }; EOF🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/generate-runtime-config.sh` around lines 8 - 14, The runtime-config.js generator currently inserts raw env values into quoted JS strings (window.RUNTIME_CONFIG), which breaks if values contain " or \; fix by JSON-escaping each injected value before writing them. Replace direct expansions like "${VITE_FAVICON_PATH:-${REACT_APP_FAVICON_PATH}}" with a JSON-encoded/escaped form of the chosen variable (e.g., use a small helper that picks the fallback variable then passes it through a JSON encoder such as jq -R -s `@json` or python/json.dumps) for each field (faviconPath, logoUrl, enablePosthog) so the output in runtime-config.js is always valid JavaScript strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/generate-runtime-config.sh`:
- Around line 8-14: The runtime-config.js generator currently inserts raw env
values into quoted JS strings (window.RUNTIME_CONFIG), which breaks if values
contain " or \; fix by JSON-escaping each injected value before writing them.
Replace direct expansions like "${VITE_FAVICON_PATH:-${REACT_APP_FAVICON_PATH}}"
with a JSON-encoded/escaped form of the chosen variable (e.g., use a small
helper that picks the fallback variable then passes it through a JSON encoder
such as jq -R -s `@json` or python/json.dumps) for each field (faviconPath,
logoUrl, enablePosthog) so the output in runtime-config.js is always valid
JavaScript strings.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9ec33c3c-a83b-4ffe-a83d-27cbec5d9b0b
📒 Files selected for processing (4)
frontend/generate-runtime-config.shfrontend/src/helpers/PostHogDeployment.jsfrontend/src/hooks/usePostHogEvents.jsfrontend/src/index.jsx
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/index.jsx
- frontend/src/hooks/usePostHogEvents.js
…ser-identity # Conflicts: # frontend/src/index.jsx
Frontend Lint Report (Biome)✅ All checks passed! No linting or formatting issues found. |
|
jaseemjaskp
left a comment
There was a problem hiding this comment.
Re-reviewed with the PR Review Toolkit (code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, comment-analyzer, code-simplifier). The substantive issues from the prior pass — PII-as-distinct_id gating, unguarded identify/register, bootstrap-abort risk, and the degenerate fallback id — are all addressed in the current code. Remaining observations are doc-wording, defensive-structure, magic-string, and test-coverage suggestions that don't rise to a blocking bar. LGTM.



What
distinct_idis now the user's email (fallbackorgId:userId) instead oforgId:orgName, so each user is an individual PostHog personorg_id,org_nameandsoftware(OSS/SAAS) are registered as super-properties, stamped on every captured eventdeploymentsuper-property derived from hostname:us-prod/eu-prod/self-hostedVITE_ENABLE_POSTHOG=false(exposed throughwindow.RUNTIME_CONFIG) — no rebuild needed; staging should set thisperson_profiles: "identified_only",respect_dnt: trueWhy
distinct_id, every user in an org collapsed into one PostHog person — per-user analytics (DAU/WAU, funnels, retention) were impossible and person properties thrashed last-writer-winsdeploymenttagging replaces the brittleorgName === "mock_org"check for segmenting OSS vs SaaS and US vs EUdistinct_idbecause bare Django user pks collide across regions/self-hosts in the shared projectHow
frontend/src/hooks/usePostHogEvents.js:setPostHogIdentitybuildsdistinct_idfrom email, keeps existing person properties (name,org,software,email,userId), and callsposthog.register()with org super-propertiesfrontend/src/index.jsx: readsenablePosthogfromwindow.RUNTIME_CONFIGfirst (falls back to build-timeimport.meta.env.VITE_ENABLE_POSTHOG); hostname→deployment map registered right afterposthog.initfrontend/generate-runtime-config.sh: emitsenablePosthogintoruntime-config.jsfrom the container'sVITE_ENABLE_POSTHOG(orREACT_APP_ENABLE_POSTHOG) env at startupCan this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
respect_dntdrops events from DNT-enabled browsersDatabase Migrations
Env Config
VITE_ENABLE_POSTHOGis now honoured at container runtime viaruntime-config.js(previously build-time only)VITE_ENABLE_POSTHOG=falseon the staging frontend deployment so staging stops reporting to the shared PostHog projectRelevant Docs
Related Issues or PRs
*_failedevents, per-installinstance_idfor OSSDependencies Versions
Notes on Testing
bunx biome checkpasses on the touched filesorg_id/org_name/software/deploymentsuper-properties on captured eventsVITE_ENABLE_POSTHOG=false→/config/runtime-config.jscarriesenablePosthog: "false"and PostHog never initializesChecklist
I have read and understood the Contribution Guidelines.
🤖 Generated with Claude Code