Skip to content

feat(analytics): implement Google Analytics#1533

Open
graycyrus wants to merge 5 commits into
tinyhumansai:mainfrom
graycyrus:feat/google-analytics
Open

feat(analytics): implement Google Analytics#1533
graycyrus wants to merge 5 commits into
tinyhumansai:mainfrom
graycyrus:feat/google-analytics

Conversation

@graycyrus
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus commented May 12, 2026

Summary

  • Add GA4 via react-ga4 with triple-gate: IS_DEV, GA_MEASUREMENT_ID env var, and analyticsEnabled user consent toggle
  • Track route changes automatically via AppShell useLocation() effect
  • Fire privacy-safe events: app_open, onboarding lifecycle (onboarding_start, onboarding_step_complete, onboarding_complete), account_connect_start/success, chat_message_sent, skill_install/skill_uninstall
  • All events validated against an allowlist — no PII, message content, credentials, or sensitive data sent to GA
  • Update privacy disclosure text in PrivacyPanel and whatLeavesItems to accurately reflect analytics scope
  • Update capability catalog in about_app/catalog.rs
  • 11 new unit tests covering init, consent gating, pageview, events, and allowlist enforcement

Configuration

Set VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX in app/.env.local to enable. Leave blank or omit to disable. GA is always disabled in dev mode (IS_DEV=true).

Privacy

  • Consent gated by existing analytics toggle in Settings > Privacy & Security
  • Event names validated against a strict allowlist; unrecognized names are dropped with a warning
  • No user IDs, message content, transcripts, credentials, or PII ever sent
  • Ad personalization signals disabled unconditionally
  • Page views use sanitized pathname only (no query strings, no local file paths)

Test plan

  • pnpm typecheck — clean
  • pnpm lint — 0 errors (39 pre-existing warnings)
  • pnpm format:check — clean
  • pnpm build — success
  • pnpm test:unit — 2075 passed, 0 failed
  • cargo check (core + Tauri shell) — clean
  • Manual: set measurement ID, launch app, verify events in GA DebugView (post-merge)
  • Manual: toggle analytics off in Settings, verify events stop (post-merge)

Closes #1479

Summary by CodeRabbit

  • New Features

    • Integrated Google Analytics 4: anonymous page-view and allowlisted event tracking for key interactions (onboarding, skill install/uninstall, account connect, messages, app_open), gated by consent and env var; disabled in dev.
  • Documentation

    • Updated privacy text and capability description to clarify analytics and confirm no personal data is included.
  • Tests

    • Added GA4-focused tests covering init, consent gating, pageviews, events, and allowlist behavior.
  • Chores

    • Updated ignore rules to exclude sentry_bugs artifacts.

Review Change Stack

graycyrus added 3 commits May 12, 2026 15:27
…tracking

Add GA4 via react-ga4 with triple-gate (IS_DEV, GA_MEASUREMENT_ID, analyticsEnabled consent).
Track route changes via AppShell useLocation effect, plus events for app_open, onboarding
lifecycle, account connect, chat send, and skill install/uninstall. All events validated
against an allowlist — no PII, message content, or credentials leave the app.

Update privacy disclosure text and capability catalog to reflect the new analytics scope.

Closes tinyhumansai#1479
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7aa51672-d324-4307-b032-aacd327a21bf

📥 Commits

Reviewing files that changed from the base of the PR and between d98ec80 and b45549f.

📒 Files selected for processing (2)
  • app/src/services/__tests__/analytics.test.ts
  • app/src/services/analytics.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/services/tests/analytics.test.ts

📝 Walkthrough

Walkthrough

Adds GA4 integration (init, pageview, event APIs) guarded by env/dev/consent; wires init into app startup; emits page-view and allowlisted feature events across onboarding, accounts, conversations, and skill flows; updates config/deps, tests GA behavior, and updates privacy/docs.

Changes

Google Analytics 4 Integration with Privacy Gating

Layer / File(s) Summary
Analytics service core & tests
app/src/services/analytics.ts, app/src/services/__tests__/analytics.test.ts
Adds initGA(), trackPageView(path), trackEvent(name, params?), GA_ALLOWED_EVENTS; manages gaInitialized/gaEnabled; updates syncAnalyticsConsent to toggle GA consent; tests mock react-ga4 and config to validate gating and allowlisting.
Configuration & Startup Wiring
app/.env.example, app/package.json, app/src/main.tsx, app/src/utils/config.ts
Adds VITE_GA_MEASUREMENT_ID and exported GA_MEASUREMENT_ID, adds react-ga4 dependency, wires initGA() and app_open event before React render.
Route Page-View Tracking
app/src/App.tsx
AppShell calls trackPageView(location.pathname) on mount and when location.pathname changes to emit anonymous page-view hits.
Feature & Engagement Event Tracking
app/src/pages/onboarding/*, app/src/pages/Accounts.tsx, app/src/pages/Conversations.tsx, app/src/components/skills/*
Emits onboarding lifecycle events (onboarding_start, onboarding_step_complete, onboarding_complete), account_connect_start, service account_connect_success, chat_message_sent, and skill_install/skill_uninstall with identifiers.
Webview Account Connect Success
app/src/services/webviewAccountService.ts
Emits account_connect_success after account load/open with optional provider params in both reveal and non-reveal flows.
User-Facing Documentation
app/src/components/settings/panels/PrivacyPanel.tsx, app/src/features/privacy/whatLeavesItems.ts, src/openhuman/about_app/catalog.rs, .claude/memory.md, .gitignore
Privacy text and capability catalog updated to describe anonymous Sentry crash reports and GA usage analytics; memory notes document GA architecture; .gitignore adds sentry_bugs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • senamakel

Poem

🐰 I hopped in to track each new page,
consent kept safe, no secrets to gauge.
Events hop along—onboard, chat, and skill,
guarded and simple, anonymous still.
Enjoy the metrics, with privacy at the cage!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(analytics): implement Google Analytics' directly and clearly summarizes the main change—implementing GA4 integration as the primary objective of this PR.
Linked Issues check ✅ Passed The PR implements all key requirements from issue #1479: GA initialization with env/config gating [#1479], route tracking via AppShell [#1479], privacy-safe event taxonomy (app_open, onboarding, account, chat, skill events) [#1479], allowlist enforcement [#1479], consent gating [#1479], privacy disclosures updated [#1479], and unit test coverage [#1479].
Out of Scope Changes check ✅ Passed All changes are directly aligned with #1479 objectives: GA4 integration, event tracking, privacy controls, configuration, testing, and documentation updates. No extraneous refactoring or unrelated feature work detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@graycyrus
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
app/src/services/analytics.ts (1)

198-210: 💤 Low value

Consider removing the redundant ReactGA.set call in syncAnalyticsConsent.

Lines 206-208 call ReactGA.set({ allow_ad_personalization_signals: false }) every time consent is toggled, but this value is already set to false in initGA() (line 244) and should never change. Since the actual consent enforcement happens via the gaEnabled flag that gates trackPageView and trackEvent, this re-set is redundant.

♻️ Proposed simplification
 export function syncAnalyticsConsent(enabled: boolean): void {
   const client = Sentry.getClient();
   if (client && !enabled) {
     void Sentry.flush(2000);
   }

   // Update the GA consent shadow and toggle ad-personalization signals.
   gaEnabled = enabled;
   if (gaInitialized) {
-    ReactGA.set({ allow_ad_personalization_signals: false });
     console.debug(`[analytics] GA consent updated: enabled=${enabled}`);
   }
 }
🤖 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 `@app/src/services/analytics.ts` around lines 198 - 210, The
syncAnalyticsConsent function contains a redundant call to ReactGA.set({
allow_ad_personalization_signals: false }) which is already established in
initGA and never changes; remove that ReactGA.set call from
syncAnalyticsConsent, keep updating the gaEnabled shadow and the console.debug
line (adjust the debug message if desired) and rely on initGA to set
allow_ad_personalization_signals once; references: function
syncAnalyticsConsent, initGA, gaEnabled, gaInitialized, and ReactGA.set.
app/src/services/__tests__/analytics.test.ts (1)

409-417: ⚡ Quick win

Ensure console.warn spy is always restored

Line 410 creates a global spy, but restore on Line 416 won’t run if an assertion throws first. Wrap the body in try/finally to prevent cross-test leakage.

Suggested patch
   test('drops events not in the allowlist and logs a warning', async () => {
     const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
-    const { initGA, trackEvent } = await freshAnalytics();
-    initGA();
-    trackEvent('internal_debug_event');
-    expect(hoisted.gaEvent).not.toHaveBeenCalled();
-    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('internal_debug_event'));
-    warnSpy.mockRestore();
+    try {
+      const { initGA, trackEvent } = await freshAnalytics();
+      initGA();
+      trackEvent('internal_debug_event');
+      expect(hoisted.gaEvent).not.toHaveBeenCalled();
+      expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('internal_debug_event'));
+    } finally {
+      warnSpy.mockRestore();
+    }
   });
🤖 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 `@app/src/services/__tests__/analytics.test.ts` around lines 409 - 417, The
test creates a global console.warn spy (warnSpy) but only restores it after
assertions, risking leaks if an assertion throws; wrap the body of the test that
calls freshAnalytics(), initGA(), trackEvent('internal_debug_event'), and the
assertions in a try/finally so warnSpy.mockRestore() is always executed in the
finally block to guarantee restoration even on failure.
🤖 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 `@app/src/main.tsx`:
- Around line 54-57: The trackEvent('app_open', { version: APP_VERSION }) call
is firing for overlay/mascot windows as well; wrap it so it only runs for the
main/non-standalone window. Add or call a single predicate (e.g. isMainWindow(),
isMainAppWindow(), or check window.type/window.name/IPC-provided flag) before
invoking trackEvent in main.tsx, leaving initSentry() and initGA()
unchanged—only call trackEvent when that predicate returns true so
overlays/mascots do not emit app_open.

In `@app/src/pages/onboarding/pages/ContextPage.tsx`:
- Around line 16-19: The onNext handler currently calls completeAndExit()
without handling rejection; update the onNext callback that wraps trackEvent and
completeAndExit to await or chain a .catch on completeAndExit (the function name
completeAndExit in this component) and handle errors explicitly—e.g., log the
error via the existing logger or trackEvent an error event and surface user
feedback (toast/modal) as appropriate—so any failure in completeAndExit is not
an unhandled rejection and the failure path is observable.

In `@app/src/services/webviewAccountService.ts`:
- Around line 312-329: The code currently fires
trackEvent('account_connect_success', connectSuccessParams) unconditionally,
which counts warm re-opens (payload.state === 'reused') as new connects; update
the logic in the webview reveal path and the else branch so that trackEvent is
only called when payload.state !== 'reused' (i.e., a real new connect).
Specifically, inside the invoke(...).finally() and the fallback else, check
payload.state and only call trackEvent('account_connect_success',
connectSuccessParams) when payload.state !== 'reused' while still always
dispatching setAccountStatus({ accountId, status: 'open' }) as before.

---

Nitpick comments:
In `@app/src/services/__tests__/analytics.test.ts`:
- Around line 409-417: The test creates a global console.warn spy (warnSpy) but
only restores it after assertions, risking leaks if an assertion throws; wrap
the body of the test that calls freshAnalytics(), initGA(),
trackEvent('internal_debug_event'), and the assertions in a try/finally so
warnSpy.mockRestore() is always executed in the finally block to guarantee
restoration even on failure.

In `@app/src/services/analytics.ts`:
- Around line 198-210: The syncAnalyticsConsent function contains a redundant
call to ReactGA.set({ allow_ad_personalization_signals: false }) which is
already established in initGA and never changes; remove that ReactGA.set call
from syncAnalyticsConsent, keep updating the gaEnabled shadow and the
console.debug line (adjust the debug message if desired) and rely on initGA to
set allow_ad_personalization_signals once; references: function
syncAnalyticsConsent, initGA, gaEnabled, gaInitialized, and ReactGA.set.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b180a648-aa89-4a26-89df-5fd57aca37b7

📥 Commits

Reviewing files that changed from the base of the PR and between 78d1f3d and 7c359df.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • .claude/memory.md
  • .gitignore
  • app/.env.example
  • app/package.json
  • app/src/App.tsx
  • app/src/components/settings/panels/PrivacyPanel.tsx
  • app/src/components/skills/InstallSkillDialog.tsx
  • app/src/components/skills/UninstallSkillConfirmDialog.tsx
  • app/src/features/privacy/whatLeavesItems.ts
  • app/src/main.tsx
  • app/src/pages/Accounts.tsx
  • app/src/pages/Conversations.tsx
  • app/src/pages/onboarding/OnboardingLayout.tsx
  • app/src/pages/onboarding/pages/ContextPage.tsx
  • app/src/pages/onboarding/pages/SkillsPage.tsx
  • app/src/pages/onboarding/pages/WelcomePage.tsx
  • app/src/services/__tests__/analytics.test.ts
  • app/src/services/analytics.ts
  • app/src/services/webviewAccountService.ts
  • app/src/utils/config.ts
  • src/openhuman/about_app/catalog.rs

Comment thread app/src/main.tsx Outdated
Comment thread app/src/pages/onboarding/pages/ContextPage.tsx
Comment thread app/src/services/webviewAccountService.ts
…ection, skip reused accounts

- Gate app_open event to main window only (skip overlay/mascot windows)
- Add .catch() to completeAndExit() in ContextPage to prevent unhandled rejection
- Skip account_connect_success for reused/warm-reopened accounts
@graycyrus graycyrus marked this pull request as ready for review May 12, 2026 13:11
@graycyrus graycyrus requested a review from a team May 12, 2026 13:11
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 12, 2026
Copy link
Copy Markdown
Contributor Author

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(analytics): implement Google Analytics

Clean, well-structured implementation. Privacy model is solid — triple-gated init, allowlisted events, no PII, ad personalization off. Tests are thorough. CodeRabbit feedback addressed. See inline comments below.

Recommendation: Approve (pending CI green on coverage).

Comment thread app/src/main.tsx
Comment thread app/src/services/analytics.ts
Comment thread app/src/services/webviewAccountService.ts
…sent

allow_ad_personalization_signals is already set unconditionally in initGA() —
no need to re-set it on every consent toggle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Google Analytics

1 participant