Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ Quick reference for anyone starting with Claude on this project. Updated by the
- **FrameProvider loops — sleep animation resets** — `FrameProvider` uses `frame % durationInFrames` so animations loop. Default `DURATION_FRAMES = FPS * 6` (6s). Sleep animation completes at 4s, then eyes re-open at 6s when frame resets to 0. Fix: use a much longer `durationInFrames` for sleep face (e.g. `FPS * 600`) so the loop never triggers while sleeping.
- **Hover detection needs circular hitbox** — The mascot panel is 79x79 but the character is visually circular. Using the full AABB (`cursor_in_panel`) for hover triggers false positives when cursor is in a panel corner. Use distance-from-center check instead. Also suppress hover events for ~1s after panel shows to let the webview load.

## Google Analytics (Issue #1479)

- **`react-ga4` injects a `<script>` tag at runtime** — It appends a `gtag.js` `<script>` to `<head>` dynamically. This works because `tauri.conf.json` CSP has `https:` in `default-src` and `connect-src`. If CEF ever tightens `script-src` separately, switch to GA4 Measurement Protocol (pure HTTP POST, no script injection).
- **Analytics module pattern** — `app/src/services/analytics.ts` is the single owner of `initGA`, `trackPageView`, `trackEvent`, plus an `ALLOWED_EVENTS` allowlist. Never call `ReactGA` directly from components; go through this module.
- **Triple gate before any GA call** — `isAnalyticsEnabled()` (user consent) AND `GA_MEASUREMENT_ID` env var present AND `!IS_DEV`. All three must pass or tracking is silently skipped.
- **Route tracking location** — `useLocation()` effect wired in AppShell (not individual pages). All page views emit from one place.
- **Capability catalog must stay in sync** — `src/openhuman/about_app/catalog.rs` needs an entry when a new user-visible feature ships. GA was added there as part of issue #1479.

## PR Checklist CI

- **N/A items need a checked checkbox** — `scripts/check-pr-checklist.mjs` requires `- [x] N/A: <reason>`. Using `- [ ] N/A:` (unchecked) fails the check even though the text starts with "N/A:".
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
workflow
create_issue
review_pr
sentry_bugs

# Diagnostic harness output (scripts/diagnose-cef-runtime.mjs)
diagnosis-*.json
Expand Down
5 changes: 5 additions & 0 deletions app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ VITE_TELEGRAM_BOT_USERNAME=openhuman_bot
# [optional] Skills GitHub repository slug (default: tinyhumansai/openhuman-skills)
VITE_SKILLS_GITHUB_REPO=tinyhumansai/openhuman-skills

# [optional] Google Analytics 4 Measurement ID (e.g. G-XXXXXXXXXX).
# Leave blank to disable GA entirely. Analytics is also skipped in dev builds.
# Only anonymous page views and feature-engagement events are sent — no PII.
VITE_GA_MEASUREMENT_ID=

# [optional] Sentry DSN for error reporting (leave blank to disable)
VITE_SENTRY_DSN=

Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"process": "^0.11.10",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-ga4": "^3.0.1",
"react-icons": "^5.6.0",
"react-joyride": "^3.1.0",
"react-markdown": "^10.1.0",
Expand Down
6 changes: 6 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { startWebviewNotificationsService } from './lib/webviewNotifications';
import ChatRuntimeProvider from './providers/ChatRuntimeProvider';
import CoreStateProvider, { useCoreState } from './providers/CoreStateProvider';
import SocketProvider from './providers/SocketProvider';
import { trackPageView } from './services/analytics';
import { startWebviewAccountService } from './services/webviewAccountService';
import { persistor, store } from './store';
// [#1123] useAppDispatch commented out — welcome-agent onboarding replaced by Joyride walkthrough
Expand Down Expand Up @@ -116,6 +117,11 @@ function AppShell() {
navigate,
]);

// Track route changes as anonymous page views.
useEffect(() => {
trackPageView(location.pathname);
}, [location.pathname]);

// [#1123] Commented out — welcome-agent onboarding replaced by Joyride walkthrough
// After the welcome agent calls `complete_onboarding` and
// `chat_onboarding_completed` flips false→true, discard the transient
Expand Down
7 changes: 4 additions & 3 deletions app/src/components/settings/panels/PrivacyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,10 @@ const PrivacyPanel = () => {
</svg>
<div>
<p className="text-xs text-stone-500 leading-relaxed">
All analytics and bug reports are fully anonymized. When enabled, we collect only
crash information, device type, and the file location of errors. We never access
your messages, session data, wallet keys, API keys, or any personally identifiable
All analytics and bug reports are fully anonymized. When enabled, we collect crash
information and device type (via Sentry), plus anonymous usage analytics such as
page views and feature engagement (via Google Analytics). We never access your
messages, session data, wallet keys, API keys, or any personally identifiable
information. You can change this setting at any time.
</p>
</div>
Expand Down
4 changes: 4 additions & 0 deletions app/src/components/skills/InstallSkillDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
type InstallSkillFromUrlResult,
type SkillSummary,
} from '../../services/api/skillsApi';
import { trackEvent } from '../../services/analytics';

const log = debug('skills:install-dialog');

Expand Down Expand Up @@ -202,6 +203,9 @@ export default function InstallSkillDialog({ onClose, onInstalled }: Props) {
installed.stdout.length,
installed.stderr.length
);
for (const skillId of installed.newSkills) {
trackEvent('skill_install', { skill_id: skillId });
}
setResult(installed);
onInstalled(installed);
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/components/skills/UninstallSkillConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type SkillSummary,
type UninstallSkillResult,
} from '../../services/api/skillsApi';
import { trackEvent } from '../../services/analytics';

const log = debug('skills:uninstall-dialog');

Expand Down Expand Up @@ -81,6 +82,7 @@ export default function UninstallSkillConfirmDialog({ skill, onClose, onUninstal
// slug — the backend resolves by slug, so pass `id`.
const result = await skillsApi.uninstallSkill(skill.id);
log('confirm: done removedPath=%s', result.removedPath);
trackEvent('skill_uninstall', { skill_id: skill.id });
onUninstalled(result);
onClose();
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/privacy/whatLeavesItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const WHAT_LEAVES_ITEMS: PrivacyLeaveItem[] = [
{
id: 'sentry',
title: 'Crash Reports & Usage Data (opt-out)',
body: 'Anonymous crash reports help us fix bugs. Usage data helps us improve the product. Toggle anytime in Settings → Privacy & Security.',
body: 'Anonymous crash reports (via Sentry) and anonymous usage analytics — page views and feature engagement (via Google Analytics) — help us fix bugs and improve the product. No personal data, messages, or credentials are ever included. Toggle anytime in Settings → Privacy & Security.',
},
];

Expand Down
7 changes: 5 additions & 2 deletions app/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { getCoreStateSnapshot } from './lib/coreState/store';
import MascotWindowApp from './mascot/MascotWindowApp';
import OverlayApp from './overlay/OverlayApp';
import './polyfills';
import { initSentry } from './services/analytics';
import { initGA, initSentry, trackEvent } from './services/analytics';
import { setStoreForApiClient } from './services/apiClient';
import { primeActiveUserId } from './store/userScopedStorage';
import { APP_VERSION } from './utils/config';
import { setupDesktopDeepLinkListener } from './utils/desktopDeepLinkListener';
import { getActiveUserIdFromCore } from './utils/tauriCommands';

Expand Down Expand Up @@ -50,8 +51,10 @@ const ensureDefaultHashRoute = () => {
}
};

// Initialize Sentry early (before React renders)
// Initialize Sentry and GA early (before React renders)
initSentry();
initGA();
trackEvent('app_open', { version: APP_VERSION });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
document.documentElement.dataset.window = currentWindowLabel;

if (!isStandaloneWindow) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/pages/Accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import WebviewHost from '../components/accounts/WebviewHost';
// [#1123] Commented out — welcome-agent onboarding replaced by Joyride walkthrough
// import { useCoreState } from '../providers/CoreStateProvider';
import { usePrewarmMostRecentAccount } from '../hooks/usePrewarmMostRecentAccount';
import { trackEvent } from '../services/analytics';
import {
hideWebviewAccount,
purgeWebviewAccount,
Expand Down Expand Up @@ -181,6 +182,7 @@ const Accounts = () => {

const handlePickProvider = (p: ProviderDescriptor) => {
setAddOpen(false);
trackEvent('account_connect_start', { provider: p.id });
const id = makeAccountId();
const acct: Account = {
id,
Expand Down
2 changes: 2 additions & 0 deletions app/src/pages/Conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import MicCloudComposer from '../features/human/MicCloudComposer';
// import { ONBOARDING_WELCOME_THREAD_LABEL } from '../constants/onboardingChat';
import { useStickToBottom } from '../hooks/useStickToBottom';
import { useUsageState } from '../hooks/useUsageState';
import { trackEvent } from '../services/analytics';
// [#1123] getCoreStateSnapshot and isWelcomeLocked commented out — welcome-agent onboarding replaced by Joyride walkthrough
// import { getCoreStateSnapshot, isWelcomeLocked } from '../lib/coreState/store';
// [#1123] Commented out — welcome-agent onboarding replaced by Joyride walkthrough
Expand Down Expand Up @@ -603,6 +604,7 @@ const Conversations = ({ variant = 'page', composer = 'text' }: ConversationsPro
// (auto-react, autocomplete, etc.) — never as a primary chat path.
try {
await chatSend({ threadId: sendingThreadId, message: trimmed, model: CHAT_MODEL_ID });
trackEvent('chat_message_sent');

// Active-thread reset happens in the global ChatRuntimeProvider events.
} catch (err) {
Expand Down
4 changes: 4 additions & 0 deletions app/src/pages/onboarding/OnboardingLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { setWalkthroughPending } from '../../components/walkthrough/AppWalkthrou
// [#1123] Commented out — welcome-agent onboarding replaced by Joyride walkthrough
// import { ONBOARDING_WELCOME_THREAD_LABEL } from '../../constants/onboardingChat';
import { useCoreState } from '../../providers/CoreStateProvider';
import { trackEvent } from '../../services/analytics';
import { userApi } from '../../services/api/userApi';
import { getDefaultEnabledTools } from '../../utils/toolDefinitions';
import BetaBanner from './components/BetaBanner';
Expand Down Expand Up @@ -129,6 +130,9 @@ const OnboardingLayout = () => {
// }
// }

// Fire onboarding_complete analytics event before navigation.
trackEvent('onboarding_complete');

// Flag the Joyride walkthrough as pending so it auto-starts on /home.
// Best-effort: localStorage failures must not block navigation.
try {
Expand Down
6 changes: 5 additions & 1 deletion app/src/pages/onboarding/pages/ContextPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useNavigate } from 'react-router-dom';

import { trackEvent } from '../../../services/analytics';
import { useOnboardingContext } from '../OnboardingContext';
import ContextGatheringStep from '../steps/ContextGatheringStep';

Expand All @@ -12,7 +13,10 @@ const ContextPage = () => {
connectedSources={draft.connectedSources}
// Chat-provider step is disabled for now, so context-gathering is
// the final step when it runs — finish onboarding directly.
onNext={() => completeAndExit()}
onNext={() => {
trackEvent('onboarding_step_complete', { step_name: 'context' });
void completeAndExit();
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
onBack={() => navigate('/onboarding/skills')}
/>
);
Expand Down
2 changes: 2 additions & 0 deletions app/src/pages/onboarding/pages/SkillsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useNavigate } from 'react-router-dom';

import { trackEvent } from '../../../services/analytics';
import { useOnboardingContext } from '../OnboardingContext';
import SkillsStep, { type SkillsConnections } from '../steps/SkillsStep';

Expand All @@ -10,6 +11,7 @@ const SkillsPage = () => {
const handleNext = async ({ sources }: SkillsConnections) => {
console.debug('[onboarding:skills-page] next', { sources });
setDraft(prev => ({ ...prev, connectedSources: sources }));
trackEvent('onboarding_step_complete', { step_name: 'skills' });

// Route to ContextGatheringStep when there's a Composio source the
// pipeline can drive. Otherwise jump straight to onboarding completion.
Expand Down
16 changes: 15 additions & 1 deletion app/src/pages/onboarding/pages/WelcomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

import { trackEvent } from '../../../services/analytics';
import WelcomeStep from '../steps/WelcomeStep';

const WelcomePage = () => {
const navigate = useNavigate();
return <WelcomeStep onNext={() => navigate('/onboarding/skills')} />;

useEffect(() => {
trackEvent('onboarding_start');
}, []);

return (
<WelcomeStep
onNext={() => {
trackEvent('onboarding_step_complete', { step_name: 'welcome' });
navigate('/onboarding/skills');
}}
/>
);
};

export default WelcomePage;
Loading
Loading