feat(analytics): migrate from Umami to PostHog for SaaS product analytics - #203
Conversation
…tics - Replace Umami with PostHog SDK (posthog-js, @posthog/react) - Add PostHog reverse proxy via e.bayanflow.com (ad-blocker resilient) - Instrument 20+ custom events across VisualizerApp, UserMenu, SignInPromptModal, useFavorites, useNoteAutosave - Implement user identification on sign-in/sign-out via AuthProvider - Add first-party session replay (text/media masked, inputs masked) - Respect Do Not Track header - Add feature flag architecture for future use - Update CSP headers for PostHog domains - Update privacy policy, SECURITY.md to reference PostHog - Add VITE_POSTHOG_API_KEY/VITE_POSTHOG_API_HOST to CI workflows
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThe PR replaces Umami with PostHog analytics, adds event and feature-flag helpers, instruments application interactions, proxies PostHog traffic through Cloudflare Workers, updates build configuration and CSP validation, and revises privacy and security disclosures. ChangesPostHog analytics integration
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Preview for Bayan Flow Staging ready!
Preview alias |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
src/contexts/AuthProvider.jsx (1)
182-194: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winSync user properties more reliably by identifying outside of
SIGNED_IN.Currently,
identifyUseris only called when theSIGNED_INevent fires. This has two drawbacks:
- When a returning user opens the app, Supabase fires an
INITIAL_SESSIONevent instead. If their local storage was cleared or they are using a cross-domain/embedded browser setup, their identity won't be consistently synced to PostHog for that session.- The user's
planis hardcoded tonullhere because the profile row hasn't been fetched yet. If a user upgrades their plan, PostHog won't see the new plan until they explicitly sign out and back in.Consider additionally calling
identifyUserinsideevaluateAccessoncerefreshProfile(activeUser)resolves. This ensures the user is accurately identified on every session load and immediately reflects database-backed billing plan updates.🤖 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 `@src/contexts/AuthProvider.jsx` around lines 182 - 194, Update evaluateAccess to call identifyUser after refreshProfile(activeUser) resolves, using the refreshed profile data and current user details so identity and billing plan properties are synchronized on every session load. Keep the existing SIGNED_IN identification flow intact unless needed to avoid duplication, and ensure the plan comes from the fetched profile rather than the hardcoded null.src/services/analytics.js (2)
29-36: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valuePrevent event queueing when analytics are bypassed in development.
When
initPostHog()returns early without callingposthog.init(), any subsequent calls to tracking functions will cause the PostHog SDK to continuously queue events in memory.To prevent this internal queue from growing indefinitely during long local development sessions, consider either overriding the exported SDK wrapper functions with no-ops when bypassed, or call
posthog.init()but disable capturing usingopt_out_capturing_by_default: true.🤖 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 `@src/services/analytics.js` around lines 29 - 36, Update initPostHog’s local-development bypass so subsequent exported tracking calls cannot queue events in the PostHog SDK. Either replace the exported SDK wrapper functions with no-ops when the localhost/127.0.0.1 condition matches, or initialize PostHog with opt_out_capturing_by_default enabled while preserving the bypassed-capture behavior.
29-36: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valuePrevent event queueing when analytics are bypassed in development.
When
initPostHog()returns early without callingposthog.init(), any subsequent calls to tracking functions will cause the PostHog SDK to continuously queue events in memory.To prevent this internal queue from growing indefinitely during long local development sessions, consider either overriding the exported SDK wrapper functions with no-ops when bypassed, or call
posthog.init()but disable capturing usingopt_out_capturing_by_default: true.🤖 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 `@src/services/analytics.js` around lines 29 - 36, Update initPostHog’s local-development bypass so subsequent exported tracking calls cannot queue events: either replace the SDK wrapper functions with no-ops when returning early, or initialize PostHog with capturing disabled by default via opt_out_capturing_by_default. Preserve analytics behavior outside localhost and 127.0.0.1 development environments..env.example (1)
10-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMaintain alphabetical ordering of environment variables.
As suggested by the
dotenv-linter,VITE_POSTHOG_API_HOSTshould appear beforeVITE_POSTHOG_API_KEYto maintain alphabetical order.🧹 Proposed fix
# PostHog analytics (optional — skip in local dev) +VITE_POSTHOG_API_HOST=https://us.i.posthog.com VITE_POSTHOG_API_KEY=phc_your_project_token -VITE_POSTHOG_API_HOST=https://us.i.posthog.com # For production with reverse proxy: VITE_POSTHOG_API_HOST=https://e.bayanflow.com🤖 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 @.env.example around lines 10 - 14, Reorder the PostHog environment variables in .env.example so VITE_POSTHOG_API_HOST appears before VITE_POSTHOG_API_KEY, preserving their values and comments.Source: Linters/SAST tools
🤖 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.
Inline comments:
In @.github/workflows/deploy-cloudflare.yml:
- Around line 54-55: Update the VITE_POSTHOG_API_HOST value in the deployment
workflow to select the proxy host based on the deployed branch: use the
production host for main and the staging host e.dev.bayanflow.com for develop.
Keep VITE_POSTHOG_API_KEY unchanged.
In `@public/_headers`:
- Line 8: Add https://e.bayanflow.com to connect-src in public/_headers while
preserving the existing Supabase and other resource sources. Update the
analytics CSP assertion in scripts/cspHeaders.js, including its relevant
validation symbols, to require the same proxy origin alongside the existing
PostHog origin checks; both sites must remain aligned.
In `@scripts/cspHeaders.js`:
- Around line 92-100: Update the script-src and connect-src validation in the
CSP checking logic to split each directive value on whitespace and require an
exact https://*.posthog.com token match, rather than using substring includes
checks. Preserve the existing error behavior and messages for directives missing
the exact token.
In `@worker/index.js`:
- Around line 49-52: Update the hostname condition in the request routing logic
around handlePostHogProxy to accept both the production and staging PostHog
proxy domains configured in wrangler.jsonc, e.bayanflow.com and
e.dev.bayanflow.com, while preserving the existing proxy handling for either
hostname.
- Around line 36-40: Restrict the static-asset cache guards to GET requests by
adding request.method === 'GET' alongside the existing host and status checks in
worker/index.js (lines 36-40) and worker/posthog-proxy.js (lines 69-74).
---
Nitpick comments:
In @.env.example:
- Around line 10-14: Reorder the PostHog environment variables in .env.example
so VITE_POSTHOG_API_HOST appears before VITE_POSTHOG_API_KEY, preserving their
values and comments.
In `@src/contexts/AuthProvider.jsx`:
- Around line 182-194: Update evaluateAccess to call identifyUser after
refreshProfile(activeUser) resolves, using the refreshed profile data and
current user details so identity and billing plan properties are synchronized on
every session load. Keep the existing SIGNED_IN identification flow intact
unless needed to avoid duplication, and ensure the plan comes from the fetched
profile rather than the hardcoded null.
In `@src/services/analytics.js`:
- Around line 29-36: Update initPostHog’s local-development bypass so subsequent
exported tracking calls cannot queue events in the PostHog SDK. Either replace
the exported SDK wrapper functions with no-ops when the localhost/127.0.0.1
condition matches, or initialize PostHog with opt_out_capturing_by_default
enabled while preserving the bypassed-capture behavior.
- Around line 29-36: Update initPostHog’s local-development bypass so subsequent
exported tracking calls cannot queue events: either replace the SDK wrapper
functions with no-ops when returning early, or initialize PostHog with capturing
disabled by default via opt_out_capturing_by_default. Preserve analytics
behavior outside localhost and 127.0.0.1 development environments.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 19bd8f06-d805-44cc-9212-9db48291f988
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (27)
.env.example.github/workflows/ci.yml.github/workflows/deploy-cloudflare.yml.github/workflows/preview-cloudflare.ymlSECURITY.mdindex.htmlpackage.jsonpublic/_headersscripts/cspHeaders.jssrc/components/SignInPromptModal.jsxsrc/components/UserMenu.jsxsrc/content/legal/privacy.en.jssrc/content/legal/privacy.en.test.jssrc/contexts/AuthProvider.jsxsrc/hooks/useFavorites.jssrc/hooks/useNoteAutosave.jssrc/main.jsxsrc/pages/PrivacyPolicy.test.jsxsrc/pages/VisualizerApp.jsxsrc/providers/PostHogProvider.jsxsrc/security/cspHeaders.test.jssrc/services/analytics.jssrc/services/analyticsEvents.jssrc/services/featureFlags.jsworker/index.jsworker/posthog-proxy.jswrangler.jsonc
💤 Files with no reviewable changes (1)
- index.html
| Permissions-Policy: camera=(), microphone=(), geolocation=(), identity-credentials-get=(self "https://accounts.google.com") | ||
| # Pyodide CDN origins: jsDelivr below; custom VITE_PYODIDE_CDN_BASE origins appended at build (vite.config.js) | ||
| Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://cloud.umami.is https://static.cloudflareinsights.com https://cdn.jsdelivr.net https://accounts.google.com; connect-src 'self' blob: https://cloud.umami.is https://gateway.umami.is https://cloudflareinsights.com https://api.github.com https://cdn.jsdelivr.net https://www.remotion.pro https://qketsapzqpzmccljfjcm.supabase.co https://accounts.google.com https://oauth2.googleapis.com; img-src 'self' data: blob: https://api.producthunt.com https://lh3.googleusercontent.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src https://www.youtube-nocookie.com https://accounts.google.com; object-src 'none'; base-uri 'self'; form-action 'self' | ||
| Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://*.posthog.com https://static.cloudflareinsights.com https://cdn.jsdelivr.net https://accounts.google.com; connect-src 'self' blob: https://*.posthog.com https://cloudflareinsights.com https://api.github.com https://cdn.jsdelivr.net https://www.remotion.pro https://qketsapzqpzmccljfjcm.supabase.co https://accounts.google.com https://oauth2.googleapis.com; img-src 'self' data: blob: https://api.producthunt.com https://lh3.googleusercontent.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' data:; worker-src 'self' blob:; media-src 'self' blob:; frame-src https://www.youtube-nocookie.com https://accounts.google.com; object-src 'none'; base-uri 'self'; form-action 'self' |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Keep the runtime CSP and build-time assertion aligned for the first-party proxy.
PostHog traffic is routed through https://e.bayanflow.com, but the runtime header omits that origin and the assertion only validates *.posthog.com. This blocks proxy requests in browsers while allowing the CSP test to pass.
public/_headers#L8-L8: addhttps://e.bayanflow.comtoconnect-src.scripts/cspHeaders.js#L89-L106: require the same proxy origin in the analytics CSP assertion.
As per coding guidelines: “Preserve the CSP requirements for Supabase, Google profile images, and Google Identity Services resources; CSP is asserted by scripts/cspHeaders.js at build time.”
📍 Affects 2 files
public/_headers#L8-L8(this comment)scripts/cspHeaders.js#L89-L106
🤖 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 `@public/_headers` at line 8, Add https://e.bayanflow.com to connect-src in
public/_headers while preserving the existing Supabase and other resource
sources. Update the analytics CSP assertion in scripts/cspHeaders.js, including
its relevant validation symbols, to require the same proxy origin alongside the
existing PostHog origin checks; both sites must remain aligned.
Source: Coding guidelines
| // Cache static assets | ||
| if (targetHost === POSTHOG_ASSETS_HOST && response.status === 200) { | ||
| const cache = caches.default; | ||
| ctx.waitUntil(cache.put(request, response.clone())); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does Cloudflare Workers caches.default.put throw an error if the request method is not GET?
💡 Result:
Yes, Cloudflare Workers' caches.default.put method will throw an error if the request provided as the cache key has a method other than GET [1][2][3]. According to the official Cloudflare Workers documentation, the cache.put method explicitly throws an error if the request passed to it is a method other than GET [1][2][3]. This is a documented constraint of the Cache API within the Cloudflare Workers runtime [1][4]. If you need to cache a request that is not a GET request, or if you want to force a request to be treated as a GET request for caching purposes, you can use the ignoreMethod option in the cache.match method [3], but for cache.put, the method constraint remains strict [1][2]. Developers typically handle this by ensuring only GET requests are passed to cache.put [5][6].
Citations:
- 1: https://developers.cloudflare.com/workers/runtime-apis/cache/index.md
- 2: https://developers.cloudflare.com/workers/runtime-apis/cache/
- 3: https://010dd5f9.preview.developers.cloudflare.com/workers/runtime-apis/cache/
- 4: https://community.cloudflare.com/t/whats-the-difference-between-cache-put-url-response-and-cache-put-request-response/254248
- 5: https://macarthur.me/posts/transform-images/
- 6: https://medium.com/@rameshkannanyt0078/%EF%B8%8F-i-designed-a-multi-region-fastapi-architecture-my-latency-went-from-800ms-to-12ms-29e36b99d269
Only cache GET requests. caches.default.put() throws when the request method is not GET, so HEAD/OPTIONS traffic can fail here. Add request.method === 'GET' to both cache guards in worker/index.js and worker/posthog-proxy.js.
📍 Affects 2 files
worker/index.js#L36-L40(this comment)worker/posthog-proxy.js#L69-L74
🤖 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 `@worker/index.js` around lines 36 - 40, Restrict the static-asset cache guards
to GET requests by adding request.method === 'GET' alongside the existing host
and status checks in worker/index.js (lines 36-40) and worker/posthog-proxy.js
(lines 69-74).
…cache guards, dev bypass - deploy-cloudflare.yml: use branch-conditional proxy host (e.bayanflow.com for main, e.dev.bayanflow.com for develop) - public/_headers: add e.bayanflow.com and e.dev.bayanflow.com to connect-src - scripts/cspHeaders.js: exact token matching instead of substring includes; add proxy origin check - worker/index.js: accept both staging and production proxy domains; restrict static-asset cache to GET - worker/posthog-proxy.js: restrict static-asset cache to GET - src/services/analytics.js: set isBypassed flag to prevent SDK event queueing in dev - src/contexts/AuthProvider.jsx: identifyUser after profile fetch with accurate plan - .env.example: alphabetical ordering of PostHog env vars
Contribution workflow
develop: This PR targetsdevelop, notmain.Description
Replace Umami with PostHog to enable product analytics, session replay, feature flags, and user identification for the upcoming Pro plan.
Type of Change
Related Issues
Changes Made
posthog-jsand@posthog/reactpackagessrc/services/analytics.js— PostHog init, identify, reset, captureEvent with Do Not Track supportsrc/services/analyticsEvents.js— 20 event constants and 16 capture helper functions (algorithm_viewed, speed_changed, category_changed, sign_in_clicked, favorite_added, etc.)src/providers/PostHogProvider.jsx— React provider wrappersrc/services/featureFlags.js— Feature flag architecture for future useworker/posthog-proxy.js— Cloudflare Worker proxy viae.bayanflow.comfor ad-blocker resilienceworker/index.js(routed viae.bayanflow.comhostname)wrangler.jsoncwith proxy routes for staging (e.dev.bayanflow.com) and production (e.bayanflow.com)index.htmlpublic/_headersCSP — replacecloud.umami.is/gateway.umami.iswith*.posthog.comassertAnalyticsCspDirectives()toscripts/cspHeaders.jsand CSP test.env.examplewith PostHog env varsVITE_POSTHOG_API_KEY/VITE_POSTHOG_API_HOSTto all 3 CI workflowsVisualizerApp.jsx— category, speed, sound, fullscreen, algorithm, steps, export, panels, completiontrackSignInClickedtoSignInPromptModal.jsx(modal source) andUserMenu.jsx(navbar source)trackFavoriteAdded/trackFavoriteRemovedtouseFavorites.jstrackNoteSavedtouseNoteAutosave.jsidentifyUser/'resetUser'/'trackSignInCompleted' toAuthProvider.jsxon auth state changesprivacy.en.js, tests) — replace Umami with PostHogSECURITY.md— reference PostHog analyticsmain.jsxprovider hierarchy (Theme → Auth → PostHog → Router)Testing
pnpm test:run) — 1834 passed, 7 pre-existing worker test failurespnpm lint) — 0 errors, 3 pre-existing warningspnpm build) — successfulTest Results
Code Quality
Performance Impact
Breaking Changes
Additional Notes
person_profiles: 'identified_only'— anonymous users stay anonymouse.bayanflow.comis integrated into the existing Cloudflare WorkerSummary by CodeRabbit
New Features
Documentation
Chores