diff --git a/.claude/context/architecture/system-design.md b/.claude/context/architecture/system-design.md index 327de8824..9f3f374b1 100644 --- a/.claude/context/architecture/system-design.md +++ b/.claude/context/architecture/system-design.md @@ -17,23 +17,26 @@ The Mixpanel Android SDK implements a **Producer-Consumer Pattern** with persist └────────┬────────┘ └──────────────────┘ │ Events ▼ -┌─────────────────┐ ┌──────────────────┐ -│AnalyticsMessages│────▶│ Message Queue │ -│ (Producer) │ │ (HandlerThread) │ -└────────┬────────┘ └────────┬─────────┘ - │ │ Batch - ▼ ▼ -┌─────────────────┐ ┌──────────────────┐ -│ MPDbAdapter │ │ HttpService │ -│ (SQLite) │────▶│ (Network I/O) │ -└─────────────────┘ └────────┬─────────┘ - │ - ▼ - ┌──────────────────┐ - │ Mixpanel Servers │ - └──────────────────┘ +┌─────────────────────────────────────────┐ +│ AnalyticsMessages (worker HandlerThread)│ +│ owns BOTH the DB adapter and the poster │ +└────────┬───────────────────┬────────────┘ + │ store/read │ send batch + ▼ ▼ +┌─────────────────┐ ┌──────────────────┐ +│ MPDbAdapter │ │ HttpService │ +│ (SQLite) │ │ (Network I/O) │ +└─────────────────┘ └────────┬─────────┘ + │ + ▼ + ┌──────────────────┐ + │ Mixpanel Servers │ + └──────────────────┘ ``` +(MPDbAdapter never talks to the network. A second, parallel network path exists: +MixpanelAPI → FeatureFlagManager → HttpService, with no SQLite involvement.) + ## Core Components ### 1. MixpanelAPI (Public Interface) @@ -230,10 +233,8 @@ try { - Weak references for callbacks ### Database Optimization -- Prepared statements -- Transaction batching -- Automatic vacuum -- Size-based cleanup +- Age/size-based cleanup +- Automatic data pruning on over-length queues ## Extension Points diff --git a/.claude/context/codebase-map.md b/.claude/context/codebase-map.md index 9804a5253..44dda1807 100644 --- a/.claude/context/codebase-map.md +++ b/.claude/context/codebase-map.md @@ -8,27 +8,31 @@ mixpanel-android/ │ ├── src/main/java/com/mixpanel/android/ │ │ ├── mpmetrics/ # Core SDK implementation │ │ │ ├── MixpanelAPI.java # Main entry point & public API +│ │ │ ├── MixpanelOptions.java # Runtime configuration (builder) │ │ │ ├── AnalyticsMessages.java # Message queue & background processing │ │ │ ├── MPDbAdapter.java # SQLite persistence layer │ │ │ ├── PersistentIdentity.java # Identity & properties management -│ │ │ ├── HttpService.java # Network communication │ │ │ ├── MPConfig.java # Configuration management │ │ │ ├── FeatureFlagManager.java # Feature flags implementation -│ │ │ ├── ResourceIds.java # R.id resource handling -│ │ │ ├── ResourceReader.java # Resource reading utilities -│ │ │ ├── DecideChecker.java # Remote configuration fetcher +│ │ │ ├── FeatureFlagOptions.java / FlagsConfig.java / MixpanelFlagVariant.java # Feature-flag models +│ │ │ ├── AutomaticEvents.java # Automatic lifecycle events +│ │ │ ├── DeviceIdProvider.java # Device/anonymous ID generation │ │ │ ├── SessionMetadata.java # Session tracking -│ │ │ ├── ConnectivityReceiver.java # Network state monitoring +│ │ │ ├── SessionReplayBroadcastReceiver.java # Session Replay integration hook +│ │ │ ├── ResourceIds.java / ResourceReader.java # Resource handling │ │ │ ├── ExceptionHandler.java # Crash reporting │ │ │ ├── MixpanelActivityLifecycleCallbacks.java # Lifecycle integration │ │ │ └── [Various data models & utilities] │ │ └── util/ # Utility classes │ │ ├── MPLog.java # Logging utility -│ │ ├── HttpService.java # HTTP utilities -│ │ ├── ImageStore.java # Image caching +│ │ ├── HttpService.java # HTTP / network communication +│ │ ├── RemoteService.java # HTTP service interface │ │ ├── OfflineMode.java # Offline mode management +│ │ ├── ProxyServerInteractor.java / W3CTraceContext.java # Proxy & trace context │ │ └── [Other utilities] -│ ├── src/androidTest/ # Instrumented tests only +│ ├── src/test/ # Unit tests (JVM — JUnit/Robolectric) +│ │ └── java/com/mixpanel/android/mpmetrics/ +│ ├── src/androidTest/ # Instrumented tests (device/emulator) │ │ └── java/com/mixpanel/android/ │ │ ├── mpmetrics/ # Core SDK tests │ │ └── util/ # Utility tests @@ -39,10 +43,15 @@ mixpanel-android/ │ ├── build.gradle # Analytics module build │ ├── proguard.txt # Consumer ProGuard rules │ └── gradle.properties # Analytics version & POM properties -├── common/ # Shared utilities (:common) +├── common/ # Shared utilities (:common — MixpanelEventBridge, JsonLogic) ├── openfeature-provider/ # OpenFeature provider (:openfeature-provider) +├── session-replay/ # Session Replay SDK (:session-replay, published; own CI + CHANGELOG) +│ └── sessionreplaydemo/ # Session Replay demo app (:session-replay:sessionreplaydemo) +├── build-logic/ # Included build: mixpanel.maven-publish / mixpanel.ktlint convention plugins +├── gradle/libs.versions.toml # Version catalog (used by session-replay) +├── scripts/ # Dev scripts (codespace helper) ├── build.gradle # Root build (cross-cutting config) -├── settings.gradle # Subproject includes +├── settings.gradle # Subproject includes (6 projects) └── gradle.properties # Shared org.gradle.* / android.* settings ``` @@ -65,17 +74,15 @@ mixpanel-android/ - `SessionMetadata` - Session tracking and timing **Feature Components:** -- `FeatureFlagManager` - Feature flag loading and caching -- `DecideChecker` - Remote configuration updates +- `FeatureFlagManager` - Feature flag loading and caching (with `FlagsConfig` / `MixpanelFlagVariant`) - `ExceptionHandler` - Automatic crash reporting ### Testing Structure (`analytics/src/androidTest/`) **Test Organization:** -- All tests are instrumented (require Android device/emulator) -- Unit tests live alongside under `analytics/src/test/` (Robolectric/JUnit) -- Tests use AndroidJUnit4 runner -- Mock implementations in TestUtils +- **Unit tests** — `analytics/src/test/` (JUnit/Robolectric, run on the JVM via `:analytics:test`) +- **Instrumented tests** — `analytics/src/androidTest/` (AndroidJUnit4, require a device/emulator, via `:analytics:connectedAndroidTest`) +- Async verification uses the BlockingQueue pattern; mock implementations in TestUtils ### Demo Application (`analytics/mixpaneldemo/`) @@ -89,8 +96,11 @@ mixpanel-android/ ``` mixpanel-android (library) + ├── com.mixpanel.android:mixpanel-android-common (Maven coordinate, not project dep) ├── androidx.annotation:annotation ├── androidx.core:core + ├── androidx.lifecycle:lifecycle-process + ├── io.github.jamsesso:json-logic-java (+ gson pin for its transitive JSON dep) └── Android SDK (min 21, target 34) :analytics:mixpaneldemo (app) @@ -101,8 +111,9 @@ mixpanel-android (library) ## Build Variants -- **debug** - Development build with debugging enabled -- **release** - Production build with ProGuard optimization +- **debug** - Development build with coverage enabled +- **release** - Production build; `minifyEnabled false` — the library ships a minimal + consumer ProGuard rule (`analytics/proguard.txt`) but does not minify itself ## Data Flow Architecture @@ -110,23 +121,22 @@ mixpanel-android (library) User Code ↓ MixpanelAPI (Public Interface) - ↓ -AnalyticsMessages (Queue Management) - ↓ -MPDbAdapter (Persistence) - ↓ -HttpService (Network) - ↓ -Mixpanel Servers + ├─ tracking → AnalyticsMessages (Queue Management) + │ ├─→ MPDbAdapter (SQLite persistence/offline queue) + │ └─→ HttpService (Network) → Mixpanel Servers + └─ feature flags → FeatureFlagManager → HttpService (separate path, no SQLite) ``` +Note: AnalyticsMessages owns both the DB adapter and the HTTP poster — +MPDbAdapter never talks to the network. + ## Key Design Decisions -1. **Single Module Library** - All code in one module for simplicity -2. **Minimal Dependencies** - Only essential AndroidX libraries +1. **Multi-Module Repo** - Independently versioned/published modules (:analytics, :common, :openfeature-provider, :session-replay) consuming each other via Maven coordinates +2. **Minimal Dependencies** - Small fixed runtime set (see Module Dependencies above); no new deps 3. **Custom HTTP** - No external networking libraries -4. **SQLite Direct** - No ORM, direct database access -5. **HandlerThread** - Background processing without Service -6. **Instrumented Tests Only** - Focus on real device testing +4. **SQLite Direct** - No ORM, direct database access (rawQuery-based) +5. **HandlerThread Workers** - Background processing on dedicated HandlerThreads (AnalyticsMessages, FeatureFlagManager) without Service components +6. **Two Test Layers** - JVM unit tests (`src/test/`) plus instrumented tests (`src/androidTest/`) for real-device coverage This map provides navigation context for understanding code organization and relationships between components. \ No newline at end of file diff --git a/.claude/context/workflows/release-process.md b/.claude/context/workflows/release-process.md index 7af5ec466..11e3cdd57 100644 --- a/.claude/context/workflows/release-process.md +++ b/.claude/context/workflows/release-process.md @@ -1,319 +1,61 @@ # Release Process for Mixpanel Android SDK -## Release Overview +## Overview -The Mixpanel Android SDK follows semantic versioning (X.Y.Z) and publishes to Maven Central via Sonatype OSSRH. +Semantic versioning (X.Y.Z) per module, published to Maven Central via the Central Portal. +**Releases are driven entirely by GitHub Actions — there is no local release script.** +(The old `release.sh` / `uploadArchives` / NEXUS-credential flow is gone.) -## Pre-Release Checklist +The source of truth for what can be released is **`.github/modules.json`** — one entry per +module with its `tag_prefix`, `gradle_properties`, `changelog`, `readme`, +`gradle_task_prefix`, and `artifact_id`: -### 1. Code Preparation -- [ ] All features merged to master -- [ ] All tests passing -- [ ] No ProGuard warnings -- [ ] Demo app builds and runs -- [ ] API compatibility verified +| Module key | Artifact | Tag prefix | Version file | +|---|---|---|---| +| `analytics` | `mixpanel-android` | `v` | `analytics/gradle.properties` | +| `common` | `mixpanel-android-common` | `common-v` | `common/gradle.properties` | +| `openfeature` | `mixpanel-android-openfeature` | `open-feature-v` | `openfeature-provider/gradle.properties` | +| `session-replay` | `mixpanel-android-session-replay` | `session-replay-v` | `session-replay/gradle.properties` | -### 2. Version Update +## Release steps -**File:** `analytics/gradle.properties` (or the per-module `gradle.properties` for `:common` / `:openfeature-provider`) -```properties -VERSION_NAME=8.3.0 -``` +### 1. Prepare (workflow: `.github/workflows/prepare-release.yml`) -**Semantic Versioning Rules:** -- **Major (X.0.0)**: Breaking API changes -- **Minor (X.Y.0)**: New features, backwards compatible -- **Patch (X.Y.Z)**: Bug fixes only +Manually dispatched with inputs `module` (a key from modules.json) and `version`. +It bumps `VERSION_NAME` in the module's `gradle.properties`, updates the module README, +generates the changelog (`.github/scripts/generate-changelog.sh`), and opens a release PR. -### 3. Update analytics/CHANGELOG.md +### 2. Review & merge the release PR -```markdown -## Version 8.3.0 (January 15, 2024) +Standard pre-release sanity: tests green, demo app builds, no new lint/animalsniffer +warnings, API compatibility preserved. -### Features -- Added feature flags refresh callback -- New group analytics API +### 3. Publish (workflow: `.github/workflows/release-maven-central.yml`) -### Improvements -- Reduced memory usage by 15% -- Optimized batch processing +Parameterized by `module`; resolves the module config from modules.json, validates the +version against `gradle.properties`, builds and tests, publishes to OSSRH staging +(staging/snapshot URLs are hardcoded in `MavenPublishConventionPlugin.kt` — the +`RELEASE_REPOSITORY_URL` entry in `analytics/gradle.properties` is dead config, read by +nothing), triggers the Central Portal upload with +`publishing_type=user_managed`, and creates a draft GitHub release + tag +(``). -### Fixes -- Fixed race condition in session tracking -- Resolved ProGuard configuration issue +Credentials (repo secrets → env, wired in +`build-logic/convention/src/main/kotlin/MavenPublishConventionPlugin.kt`): -### Breaking Changes (if major version) -- Removed deprecated methods -``` +- `MAVEN_CENTRAL_USERNAME` → `MAVEN_CENTRAL_USERNAME` +- `MAVEN_CENTRAL_TOKEN` → `MAVEN_CENTRAL_PASSWORD` +- `GPG_PRIVATE_KEY` → `SIGNING_KEY` +- `GPG_PASSPHRASE` → `SIGNING_PASSWORD` -## Build Verification +### 4. Release from the Portal -### 1. Clean Build -```bash -# Clean everything -./gradlew clean +Deployments appear at https://central.sonatype.com/publishing/deployments — finish with a +manual release from the Portal UI, then publish the draft GitHub release. -# Build library -./gradlew :analytics:build +There is also `.github/workflows/release-snapshot.yml` for snapshot publishing. -# Verify no warnings -``` +## Ordering constraint -### 2. Run All Tests -```bash -# Unit tests (Robolectric/JVM) -./gradlew :analytics:test - -# Instrumented tests (requires device/emulator) -./gradlew :analytics:connectedAndroidTest - -# Generate coverage report -./gradlew :analytics:createDebugCoverageReport -``` - -### 3. Lint Verification -```bash -# Run lint checks -./gradlew :analytics:lint - -# Review lint report at: -# analytics/build/reports/lint-results-debug.html -``` - -### 4. ProGuard Testing -```bash -# Build release variant -./gradlew :analytics:assembleRelease - -# Test ProGuard rules -./gradlew :analytics:testReleaseUnitTest -``` - -### 5. Documentation Generation -```bash -# Generate JavaDocs -./gradlew :analytics:androidJavadocs - -# Review at analytics/build/docs/javadoc/ -``` - -## Demo App Verification - -### 1. Update Demo Dependencies -**File:** `analytics/mixpaneldemo/build.gradle.kts` -```kotlin -dependencies { - implementation(project(":analytics")) - // Or for testing published version: - // implementation("com.mixpanel.android:mixpanel-android:8.6.0") -} -``` - -### 2. Test Demo Features -```bash -# Build demo app -./gradlew :analytics:mixpaneldemo:assembleDebug - -# Install on device -./gradlew :analytics:mixpaneldemo:installDebug -``` - -**Manual Testing Checklist:** -- [ ] Track events -- [ ] Update people properties -- [ ] Test feature flags -- [ ] Verify flush behavior -- [ ] Check offline mode -- [ ] Test configuration options - -## Publishing Process - -### 1. Configure Credentials - -**File:** `~/.gradle/gradle.properties` -```properties -NEXUS_USERNAME=your-sonatype-username -NEXUS_PASSWORD=your-sonatype-password -signing.keyId=YOUR-KEY-ID -signing.password=your-key-password -signing.secretKeyRingFile=/path/to/secring.gpg -``` - -### 2. Run Release Script - -```bash -# Make script executable -chmod +x release.sh - -# Run release -./release.sh 8.3.0 -``` - -**What the script does:** -1. Updates version in gradle.properties -2. Builds release artifacts -3. Signs artifacts with GPG -4. Uploads to Sonatype staging -5. Creates git tag - -### 3. Manual Publishing Steps - -If not using release script: - -```bash -# Build and sign artifacts -./gradlew clean build - -# Upload to Sonatype -./gradlew uploadArchives - -# Create git tag -git tag v8.3.0 -git push origin v8.3.0 -``` - -### 4. Sonatype Release - -1. Log into [Sonatype OSSRH](https://oss.sonatype.org/) -2. Go to "Staging Repositories" -3. Find your repository (com.mixpanel-XXXX) -4. Click "Close" and verify -5. Click "Release" to publish - -### 5. Verify Publication - -**Maven Central (may take 2-4 hours):** -```bash -# Check availability -curl https://repo1.maven.org/maven2/com/mixpanel/android/mixpanel-android/8.3.0/ - -# Test in new project -dependencies { - implementation 'com.mixpanel.android:mixpanel-android:8.3.0' -} -``` - -## Post-Release Tasks - -### 1. GitHub Release - -```bash -# Create release on GitHub -gh release create v8.3.0 \ - --title "Version 8.3.0" \ - --notes-file CHANGELOG.md \ - --target master -``` - -### 2. Update Documentation - -- [ ] Update README.md version references -- [ ] Update integration guides -- [ ] Update API documentation -- [ ] Notify documentation team - -### 3. Communication - -**Internal:** -- [ ] Update internal version tracking -- [ ] Notify customer success team -- [ ] Update support documentation - -**External:** -- [ ] Blog post for major features -- [ ] Update Stack Overflow answers -- [ ] Tweet from @mixpanel - -### 4. Version Bump - -**Prepare for next version:** -```properties -# gradle.properties -VERSION_NAME=8.3.1-SNAPSHOT -``` - -## Rollback Procedure - -If critical issues found: - -### 1. Immediate Actions -```bash -# Delete tag -git tag -d v8.3.0 -git push origin :refs/tags/v8.3.0 - -# Revert commits if needed -git revert -``` - -### 2. Sonatype Actions -- Contact Sonatype support for removal -- Cannot remove from Maven Central mirrors - -### 3. Mitigation -- Release patch version immediately -- Communicate known issues -- Update documentation - -## Release Artifacts - -### Published Files -``` -mixpanel-android-8.3.0.aar # Main library -mixpanel-android-8.3.0.pom # Maven metadata -mixpanel-android-8.3.0-sources.jar # Source code -mixpanel-android-8.3.0-javadoc.jar # Documentation -``` - -### Signatures -Each artifact includes: -- `.asc` - GPG signature -- `.md5` - MD5 checksum -- `.sha1` - SHA1 checksum - -## Integration Testing - -### Test New Release -```gradle -// Create test project -android { - compileSdkVersion 34 - - defaultConfig { - minSdkVersion 21 - targetSdkVersion 34 - } -} - -dependencies { - implementation 'com.mixpanel.android:mixpanel-android:8.3.0' -} -``` - -### Compatibility Matrix -Test with: -- [ ] Min SDK version (21) -- [ ] Target SDK version (34) -- [ ] Latest Android Studio -- [ ] ProGuard enabled -- [ ] R8 enabled -- [ ] MultiDex scenarios - -## Common Issues - -### Build Failures -- Check signing configuration -- Verify network connectivity -- Ensure clean build - -### Upload Failures -- Check Sonatype credentials -- Verify repository permissions -- Check artifact signatures - -### Publication Delays -- Maven Central sync: 2-4 hours -- Mirror propagation: up to 24 hours -- Use Sonatype directly if urgent - -This comprehensive release process ensures high-quality, reliable releases of the Mixpanel Android SDK to thousands of applications worldwide. \ No newline at end of file +`:analytics` consumes `:common` via its published Maven coordinate — release `common` +first when the main SDK needs `:common` changes (see root AGENTS.md, Subprojects). diff --git a/.claude/context/workflows/testing-strategy.md b/.claude/context/workflows/testing-strategy.md index 50eec1454..f5d10886a 100644 --- a/.claude/context/workflows/testing-strategy.md +++ b/.claude/context/workflows/testing-strategy.md @@ -2,19 +2,20 @@ ## Testing Philosophy -The SDK uses **instrumented tests only** - all tests require an Android device or emulator. This ensures tests validate real Android behavior rather than mocked implementations. +The SDK has **two test layers**: JVM **unit tests** (`analytics/src/test/`, JUnit/Robolectric, run via `:analytics:test`) and **instrumented tests** (`analytics/src/androidTest/`, AndroidJUnit4, requiring a device/emulator, run via `:analytics:connectedAndroidTest`). Instrumented tests validate real Android behavior rather than mocked implementations. ## Test Structure ### Location ``` -analytics/src/androidTest/java/com/mixpanel/android/ +analytics/src/test/java/com/mixpanel/android/ # Unit tests (JVM) +└── mpmetrics/ # e.g. PersistentIdentityTest, FeatureFlagManagerTest, MixpanelOptionsTest + +analytics/src/androidTest/java/com/mixpanel/android/ # Instrumented tests (device/emulator) ├── mpmetrics/ # Core SDK tests │ ├── MixpanelBasicTest.java -│ ├── MPDbAdapterTest.java │ ├── PersistentIdentityTest.java -│ ├── DecideCheckerTest.java -│ ├── HttpServiceTest.java +│ ├── FeatureFlagManagerTest.java │ └── [other test classes] └── util/ # Utility tests └── HttpServiceTest.java diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md index 30fd11cc5..277d15b3f 100644 --- a/.cursor/rules/README.md +++ b/.cursor/rules/README.md @@ -18,7 +18,7 @@ Specific patterns for major SDK components: ### Feature Rules (`features/`) Domain-specific patterns: -- **testing-patterns.mdc** - Instrumented test patterns +- **testing-patterns.mdc** - Unit and instrumented test patterns - **android-patterns.mdc** - Android SDK best practices ### Workflow Rules (`workflows/`) @@ -31,7 +31,7 @@ Multi-step procedures: 1. **Never Crash** - The SDK must never crash the host application 2. **Thread Safety** - All public APIs must be thread-safe 3. **Defensive Programming** - Validate inputs, handle nulls gracefully -4. **No Unit Tests** - Only instrumented tests for real device validation +4. **Two Test Layers** - JVM unit tests (`src/test/`) plus instrumented tests (`src/androidTest/`) for real-device validation 5. **Minimal Dependencies** - Avoid external libraries ## Usage diff --git a/.cursor/rules/always/architecture-principles.mdc b/.cursor/rules/always/architecture-principles.mdc index 97ca174bf..74cd80fa1 100644 --- a/.cursor/rules/always/architecture-principles.mdc +++ b/.cursor/rules/always/architecture-principles.mdc @@ -88,8 +88,11 @@ mContext = context; // Could be Activity **ALWAYS** follow the established data flow: ``` -MixpanelAPI → AnalyticsMessages → MPDbAdapter → HttpService +MixpanelAPI ─ tracking ─→ AnalyticsMessages ─→ { MPDbAdapter (SQLite), HttpService } + └ feature flags ─→ FeatureFlagManager ─→ HttpService ``` +AnalyticsMessages owns both the DB adapter and the HTTP poster; MPDbAdapter never +touches the network. Only AnalyticsMessages and FeatureFlagManager may perform HTTP. **NEVER** bypass layers: ```java diff --git a/.cursor/rules/features/testing-patterns.mdc b/.cursor/rules/features/testing-patterns.mdc index b78d2b181..dca525277 100644 --- a/.cursor/rules/features/testing-patterns.mdc +++ b/.cursor/rules/features/testing-patterns.mdc @@ -1,6 +1,9 @@ -# Testing Patterns - Instrumented Tests Only +# Testing Patterns -**Description**: Rules for writing Android instrumented tests +**Description**: Rules for writing tests. The SDK has two layers — JVM unit tests +(`src/test/`, JUnit/Robolectric, run via `:analytics:test`) and instrumented tests +(`src/androidTest/`, AndroidJUnit4, real device/emulator, run via `:analytics:connectedAndroidTest`). +The patterns below cover the instrumented layer. **Glob**: src/androidTest/java/**/*.java ## Test Setup diff --git a/.cursor/rules/workflows/adding-features.mdc b/.cursor/rules/workflows/adding-features.mdc index 0ecc808a9..a1305c833 100644 --- a/.cursor/rules/workflows/adding-features.mdc +++ b/.cursor/rules/workflows/adding-features.mdc @@ -17,11 +17,11 @@ public void newFeature(String param1, JSONObject properties) { return; } - // Queue for processing - Message msg = Message.obtain(); - msg.what = NEW_FEATURE_MESSAGE; - msg.obj = new NewFeatureDescription(param1, properties, mToken); - mMessages.enqueueMessage(msg); + // Queue for processing via a typed AnalyticsMessages method + // (which internally does mWorker.runMessage(msg) — there is no + // public enqueueMessage; add a typed method for new message types) + mMessages.newFeatureMessage( + new NewFeatureDescription(param1, properties, mToken)); } catch (Exception e) { MPLog.e(LOGTAG, "Exception in newFeature", e); diff --git a/.github/copilot-instructions-guide.md b/.github/copilot-instructions-guide.md index ce3a25693..4e3ccfb8c 100644 --- a/.github/copilot-instructions-guide.md +++ b/.github/copilot-instructions-guide.md @@ -9,7 +9,7 @@ The main file loaded by Copilot for every coding session. Contains: - Critical SDK principles (never crash, thread-safe, minimal deps) - Essential code patterns (visibility, error handling, threading) - Architecture rules (public API, data flow) -- Testing approach (instrumented only) +- Testing approach (JVM unit tests + instrumented tests) Kept under 500 lines to fit in Copilot's context window alongside your actual code. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bca909eea..b9cc3bf5a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -53,14 +53,15 @@ mWorker.runMessage(msg); ``` ## Testing -- **Instrumented tests only** - no unit tests +- **Two test layers**: JVM unit tests (`analytics/src/test/`, run via `:analytics:test`) and instrumented tests (`analytics/src/androidTest/`, require a device/emulator) - Use `BlockingQueue` for async verification - Test with real SQLite, not mocks - Always provide timeout for async operations -- **IMPORTANT**: Run tests from main module using `:connectedAndroidTest` - - All tests: `./gradlew :connectedAndroidTest` - - Specific class: `./gradlew :connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName` - - Specific method: `./gradlew :connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName#testMethodName` +- **IMPORTANT**: Run instrumented tests from the `:analytics` module (not `:analytics:mixpaneldemo`) + - Unit tests: `./gradlew :analytics:test` + - All instrumented tests: `./gradlew :analytics:connectedAndroidTest` + - Specific class: `./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName` + - Specific method: `./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName#testMethodName` ## API Design ```java diff --git a/.github/instructions/code-generation.instructions.md b/.github/instructions/code-generation.instructions.md index 064541f82..4f2708ed6 100644 --- a/.github/instructions/code-generation.instructions.md +++ b/.github/instructions/code-generation.instructions.md @@ -44,11 +44,10 @@ public void newFeature(String param, JSONObject properties) { // 3. Try-catch everything try { - // 4. Queue to background thread - Message msg = Message.obtain(); - msg.what = NEW_FEATURE_MESSAGE; - msg.obj = new FeatureDescription(param, properties, mToken); - mMessages.enqueueMessage(msg); + // 4. Queue to background thread via a typed AnalyticsMessages method + // (which internally does mWorker.runMessage(msg) — there is no public + // enqueueMessage; add a typed method for new message types) + mMessages.featureMessage(new FeatureDescription(param, properties, mToken)); } catch (Exception e) { MPLog.e(LOGTAG, "Failed to process feature", e); diff --git a/.github/instructions/test-generation.instructions.md b/.github/instructions/test-generation.instructions.md index 21840c164..2563f4117 100644 --- a/.github/instructions/test-generation.instructions.md +++ b/.github/instructions/test-generation.instructions.md @@ -1,6 +1,10 @@ # Test Generation Instructions - Mixpanel Android SDK -Generate **instrumented tests only** - no unit tests. All tests require an Android device/emulator. +Choose the right test layer: +- **Unit tests** (`analytics/src/test/`, JUnit/Robolectric, run on the JVM via `:analytics:test`) — prefer these for logic that doesn't need a real device. +- **Instrumented tests** (`analytics/src/androidTest/`, AndroidJUnit4, run via `:analytics:connectedAndroidTest`) — use when the behavior needs a real Android runtime (SQLite, SharedPreferences, lifecycle, concurrency against real components). + +The patterns below illustrate the instrumented layer. ## Test Structure ```java diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 4d90b307a..bbcf8a9e3 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -115,7 +115,8 @@ jobs: rm -f *.deb # Set up environment for running instrumented tests - # The SDK uses instrumented tests exclusively (no unit tests) + # The SDK has JVM unit tests (:analytics:test) plus instrumented tests + # (:analytics:connectedAndroidTest), which need the emulator set up below. - name: Set up test environment run: | # Create AVD directory if it doesn't exist diff --git a/AGENTS.md b/AGENTS.md index 74e63e722..5bbd14a04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,246 +1,221 @@ -# AGENTS.md - Mixpanel Android SDK +# AGENTS.md — Mixpanel Android SDK + +Canonical instructions for AI coding agents (Claude Code, Cursor, Copilot, Codex, etc.) +working on this repository. `CLAUDE.md` imports this file via `@AGENTS.md` — edit **this** +file; put Claude-specific additions (if ever needed) below the import in `CLAUDE.md`. + +**Mixpanel Android SDK** is a production analytics library used by thousands of Android +apps. It prioritizes **reliability, thread safety, and backward compatibility above all +else.** Reliability > features. When in doubt, follow existing patterns. + +## Context map — read on demand, not all at once + +This file is the router. Open the deeper docs only when the task needs them: + +- `.claude/context/codebase-map.md` — where things live (concern → file) +- `.claude/context/discovered-patterns.md` — detailed coding standards +- `.claude/context/architecture/system-design.md` — system architecture +- `.claude/context/workflows/` — feature-development, testing, release workflows +- `.cursor/rules/` — the same rules as Cursor `.mdc` enforcement files +- `.github/copilot-instructions.md` — Copilot persistent guidance +- Nested `AGENTS.md` files in `analytics/src/main/java/com/mixpanel/android/mpmetrics/` + and `analytics/src/androidTest/` — directory-local detail, loaded when working there + +## Project configuration + +- Min SDK 21; Target/Compile SDK 34 (`:session-replay` builds with compileSdk 35) +- Gradle 9.3.1 (wrapper), AGP 8.13.2 (root `build.gradle` classpath), Kotlin 2.1.0 + (`:common` pins language level 2.0), JDK 17 toolchains (`:analytics:mixpaneldemo` targets Java 8) +- Runtime dependencies of `:analytics` are deliberately minimal (see `analytics/build.gradle`): + `mixpanel-android-common`, `androidx.annotation`, `androidx.core`, + `androidx.lifecycle:lifecycle-process`, `io.github.jamsesso:json-logic-java` + (plus a `gson` pin for its transitive JSON dep). **Do not add new runtime deps.** + +### Subprojects + +- **`:analytics`** (`analytics/`) — main `mixpanel-android` SDK. Consumes `:common` via its + **published Maven coordinate** (`com.mixpanel.android:mixpanel-android-common:X.Y.Z`), not + as a `project(':common')` dependency, so `:common` must be released before the main SDK + can pick up changes. Buys back independent snapshot publishing for `:common`. +- **`:common`** — published as `com.mixpanel.android:mixpanel-android-common`. Holds + `MixpanelEventBridge` (Kotlin `SharedFlow` cross-SDK event dispatcher) and a Kotlin + JsonLogic implementation. Versioned independently (own `gradle.properties`). +- **`:openfeature-provider`** — published as `com.mixpanel.android:mixpanel-android-openfeature`. + Consumes the main SDK via its published Maven coordinate. +- **`:session-replay`** (`session-replay/`) — published as + `com.mixpanel.android:mixpanel-android-session-replay`, versioned independently + (own `gradle.properties`, CHANGELOG, README, and CI: `.github/workflows/session-replay-ci.yml`). +- **`:analytics:mixpaneldemo`** and **`:session-replay:sessionreplaydemo`** — sample apps, not published. +- **`build-logic/`** — included build with the `mixpanel.maven-publish` and `mixpanel.ktlint` + convention plugins used by the Kotlin modules. + +For local iteration across modules, swap the Maven dep for the commented-out `project(':...')` +line in the consumer's build script (both `analytics/build.gradle` and +`openfeature-provider/build.gradle.kts` carry one). + +## Environment setup -This file enables AI agents to work autonomously on the Mixpanel Android SDK codebase. It synthesizes patterns from local AI systems into comprehensive cloud execution instructions. - -## Project Overview - -**Mixpanel Android SDK** - A production analytics library used by thousands of Android applications worldwide. The SDK prioritizes reliability, thread safety, and backward compatibility above all else. - -**Critical Context Files:** -- `CLAUDE.md` - Core patterns and conventions -- `.claude/context/discovered-patterns.md` - Detailed coding standards -- `.claude/context/architecture/system-design.md` - System architecture -- `.cursor/rules/` - Behavioral enforcement rules -- `.github/copilot-instructions.md` - Persistent coding guidance - -## Environment Setup +```bash +./gradlew --version # Gradle with JDK 17 +adb devices # connected device/emulator (needed for instrumented tests only) +./gradlew clean build +``` -Before beginning any task: +## Build & test commands ```bash -# Verify environment -./gradlew --version # Should show Gradle with JDK 17 -adb devices # Should show connected device/emulator for tests +# Build the library +./gradlew :analytics:build -# Clean build to ensure fresh state -./gradlew clean +# Unit tests (JVM, no device) — analytics/src/test/, JUnit/Robolectric +./gradlew :analytics:test -# Run quick verification -./gradlew build -``` +# Instrumented tests (require a device/emulator) — analytics/src/androidTest/, AndroidJUnit4 +# IMPORTANT: run from :analytics, NOT :analytics:mixpaneldemo +./gradlew :analytics:connectedAndroidTest -## Core Principles (MANDATORY) +# A single instrumented class / method / methods +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName#testMethodName +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName#testMethod1,testMethod2 -1. **NEVER CRASH THE HOST APP** - Wrap all operations in try-catch, fail silently with logging -2. **THREAD SAFETY** - All public APIs must handle concurrent access correctly -3. **NO EXTERNAL DEPENDENCIES** - Use only Android SDK and Java standard library -4. **BACKWARDS COMPATIBILITY** - Never break existing public APIs -5. **DEFENSIVE PROGRAMMING** - Check nulls, validate inputs, handle edge cases +# Build / install the demo app +./gradlew :analytics:mixpaneldemo:build +./gradlew :analytics:mixpaneldemo:installDebug -## Task Categories +# Docs & coverage +./gradlew :analytics:androidJavadocs +./gradlew :analytics:createDebugCoverageReport # combined coverage; needs device/emulator (CI uses this) +./gradlew :analytics:jacocoTestReport # custom unit-test coverage task (jacoco 0.8.12) -### ✅ GOOD for Delegation +# Lint — review analytics/build/reports/lint-results-debug.html +./gradlew :analytics:lint -**Test Coverage Tasks:** -- "Add comprehensive tests for feature flags functionality" -- "Create thread safety tests for all public APIs" -- "Add instrumented tests for offline mode behavior" +# AnimalSniffer — fails on Java 9+ APIs and unguarded Android calls above minSdk. +# Report: analytics/build/reports/animalsniffer/release.text +./gradlew :analytics:animalsnifferRelease +``` -**Systematic Refactoring:** -- "Update all database operations to use new transaction pattern" -- "Add defensive null checks to all public methods" -- "Implement proper resource cleanup in all try-finally blocks" +### Testing approach -**Documentation:** -- "Generate JavaDoc for all public API methods" -- "Create examples for each People API operation" -- "Document all configuration options with examples" +- **Test source sets:** + - `analytics/src/test/` — **unit tests** (JVM via `:analytics:test`; JUnit 4, Robolectric 4.11.1, Mockito 5.x). + - `analytics/src/androidTest/` — **instrumented tests** (AndroidJUnit4 + espresso/truth/mockito-android, real device/emulator). + - `analytics/src/sharedTest/` — helpers wired into both source sets. +- Async behavior is verified with the **BlockingQueue pattern**; `TestUtils` provides mocks. +- Real database testing (not mocked). Thread-safety validation belongs in instrumented tests. -**Code Quality:** -- "Add MPLog statements to trace event flow" -- "Implement lazy initialization for expensive objects" -- "Ensure all constants follow CAPS_WITH_UNDERSCORES" +## Core principles (MANDATORY) -### ❌ POOR for Delegation +1. **NEVER CRASH THE HOST APP** — wrap operations in try-catch, log via `MPLog`, fail silently, never re-throw. +2. **THREAD SAFETY** — every public API must handle concurrent access. Use dedicated lock objects, not `this`. +3. **MINIMAL DEPENDENCIES** — never add a runtime dependency; the small allowed set is listed under Project configuration. +4. **BACKWARD COMPATIBILITY** — never break existing public APIs. +5. **DEFENSIVE PROGRAMMING** — null-check, validate inputs, handle edge cases, degrade gracefully. -- UI/UX decisions (this is a library) -- Performance optimization without metrics -- Architecture changes -- API design decisions -- Breaking changes +## Conventions -## Code Patterns Reference +- **Visibility:** prefer package-private for internals (a number of existing internals are historically public — don't add new public classes without cause). +- **Fields:** `private final` with `m` prefix (`mContext`). Static nested classes for data models. +- **Context:** always application context (`context.getApplicationContext()`); `WeakReference` for activity/callback refs. +- **Concurrency:** background work runs on dedicated `HandlerThread`s (`AnalyticsWorker` in `AnalyticsMessages`; a second in `FeatureFlagManager`, which also owns a single-thread network executor) with `Message`-based dispatch. No `Service`/`ContentProvider` components. Use dedicated lock objects (not `this`); prefer synchronized blocks over synchronized methods. +- **Resources:** close `Cursor`s and DB handles in `finally`; lazy-init expensive objects. +- **Database:** direct SQLite (no ORM), enum-based table management, age-based cleanup. Queries are built with `rawQuery`. +- **API design:** singleton keyed by instance name (or token) + app context; builder-style `MixpanelOptions`; fluent People/Group ops; method overloading for progressive disclosure. +- **Config hierarchy:** Runtime (`MixpanelOptions`) > Manifest meta-data > compile-time defaults (`MPConfig`). -### Class Structure -```java -package com.mixpanel.android.mpmetrics; - -// Package-private by default -class InternalHelper { - private static final String LOGTAG = "MixpanelAPI.Helper"; - - // Immutable fields - private final Context mContext; - private final Object mLock = new Object(); - - InternalHelper(Context context) { - mContext = context.getApplicationContext(); // Prevent leaks - } -} -``` +## Architecture -### Error Handling -```java -// ALWAYS wrap operations -try { - riskyOperation(); -} catch (Exception e) { - MPLog.e(LOGTAG, "Operation failed", e); - // Continue gracefully - NEVER re-throw -} -``` +Producer-consumer with persistent storage. There are **two network paths**: -### Threading -```java -// Queue to background thread -Message msg = Message.obtain(); -msg.what = ENQUEUE_EVENTS; -msg.obj = new EventDescription(event, properties, token); -mMessages.enqueueMessage(msg); ``` - -### Testing Pattern -```java -@RunWith(AndroidJUnit4.class) -@LargeTest -public class FeatureTest { - private BlockingQueue mMessages; - - @Test - public void testAsync() throws Exception { - mMixpanel.track("Event"); - String message = mMessages.poll(2, TimeUnit.SECONDS); - assertNotNull("Should receive message", message); - } -} +User code → MixpanelAPI ─ tracking ─→ AnalyticsMessages ─→ MPDbAdapter (SQLite queue) + │ └─→ HttpService ─→ Mixpanel servers + └─ feature flags ─→ FeatureFlagManager ─→ HttpService (no SQLite) ``` -## Validation Requirements +**Respect the layering:** `MixpanelAPI` never performs HTTP itself, and tracking never +bypasses `AnalyticsMessages`/`MPDbAdapter`. `AnalyticsMessages` owns *both* the DB adapter +and its poster (`MPDbAdapter` never talks to the network). The only `performRequest` +callers are `AnalyticsMessages` and `FeatureFlagManager`. -Before submitting any PR, you MUST: +- **MixpanelAPI** — singleton entry point (`getInstance()`); events, people, groups, feature flags. +- **AnalyticsMessages** — user-thread↔background message queue; batching, retry, offline. +- **MPDbAdapter** — SQLite persistence and offline queue. +- **PersistentIdentity** — distinct/anonymous IDs, super properties (SharedPreferences). +- **HttpService** (`util/`) — HTTP client; GZIP is opt-in via `MPConfig` (default off); + hardcoded timeouts; 3-attempt retry with short linear backoff. +- **FeatureFlagManager** — TTL-based flag caching (in-memory + SharedPreferences blob), + refreshed on demand (identify/reset, first foreground, TTL expiry) — no periodic timer. -### 1. Build Validation -```bash -./gradlew clean :analytics:build -# Must pass with no errors or warnings -``` +Implementation notes: events batch every ~60s (`FlushInterval`) or on app background; +SQLite queues offline; automatic lifecycle events are configurable; a minimal consumer +ProGuard rule ships in `analytics/proguard.txt` (release builds do not minify). -### 2. Test Validation -```bash -# Run all instrumented tests (requires device/emulator) -# IMPORTANT: Run from the analytics module, not :analytics:mixpaneldemo -./gradlew :analytics:connectedAndroidTest -# All tests must pass +## Working on this codebase -# Run specific test class -./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName +**Good for autonomous work:** test coverage, systematic refactors (defensive null checks, +resource cleanup, tracing logs), JavaDoc/examples, mechanical style conformance. -# Run specific test method -./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName#testMethodName -``` +**Needs a human:** API design, breaking changes, architecture changes, performance work +without metrics, anything UI/UX (this is a library). -### 3. Code Style Check -```bash -./gradlew :analytics:lint -# Review any warnings in analytics/build/reports/lint-results-debug.html +**Common pitfalls:** Activity context (use application context); synchronizing on `this`; +unclosed `Cursor`s; forgetting BlockingQueue for async assertions; ignoring the config hierarchy. -./gradlew :analytics:animalsnifferRelease -# Fails on Java 9+ APIs and unguarded Android calls above minSdk. -# Report at analytics/build/reports/animalsniffer/release.text -``` +### Illustrative patterns -### 4. Manual Verification -```bash -# Test with demo app -./gradlew :analytics:mixpaneldemo:installDebug -# Manually verify the feature works -``` +```java +// Error handling — always wrap, log, continue. NEVER re-throw. +try { riskyOperation(); } +catch (Exception e) { MPLog.e(LOGTAG, "Operation failed", e); } -### 5. PR Checklist -- [ ] No new external dependencies added -- [ ] All new code has try-catch blocks -- [ ] Thread safety verified for concurrent access -- [ ] Instrumented tests added for new features -- [ ] No breaking changes to public APIs -- [ ] JavaDoc added for public methods -- [ ] Follows 'm' prefix convention for fields -- [ ] Uses application context (not Activity) - -## Architecture Boundaries - -**Data Flow:** MixpanelAPI → AnalyticsMessages → MPDbAdapter → HttpService - -**NEVER:** -- Skip layers (e.g., MixpanelAPI calling HttpService directly) -- Create new public classes (keep internals package-private) -- Add Service or ContentProvider components -- Use reflection or dynamic class loading -- Create unit tests (instrumented only) - -## Common Pitfalls - -1. **Activity Context** - Always use application context -2. **Synchronization** - Use dedicated lock objects, not 'this' -3. **Resource Cleanup** - Always close Cursors in finally blocks -4. **Testing** - Must use BlockingQueue for async verification -5. **Configuration** - Respect hierarchy: Runtime > Manifest > Default - -## PR Preparation - -Your PR description should include: - -```markdown -## Summary -- Added comprehensive tests for [feature] -- Improved thread safety in [component] -- Fixed resource leak in [class] - -## Changes -- Added X new test cases -- Updated Y classes to follow patterns -- Cleaned up Z resources properly - -## Testing -- [x] All tests pass locally -- [x] Demo app tested manually -- [x] No memory leaks detected -- [x] Thread safety verified - -## Validation -- `./gradlew :analytics:build` - ✅ Passed -- `./gradlew :analytics:connectedAndroidTest` - ✅ All tests pass -- `./gradlew :analytics:lint` - ✅ No new warnings -- `./gradlew :analytics:animalsnifferRelease` - ✅ No API compatibility violations +// Threading — queue to the background thread (internal to AnalyticsMessages; +// callers use its typed methods like eventsMessage()/peopleMessage()/postToServer()) +Message msg = Message.obtain(); +msg.what = ENQUEUE_EVENTS; +msg.obj = new EventDescription(event, properties, token); +mWorker.runMessage(msg); ``` -## Success Metrics - -Your task is successful when: -1. All existing tests still pass -2. New tests provide meaningful coverage -3. Code follows established patterns exactly -4. No external dependencies introduced -5. PR can be merged without modification - -## Quick Reference - -- **Visibility**: Package-private by default -- **Fields**: Private final with 'm' prefix -- **Errors**: Catch all, log, continue -- **Threading**: HandlerThread + Messages -- **Testing**: Instrumented only with BlockingQueue -- **Context**: Application context always -- **Database**: Try-finally for Cursors -- **API**: Overload for convenience, null for optional +```java +// Async test — BlockingQueue with a timeout +mMixpanel.track("Event"); +String message = mMessages.poll(2, TimeUnit.SECONDS); +assertNotNull("Should receive message", message); +``` -Remember: This SDK is critical infrastructure. Reliability > Features. When in doubt, check the patterns in `.claude/context/discovered-patterns.md`. \ No newline at end of file +## Validation before opening a PR + +1. `./gradlew clean :analytics:build` — no errors/warnings. +2. `./gradlew :analytics:test` — unit tests pass. +3. `./gradlew :analytics:connectedAndroidTest` — instrumented tests pass (device/emulator). +4. `./gradlew :analytics:lint` and `./gradlew :analytics:animalsnifferRelease` — clean. +5. Manually verify via the demo app when behavior changed. + +**PR checklist:** no new runtime deps · try-catch around new operations · thread safety +verified · tests added (unit and/or instrumented) · no public-API breaks · JavaDoc on public +methods · `m`-prefixed fields · application context only. + +## Release process + +Semantic versioning (X.Y.Z) per module, published to Maven Central via the Central Portal. +**Releases are driven entirely by GitHub Actions — there is no local release script.** + +- `VERSION_NAME` lives in each module's `gradle.properties` + (`analytics/`, `common/`, `openfeature-provider/`, `session-replay/`). +- `.github/workflows/prepare-release.yml` — bumps `VERSION_NAME`, updates README, + generates the changelog, and opens a release PR. +- `.github/workflows/release-maven-central.yml` — parameterized by module (`inputs.module`, + per-module tag prefixes): validates the version, builds, tests, publishes to OSSRH staging, + triggers the Portal upload (`publishing_type=user_managed`), and creates a draft GitHub + release/tag. +- Credentials (CI secrets → env): `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD` + (secret `MAVEN_CENTRAL_TOKEN`), `SIGNING_KEY` (`GPG_PRIVATE_KEY`), + `SIGNING_PASSWORD` (`GPG_PASSPHRASE`) — wired in + `build-logic/convention/src/main/kotlin/MavenPublishConventionPlugin.kt`. +- Deployments appear at https://central.sonatype.com/publishing/deployments; finish with a + manual release from the Portal UI. +- Published coordinates: `com.mixpanel.android:mixpanel-android`, + `…:mixpanel-android-common`, `…:mixpanel-android-openfeature`, + `…:mixpanel-android-session-replay`. diff --git a/CLAUDE.md b/CLAUDE.md index a6bf52b31..43c994c2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,219 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build Commands - -```bash -# Build the library -./gradlew :analytics:build - -# Run unit tests -./gradlew :analytics:test - -# Run instrumented tests (requires Android device/emulator) -./gradlew :analytics:connectedAndroidTest - -# Install to local Maven repository -./gradlew :analytics:install - -# Clean build artifacts -./gradlew clean - -# Generate Javadocs -./gradlew :analytics:androidJavadocs - -# Run tests with coverage -./gradlew :analytics:createDebugCoverageReport - -# Lint checks -./gradlew :analytics:lint - -# AnimalSniffer (Java/Android API compatibility — fails on Java 9+ APIs and Android calls -# above minSdk that aren't SDK_INT-gated; report at analytics/build/reports/animalsniffer/) -./gradlew :analytics:animalsnifferRelease - -# Build demo app -./gradlew :analytics:mixpaneldemo:build -``` - -## Testing - -- **No unit tests**: The SDK uses instrumented tests only for real device validation -- **Instrumented tests**: Located in `/analytics/src/androidTest/` (require Android device/emulator) - - Use AndroidJUnit4 runner - - **IMPORTANT**: Run tests from the analytics module (not `:analytics:mixpaneldemo`) using `:analytics:connectedAndroidTest` - - Run all tests: `./gradlew :analytics:connectedAndroidTest` - - Run specific test class: `./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName` - - Run specific test method: `./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName#testMethodName` - - Run multiple test methods: `./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.TestClassName#testMethod1,testMethod2` - - BlockingQueue pattern for async testing - - TestUtils provides mock implementations - -## Architecture Overview - -The Mixpanel Android SDK follows a producer-consumer pattern with persistent storage: - -1. **MixpanelAPI** (Main Entry Point) - - Singleton access via `getInstance()` - - Handles event tracking, people properties, feature flags - - Thread-safe, supports multiple instances - -2. **AnalyticsMessages** (Message Queue) - - Manages communication between user threads and background worker - - Batches events before sending to servers - - Implements retry logic and offline handling - -3. **MPDbAdapter** (Persistence Layer) - - SQLite-based storage for events and people updates - - Handles offline queuing and data retrieval - -4. **PersistentIdentity** (Identity Management) - - Manages distinct IDs, anonymous IDs, super properties - - Uses SharedPreferences for persistence - -5. **HttpService** (Network Layer) - - Handles all HTTP communication with Mixpanel servers - - Supports GZIP compression - - Configurable timeouts and retry logic - -## Key Implementation Details - -- Events are batched and sent every 60 seconds or on app background -- SQLite database stores events when offline -- Feature flags are cached and refreshed periodically -- Automatic events track app lifecycle (configurable) -- ProGuard rules are provided in `analytics/proguard.txt` - -## Release Process - -The project uses semantic versioning (X.Y.Z) and publishes to Maven Central: -- Version is defined in each module's `gradle.properties` as `VERSION_NAME` (analytics: `analytics/gradle.properties`) -- Release script: `./release.sh [version]` -- Published as: `com.mixpanel.android:mixpanel-android:X.Y.Z` - -### Automated Release Process -The `release.sh` script handles the complete release workflow: -1. Updates version in gradle.properties and README.md -2. Builds and publishes artifacts to OSSRH staging -3. Automatically uploads to Maven Central Portal (requires env vars) -4. Creates git tag and updates documentation -5. Updates to next snapshot version - -**Required Environment Variables**: -```bash -export CENTRAL_PORTAL_TOKEN= -export CENTRAL_PORTAL_PASSWORD= -``` - -### Maven Central Portal Setup - -The SDK publishes via the new Maven Central Portal: - -1. **Generate Portal Tokens**: - - Log in to https://central.sonatype.com with your OSSRH credentials - - Navigate to your account settings - - Generate a user token (username and password pair) - - Store these securely in `~/.gradle/gradle.properties`: - ``` - centralPortalToken= - centralPortalPassword= - ``` - - **Security Note**: For enhanced security, consider using encrypted storage options instead of plain text: - - Environment variables: `export CENTRAL_PORTAL_TOKEN=...` - - gradle-credentials-plugin for encrypted storage - - System keychain integration (e.g., macOS Keychain, Windows Credential Store) - - CI/CD secret management systems - -2. **Publishing Process**: - - Artifacts are uploaded to OSSRH staging API: `https://ossrh-staging-api.central.sonatype.com/` - - The Portal upload is triggered via the manual API endpoint - - Deployments appear at https://central.sonatype.com/publishing/deployments - - Manual release to Maven Central is required from the Portal UI (unless using automatic publishing) - -3. **GitHub Actions**: - - Use the `publish-maven.yml` workflow for automated publishing - - Portal tokens should be stored as repository secrets: - - `CENTRAL_PORTAL_TOKEN` (the token username) - - `CENTRAL_PORTAL_PASSWORD` (the token password) - - Publishing types available: - - `user_managed`: Manual release from Portal UI (default) - - `automatic`: Auto-release if validation passes - -4. **Manual Portal Upload** (if automation fails): - ```bash - AUTH_TOKEN=$(echo -n "username:password" | base64) - curl -X POST \ - -H "Authorization: Bearer $AUTH_TOKEN" \ - "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.mixpanel.android?publishing_type=user_managed" - ``` - -## Project Configuration - -- Min SDK: 21 -- Target/Compile SDK: 34 -- Android Gradle Plugin: 8.13.2 -- Kotlin: 2.1.0 - -### Subprojects - -- **`:analytics`** (`analytics/`) — main `mixpanel-android` SDK. Consumes `:common` via its **published Maven coordinate** (`com.mixpanel.android:mixpanel-android-common:X.Y.Z`), not as a `project(':common')` dependency. This means `:common` must be released to Maven Central before the main SDK can pick up changes; the trade-off buys back independent snapshot publishing for `:common` and matches the consumption pattern `:openfeature-provider` uses for the main SDK. -- **`:common`** — published as `com.mixpanel.android:mixpanel-android-common`. Holds `MixpanelEventBridge` (Kotlin `SharedFlow` event dispatcher for cross-SDK consumption) and a Kotlin JsonLogic implementation. Has its own `gradle.properties` and is versioned independently of the main SDK. -- **`:openfeature-provider`** — published as `com.mixpanel.android:mixpanel-android-openfeature`. Consumes the main SDK via its published Maven coordinate. -- **`:analytics:mixpaneldemo`** (`analytics/mixpaneldemo/`) — sample app, not published. - -For local iteration on `:common` against `:analytics` or `:openfeature-provider`, swap the Maven dep for the commented-out `project(':...')` line in the consumer's build script (same workflow `:openfeature-provider/build.gradle.kts` already uses). - -## Key Patterns and Conventions - -### Threading Model -- Single HandlerThread for background processing -- No Service components used -- Message-based communication between threads -- All public APIs are thread-safe - -### Error Handling Philosophy -- **Never crash the host app** - catch all exceptions -- Silent failures with logging for non-critical errors -- Defensive null checking throughout -- Graceful degradation when features unavailable - -### Code Style -- Package-private visibility by default for internal classes -- Member variables prefixed with 'm' (e.g., `mContext`) -- Final fields for immutability -- Static nested classes for data models -- Synchronized blocks over synchronized methods - -### API Design -- Singleton with token-based instances -- Builder pattern for configuration (MixpanelOptions) -- Fluent interfaces for People/Group operations -- Progressive disclosure through method overloading - -### Resource Management -- Always use application context to prevent leaks -- Cursor and database cleanup in finally blocks -- Lazy initialization for expensive objects -- WeakReference for activity/callback references - -### Testing Approach -- Instrumented tests only (no unit tests) -- BlockingQueue pattern for async verification -- Real database testing (not mocked) -- Thread safety validation in tests - -### Configuration Hierarchy -1. Runtime options (MixpanelOptions) -2. AndroidManifest meta-data -3. Compile-time defaults (MPConfig) - -### Database Patterns -- Direct SQLite usage (no ORM) -- Enum-based table management -- Prepared statements for performance -- Automatic cleanup based on data age - -## Memories - -- Ensured CLAUDE.md accurately reflects the most recent changes \ No newline at end of file +@AGENTS.md diff --git a/analytics/src/androidTest/AGENTS.md b/analytics/src/androidTest/AGENTS.md index 92dcb9cab..81e3417bf 100644 --- a/analytics/src/androidTest/AGENTS.md +++ b/analytics/src/androidTest/AGENTS.md @@ -1,182 +1,95 @@ -# AGENTS.md - Instrumented Tests +# AGENTS.md — Instrumented Tests (`analytics/src/androidTest/`) -This directory contains Android instrumented tests that validate the SDK's behavior on real devices. +Directory-local guidance, additive to the root `AGENTS.md`. `CLAUDE.md` here imports this file. -## Test Philosophy +## Scope -**CRITICAL**: This SDK uses instrumented tests ONLY. No unit tests. All tests must run on an Android device or emulator to validate real behavior. +These are the SDK's **instrumented tests** — they run on a real device/emulator and validate +real SQLite, async timing, and framework integration. The SDK **also has JVM unit tests** in +`analytics/src/test/` (JUnit/Robolectric/Mockito, run via `:analytics:test`); put logic that +doesn't need a device there instead. Shared helpers live in `analytics/src/sharedTest/`. -## Test Structure +## Test structure -Every test class MUST follow this pattern: +The real capture idiom (from `MixpanelBasicTest`): a `BlockingQueue` fed by inline +overrides of the layers under test, wired in via `TestUtils.CleanMixpanelAPI`: ```java -@RunWith(AndroidJUnit4.class) -@LargeTest -public class ComponentTest { - private Context mContext; - private MixpanelAPI mMixpanel; - private BlockingQueue mMessages; - - @Before - public void setUp() throws Exception { - // 1. Get instrumentation context (NOT test context) - mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - // 2. Clear all preferences - SharedPreferences prefs = mContext.getSharedPreferences( - "com.mixpanel.android.mpmetrics.MixpanelAPI_" + TEST_TOKEN, - Context.MODE_PRIVATE - ); - prefs.edit().clear().commit(); - - // 3. Create test instance with message queue - mMessages = new LinkedBlockingQueue<>(); - mMixpanel = TestUtils.createMixpanelApiWithMockedMessages( - mContext, mMessages - ); - } - - @After - public void tearDown() { - mMixpanel.flush(); - // Clean up any test data - } -} +final BlockingQueue messages = new LinkedBlockingQueue<>(); + +final MPDbAdapter captureAdapter = + new MPDbAdapter(context, MPConfig.getInstance(context, null)) { + @Override + public int addJSON(JSONObject message, String token, MPDbAdapter.Table table) { + messages.add(message); + return 1; + } + }; + +final AnalyticsMessages captureMessages = + new AnalyticsMessages(context, MPConfig.getInstance(context, null)) { + @Override + public MPDbAdapter makeDbAdapter(Context context) { return captureAdapter; } + }; + +MixpanelAPI mixpanel = + new TestUtils.CleanMixpanelAPI(context, mMockPreferences, "Test token") { + @Override + protected AnalyticsMessages getAnalyticsMessages() { return captureMessages; } + }; ``` -## Async Testing Pattern +`TestUtils` (in `sharedTest/`) provides `CleanMixpanelAPI` (fresh state) and +`createMixpanelAPIWithMockHttpService(context, mockService)` for network-level mocking. +Use the instrumentation **target** context. -**MANDATORY**: Use BlockingQueue for all async verification: +## Key patterns + +**BlockingQueue for async (MANDATORY)** — never bare `Thread.sleep()` as the primary wait: ```java -@Test -public void testAsyncBehavior() throws Exception { - // 1. Perform operation - mMixpanel.track("TestEvent"); - - // 2. Wait with timeout (2 seconds typical) - String message = mMessages.poll(2, TimeUnit.SECONDS); - - // 3. Always provide assertion messages - assertNotNull("Event message should be queued within timeout", message); - - // 4. Verify content - JSONObject event = new JSONObject(message); - assertEquals("Event name should match", "TestEvent", event.getString("event")); -} +mixpanel.track("TestEvent"); +JSONObject message = messages.poll(2, TimeUnit.SECONDS); +assertNotNull("Event message should be queued within timeout", message); +assertEquals("TestEvent", message.getString("event")); ``` -## Common Test Scenarios +**Real SQLite** — exercise `MPDbAdapter` directly against the real DB (`addJSON`, +`generateDataString`, `cleanupEvents`); no mocking the database. -### Basic Functionality Test -```java -@Test -public void testBasicFunctionality() throws Exception { - // Test the happy path - mMixpanel.track("Event", null); - - String message = mMessages.poll(2, TimeUnit.SECONDS); - assertNotNull("Event should be queued", message); -} -``` +**Thread safety** — fan out N threads with a `CountDownLatch`, await, then drain the +queue and assert all events arrived. -### Error Handling Test -```java -@Test -public void testErrorHandling() throws Exception { - // Should not crash - mMixpanel.track(null, null); - mMixpanel.track("", null); - - // May or may not queue messages - assertTrue("App should not crash", true); -} -``` +**Error handling** — feed nulls/empty strings/invalid JSON; the SDK must never crash and +must remain functional afterward. -### Thread Safety Test -```java -@Test -public void testThreadSafety() throws Exception { - final int THREAD_COUNT = 10; - final CountDownLatch latch = new CountDownLatch(THREAD_COUNT); - - // Launch concurrent operations - for (int i = 0; i < THREAD_COUNT; i++) { - final int id = i; - new Thread(() -> { - mMixpanel.track("Event " + id); - latch.countDown(); - }).start(); - } - - // Wait for completion - assertTrue("All threads should complete", - latch.await(5, TimeUnit.SECONDS)); - - // Verify all events recorded - Thread.sleep(500); // Let queue settle - List messages = new ArrayList<>(); - mMessages.drainTo(messages); - assertEquals("All events should be queued", THREAD_COUNT, messages.size()); -} -``` +## Guidelines -### Database Test -```java -@Test -public void testDatabaseOperations() throws Exception { - MPDbAdapter db = new MPDbAdapter(mContext); - - // Test with real SQLite - JSONObject data = new JSONObject(); - data.put("key", "value"); - - int count = db.addJSON(data, "token", MPDbAdapter.Table.EVENTS); - assertEquals("Should add one record", 1, count); - - // Clean up - db.cleanupEvents(System.currentTimeMillis() + 1000, MPDbAdapter.Table.EVENTS); -} -``` +- Real components over mocks; never mock the Android framework. +- Descriptive assertion messages; realistic timeouts (2–5 s). +- Clean state in `setUp` — `TestUtils.cleanUpMixpanelData(context)` wipes DB + prefs; + `TestUtils.EmptyPreferences` stubs referrer prefs. Clean up in `tearDown`/`finally`. +- Test real workflows, edge cases, concurrency, persistence, error recovery — + not implementation details, exact timing, or UI. +- Naming: `[Feature]Test.java`; methods like `testErrorHandling_NullInput_DoesNotCrash`. -## Running Tests +## Running tests + +**IMPORTANT**: run from the `:analytics` module (not `:analytics:mixpaneldemo`). +Class/method selection uses instrumentation runner args — `--tests` does NOT work for +connected tests. ```bash -# Run all tests in this directory +# All instrumented tests ./gradlew :analytics:connectedAndroidTest -# Run specific test class -./gradlew :analytics:connectedAndroidTest --tests "*.MixpanelBasicTest" +# One class / one method / several methods +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.MixpanelBasicTest +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.MixpanelBasicTest#testMethodName +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.MixpanelBasicTest#testMethod1,testMethod2 -# Run with coverage +# Combined coverage report (needs the device/emulator; what CI runs) ./gradlew :analytics:createDebugCoverageReport ``` -## Test Guidelines - -1. **Real Components Only** - No mocking Android framework -2. **Descriptive Assertions** - Always explain what should happen -3. **Reasonable Timeouts** - 2-5 seconds for async operations -4. **Clean State** - Clear preferences and data in setUp -5. **Resource Cleanup** - Always clean up in tearDown/finally - -## Adding New Tests - -When adding tests for new features: - -1. Create test class following naming: `[Feature]Test.java` -2. Test success cases, error cases, and edge cases -3. Verify thread safety if applicable -4. Test offline behavior if relevant -5. Include performance/stress tests for critical paths - -## DO NOT - -- Create unit tests (src/test/) - instrumented only -- Mock Android components - use real implementations -- Use arbitrary Thread.sleep() - use BlockingQueue.poll() -- Skip timeout handling - always use timeouts -- Leave resources open - clean up everything - -Remember: These tests are the safety net for a critical SDK. Comprehensive tests prevent production issues. \ No newline at end of file +These tests are the safety net for a critical SDK — comprehensive tests prevent production issues. diff --git a/analytics/src/androidTest/CLAUDE.md b/analytics/src/androidTest/CLAUDE.md index 71659c31f..43c994c2d 100644 --- a/analytics/src/androidTest/CLAUDE.md +++ b/analytics/src/androidTest/CLAUDE.md @@ -1,307 +1 @@ -# CLAUDE.md - Mixpanel Android SDK Tests - -This file provides specific guidance for writing and maintaining instrumented tests for the Mixpanel Android SDK. - -## Testing Philosophy - -The Mixpanel Android SDK uses **instrumented tests exclusively** - no unit tests. This ensures: -- Real device behavior validation -- Actual SQLite database operations -- True async timing verification -- Android framework integration testing - -## Test Structure - -### Basic Test Setup -```java -@RunWith(AndroidJUnit4.class) -@LargeTest -public class MixpanelFeatureTest { - private static final String TEST_TOKEN = "Test Token"; - private static final int TIMEOUT_SECONDS = 5; - - private MixpanelAPI mMixpanel; - private TestUtils mTestUtils; - - @Before - public void setUp() { - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - mTestUtils = new TestUtils(context); - mMixpanel = mTestUtils.getCleanMixpanelAPI(TEST_TOKEN); - } - - @After - public void tearDown() { - mTestUtils.cleanDatabase(); - mMixpanel.flush(); - } -} -``` - -## Key Testing Patterns - -### 1. BlockingQueue for Async Operations -The most important pattern for testing async SDK behavior: - -```java -public class AnalyticsMessagesTest { - private BlockingQueue mMessages; - - @Before - public void setUp() { - mMessages = new LinkedBlockingQueue<>(); - // Mock the HttpService to capture messages - HttpService mockService = new HttpService() { - @Override - public String performRequest(String endpoint, JSONObject message) { - mMessages.add(new AnalyticsMessageDescription(endpoint, message)); - return "1\n"; - } - }; - } - - @Test - public void testEventQueuing() throws InterruptedException { - mMixpanel.track("Test Event"); - mMixpanel.flush(); - - // Wait for async processing - AnalyticsMessageDescription msg = mMessages.poll(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull("Event should be sent", msg); - assertEquals("Test Event", msg.getMessage().getString("event")); - } -} -``` - -### 2. Database Testing -Test actual SQLite operations: - -```java -@Test -public void testDatabasePersistence() { - // Add events - mMixpanel.track("Event 1"); - mMixpanel.track("Event 2"); - - // Force database write - mMixpanel.flush(); - - // Verify database content - MPDbAdapter db = mTestUtils.getDbAdapter(); - String[] events = db.generateDataString(Table.EVENTS, TEST_TOKEN); - assertEquals(2, events.length); -} -``` - -### 3. Thread Safety Testing -Verify concurrent access handling: - -```java -@Test -public void testConcurrentAccess() throws InterruptedException { - final int THREAD_COUNT = 10; - final CountDownLatch latch = new CountDownLatch(THREAD_COUNT); - final AtomicInteger successCount = new AtomicInteger(0); - - for (int i = 0; i < THREAD_COUNT; i++) { - final int threadId = i; - new Thread(() -> { - try { - mMixpanel.track("Thread " + threadId); - mMixpanel.getPeople().set("thread", threadId); - successCount.incrementAndGet(); - } finally { - latch.countDown(); - } - }).start(); - } - - assertTrue(latch.await(10, TimeUnit.SECONDS)); - assertEquals(THREAD_COUNT, successCount.get()); -} -``` - -### 4. Error Handling Verification -Ensure SDK never crashes: - -```java -@Test -public void testNullHandling() { - // These should all handle gracefully - mMixpanel.track(null); - mMixpanel.track("Event", null); - mMixpanel.getPeople().set(null, "value"); - mMixpanel.getPeople().set("key", null); - - // Verify SDK still functional - mMixpanel.track("Valid Event"); - mMixpanel.flush(); - // Should complete without crashes -} -``` - -## Test Utilities - -### TestUtils Helper Methods -```java -// Get clean instance for each test -MixpanelAPI api = mTestUtils.getCleanMixpanelAPI(token); - -// Clear all data -mTestUtils.cleanDatabase(); - -// Get direct database access -MPDbAdapter db = mTestUtils.getDbAdapter(); - -// Wait for async operations -mTestUtils.waitForAsyncQueue(); -``` - -### Custom Assertions -```java -private void assertEventSent(String eventName, BlockingQueue queue) - throws InterruptedException, JSONException { - JSONObject event = queue.poll(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull("Event should be sent within timeout", event); - assertEquals(eventName, event.getString("event")); -} -``` - -## Testing Guidelines - -### DO Test -- **Real workflows** - How developers actually use the SDK -- **Edge cases** - Null values, empty strings, invalid JSON -- **Concurrency** - Multiple threads accessing same instance -- **Persistence** - Data survives app restart -- **Error recovery** - SDK continues after failures - -### DON'T Test -- **Implementation details** - Focus on public API -- **Timing specifics** - Allow reasonable timeouts -- **Mock everything** - Use real components when possible -- **UI interactions** - This is a data SDK - -## Common Test Scenarios - -### 1. Feature Flag Testing -```java -@Test -public void testFeatureFlagCaching() throws Exception { - // Setup mock response - when(mHttpService.fetchFeatureFlags()).thenReturn(mockFlags); - - // First fetch - mMixpanel.isFeatureEnabled("test_flag"); - - // Verify cached (no second network call) - verify(mHttpService, times(1)).fetchFeatureFlags(); - - // Multiple checks use cache - for (int i = 0; i < 10; i++) { - mMixpanel.isFeatureEnabled("test_flag"); - } - verify(mHttpService, times(1)).fetchFeatureFlags(); -} -``` - -### 2. Identity Management -```java -@Test -public void testIdentityTransition() { - // Start anonymous - String anonId = mMixpanel.getDistinctId(); - assertNotNull(anonId); - - // Identify user - mMixpanel.identify("user123"); - assertEquals("user123", mMixpanel.getDistinctId()); - - // Verify alias created - mMixpanel.flush(); - // Check alias event was sent -} -``` - -### 3. Automatic Events -```java -@Test -public void testAutomaticEvents() { - MixpanelOptions options = new MixpanelOptions() - .setTrackAutomaticEvents(true); - - MixpanelAPI api = MixpanelAPI.getInstance(context, TEST_TOKEN, options); - - // Simulate app lifecycle - api.onActivityCreated(mockActivity, null); - api.onActivityStarted(mockActivity); - - // Verify $app_open event - api.flush(); - // Assert event was tracked -} -``` - -## Performance Testing - -```java -@Test -@LargeTest -public void testBulkEventPerformance() { - long startTime = System.currentTimeMillis(); - - // Track many events - for (int i = 0; i < 1000; i++) { - mMixpanel.track("Bulk Event " + i); - } - - long trackTime = System.currentTimeMillis() - startTime; - assertTrue("Tracking should be fast", trackTime < 1000); - - // Flush to server - startTime = System.currentTimeMillis(); - mMixpanel.flush(); - - long flushTime = System.currentTimeMillis() - startTime; - assertTrue("Flush should complete reasonably", flushTime < 5000); -} -``` - -## Test Naming Conventions - -- `testFeature_Scenario_ExpectedResult` -- `testErrorHandling_NullInput_DoesNotCrash` -- `testConcurrency_MultipleThreads_AllEventsTracked` -- `testPerformance_BulkOperations_CompletesQuickly` - -## Running Tests - -**IMPORTANT**: Run tests from the analytics module (not `:analytics:mixpaneldemo`). Use `:analytics:connectedAndroidTest` to run tests from the library module. - -```bash -# Run all tests (from analytics module) -./gradlew :analytics:connectedAndroidTest - -# Run specific test class (from analytics module) -./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.MixpanelBasicTest - -# Run specific test method within a class (from analytics module) -./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.MixpanelBasicTest#testMethodName - -# Run multiple test methods within a class (comma-separated, from analytics module) -./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.MixpanelBasicTest#testMethod1,testMethod2 - -# Run with coverage -./gradlew :analytics:createDebugCoverageReport -``` - -## Writing New Tests - -1. **Identify the scenario** - What are you testing? -2. **Setup clean state** - Use TestUtils for isolation -3. **Execute the operation** - Call the API under test -4. **Wait for async** - Use BlockingQueue or latches -5. **Assert the outcome** - Verify expected behavior -6. **Clean up** - Reset for next test - -Remember: Tests are documentation. Write them clearly so others understand the SDK's expected behavior. \ No newline at end of file +@AGENTS.md diff --git a/analytics/src/main/java/com/mixpanel/android/mpmetrics/AGENTS.md b/analytics/src/main/java/com/mixpanel/android/mpmetrics/AGENTS.md index 866be6931..d90b78d45 100644 --- a/analytics/src/main/java/com/mixpanel/android/mpmetrics/AGENTS.md +++ b/analytics/src/main/java/com/mixpanel/android/mpmetrics/AGENTS.md @@ -1,135 +1,86 @@ -# AGENTS.md - Core SDK Components - -This file provides focused instructions for AI agents working on the core Mixpanel SDK components. - -## Component Overview - -This directory contains the heart of the Mixpanel Android SDK: -- **MixpanelAPI.java** - Public API facade -- **AnalyticsMessages.java** - Message queue and background processing -- **MPDbAdapter.java** - SQLite persistence layer -- **PersistentIdentity.java** - User identity management -- **HttpService.java** - Network communication - -## Critical Rules for This Directory - -1. **MixpanelAPI Changes** - - This is the ONLY public class - maintain backwards compatibility - - Every public method must be thread-safe - - Add JavaDoc with examples for any new methods - - Never throw exceptions - catch and log - -2. **AnalyticsMessages Pattern** +# AGENTS.md — Core SDK Components (`com.mixpanel.android.mpmetrics`) + +Directory-local guidance, additive to the root `AGENTS.md`. `CLAUDE.md` here imports this file. + +## Component overview + +The heart of the SDK lives here: +- **MixpanelAPI** — public API facade (events, people, groups, feature flags) +- **AnalyticsMessages** — message queue + worker `HandlerThread`; owns both the DB adapter and the HTTP poster +- **MPDbAdapter** — SQLite persistence layer +- **PersistentIdentity** — identity and super properties (SharedPreferences) +- **FeatureFlagManager** — flag caching/fetching (its own worker thread + network executor) + +Network code (`HttpService`, `RemoteService`) lives in `../util/`, not here. + +Note on visibility: several types here are deliberately public API surface +(`MixpanelAPI`, `MixpanelOptions`, `MPConfig`, `FeatureFlagOptions`, `MixpanelFlagVariant`, +`VariantLookupPolicy`, `DeviceIdProvider`, `SuperPropertyUpdate`, `ExceptionHandler`, …). +Keep **new** helpers package-private; every public type is compatibility surface. + +## Critical rules + +1. **MixpanelAPI** — maintain backward compatibility; every public method thread-safe and + never throws. Tracking/mutating operations additionally null-check inputs, check + `hasOptedOutTracking()`, and wrap work in try-catch. Plain accessors (`getToken()`, + `getDistinctId()`, …) skip those checks by design, and opt-out control methods + (`optInTracking()`) must run even while opted out. JavaDoc with examples on new methods. +2. **Thread safety** — dedicated lock objects (`private final Object mLock = new Object()`), + never `synchronized (this)`. +3. **Thread boundaries** — public API on caller thread; message processing, DB, and tracking + network I/O on the `AnalyticsWorker` HandlerThread; flag fetches on FeatureFlagManager's + executor. Never block or touch the DB on the main thread. +4. **Message passing** — inside `AnalyticsMessages`, work is dispatched as Handler messages + (`mWorker.runMessage(msg)`); external callers use its typed methods + (`eventsMessage()`, `peopleMessage()`, `postToServer()`, …), not raw messages. ```java - // Always add new message types following this pattern - private static final int NEW_MESSAGE_TYPE = X; - - public static final class NewDescription { - private final String data; - private final String token; - // Immutable - only constructor and getters - } + Message msg = Message.obtain(); + msg.what = ENQUEUE_EVENTS; + msg.obj = new EventDescription(event, properties, token); + mWorker.runMessage(msg); ``` +5. **Database operations** — `rawQuery`-based; always close cursors in `finally`. -3. **Database Operations** - ```java - // Always follow this pattern in MPDbAdapter - Cursor cursor = null; - try { - cursor = db.query(...); - // Process cursor - } finally { - if (cursor != null) cursor.close(); - } - ``` - -4. **Thread Boundaries** - - Public API methods: Main thread - - Message processing: Worker thread (HandlerThread) - - Database operations: Worker thread only - - Network requests: Spawned from worker thread +## Common tasks -## Common Tasks +**New public API method:** overload for progressive disclosure; for tracking/mutating +methods, validate inputs + check opt-out + try-catch in `MixpanelAPI`; hand off via an +`AnalyticsMessages` typed method backed by a new message type with an immutable description +class; add tests. -### Adding a New Public API Method - -1. Add to MixpanelAPI.java with overloads: - ```java - public void newMethod(String param) { - newMethod(param, null); - } - - public void newMethod(String param, JSONObject properties) { - if (!hasOptedOut()) { - try { - // Validate - if (param == null) { - MPLog.e(LOGTAG, "Invalid param"); - return; - } - // Queue to worker - Message msg = Message.obtain(); - msg.what = NEW_METHOD_MESSAGE; - msg.obj = new MethodDescription(param, properties, mToken); - mMessages.enqueueMessage(msg); - } catch (Exception e) { - MPLog.e(LOGTAG, "Failed", e); - } - } - } - ``` +**Database schema change:** increment `DATABASE_VERSION`; add migration in `onUpgrade` +(never drop existing tables/data); update the `Table` enum if needed; test the upgrade path. -2. Add handler in AnalyticsMessages -3. Add test in androidTest/ +**New configuration option:** add to `MPConfig` (read from manifest `metaData` with a +default), expose in `MixpanelOptions.Builder` if runtime-settable, document the manifest key. -### Modifying Database Schema +## Testing -1. Increment DATABASE_VERSION in MPDbAdapter -2. Add migration in onUpgrade: - ```java - if (oldVersion < NEW_VERSION) { - // Add column or create table - // NEVER drop existing tables/data - } - ``` -3. Update Table enum if new table -4. Test upgrade path from previous version - -### Adding Configuration Option - -1. Add to MPConfig.java: - ```java - private final boolean mNewOption; - - // In constructor - mNewOption = metaData.getBoolean( - "com.mixpanel.android.MPConfig.NewOption", - DEFAULT_VALUE - ); - ``` +```bash +# Unit tests (JVM) +./gradlew :analytics:test -2. Add to MixpanelOptions.Builder -3. Document in AndroidManifest example +# Instrumented — class selection uses instrumentation runner args, NOT --tests +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.MixpanelBasicTest +./gradlew :analytics:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.mixpanel.android.mpmetrics.PersistentIdentityTest +``` -## Testing Requirements +Use the BlockingQueue pattern for async assertions (poll with a timeout). -For ANY change in this directory: +## Performance/behavior facts (verified) -```bash -# Minimum test run -./gradlew :analytics:connectedAndroidTest --tests "com.mixpanel.android.mpmetrics.*" - -# Specific component tests -./gradlew :analytics:connectedAndroidTest --tests "*MixpanelBasicTest" -./gradlew :analytics:connectedAndroidTest --tests "*MPDbAdapterTest" -./gradlew :analytics:connectedAndroidTest --tests "*AnalyticsMessagesTest" -``` +- Event batching every 60 s (`FlushInterval` default), flush on background (configurable) +- HTTP timeouts are **hardcoded** in `HttpService` (2 s/30 s and 15 s/60 s connect/read pairs); + retry is 3 attempts with 100 ms/200 ms delays before attempts 2 and 3 (no delay after the + final attempt — the in-code "100ms, 200ms, 300ms" comment is stale) +- GZIP is opt-in via `MPConfig` (default off) +- Database cleanup is age-based; SharedPreferences values are cached in memory -## Do NOT Modify Without Approval +## Do NOT modify without approval -- DATABASE_VERSION (requires migration testing) +- `DATABASE_VERSION` (requires migration testing) - Public API method signatures (breaks compatibility) - Message type constants (affects message processing) - Table names or schemas (requires migration) -Remember: This is the core of a widely-used SDK. Every change here affects thousands of apps. \ No newline at end of file +This is the core of a widely-used SDK — every change here affects thousands of apps. diff --git a/analytics/src/main/java/com/mixpanel/android/mpmetrics/CLAUDE.md b/analytics/src/main/java/com/mixpanel/android/mpmetrics/CLAUDE.md index 5fdba728c..43c994c2d 100644 --- a/analytics/src/main/java/com/mixpanel/android/mpmetrics/CLAUDE.md +++ b/analytics/src/main/java/com/mixpanel/android/mpmetrics/CLAUDE.md @@ -1,170 +1 @@ -# CLAUDE.md - Mixpanel Core Components - -This file provides specific guidance for working with core SDK components in the `com.mixpanel.android.mpmetrics` package. - -## Component Overview - -This package contains the heart of the Mixpanel Android SDK: -- **MixpanelAPI** - Public entry point (the ONLY public class) -- **AnalyticsMessages** - Message queue and worker thread management -- **MPDbAdapter** - SQLite persistence layer -- **PersistentIdentity** - Identity and super properties management -- **HttpService** - Network communication layer - -## Critical Patterns for Core Components - -### 1. Visibility Rules -```java -// WRONG - Making internal classes public -public class NewHelper { // NO! - -// CORRECT - Package-private by default -class NewHelper { // YES! -``` - -### 2. Thread Safety Requirements -All classes in this package MUST be thread-safe: -```java -// ALWAYS use dedicated lock objects -private final Object mLock = new Object(); - -// NEVER use 'this' for synchronization -synchronized (mLock) { // CORRECT - // critical section -} -``` - -### 3. Never Crash the Host App -```java -// EVERY public method must be defensive -public void track(String event, JSONObject properties) { - try { - if (hasOptedOut()) { - return; - } - if (event == null) { - MPLog.e(LOGTAG, "Event cannot be null"); - return; - } - // implementation - } catch (Exception e) { - MPLog.e(LOGTAG, "Failed to track event", e); - } -} -``` - -### 4. Message Passing Pattern -When modifying AnalyticsMessages or worker thread behavior: -```java -// Use Handler messages, not direct method calls -Message msg = Message.obtain(); -msg.what = ENQUEUE_EVENTS; -msg.obj = new AnalyticsMessageDescription(token, event); -mWorker.runMessage(msg); -``` - -### 5. Database Operations -When working with MPDbAdapter: -```java -Cursor cursor = null; -try { - cursor = db.query(...); - // use cursor -} finally { - if (cursor != null) { - cursor.close(); - } -} -``` - -## Component-Specific Guidelines - -### MixpanelAPI -- This is the ONLY public class - guard its API carefully -- Every public method needs null checks and opt-out checks -- Changes here affect ALL SDK users -- Maintain backward compatibility - -### AnalyticsMessages -- Single HandlerThread for all background work -- Never block the main thread -- Batch operations for efficiency -- Handle offline gracefully - -### MPDbAdapter -- Direct SQLite usage (no ORM) -- Always use transactions for bulk operations -- Clean up old data automatically -- Handle database upgrades carefully - -### PersistentIdentity -- Thread-safe SharedPreferences access -- Lazy loading of values -- Cache for performance -- Never lose user identity - -### HttpService -- Configurable timeouts -- Automatic retry with backoff -- GZIP compression -- Never expose raw responses - -## Testing Requirements - -All changes to core components require instrumented tests: -```java -@RunWith(AndroidJUnit4.class) -@LargeTest -public class ComponentTest { - private BlockingQueue mMessages; - - @Before - public void setUp() { - mMessages = new LinkedBlockingQueue<>(); - } - - @Test - public void testAsyncOperation() throws InterruptedException { - // Trigger async operation - api.track("test"); - - // Wait for completion - String result = mMessages.poll(5, TimeUnit.SECONDS); - assertNotNull(result); - } -} -``` - -## Common Pitfalls - -### DON'T -- Create new public classes -- Throw exceptions from public methods -- Use Activity context (memory leaks) -- Access database on main thread -- Create new threads (use HandlerThread) - -### DO -- Keep classes package-private -- Catch and log all exceptions -- Use application context -- Batch database operations -- Use message passing for async work - -## Making Changes - -1. **Before modifying**, understand the component's role in the system -2. **Maintain thread safety** - these are core components -3. **Test on real devices** - emulators hide timing issues -4. **Consider backward compatibility** - SDK is widely used -5. **Update tests** - every change needs test coverage - -## Performance Considerations - -- Event batching happens every 60 seconds -- Database cleanup runs periodically -- HTTP requests timeout after 10 seconds -- SharedPreferences cached in memory -- Minimize synchronization scope - -Remember: These core components are the foundation of the SDK. Changes here have the highest impact and risk. Be extra careful with thread safety, error handling, and backward compatibility. \ No newline at end of file +@AGENTS.md