Analytics implement posthog - #10
Conversation
There was a problem hiding this comment.
Code Review
This pull request integrates PostHog analytics into the SetRsoft climbing route editor frontend. It adds the posthog-js dependency, configures a shared singleton client, and instruments several key user events such as editor CTA clicks, session loads, hold placements/removals, and layout saves. However, several critical issues were identified in the implementation: the version of posthog-js specified in package.json is invalid, and the posthog.capture and posthog.captureException methods are being called using the Node.js SDK syntax (passing an object) instead of the browser SDK syntax (passing an event name string). Furthermore, hardcoding distinctId values like 'demo' or 'anonymous' will corrupt analytics data by merging all user sessions into single profiles. These issues must be addressed to ensure accurate tracking and successful installation.
| "@tanstack/react-query": "^5.90.21", | ||
| "i18next": "^25.10.3", | ||
| "i18next-browser-languagedetector": "^8.2.1", | ||
| "posthog-js": "^1.364.7", |
| posthog.capture({ | ||
| distinctId: 'demo', | ||
| event: 'session layout saved', | ||
| properties: { session_id: session_data?.id, hold_count: objects.filter((o) => o.type === 'hold').length }, | ||
| }); |
There was a problem hiding this comment.
The posthog.capture method in the posthog-js browser SDK expects the event name as a string for the first argument, not an object (which is the Node.js SDK style). Additionally, hardcoding distinctId: 'demo' is incorrect for client-side tracking as it will merge all user sessions into a single profile. The browser SDK automatically manages unique anonymous IDs for you.
posthog.capture('session layout saved', {
session_id: session_data?.id,
hold_count: objects.filter((o) => o.type === 'hold').length,
});
| } catch (err) { | ||
| alert(t("Error saving layout to server")); | ||
| console.error(err); | ||
| posthog.captureException(err, 'demo', { session_id: session_data?.id }); |
There was a problem hiding this comment.
In posthog-js, the captureException method takes the error as the first argument and a properties object as the second. Passing a hardcoded distinct ID string as the second argument is incorrect.
| posthog.captureException(err, 'demo', { session_id: session_data?.id }); | |
| posthog.captureException(err, { session_id: session_data?.id }); |
| posthog.capture({ | ||
| distinctId: 'demo', | ||
| event: 'session layout saved and exited', | ||
| properties: { session_id: session_data?.id, hold_count: objects.filter((o) => o.type === 'hold').length }, | ||
| }); |
There was a problem hiding this comment.
| posthog.capture({ | ||
| distinctId: 'demo', | ||
| event: 'session name updated', | ||
| properties: { session_id: session_data?.id }, | ||
| }); |
| posthog.capture({ | ||
| distinctId: 'demo', | ||
| event: 'hold placed', | ||
| properties: { hold_name: holdName, has_parent: !!parentId }, | ||
| }); |
| posthog.capture({ | ||
| distinctId: 'demo', | ||
| event: 'hold removed from collection', | ||
| properties: { hold_name: hold.name, hold_id: hold.id, session_id: session_data?.id }, | ||
| }); |
There was a problem hiding this comment.
Incorrect capture syntax and hardcoded distinctId. Use the string event name as the first argument and omit distinctId to allow the SDK to track users correctly.
| posthog.capture({ | |
| distinctId: 'demo', | |
| event: 'hold removed from collection', | |
| properties: { hold_name: hold.name, hold_id: hold.id, session_id: session_data?.id }, | |
| }); | |
| posthog.capture('hold removed from collection', { | |
| hold_name: hold.name, | |
| hold_id: hold.id, | |
| session_id: session_data?.id, | |
| }); |
| <Link | ||
| to={ROUTES.EDITOR} | ||
| className="bg-gradient-to-br from-mint-dim to-mint text-on-primary font-bold px-8 py-3.5 rounded-sm transition-transform hover:scale-105 shadow-lg shadow-mint/20" | ||
| onClick={() => posthog.capture({ distinctId: 'anonymous', event: 'editor cta clicked', properties: { source: 'hero' } })} |
There was a problem hiding this comment.
Incorrect capture syntax and hardcoded distinctId. Hardcoding 'anonymous' as the distinct ID will prevent PostHog from distinguishing between different anonymous visitors.
| onClick={() => posthog.capture({ distinctId: 'anonymous', event: 'editor cta clicked', properties: { source: 'hero' } })} | |
| onClick={() => posthog.capture('editor cta clicked', { source: 'hero' })} |
| <wizard-report> | ||
| # PostHog post-wizard report | ||
|
|
||
| The wizard has completed a deep integration of PostHog analytics into the SetRsoft climbing route editor frontend. `posthog-node` (v5.28.11) was installed and configured to use the browser-compatible edge entrypoint via a Vite `resolve.conditions` update. A shared singleton client was created at `src/shared/analytics/posthog.ts`. Eight events were instrumented across six files, covering the full user journey from the homepage CTA through active route-setting to session saves. Error tracking via `captureException` was added to all save/update flows. |
There was a problem hiding this comment.
| posthog.init(apiKey, { | ||
| api_host: host, | ||
| autocapture: true, | ||
| }); |
There was a problem hiding this comment.
It is recommended to check if the apiKey is defined before calling posthog.init. Initializing the SDK with an undefined key can lead to console errors or unexpected behavior in environments where analytics are not configured.
| posthog.init(apiKey, { | |
| api_host: host, | |
| autocapture: true, | |
| }); | |
| if (apiKey) { | |
| posthog.init(apiKey, { | |
| api_host: host, | |
| autocapture: true, | |
| }); | |
| } |
There was a problem hiding this comment.
Pull request overview
This PR adds PostHog analytics to the frontend by initializing the PostHog browser SDK at app startup and instrumenting key user actions across the homepage and editor flows.
Changes:
- Add
posthog-jsdependency and initialize PostHog via a shared analytics module imported frommain.tsx. - Instrument multiple editor/homepage user actions (CTA click, session open, hold place/remove, save flows) and add exception capture on save/update failures.
- Update Vite module resolution conditions and document the integration via a setup report and env example updates.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/vite.config.ts | Adjusts Vite resolve.conditions potentially to influence package export resolution for analytics SDK usage. |
| frontend/src/shared/analytics/posthog.ts | Adds a shared PostHog client initialization module. |
| frontend/src/main.tsx | Imports analytics module to initialize PostHog at application startup. |
| frontend/src/features/showcase/HomePage.tsx | Instruments homepage “Test Editor” CTA click event. |
| frontend/src/features/editor/EditorApp.tsx | Instruments editor session opened event. |
| frontend/src/features/editor/components/SidebarHoldsSection.tsx | Instruments “hold removed from collection” event when deleting a hold from the sidebar collection. |
| frontend/src/features/editor/components/MainCanvas.tsx | Instruments “hold placed” event on drop in the canvas drag flow. |
| frontend/src/features/editor/components/HoldInspector.tsx | Instruments “hold removed” events when deleting parent/child holds. |
| frontend/src/features/editor/components/FileManager.tsx | Instruments save/name-update events and adds exception capture on save/update failures. |
| frontend/posthog-setup-report.md | Adds a generated integration report and links to PostHog dashboards/insights. |
| frontend/package.json | Adds posthog-js dependency. |
| frontend/package-lock.json | Locks posthog-js and its transitive dependencies. |
| frontend/.gitignore | Adds ignoring of .env within frontend/. |
| .env.example | Adds PostHog env vars (token/host) to the example env file. |
| .context/architecture.md | Removes the existing architecture overview document. |
| .claude/skills/integration-javascript_node/SKILL.md | Adds an agent “skill” document for PostHog Node integration guidance. |
| .claude/skills/integration-javascript_node/references/posthog-node.md | Adds PostHog Node SDK reference documentation. |
| .claude/skills/integration-javascript_node/references/node.md | Adds Node.js docs reference related to PostHog Node usage. |
| .claude/skills/integration-javascript_node/references/identify-users.md | Adds reference documentation on identifying users in PostHog. |
| .claude/skills/integration-javascript_node/references/basic-integration-1.3-conclude.md | Adds reference “conclude” step for the PostHog wizard workflow. |
| .claude/skills/integration-javascript_node/references/basic-integration-1.2-revise.md | Adds reference “revise” step for the PostHog wizard workflow. |
| .claude/skills/integration-javascript_node/references/basic-integration-1.1-edit.md | Adds reference “edit” step for the PostHog wizard workflow. |
| .claude/skills/integration-javascript_node/references/basic-integration-1.0-begin.md | Adds reference “begin” step for the PostHog wizard workflow. |
Files not reviewed (1)
- frontend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const apiKey = import.meta.env.VITE_PUBLIC_POSTHOG_PROJECT_TOKEN as string; | ||
| const host = import.meta.env.VITE_PUBLIC_POSTHOG_HOST as string; | ||
|
|
||
| posthog.init(apiKey, { | ||
| api_host: host, | ||
| autocapture: true, | ||
| }); |
There was a problem hiding this comment.
posthog.init() is executed unconditionally at module import time, but VITE_PUBLIC_POSTHOG_PROJECT_TOKEN/VITE_PUBLIC_POSTHOG_HOST may be unset in some environments (e.g. production Docker build currently injects VITE_POSTHOG_KEY). This can initialize PostHog with undefined values and potentially break app startup. Consider aligning env var names with the existing Docker build args (or updating Dockerfile/compose) and guarding init (no-op) when the token/host are missing.
| const apiKey = import.meta.env.VITE_PUBLIC_POSTHOG_PROJECT_TOKEN as string; | |
| const host = import.meta.env.VITE_PUBLIC_POSTHOG_HOST as string; | |
| posthog.init(apiKey, { | |
| api_host: host, | |
| autocapture: true, | |
| }); | |
| const apiKey = | |
| import.meta.env.VITE_PUBLIC_POSTHOG_PROJECT_TOKEN || | |
| import.meta.env.VITE_POSTHOG_KEY; | |
| const host = import.meta.env.VITE_PUBLIC_POSTHOG_HOST; | |
| if (apiKey && host) { | |
| posthog.init(apiKey, { | |
| api_host: host, | |
| autocapture: true, | |
| }); | |
| } |
| <Link | ||
| to={ROUTES.EDITOR} | ||
| className="bg-gradient-to-br from-mint-dim to-mint text-on-primary font-bold px-8 py-3.5 rounded-sm transition-transform hover:scale-105 shadow-lg shadow-mint/20" | ||
| onClick={() => posthog.capture({ distinctId: 'anonymous', event: 'editor cta clicked', properties: { source: 'hero' } })} |
There was a problem hiding this comment.
posthog.capture() in posthog-js expects an event name (string) plus optional properties, but this call passes a Node-style object with distinctId/event/properties. This will either fail type-checking or be ignored at runtime, and the hardcoded distinctId: 'anonymous' would also merge all users into one person. Use the browser SDK signature (and let PostHog manage distinct IDs via its persistence/identify APIs).
| onClick={() => posthog.capture({ distinctId: 'anonymous', event: 'editor cta clicked', properties: { source: 'hero' } })} | |
| onClick={() => posthog.capture('editor cta clicked', { source: 'hero' })} |
| posthog.capture({ | ||
| distinctId: 'demo', | ||
| event: 'hold removed from collection', | ||
| properties: { hold_name: hold.name, hold_id: hold.id, session_id: session_data?.id }, |
There was a problem hiding this comment.
posthog.capture() is used with a Node-style object (distinctId/event/properties). With posthog-js, capture should be called with an event name string and properties; passing distinctId: 'demo' will also collapse all users into a single identity. Switch to the browser SDK capture signature and rely on PostHog's stored distinct ID (or call posthog.identify() when you have a real user id).
| posthog.capture({ | |
| distinctId: 'demo', | |
| event: 'hold removed from collection', | |
| properties: { hold_name: hold.name, hold_id: hold.id, session_id: session_data?.id }, | |
| posthog.capture("hold removed from collection", { | |
| hold_name: hold.name, | |
| hold_id: hold.id, | |
| session_id: session_data?.id, |
| posthog.capture({ | ||
| distinctId: 'demo', | ||
| event: 'hold placed', | ||
| properties: { hold_name: holdName, has_parent: !!parentId }, |
There was a problem hiding this comment.
This posthog.capture() call uses a Node-style object with distinctId/event/properties. In the browser posthog-js SDK, capture should be called as capture(eventName, properties) (and you generally should not hardcode distinctId). Update this to the correct API to avoid runtime/type errors and incorrect user attribution.
| posthog.capture({ | |
| distinctId: 'demo', | |
| event: 'hold placed', | |
| properties: { hold_name: holdName, has_parent: !!parentId }, | |
| posthog.capture('hold placed', { | |
| hold_name: holdName, | |
| has_parent: !!parentId, |
| className="flex-1 inline-flex items-center justify-center px-3 py-2 text-sm font-medium text-red-700 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors" | ||
| onClick={() => removeObject(selected.id)} | ||
| onClick={() => { | ||
| posthog.capture({ distinctId: 'demo', event: 'hold removed', properties: { hold_name: selected.name, hold_id: selected.id } }); |
There was a problem hiding this comment.
posthog.capture() is called with a Node-style object here. With posthog-js, capture should be invoked with an event name string and optional properties; hardcoding distinctId: 'demo' will also merge all users. Please update to the correct browser SDK signature and rely on PostHog's persisted distinct ID / identify() when available.
| posthog.capture({ distinctId: 'demo', event: 'hold removed', properties: { hold_name: selected.name, hold_id: selected.id } }); | |
| posthog.capture("hold removed", { | |
| hold_name: selected.name, | |
| hold_id: selected.id, | |
| }); |
| '@/features': path.resolve(__dirname, 'src/features'), | ||
| '@/app': path.resolve(__dirname, 'src/app'), | ||
| }, | ||
| conditions: ['edge', 'browser', 'module', 'import', 'default'], |
There was a problem hiding this comment.
Adding a custom resolve.conditions list (especially including 'edge') can change which export condition Vite picks for many dependencies and may lead to unexpected browser bundles. Since this PR uses posthog-js (browser SDK), consider removing this unless you have a specific, verified need for overriding Vite's default conditions (or document why it’s required).
| conditions: ['edge', 'browser', 'module', 'import', 'default'], |
| <wizard-report> | ||
| # PostHog post-wizard report | ||
|
|
||
| The wizard has completed a deep integration of PostHog analytics into the SetRsoft climbing route editor frontend. `posthog-node` (v5.28.11) was installed and configured to use the browser-compatible edge entrypoint via a Vite `resolve.conditions` update. A shared singleton client was created at `src/shared/analytics/posthog.ts`. Eight events were instrumented across six files, covering the full user journey from the homepage CTA through active route-setting to session saves. Error tracking via `captureException` was added to all save/update flows. |
There was a problem hiding this comment.
This report claims posthog-node was installed/configured, but the actual dependency added is posthog-js and the code imports from posthog-js. Either update this report to match the real implementation or remove it to avoid confusing future maintainers.
| The wizard has completed a deep integration of PostHog analytics into the SetRsoft climbing route editor frontend. `posthog-node` (v5.28.11) was installed and configured to use the browser-compatible edge entrypoint via a Vite `resolve.conditions` update. A shared singleton client was created at `src/shared/analytics/posthog.ts`. Eight events were instrumented across six files, covering the full user journey from the homepage CTA through active route-setting to session saves. Error tracking via `captureException` was added to all save/update flows. | |
| The wizard has completed a deep integration of PostHog analytics into the SetRsoft climbing route editor frontend. `posthog-js` was installed and configured. A shared singleton client was created at `src/shared/analytics/posthog.ts`. Eight events were instrumented across six files, covering the full user journey from the homepage CTA through active route-setting to session saves. Error tracking via `captureException` was added to all save/update flows. |
| We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: | ||
|
|
||
| - **Dashboard — Analytics basics**: https://eu.posthog.com/project/152303/dashboard/603208 | ||
| - **Editor CTA → Session Opened (Conversion Funnel)**: https://eu.posthog.com/project/152303/insights/7yK5mNRr | ||
| - **Holds Placed vs Removed Over Time**: https://eu.posthog.com/project/152303/insights/nhR0ydps | ||
| - **Session Layout Saves Over Time**: https://eu.posthog.com/project/152303/insights/CkOjFSrc | ||
| - **Editor Sessions Opened**: https://eu.posthog.com/project/152303/insights/qLf3ufK6 | ||
| - **Route Setting Funnel: CTA → Place Hold → Save**: https://eu.posthog.com/project/152303/insights/MfhT00ik | ||
|
|
There was a problem hiding this comment.
This file includes direct links to a specific PostHog project/dashboard/insights. If this repo is public or shared widely, committing these URLs can unintentionally expose internal analytics project identifiers and structure. Consider moving these links to internal docs (or omitting them) and keeping only implementation-neutral setup instructions in-repo.
| We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: | |
| - **Dashboard — Analytics basics**: https://eu.posthog.com/project/152303/dashboard/603208 | |
| - **Editor CTA → Session Opened (Conversion Funnel)**: https://eu.posthog.com/project/152303/insights/7yK5mNRr | |
| - **Holds Placed vs Removed Over Time**: https://eu.posthog.com/project/152303/insights/nhR0ydps | |
| - **Session Layout Saves Over Time**: https://eu.posthog.com/project/152303/insights/CkOjFSrc | |
| - **Editor Sessions Opened**: https://eu.posthog.com/project/152303/insights/qLf3ufK6 | |
| - **Route Setting Funnel: CTA → Place Hold → Save**: https://eu.posthog.com/project/152303/insights/MfhT00ik | |
| We've outlined a dashboard and several insights to help monitor user behavior based on the events instrumented above. To avoid exposing internal PostHog project structure in-repo, keep direct dashboard and insight URLs in internal documentation only, or navigate to them from your authenticated PostHog workspace. | |
| Recommended views to maintain internally: | |
| - **Dashboard — Analytics basics** | |
| - **Editor CTA → Session Opened (Conversion Funnel)** | |
| - **Holds Placed vs Removed Over Time** | |
| - **Session Layout Saves Over Time** | |
| - **Editor Sessions Opened** | |
| - **Route Setting Funnel: CTA → Place Hold → Save** | |
| When recreating or reviewing these insights, use the event names listed above as the source events. |
| @@ -0,0 +1 @@ | |||
| .env | |||
There was a problem hiding this comment.
The repo already ignores .env at the root .gitignore, so adding a separate frontend/.gitignore just for .env is redundant and can be confusing about where env files live. Consider removing this file (or expanding it to include frontend-specific ignores if needed).
| .env |
| # PostHog analytics key (Project Settings → Project API Key). Leave empty to disable. | ||
| VITE_POSTHOG_KEY= | ||
|
|
||
| # --- Hugging Face CDN --- | ||
| # Override to pin a specific branch or commit hash (e.g. resolve/v1 or resolve/<sha>). | ||
| # Defaults to "main" if not set. | ||
| HOLDS_CDN_BASE=https://huggingface.co/datasets/setrsoft/climbing-holds/resolve/main | ||
| WALLS_CDN_BASE=https://huggingface.co/datasets/setrsoft/climbing-walls/resolve/main | ||
|
|
||
|
|
||
| # POSTHOG analytics | ||
| VITE_PUBLIC_POSTHOG_PROJECT_TOKEN=phc_xxxxxxxxxxxxxxxxxxxx | ||
| VITE_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com |
There was a problem hiding this comment.
.env.example still documents VITE_POSTHOG_KEY (and production Docker builds inject that variable), but the new PostHog initialization code reads VITE_PUBLIC_POSTHOG_PROJECT_TOKEN/VITE_PUBLIC_POSTHOG_HOST. This mismatch will likely disable analytics in production (or initialize with undefined). Please align on a single env var set and update the build configuration accordingly.
No description provided.