Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
PostHog Analytics Documentation
This document explains the comprehensive PostHog analytics implementation in the Storyteller mobile app.
Table of Contents
Setup
1. Environment Configuration
Add your PostHog credentials to mobile/.env:
# PostHog Analytics
EXPO_PUBLIC_POSTHOG_API_KEY=your-posthog-api-key
EXPO_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Note: Replace
your-posthog-api-keywith your actual PostHog project API key from app.posthog.com
2. Installation
The required dependencies are already installed:
posthog-react-native- Core PostHog SDK for React Nativeposthog-js- PostHog JavaScript SDKexpo-file-system,expo-application,expo-localization- Required peer dependencies
3. Initialization
Analytics are automatically initialized in the app through the PostHogWebProvider in app/_layout.tsx. No manual initialization is required.
Architecture
Core Files
mobile/
├── src/
│ ├── services/
│ │ └── analytics.ts # Core analytics service with type-safe events
│ ├── providers/
│ │ └── PostHogWebProvider.tsx # Provider that initializes analytics
│ └── hooks/
│ ├── usePostHogWeb.ts # Backward-compatible PostHog hook
│ └── useAnalytics.ts # New React hooks for analytics
Analytics Service
The analytics.ts service provides:
- Type-safe event tracking - All events are strongly typed using TypeScript
- Automatic enrichment - Events automatically include device info, session data, etc.
- Error handling - Graceful degradation if PostHog is not configured
- User identification - Link events to specific users
- Screen tracking - Automatic screen view tracking
Event Types
Authentication Events
// Sign in
analytics.track('auth_signin_started', { method: 'email' | 'google' | 'apple' });
analytics.track('auth_signin_completed', { method: 'email', userId: string });
analytics.track('auth_signin_failed', { method: 'email', error: string });
// Sign up
analytics.track('auth_signup_started', { method: 'email' | 'google' | 'apple' });
analytics.track('auth_signup_completed', { method: 'email', userId: string });
analytics.track('auth_signup_failed', { method: 'email', error: string });
// Sign out
analytics.track('auth_signout', { userId: string });
Story Events
// Story creation
analytics.track('story_creation_started', {
characterId: string,
characterName: string,
});
analytics.track('story_prompt_entered', {
promptLength: number,
language: string,
});
analytics.track('story_generation_completed', {
storyId: string,
characterId: string,
duration: number,
pageCount: number,
language: string,
});
analytics.track('story_generation_failed', {
characterId: string,
error: string,
duration: number,
});
// Story interaction
analytics.track('story_viewed', { storyId: string, title: string });
analytics.track('story_page_changed', {
storyId: string,
pageNumber: number,
totalPages: number,
});
analytics.track('story_shared', { storyId: string, method: string });
analytics.track('story_deleted', { storyId: string });
Story Engagement Events
These events track how users interact with and consume stories, providing deep insights into engagement patterns:
// Story session tracking
analytics.track('story_session_started', {
storyId: string,
title: string,
pageCount: number,
});
analytics.track('story_session_ended', {
storyId: string,
duration: number, // Total time in milliseconds
pagesViewed: number, // Number of unique pages viewed
completed: boolean, // Whether user reached the end
furthestPage: number, // Highest page number reached
});
// Page-level engagement
analytics.track('story_page_viewed', {
storyId: string,
pageNumber: number,
totalPages: number,
isStartScreen: boolean, // Start screen (page 0)
isEndScreen: boolean, // End screen (last page)
});
analytics.track('story_page_duration', {
storyId: string,
pageNumber: number,
duration: number, // Time spent on this page in milliseconds
isStartScreen: boolean,
isEndScreen: boolean,
});
// Story completion
analytics.track('story_completed', {
storyId: string,
totalDuration: number, // Total time to complete story
pageCount: number, // Number of story pages (excluding start/end)
averagePageDuration: number, // Average time per page
});
// Abandonment tracking
analytics.track('story_abandoned', {
storyId: string,
lastPage: number, // Last page viewed before leaving
totalPages: number,
duration: number, // Time before abandonment
completionPercentage: number, // How far through the story (0-100)
});
// Story restart
analytics.track('story_restarted', {
storyId: string,
fromPage: number, // Page number when restart was triggered
});
// Button interactions
analytics.track('story_end_button_clicked', {
storyId: string,
title: string,
});
analytics.track('story_restart_button_clicked', {
storyId: string,
fromPage: number, // Page when restart was clicked
});
analytics.track('story_archive_button_clicked', {
storyId: string,
title: string,
});
analytics.track('story_archived', {
storyId: string,
title: string,
});
Character Events
// Character creation
analytics.track('character_creation_started', {
method: 'description' | 'photo',
});
analytics.track('character_description_entered', {
descriptionLength: number,
});
analytics.track('character_photo_selected', {
source: 'camera' | 'gallery',
});
analytics.track('character_generation_completed', {
characterId: string,
name: string,
method: 'description' | 'photo',
duration: number,
});
analytics.track('character_generation_failed', {
method: 'description' | 'photo',
error: string,
duration: number,
});
// Character interaction
analytics.track('character_viewed', {
characterId: string,
characterName: string,
});
analytics.track('character_shared', { characterId: string, method: string });
analytics.track('character_deleted', { characterId: string });
Credit Events
analytics.track('credits_checked', { balance: number });
analytics.track('credits_insufficient', {
required: number,
available: number,
action: string,
});
analytics.track('credits_consumed', {
amount: number,
action: string,
balance: number,
});
analytics.track('credits_purchase_initiated', { amount: number });
analytics.track('credits_purchase_completed', {
amount: number,
paymentMethod: string,
});
// Credits screen tracking
analytics.track('credits_screen_viewed', {
source: string, // Where user came from (e.g., 'insufficient_modal', 'settings')
balance: number, // Current credit balance
});
analytics.track('credits_screen_duration', {
duration: number, // Time spent on credits screen
purchaseMade: boolean, // Whether a purchase was completed
});
Screen Navigation
analytics.track('screen_viewed', {
screenName: string,
params?: Record<string, any>,
previousScreen?: string
});
Performance & Errors
analytics.track('api_request', {
endpoint: string,
method: string,
duration: number,
statusCode: number,
success: boolean
});
analytics.track('error_occurred', {
error: string,
errorCode?: string,
screen: string,
action?: string,
metadata?: Record<string, any>
});
Usage Examples
Basic Event Tracking
import { analytics } from '../src/services/analytics';
// Track a simple event
analytics.track('feature_discovered', {
feature: 'story_sharing',
source: 'button_click',
});
Using React Hooks
import { useAnalytics, useScreenTracking, useErrorTracking } from '../hooks/useAnalytics';
function MyComponent() {
// Automatic screen tracking
useScreenTracking('MyScreen', { userId: '123' });
// Error tracking with automatic context
const { trackError } = useErrorTracking('MyScreen');
const handleAction = async () => {
try {
await riskyOperation();
} catch (error) {
trackError(error, {
action: 'riskyOperation',
metadata: { attemptCount: 3 }
});
}
};
return <View>...</View>;
}
Performance Tracking
import { usePerformanceTracking } from '../hooks/useAnalytics';
function MyComponent() {
const { startTimer, endTimer } = usePerformanceTracking();
const handleLoadData = async () => {
startTimer('load_data');
const data = await fetchData();
endTimer('load_data', { recordCount: data.length });
};
return <View>...</View>;
}
Story Engagement Tracking
Track detailed user engagement with stories including time spent, page views, and completion rates:
import { useStoryEngagement } from '../hooks/useStoryEngagement';
function StoryViewerScreen({ story }) {
// Initialize engagement tracking
const { trackPageView, trackRestart } = useStoryEngagement({
storyId: story.id,
title: story.title,
pageCount: story.pages.length
});
// Track when user changes pages
const handlePageChange = (pageIndex: number) => {
trackPageView(pageIndex);
// Automatically tracks:
// - Page view time
// - Session duration
// - Story completion
// - Abandonment
};
// Track when user restarts the story
const handleRestart = () => {
trackRestart(currentPage);
};
return <StoryViewer onPageChange={handlePageChange} onRestart={handleRestart} />;
}
Credits Screen Tracking
Track when users view the credits/purchase screen and how long they spend there:
import { useCreditsScreenTracking } from '../hooks/useStoryEngagement';
function CreditsScreen({ balance }) {
// Track screen viewing and duration
const { markPurchaseMade } = useCreditsScreenTracking('insufficient_modal', balance);
const handlePurchase = async (amount: number) => {
const result = await purchaseCredits(amount);
if (result.success) {
// Mark that a purchase was made for analytics
markPurchaseMade();
}
};
return <View>...</View>;
// Duration is automatically tracked on unmount
}
User Identification
import { analytics } from '../src/services/analytics';
// Identify user after login
await analytics.identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan,
signupDate: user.createdAt,
});
// Update user properties
await analytics.setUserProperties({
preferredLanguage: 'de',
storiesCreated: 5,
});
// Reset on logout
await analytics.reset();
Screen View Tracking
import { analytics } from '../src/services/analytics';
// Track screen view
analytics.trackScreenView('StoryList', {
storyCount: stories.length,
filter: 'recent'
});
// Or use the hook for automatic tracking
import { useScreenTracking } from '../hooks/useAnalytics';
function StoryListScreen() {
useScreenTracking('StoryList', {
storyCount: stories.length
});
return <View>...</View>;
}
Error Tracking
import { analytics } from '../src/services/analytics';
try {
await createStory(data);
} catch (error) {
analytics.trackError(error, {
screen: 'CreateStory',
action: 'submit_story',
metadata: {
characterId: selectedCharacter.id,
promptLength: prompt.length,
},
});
}
Best Practices
1. Use Type-Safe Events
Always use the strongly-typed events defined in the AnalyticsEvent type:
// ✅ Good - Type-safe
analytics.track('story_created', {
storyId: '123',
characterId: 'abc',
});
// ❌ Bad - No type safety
posthog?.capture('story_created', { id: '123' });
2. Track User Flows
Track the complete user journey:
// Start of flow
analytics.track('story_creation_started', { characterId: '123' });
// User action
analytics.track('story_prompt_entered', { promptLength: 50 });
// End of flow (success)
analytics.track('story_generation_completed', {
storyId: '456',
characterId: '123',
duration: 5000,
});
// Or end of flow (failure)
analytics.track('story_generation_failed', {
characterId: '123',
error: 'API timeout',
duration: 30000,
});
3. Include Context
Always include relevant context with events:
// ✅ Good - Rich context
analytics.track('story_generation_completed', {
storyId: story.id,
characterId: character.id,
duration: elapsed,
pageCount: 10,
language: 'de',
});
// ❌ Bad - Minimal context
analytics.track('story_generation_completed', {
storyId: story.id,
});
4. Track Performance
Track timing for important operations:
const startTime = Date.now();
try {
const result = await expensiveOperation();
const duration = Date.now() - startTime;
analytics.track('operation_completed', {
duration,
success: true,
resultCount: result.length,
});
} catch (error) {
analytics.track('operation_failed', {
duration: Date.now() - startTime,
error: error.message,
});
}
5. Avoid PII
Never track Personally Identifiable Information (PII) in event properties:
// ✅ Good - No PII
analytics.track('profile_updated', {
fields: ['name', 'avatar'],
fieldCount: 2,
});
// ❌ Bad - Contains PII
analytics.track('profile_updated', {
email: 'user@example.com',
name: 'John Doe',
});
Use identify() for user-level data instead of event properties.
6. Handle Errors Gracefully
Analytics should never break your app:
// The analytics service already handles errors internally
analytics.track('my_event', { data: 'value' });
// If PostHog is not configured or fails, this won't throw
Debugging
Check if Analytics is Enabled
import { analytics } from '../src/services/analytics';
if (analytics.isEnabled()) {
console.log('Analytics is enabled');
} else {
console.log('Analytics is disabled or not configured');
}
View Events in Console
All events are logged to the console in development:
[Analytics] Event tracked: story_creation_started {
characterId: '123',
characterName: 'Fluffy',
timestamp: '2025-10-31T...',
current_screen: 'CreateStory',
session_duration: 45000
}
Force Flush Events
Events are automatically batched. To force immediate sending:
import { analytics } from '../src/services/analytics';
await analytics.flush();
Common Issues
Analytics not tracking:
- Check that
EXPO_PUBLIC_POSTHOG_API_KEYis set in.env - Restart Expo with cache clear:
npx expo start -c - Check console for initialization errors
Events not appearing in PostHog:
- Events are batched - wait a few seconds or call
flush() - Check that your PostHog project is active
- Verify the API key is correct
TypeScript errors:
- Make sure event names match exactly (case-sensitive)
- Ensure properties match the type definition
- Update the
AnalyticsEventtype if adding new events
Adding New Events
To add a new event type:
- Add the event definition to
AnalyticsEventtype insrc/services/analytics.ts:
export type AnalyticsEvent = {
// ... existing events ...
// Your new event
feature_used: {
featureName: string;
context: string;
};
};
- Track the event:
analytics.track('feature_used', {
featureName: 'dark_mode',
context: 'settings',
});
- The event is now type-safe and will autocomplete in your IDE!
PostHog Dashboard
Access your analytics dashboard at app.posthog.com
Useful Views
- Live Events: See events as they happen in real-time
- Insights: Create charts and graphs from your events
- Funnels: Analyze user flows (e.g., sign up → create character → create story)
- Cohorts: Group users by behavior
- Session Recordings: Watch user sessions (if enabled)
Recommended Insights
Creation & Generation
-
Story Creation Funnel:
story_creation_started→story_prompt_entered→story_generation_completed
-
Character Creation Success Rate:
character_generation_completed/character_creation_started* 100
-
Average Story Generation Time:
- Average of
durationproperty onstory_generation_completedevents
- Average of
Story Engagement
-
Story Completion Rate:
story_completed/story_session_started* 100- Shows what percentage of users finish stories they start
-
Average Time Per Story Page:
- Average of
durationproperty onstory_page_durationevents (excluding start/end screens) - Indicates engagement level - too short might mean skipping, too long might mean confusion
- Average of
-
Story Abandonment Points:
- Chart of
lastPageproperty fromstory_abandonedevents - Identify which pages cause users to leave
- Chart of
-
Story Engagement Funnel:
story_session_started→story_page_viewed(page 1) →story_page_viewed(page 5) →story_completed- Shows drop-off at different story stages
-
Average Pages Viewed Per Session:
- Average of
pagesViewedproperty onstory_session_endedevents - Measures how much content users consume
- Average of
-
Story Restart Rate:
- Count of
story_restartedevents / count ofstory_session_startedevents - Indicates replayability/engagement
- Count of
User Actions & Behavior
-
Archive Rate:
story_archived/story_archive_button_clicked* 100- Shows if users follow through with archiving
-
Restart vs. End Behavior:
- Compare
story_restart_button_clickedvsstory_end_button_clicked - Indicates if stories encourage re-reading or are one-time reads
- Compare
-
Action Click Analysis:
- Count of each button click type
- Helps understand which features are most used
Monetization
-
Credit Consumption:
- Sum of
amountproperty oncredits_consumedevents grouped byaction
- Sum of
-
Credits Screen Conversion:
credits_purchase_completed/credits_screen_viewed* 100- Shows effectiveness of purchase flow
-
Time to Purchase Decision:
- Average of
durationproperty oncredits_screen_durationevents wherepurchaseMade= true - Helps understand purchase friction
- Average of
Support
For issues or questions about analytics:
- Check the console logs for error messages
- Verify environment configuration
- Review PostHog documentation: posthog.com/docs
- Check this documentation for usage examples