brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
mobile-analyticsUse this skill when the user says 'analytics', 'event tracking', 'Firebase Analytics', 'Mixpanel', 'Amplitude', 'app analytics', 'event logging', 'user properties', 'screen tracking', 'funnel analytics', 'user behavior', 'telemetry'. Track events, screen views, and user properties in mobile apps with privacy compliance. Do NOT use for: server-side analytics…
Independent GitHub snapshot · Automated classification · Original documentation governs
Guide for implementing mobile app analytics: event tracking, screen views, user properties, and privacy compliance.
Phrases: "analytics", "event tracking", "Firebase Analytics", "Mixpanel", "Amplitude", "app analytics", "event logging", "user properties", "screen tracking", "funnel analytics", "user behavior", "telemetry"
Analytics module: provider initialization, event tracking service, screen view auto-tracker, user properties manager, consent management, privacy controls.
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
6000 tokens
AnalyticsService (Facade)
├── Consent checking before forwarding
├── Property enrichment (device, session, version)
├── Rate limiting and batching
├── Multi-provider routing
│
├── Provider A (Firebase)
├── Provider B (Mixpanel)
└── File/Log Fallback
What is your budget?
├── $0/month → Firebase Analytics
│ Sufficient for: event tracking, basic funnels, 500 user properties
│ Not sufficient for: behavioral cohorts, advanced retention, A/B testing
├── $500-2000/month MTU → Mixpanel or Amplitude
│ Choose Mixpanel: better for product-led growth, viral loops
│ Choose Amplitude: better for behavioral analytics, predictive models
└── Enterprise → Custom server + warehouse
Full data control, custom dashboards, no per-event cost
Init timing?
├── Before app root renders → Firebase, Mixpanel, Amplitude (they start collecting immediately)
│ Must: wrap in try/catch, never crash on init failure
├── After first frame → Non-critical analytics (custom server, niche providers)
│ Screen view events from first frame won't be captured
└── Lazy init on first user action → Privacy-first approach
Show consent dialog first, init only after consent
User lifecycle?
├── Anonymous first, then sign up → Identity stitching
│ On signup: alias anonymous ID to user ID
├── Login required to use app → User ID immediately
│ Set user ID on auth success, reset on logout
└── No auth → Device ID
Use advertising ID (ATT on iOS) or vendor ID
Analytics SDK selection — Three categories of analytics providers. Free tier: Firebase Analytics (Google) — unlimited events, 500 user properties per app, basic segmentation (by device, country, version), funnels in Google Analytics 4, no cost. Good for most apps. Paid tiers: Mixpanel and Amplitude — advanced user profiling, behavioral cohorts, retention analysis, A/B testing, revenue tracking, funnel analysis with time-to-convert, custom dashboards, data warehouses export. Pricing based on MTU (monthly tracked users). Custom server: full data control, no third-party dependency, implement your own event ingestion, storage, dashboard, GDPR compliance certainty, higher engineering cost. Recommendation: start with Firebase Analytics (free + sufficient), migrate to Mixpanel/Amplitude when advanced segmentation needed.
Event naming taxonomy — Consistent naming convention is critical for usable analytics. Event names: snake_case, max 40 characters. Pattern: {domain}_{action}_{object} or {screen}_{action}. Examples: profile_edit_name, cart_add_item, settings_toggle_notifications, checkout_payment_complete. Reserved prefixes: screen_, error_, funnel_, perf_. Event properties: up to 25 per event, typed (string, number, boolean), keys also snake_case, max 40 chars per key, max 100 chars per string value. Never pass PII in properties. Use enum or constant class for event names and property keys to avoid typos and ensure discoverability. Version event schemas in a tracking plan document shared across team.
Automatic screen tracking — Hook into navigation system to dispatch screen_view events automatically. iOS: swizzle or UINavigationControllerDelegate to catch push/pop transitions. Android: NavController.OnDestinationChangedListener for Jetpack Navigation, or FragmentManager.FragmentLifecycleCallbacks. Cross-platform: React Navigation state listener, Flutter Navigator observer. Screen view event should include: screen_name (human-readable, e.g., "Profile"), screen_class (class name, e.g., "ProfileViewController"), screen_route (route pattern, e.g., "/user/:id"). Never manually fire screen view events — use the automatic hook. If a screen doesn't appear in dashboard, fix the navigation hook, not by adding manual calls.
User properties and identity — Set user properties at login/signup and on change. Properties: plan type, subscription status, days since install, push enabled, language, region, A/B test cohort. Use setUserId for cross-session user identity — use a stable identifier (not email or phone). Anonymize user ID for GDPR compliance (hash-based, server-generated). Identity stitching: when user signs up after anonymous usage, call identify() with new ID and alias anonymous events to new ID. Property limits: Firebase allows up to 500 user properties, Mixpanel/Amplitude have higher limits. Set all known properties at login to establish baseline for funnels and retention cohorts.
Custom event tracking — Feature usage: {feature}_{action} with relevant properties (e.g., search_filter_applied with filter type, result count). Non-fatal errors: error_{domain} with error_message, error_code, screen_name. Performance: perf_screen_load with duration_ms, perf_api_latency with endpoint, status_code. Conversion funnels: funnel_{name}_{step} with step number, step name. Use a single AnalyticsService facade class that all features call — never call provider SDKs directly. The facade handles consent checking, provider routing, property enrichment (device info, app version, session ID). This enables changing providers without modifying feature code.
Consent management and GDPR compliance — GDPR: consent dialog on first launch (opt-in required for non-essential events). Categories: essential (app lifecycle, crashes — always tracked), functional (user preferences, feature usage), analytics (behavioral tracking), personalization (advertising, content recommendation). CCPA: "Do Not Sell My Info" toggle in app settings. ATT (iOS 14.5+): ATTrackingManager.requestTrackingAuthorization() before accessing IDFA — required for personalized analytics, ad attribution, and cross-app tracking. Flow: Launch -> check consent status -> if not decided -> show consent dialog -> user choices -> persist preferences -> configure analytics provider accordingly. Android: Google Consent Management SDK for serving consent dialogs. For EU users, analytics must be stopped until consent is granted.
Data retention and deletion — Configure data retention in analytics provider: Firebase Analytics max 24 months, Mixpanel/Amplitude configurable (up to unlimited on enterprise). Implement user data deletion: (a) Call provider's deletion API (analytics.deleteUserData()), (b) Clear local analytics cache/queue, (c) Reset analytics user ID (generate new anonymous ID). For custom server: implement DELETE /api/analytics/user/{userId} endpoint that removes all events, properties, and profiles. Trigger deletion from app settings with confirmation dialog. Audit compliance: maintain a record of what data is collected, where it's stored, and how it can be deleted. Review at each major release.
Event batching and offline queue — Mobile analytics must handle unreliable network conditions. Implement an event queue that persists events to local storage when the device is offline and flushes when connectivity returns. Batch configuration: batch size of 25-50 events or 60-second interval (whichever comes first). Queue limits: cap at 500 events in the offline queue to avoid excessive storage usage. Oldest events are dropped first if the cap is exceeded. Flush priority: critical events (purchases, errors) flush on an expedited timer of 10 seconds. Implement exponential backoff for failed flushes: 2s, 4s, 8s, 16s, max 60s. On app background: force flush all queued events synchronously.
A/B test integration — When using Mixpanel or Amplitude, analytics and experimentation are coupled. Configure experiments in the analytics provider dashboard with targeting rules (country, user property, cohort). The SDK returns the variant assignment for each active experiment. Log experiment exposure as a dedicated event with experiment_id, variant_name, and user_properties so analysis can filter by experiment cohort. Never hardcode experiment logic — use a feature flag abstraction layer that consults the analytics provider's experiment API. Roll out gradually: 1% -> 5% -> 25% -> 50% -> 100%, monitoring core metrics at each step for regression.
Data quality monitoring — Analytics is only useful if the data is correct. Implement event schema validation: assert that every event has the expected properties with correct types. Log schema violations to a separate monitoring dashboard. Track event volume anomalies: if an event fires 10x more or less than the daily average, alert the team. Run daily data quality checks: expected events received, null property ratios, unexpected NaN or negative values. Monitor event latency: 95th percentile time from event fire to dashboard appearance should be under 5 minutes for real-time providers and under 2 hours for batch-export providers.
| Feature | Firebase | Mixpanel | Amplitude | Custom |
|---|---|---|---|---|
| Cost | Free | Paid (MTU) | Paid (MTU) | Engineering |
| Event limit | Unlimited | 20M/mo (starter) | 10M/mo (starter) | Unlimited |
| User properties | 500 | Unlimited | Unlimited | Unlimited |
| Funnels | Basic | Advanced | Advanced | Custom |
| Retention | Yes | Yes | Yes | Custom |
| A/B testing | No | Yes | Yes | Custom |
| Data export | BigQuery | Warehouse sync | Warehouse sync | Direct |
| Real-time | Near-real-time | Real-time | Real-time | Configurable |
| Offline support | Built-in | Built-in | Built-in | Custom |
AnalyticsService facade — never call provider SDKs from feature codesnake_case, <=40 chars — consistent naming prevents analytics debtuser_logged_in vs login_complete creates split funnels. Define naming convention upfront and enforce with lint rules.snake_case with max 40 characters per nameDeep linking attribution requires mapping installs back to the marketing source that drove them. The attribution chain works as follows: (1) user taps an analytics-tracked link containing campaign parameters, (2) link redirects through the attribution SDK's click tracker, (3) if the app isn't installed, the Store redirect saves the click data to the SDK's server, (4) on first app launch, the attributions SDK queries its server for pending attribution data, (5) the analytics SDK then sets user properties for install_source, campaign_id, ad_network, and click_timestamp. This allows analytics queries to segment by acquisition channel. The attribution window is typically 7-30 days after click. Implement attribution as a separate concern from analytics — use an attribution SDK (Adjust, Branch, AppsFlyer) alongside your analytics SDK.
Event data flowing from mobile apps should be validated server-side before entering the analytics pipeline. Implement a validation proxy between the mobile client and your analytics provider: (1) validate event schema (required properties present, correct types, no PII leaking), (2) validate property cardinality (no more than 25 unique values for partition keys), (3) reject events exceeding 32KB payload limit, (4) apply rate limits per user (100 events/second max), (5) filter bot traffic and test device IDs. Use a serverless function or dedicated validation service. Log rejected events to a separate dead-letter queue for auditing and debugging. Schema validation catches tracking plan drift before it pollutes dashboards.
Experimentation needs?
├── No experimentation → Skip. Don't add A/B framework until needed.
├── Simple feature flags → Firebase Remote Config / LaunchDarkly
│ └── Boolean flags, gradual rollout, kill switch
├── Product A/B tests → Mixpanel Experiments / Amplitude Experiment
│ └── Full statistical engine, sample size calculator, MVT
└── Enterprise experimentation → Google Optimize / Optimizely
└── Server-side, personalization, audience targeting
Event volume?
├── <10M events/month → No sampling. Track everything.
├── 10M-100M events/month → Adaptive sampling
│ └── High-value events: 100% (purchase, login, error)
│ └── Low-value events: 10% (scroll, hover, background)
└── >100M events/month → Fixed-rate sampling per event type
└── Set sample rates per event: critical=100%, important=50%, verbose=5%
└── Document sampling rate per event in tracking plan
| Failure | Symptom | Mitigation |
|---|---|---|
| SDK init crash | App crashes on launch | Wrap init in try/catch, never block launch |
| Queue overflow | Events lost after queue cap | Set bounded queue (max 500), oldest-drop |
| Network timeouts | Events stuck in queue | Exponential backoff flush (2s→60s), flush on app bg |
| Schema drift | Dashboard values look wrong | CI schema validation, tracking plan audit |
| Provider outage | No events for hours | Multi-provider routing, failover to log file |
| Consent lost | Essential events also stop | Separate essential from non-essential queues |
import FirebaseAnalytics
final class AnalyticsService {
static let shared = AnalyticsService()
private var isInitialized = false
func initialize() {
FirebaseApp.configure()
isInitialized = true
}
func logEvent(_ name: String, parameters: [String: Any]? = nil) {
guard isInitialized, ConsentManager.shouldTrack(.analytics) else { return }
var enriched = parameters ?? [:]
enriched["app_version"] = App.version
enriched["session_id"] = SessionManager.current.id
Analytics.logEvent(name, parameters: enriched)
}
func setUserProperty(_ value: String?, forName name: String) {
guard ConsentManager.shouldTrack(.analytics) else { return }
Analytics.setUserProperty(value, forName: name)
}
}
class MixpanelAnalyticsProvider(private val context: Context) : AnalyticsProvider {
private lateinit var mixpanel: MixpanelAPI
override fun initialize(token: String) {
mixpanel = MixpanelAPI.getInstance(context, token)
}
override fun trackEvent(name: String, properties: Map<String, Any>?) {
mixpanel.track(name, properties)
}
override fun identify(userId: String) {
mixpanel.identify(userId)
}
override fun alias(anonymousId: String, userId: String) {
mixpanel.alias(anonymousId, userId)
}
override fun setUserProperties(properties: Map<String, Any>) {
val updater = mixpanel.people
properties.forEach { (key, value) -> updater.set(key, value) }
}
override fun flush() {
mixpanel.flush()
}
}
import { init, track, identify, Identify } from '@amplitude/analytics-react-native';
export class AmplitudeService {
private initialized = false;
async init(apiKey: string, userId?: string) {
await init(apiKey, userId, {
flushIntervalMillis: 30000,
flushQueueSize: 30,
optOut: !ConsentManager.consentGiven,
}).promise;
this.initialized = true;
}
track(eventName: string, properties?: Record<string, any>) {
if (!this.initialized || !ConsentManager.shouldTrack('analytics')) return;
track(eventName, properties);
}
setUserId(id: string) {
identify(new Identify().set('userId', id));
}
setUserProperty(name: string, value: string | number | boolean) {
identify(new Identify().set(name, value));
}
}
Event type?
├── User action → `{domain}_{verb}_{object}`
│ e.g., `cart_add_item`, `profile_edit_photo`, `search_submit_query`
│ Properties: object_id, object_type, result_count, duration_ms
├── Screen view → `screen_{name}`
│ e.g., `screen_home`, `screen_product_detail`, `screen_checkout_payment`
│ Properties: referrer, previous_screen, route
├── System event → `{domain}_{event}`
│ e.g., `session_start`, `push_received`, `sync_completed`
│ Properties: source, trigger, duration_ms, success
├── Error / non-fatal → `error_{domain}`
│ e.g., `error_network_timeout`, `error_api_validation`, `error_ui_render`
│ Properties: error_code, error_message, screen, retry_count
└── Performance → `perf_{metric}`
e.g., `perf_screen_load`, `perf_api_latency`, `perf_db_query`
Properties: duration_ms, byte_size, endpoint, cache_hit
| Convention | Example | Pros | Cons |
|---|---|---|---|
snake_case | cart_add_item | Readable, standard in SQL/analytics DBs | Requires conversion for JS clients |
camelCase | cartAddItem | Matches JS/TS code style | Awkward in SQL queries |
SCREAMING_SNAKE | CART_ADD_ITEM | Clearly separates from code | Verbose, looks like constants |
| dot-notation | cart.add.item | Hierarchical, good for grouping | Special handling in BigQuery column names |
{object}:{action} | cart:add_item | Clear domain boundary | Colon requires escaping in some systems |
Recommendation: snake_case for all analytics events — it's SQL-friendly, readable, and consistent across platforms.
User state at event time?
├── No user ID available → Log with device_id + session_id
│ device_id: vendor identifier (iOS) or Advertising ID (Android with ATT)
│ session_id: generated on each app launch, rotated on background >30min
├── User logs in mid-session → Call identify() with user_id
│ Server aliases anonymous session to user_id for stitching
├── User logs out → Reset analytics user ID
│ Generate new anonymous ID, do NOT reuse previous
└── Multi-account → Each login creates a new analytics identity chain
Previous account events retain old user_id, no cross-account linking
snake_case, max 40 charspush_sent, push_delivered, push_opened — the gap between sent and delivered reveals delivery issues.Analytics costs grow linearly with tracked users. Strategies to manage cost:
Event not appearing in dashboard?
├── Was the SDK initialized? → Check initialization log, verify API key
├── Is consent granted? → Check consent manager status for the event category
├── Is the event being filtered? → Check debug/release build config, environment tag
├── Was the device offline? → Check offline queue flush on reconnection
├── Is the event name correct? → Verify exact string matches tracking plan
├── Are properties within limits? → Max 25 properties, values <100 chars, no PII filter
├── Is the user rate limited? → Check per-user event rate limits (100/s typical)
└── Is the provider experiencing an outage? → Check status dashboard
Hand off to mobile-crash-reporting skill when Crashlytics integration is needed, or to mobile-networking when custom analytics server endpoint is required.
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).