From 1175d2f7c3ba800c5c38726a2637a2a789523495 Mon Sep 17 00:00:00 2001 From: Patricio Date: Mon, 1 Sep 2025 09:52:11 +0200 Subject: [PATCH] claude mds instrumentation --- CLAUDE.md | 332 +++++++++++++++++++++++++++++++ snowplow-demo-kotlin/CLAUDE.md | 322 ++++++++++++++++++++++++++++++ snowplow-tracker/CLAUDE.md | 349 +++++++++++++++++++++++++++++++++ 3 files changed, 1003 insertions(+) create mode 100644 CLAUDE.md create mode 100644 snowplow-demo-kotlin/CLAUDE.md create mode 100644 snowplow-tracker/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..be2a3946f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,332 @@ +# Snowplow Android Tracker - CLAUDE.md + +## Project Overview + +The Snowplow Android Tracker is an analytics SDK for collecting behavioral event data from Android applications and sending it to Snowplow collectors. It provides comprehensive event tracking capabilities including ecommerce, media playback, screen views, and custom events. The tracker supports multiple concurrent tracker instances, remote configuration, and WebView integration. + +## Development Commands + +```bash +# Build the project +./gradlew build + +# Run unit tests +./gradlew test + +# Run Android instrumentation tests +./gradlew connectedAndroidTest + +# Generate API documentation +./gradlew dokkaHtml + +# Clean build artifacts +./gradlew clean + +# Publish to Maven repository +./gradlew publishToMavenLocal +``` + +## Architecture + +The tracker follows a layered architecture with clear separation of concerns: + +1. **Public API Layer** (`com.snowplowanalytics.snowplow`): User-facing API with Snowplow singleton and configuration classes +2. **Core Layer** (`com.snowplowanalytics.core`): Internal implementation including tracker, emitter, and state management +3. **Event Layer**: Event types and payload construction +4. **Network Layer**: HTTP communication with collectors +5. **Storage Layer**: SQLite-based event persistence + +### Key Components + +- **Tracker**: Core tracking engine managing event creation and processing +- **Emitter**: Batches and sends events to the collector +- **Session**: Manages user session state and lifecycle +- **StateManager**: Coordinates state machines for various tracking features +- **EventStore**: SQLite persistence for offline event storage + +## Core Architectural Principles + +### 1. Namespace-based Multi-Tracker Support +```kotlin +// ✅ Create trackers with unique namespaces +val tracker1 = Snowplow.createTracker(context, "ecommerce", network1) +val tracker2 = Snowplow.createTracker(context, "analytics", network2) +// ❌ Don't reuse namespaces without removing first +val duplicate = Snowplow.createTracker(context, "ecommerce", network3) // Will reset existing +``` + +### 2. Configuration Immutability +```kotlin +// ✅ Configure at creation time +val tracker = Snowplow.createTracker(context, namespace, network, + TrackerConfiguration(appId).apply { base64 = false }) +// ❌ Don't modify configuration after creation +trackerConfig.base64 = true // Has no effect after tracker creation +``` + +### 3. Event Builder Pattern +```kotlin +// ✅ Use builder pattern for events +val event = Structured("category", "action") + .label("label") + .property("property") + .value(10.0) + .entities(listOf(customContext)) +// ❌ Don't modify events after tracking +tracker.track(event) +event.label = "newLabel" // Too late, event already sent +``` + +### 4. Asynchronous Event Processing +```kotlin +// ✅ Events are queued and sent asynchronously +tracker.track(event) // Returns immediately +// ❌ Don't expect synchronous sending +tracker.track(event) +// Event may not be sent yet at this point +``` + +## Layer Organization & Responsibilities + +### Public API Layer (`snowplow` package) +- User-facing configuration classes +- Event type definitions +- Controller interfaces for runtime management +- Snowplow singleton for tracker management + +### Core Implementation Layer (`core` package) +- Internal tracker implementation +- Emitter and network management +- State machine implementations +- Platform-specific utilities + +### Event Processing Pipeline +1. Event Creation → 2. Enrichment → 3. Validation → 4. Storage → 5. Batching → 6. Network Transmission + +## Critical Import Patterns + +### Configuration Imports +```kotlin +// ✅ Correct configuration imports +import com.snowplowanalytics.snowplow.Snowplow +import com.snowplowanalytics.snowplow.configuration.* +import com.snowplowanalytics.snowplow.network.HttpMethod +// ❌ Don't import internal core classes +import com.snowplowanalytics.core.tracker.Tracker // Internal only +``` + +### Event Imports +```kotlin +// ✅ Correct event imports +import com.snowplowanalytics.snowplow.event.* +import com.snowplowanalytics.snowplow.payload.SelfDescribingJson +// ❌ Don't mix with core event classes +import com.snowplowanalytics.core.event.* // Internal processing +``` + +## Essential Library Patterns + +### 1. Tracker Initialization Pattern +```kotlin +// ✅ Standard initialization +val networkConfig = NetworkConfiguration("https://collector.example.com") +val trackerConfig = TrackerConfiguration("app-id") + .sessionContext(true) + .platformContext(true) +val tracker = Snowplow.createTracker(context, "namespace", networkConfig, trackerConfig) +``` + +### 2. Event Tracking Pattern +```kotlin +// ✅ Track with entities +val event = ScreenView("home_screen") + .entities(listOf( + SelfDescribingJson("iglu:com.example/context/jsonschema/1-0-0", mapOf("key" to "value")) + )) +tracker.track(event) +``` + +### 3. Remote Configuration Pattern +```kotlin +// ✅ Setup with remote config +Snowplow.setup(context, + RemoteConfiguration("https://config.example.com/config.json", HttpMethod.GET), + listOf(defaultBundle) +) { result -> + val namespaces = result.first + val configState = result.second +} +``` + +### 4. Subject Configuration Pattern +```kotlin +// ✅ Configure subject (user) properties +tracker.subject.apply { + userId = "user123" + screenResolution = Size(1920, 1080) + language = "en-US" +} +``` + +## Model Organization Pattern + +### Event Hierarchy +```kotlin +// Base event classes +abstract class AbstractEvent : Event +abstract class AbstractPrimitive : AbstractEvent() // For structured events +abstract class AbstractSelfDescribing : AbstractEvent() // For self-describing events + +// Concrete events extend appropriate base +class ScreenView : AbstractSelfDescribing() +class Structured : AbstractPrimitive() +``` + +### Entity Pattern +```kotlin +// ✅ Create entities with schema and data +val entity = SelfDescribingJson( + schema = "iglu:com.example/entity/jsonschema/1-0-0", + data = mapOf("property" to "value") +) +// ❌ Don't use raw JSON strings +val entity = SelfDescribingJson(jsonString) // Use structured data +``` + +## Common Pitfalls & Solutions + +### 1. Event Store Zombie Events +```kotlin +// ❌ Problem: Changing namespace leaves orphaned events +val tracker1 = createTracker(context, "oldNamespace", network) +// Later... +val tracker2 = createTracker(context, "newNamespace", network) +// Old events stuck in database + +// ✅ Solution: Clean up orphaned events +SQLiteEventStore.removeUnsentEventsExceptForNamespaces(listOf("newNamespace")) +``` + +### 2. Configuration Timing +```kotlin +// ❌ Problem: Configuring after creation +val tracker = Snowplow.createTracker(context, namespace, network) +tracker.emitter.bufferOption = BufferOption.LargeGroup // No effect + +// ✅ Solution: Configure during creation +val emitterConfig = EmitterConfiguration() + .bufferOption(BufferOption.LargeGroup) +val tracker = Snowplow.createTracker(context, namespace, network, emitterConfig) +``` + +### 3. WebView Integration +```kotlin +// ❌ Problem: Forgetting to subscribe WebView +webView.loadUrl("https://example.com") + +// ✅ Solution: Subscribe before loading +Snowplow.subscribeToWebViewEvents(webView) +webView.loadUrl("https://example.com") +``` + +### 4. Session Management +```kotlin +// ❌ Problem: Not handling background/foreground transitions +// Sessions may timeout incorrectly + +// ✅ Solution: Use lifecycle-aware session configuration +val sessionConfig = SessionConfiguration( + TimeMeasure(30, TimeUnit.MINUTES), // Foreground timeout + TimeMeasure(30, TimeUnit.MINUTES) // Background timeout +) +``` + +## File Structure Template + +``` +snowplow-android-tracker/ +├── snowplow-tracker/ # Main library module +│ ├── src/main/java/ +│ │ ├── com/snowplowanalytics/ +│ │ │ ├── snowplow/ # Public API +│ │ │ │ ├── configuration/ # Configuration classes +│ │ │ │ ├── controller/ # Runtime controllers +│ │ │ │ ├── event/ # Event types +│ │ │ │ ├── ecommerce/ # Ecommerce tracking +│ │ │ │ ├── media/ # Media tracking +│ │ │ │ └── Snowplow.kt # Main entry point +│ │ │ └── core/ # Internal implementation +│ │ │ ├── emitter/ # Event batching/sending +│ │ │ ├── tracker/ # Core tracker logic +│ │ │ └── statemachine/ # State management +│ └── src/androidTest/ # Instrumentation tests +├── snowplow-demo-kotlin/ # Kotlin demo app +├── snowplow-demo-java/ # Java demo app +├── snowplow-demo-compose/ # Compose demo app +└── build.gradle # Root build configuration +``` + +## Quick Reference + +### Tracker Initialization Checklist +- [ ] Create NetworkConfiguration with collector URL +- [ ] Create TrackerConfiguration with app ID +- [ ] Add optional configurations (Session, Subject, GlobalContexts) +- [ ] Call Snowplow.createTracker with unique namespace +- [ ] Store TrackerController reference for runtime control + +### Event Tracking Checklist +- [ ] Choose appropriate event type (ScreenView, Structured, SelfDescribing, etc.) +- [ ] Set required event properties +- [ ] Add custom entities if needed +- [ ] Call tracker.track(event) +- [ ] Handle any tracking failures via EmitterController + +### Common Configuration Objects +- `NetworkConfiguration`: Collector endpoint and HTTP method +- `TrackerConfiguration`: Core tracker settings +- `SessionConfiguration`: Session timeout and lifecycle +- `EmitterConfiguration`: Batching and retry settings +- `SubjectConfiguration`: User properties +- `GlobalContextsConfiguration`: Automatic context addition +- `GdprConfiguration`: GDPR context management + +## Contributing to CLAUDE.md + +When adding or updating content in this document, please follow these guidelines: + +### File Size Limit +- **CLAUDE.md must not exceed 40KB** (currently ~19KB) +- Check file size after updates: `wc -c CLAUDE.md` +- Remove outdated content if approaching the limit + +### Code Examples +- Keep all code examples **4 lines or fewer** +- Focus on the essential pattern, not complete implementations +- Use `// ❌` and `// ✅` to clearly show wrong vs right approaches + +### Content Organization +- Add new patterns to existing sections when possible +- Create new sections sparingly to maintain structure +- Update the architectural principles section for major changes +- Ensure examples follow current codebase conventions + +### Quality Standards +- Test any new patterns in actual code before documenting +- Verify imports and syntax are correct for the codebase +- Keep language concise and actionable +- Focus on "what" and "how", minimize "why" explanations + +### Multiple CLAUDE.md Files +- **Directory-specific CLAUDE.md files** can be created for specialized modules +- Follow the same structure and guidelines as this root CLAUDE.md +- Keep them focused on directory-specific patterns and conventions +- Maximum 20KB per directory-specific CLAUDE.md file + +### Instructions for LLMs +When editing files in this repository, **always check for CLAUDE.md guidance**: + +1. **Look for CLAUDE.md in the same directory** as the file being edited +2. **If not found, check parent directories** recursively up to project root +3. **Follow the patterns and conventions** described in the applicable CLAUDE.md +4. **Prioritize directory-specific guidance** over root-level guidance when conflicts exist \ No newline at end of file diff --git a/snowplow-demo-kotlin/CLAUDE.md b/snowplow-demo-kotlin/CLAUDE.md new file mode 100644 index 000000000..0e9c08a5c --- /dev/null +++ b/snowplow-demo-kotlin/CLAUDE.md @@ -0,0 +1,322 @@ +# Snowplow Kotlin Demo App - CLAUDE.md + +## Demo App Overview + +The Kotlin demo app showcases best practices for integrating the Snowplow Android Tracker in a Kotlin application. It demonstrates event tracking, media tracking, configuration management, and various tracking scenarios using idiomatic Kotlin patterns. + +## Key Implementation Patterns + +### Tracker Initialization +```kotlin +// ✅ Initialize tracker with Kotlin DSL style +private fun setupTracker() { + val networkConfig = NetworkConfiguration(collectorEndpoint, HttpMethod.POST) + val trackerConfig = TrackerConfiguration(appId).apply { + base64Encoding = false + sessionContext = true + platformContext = true + } + tracker = Snowplow.createTracker(this, namespace, networkConfig, trackerConfig) +} +``` + +### Event Tracking Patterns +```kotlin +// ✅ Use Kotlin's apply for fluent configuration +fun trackScreenView(name: String) { + tracker.track(ScreenView(name).apply { + entities = mutableListOf(createCustomContext()) + trueTimestamp = System.currentTimeMillis() + }) +} +``` + +### Extension Functions +```kotlin +// ✅ Create extension functions for common patterns +fun TrackerController.trackUserAction(action: String, category: String) { + track(Structured(category, action).apply { + label = "user-interaction" + value = 1.0 + }) +} +``` + +### Coroutine Integration +```kotlin +// ✅ Use coroutines for async operations +lifecycleScope.launch { + withContext(Dispatchers.IO) { + tracker.track(event) + } + withContext(Dispatchers.Main) { + updateUI() + } +} +``` + +## Media Tracking Implementation + +### Video Player Integration +```kotlin +// ✅ Implement media tracking controller +class VideoViewController(private val videoView: VideoView) { + private lateinit var mediaTracking: MediaTracking + + fun startTracking(tracker: TrackerController) { + val config = MediaTrackingConfiguration( + id = "video-${System.currentTimeMillis()}", + player = MediaPlayerEntity("android-videoplayer") + ).apply { + boundaries = listOf(10, 25, 50, 75) + captureEvents = listOf(MediaEvent.PLAY, MediaEvent.PAUSE) + } + mediaTracking = tracker.media.startMediaTracking(config) + } +} +``` + +### Media Event Handling +```kotlin +// ✅ Track media events with player state +videoView.setOnPreparedListener { player -> + mediaTracking.track(MediaReadyEvent()) + mediaTracking.update(player = MediaPlayerEntity( + duration = player.duration.toDouble() / 1000 + )) +} +``` + +## Activity Patterns + +### Base Activity Pattern +```kotlin +// ✅ Create base activity for common tracking +abstract class BaseTrackingActivity : AppCompatActivity() { + protected val tracker: TrackerController by lazy { + Snowplow.defaultTracker ?: throw IllegalStateException("Tracker not initialized") + } + + override fun onResume() { + super.onResume() + trackScreenView() + } + + abstract fun trackScreenView() +} +``` + +### Lifecycle-aware Tracking +```kotlin +// ✅ Use lifecycle observers +class ScreenTrackingObserver(private val tracker: TrackerController) : + DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + tracker.track(Foreground()) + } + override fun onStop(owner: LifecycleOwner) { + tracker.track(Background()) + } +} +``` + +## Configuration Management + +### BuildConfig Usage +```kotlin +// ✅ Use BuildConfig for environment-specific values +object TrackerConfig { + val COLLECTOR_URL = if (BuildConfig.DEBUG) { + "https://staging-collector.example.com" + } else { + "https://collector.example.com" + } +} +``` + +### Shared Preferences Integration +```kotlin +// ✅ Store user preferences +class TrackingPreferences(context: Context) { + private val prefs = context.getSharedPreferences("tracking", Context.MODE_PRIVATE) + + var isTrackingEnabled: Boolean + get() = prefs.getBoolean("enabled", true) + set(value) = prefs.edit().putBoolean("enabled", value).apply() +} +``` + +## UI Integration Patterns + +### Click Tracking +```kotlin +// ✅ Track UI interactions +binding.purchaseButton.setOnClickListener { + tracker.track(Structured("ui", "click").apply { + label = "purchase_button" + property = productId + value = productPrice + }) + processPurchase() +} +``` + +### Form Tracking +```kotlin +// ✅ Track form submissions +fun trackFormSubmission(formData: Map) { + val entity = SelfDescribingJson( + "iglu:com.example/form_submission/jsonschema/1-0-0", + formData + ) + tracker.track(SelfDescribing(entity)) +} +``` + +## Testing Patterns + +### Mock Tracker for Testing +```kotlin +// ✅ Create test doubles +class MockTrackerController : TrackerController { + val trackedEvents = mutableListOf() + + override fun track(event: Event): UUID? { + trackedEvents.add(event) + return UUID.randomUUID() + } +} +``` + +### UI Testing with Tracking +```kotlin +// ✅ Verify tracking in UI tests +@Test +fun testButtonClickTracking() { + onView(withId(R.id.button)).perform(click()) + + verify(mockTracker).track(argThat { event -> + event is Structured && event.action == "click" + }) +} +``` + +## Common Demo Pitfalls + +### 1. Hardcoded Configuration +```kotlin +// ❌ Problem: Hardcoded values +val tracker = Snowplow.createTracker(this, "demo", "http://localhost:9090") + +// ✅ Solution: Use configuration +val tracker = Snowplow.createTracker(this, BuildConfig.NAMESPACE, BuildConfig.COLLECTOR_URL) +``` + +### 2. Missing Error Handling +```kotlin +// ❌ Problem: No error handling +tracker.track(event) + +// ✅ Solution: Handle potential failures +try { + tracker.track(event) +} catch (e: Exception) { + Log.e(TAG, "Failed to track event", e) +} +``` + +### 3. Memory Leaks +```kotlin +// ❌ Problem: Holding activity reference +class EventTracker(private val activity: Activity) { + fun track() { /* ... */ } +} + +// ✅ Solution: Use application context +class EventTracker(private val context: Context) { + private val appContext = context.applicationContext +} +``` + +## Demo-Specific Features + +### Debug Menu +```kotlin +// ✅ Provide debug controls +class DebugMenuActivity : AppCompatActivity() { + fun setupDebugControls() { + binding.flushEvents.setOnClickListener { + tracker.emitter.flush() + } + binding.pauseTracking.setOnClickListener { + tracker.pause() + } + } +} +``` + +### Event Inspection +```kotlin +// ✅ Log events in debug mode +if (BuildConfig.DEBUG) { + tracker.emitter.requestCallback = { request -> + Log.d(TAG, "Sending events: ${request.payload}") + } +} +``` + +## Quick Reference - Demo Implementation + +### Setting Up Demo Checklist +- [ ] Configure collector endpoint in gradle.properties +- [ ] Set up namespace and app ID +- [ ] Initialize tracker in Application class +- [ ] Add network security config for local testing +- [ ] Configure ProGuard rules if using R8 + +### Adding Demo Features Checklist +- [ ] Create dedicated activity for feature +- [ ] Implement tracking for all user interactions +- [ ] Add UI to display tracking status +- [ ] Include debug controls for testing +- [ ] Document expected tracking behavior + +## Contributing to CLAUDE.md + +When adding or updating content in this document, please follow these guidelines: + +### File Size Limit +- **CLAUDE.md must not exceed 40KB** (currently ~19KB) +- Check file size after updates: `wc -c CLAUDE.md` +- Remove outdated content if approaching the limit + +### Code Examples +- Keep all code examples **4 lines or fewer** +- Focus on the essential pattern, not complete implementations +- Use `// ❌` and `// ✅` to clearly show wrong vs right approaches + +### Content Organization +- Add new patterns to existing sections when possible +- Create new sections sparingly to maintain structure +- Update the architectural principles section for major changes +- Ensure examples follow current codebase conventions + +### Quality Standards +- Test any new patterns in actual code before documenting +- Verify imports and syntax are correct for the codebase +- Keep language concise and actionable +- Focus on "what" and "how", minimize "why" explanations + +### Multiple CLAUDE.md Files +- **Directory-specific CLAUDE.md files** can be created for specialized modules +- Follow the same structure and guidelines as this root CLAUDE.md +- Keep them focused on directory-specific patterns and conventions +- Maximum 20KB per directory-specific CLAUDE.md file + +### Instructions for LLMs +When editing files in this repository, **always check for CLAUDE.md guidance**: + +1. **Look for CLAUDE.md in the same directory** as the file being edited +2. **If not found, check parent directories** recursively up to project root +3. **Follow the patterns and conventions** described in the applicable CLAUDE.md +4. **Prioritize directory-specific guidance** over root-level guidance when conflicts exist \ No newline at end of file diff --git a/snowplow-tracker/CLAUDE.md b/snowplow-tracker/CLAUDE.md new file mode 100644 index 000000000..5b446dbc7 --- /dev/null +++ b/snowplow-tracker/CLAUDE.md @@ -0,0 +1,349 @@ +# Snowplow Tracker Module - CLAUDE.md + +## Module Overview + +The `snowplow-tracker` module is the core Android library that implements the Snowplow event tracking SDK. It provides the complete implementation of event tracking, network communication, state management, and data persistence. This module is published as a standalone AAR library that applications can include as a dependency. + +## Module Structure + +### Package Organization + +``` +com.snowplowanalytics/ +├── snowplow/ # Public API surface +│ ├── configuration/ # User-facing configuration classes +│ ├── controller/ # Runtime control interfaces +│ ├── event/ # Event type definitions +│ ├── payload/ # Payload construction +│ └── Snowplow.kt # Main entry point +└── core/ # Internal implementation (not public API) + ├── emitter/ # Event batching and sending + ├── tracker/ # Core tracking logic + ├── session/ # Session management + └── statemachine/ # State coordination +``` + +## Testing Patterns + +### Instrumentation Test Structure +```kotlin +// ✅ Use test utilities and mocks +class TrackerTest { + @Before fun setUp() { + TestUtils.createSessionSharedPreferences(context, namespace) + tracker = createTestTracker() + } + @Test fun testEventTracking() { + val mockEventStore = MockEventStore() + // Test implementation + } +} +``` + +### Mock Implementations +```kotlin +// ✅ Create focused mock implementations +class MockNetworkConnection : NetworkConnection { + var sendingCount = AtomicInteger(0) + override fun sendRequests(requests: List) { + sendingCount.addAndGet(requests.size) + } +} +``` + +## Event Implementation Patterns + +### Creating New Event Types +```kotlin +// ✅ Extend appropriate base class +class CustomEvent(val customProperty: String) : AbstractSelfDescribing() { + override val schema = "iglu:com.example/custom/jsonschema/1-0-0" + override val dataPayload: Map + get() = mapOf("custom_property" to customProperty) +} +// ❌ Don't implement Event interface directly +class BadEvent : Event { } // Missing required functionality +``` + +### Event Processing Lifecycle +```kotlin +// ✅ Implement lifecycle methods for stateful events +override fun beginProcessing(tracker: Tracker) { + // Add processing-time data + isProcessing = true +} +override fun endProcessing(tracker: Tracker) { + // Clean up after processing + isProcessing = false +} +``` + +## State Machine Patterns + +### Implementing State Machines +```kotlin +// ✅ Extend StateMachineInterface +class CustomStateMachine : StateMachineInterface { + override val identifier = "CustomStateMachine" + override val subscribedEventSchemasForTransitions = listOf("iglu:com.snowplowanalytics.*/event/*/1-*-*") + + override fun transition(event: Event, state: State?): State? { + // Implement state transition logic + return CustomState() + } +} +``` + +### State Management +```kotlin +// ✅ Use StateManager for coordination +stateManager.addOrReplaceStateMachine(CustomStateMachine()) +val state = stateManager.trackerState.getState(CustomStateMachine.ID) +``` + +## Network Layer Patterns + +### Custom Network Implementation +```kotlin +// ✅ Implement NetworkConnection interface +class CustomNetworkConnection : NetworkConnection { + override fun sendRequests(requests: List) { + requests.forEach { request -> + // Custom sending logic + val result = sendRequest(request) + request.callback?.onComplete(result) + } + } +} +``` + +### Request Handling +```kotlin +// ✅ Use RequestCallback for async results +val request = Request(payload, emitterUri).apply { + callback = object : RequestCallback { + override fun onComplete(result: RequestResult) { + if (result.isSuccessful) handleSuccess() + else handleFailure() + } + } +} +``` + +## Storage Layer Patterns + +### Event Store Implementation +```kotlin +// ✅ SQLiteEventStore usage +val eventStore = SQLiteEventStore(context, namespace).apply { + // Events are automatically persisted +} +// ❌ Don't access database directly +val db = SQLiteDatabase.openDatabase(...) // Use EventStore abstraction +``` + +### Event Persistence +```kotlin +// ✅ Events are persisted before sending +tracker.track(event) // Saved to EventStore +// Network failure doesn't lose events +emitter.flush() // Retry from EventStore +``` + +## Platform Context Management + +### Custom Platform Context Properties +```kotlin +// ✅ Use PlatformContextRetriever for custom values +val retriever = PlatformContextRetriever().apply { + appleIdfa = { "custom-idfa-value" } + deviceManufacturer = { "Custom Manufacturer" } +} +val tracker = Tracker(emitter, namespace, appId, + platformContextRetriever = retriever, context = context) +``` + +### Platform Context Properties Selection +```kotlin +// ✅ Select specific properties to track +val properties = listOf( + PlatformContextProperty.DEVICE_MODEL, + PlatformContextProperty.OS_VERSION, + PlatformContextProperty.APP_VERSION +) +``` + +## Emitter Configuration Patterns + +### Buffer Management +```kotlin +// ✅ Configure buffer options +val emitterConfig = EmitterConfiguration() + .bufferOption(BufferOption.LargeGroup) // 25 events + .emitRange(10) // Send 10 events at a time + .byteLimitPost(40_000) // 40KB POST limit +``` + +### Custom Emitter Implementation +```kotlin +// ✅ Extend Emitter class +class CustomEmitter(context: Context, namespace: String) : + Emitter(networkConnection, context, namespace) { + override fun flush() { + // Custom flush logic + super.flush() + } +} +``` + +## Common Implementation Pitfalls + +### 1. Direct Core Package Access +```kotlin +// ❌ Problem: Using internal classes +import com.snowplowanalytics.core.tracker.Tracker +val tracker = Tracker(...) // Internal API + +// ✅ Solution: Use public API +import com.snowplowanalytics.snowplow.Snowplow +val tracker = Snowplow.createTracker(...) +``` + +### 2. Synchronous Event Sending +```kotlin +// ❌ Problem: Expecting immediate send +tracker.track(event) +assert(mockServer.requestCount == 1) // May fail + +// ✅ Solution: Wait for async processing +tracker.track(event) +tracker.emitter.flush() +Thread.sleep(100) // Or use CountDownLatch +``` + +### 3. State Machine Conflicts +```kotlin +// ❌ Problem: Overlapping event schemas +stateMachine1.subscribedEventSchemasForTransitions = listOf("*") +stateMachine2.subscribedEventSchemasForTransitions = listOf("*") + +// ✅ Solution: Use specific schemas +stateMachine1.subscribedEventSchemasForTransitions = + listOf("iglu:com.example/specific/*/1-*-*") +``` + +## Build Configuration + +### Gradle Dependencies +```kotlin +// ✅ Module dependencies +dependencies { + implementation 'androidx.annotation:annotation:1.6.0' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + + // Testing + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.7.2' +} +``` + +### ProGuard Rules +``` +# ✅ Keep public API classes +-keep class com.snowplowanalytics.snowplow.** { *; } +# ❌ Don't keep internal core classes +-keep class com.snowplowanalytics.core.** { *; } # Too broad +``` + +## Module-Specific Conventions + +### Kotlin-Java Interoperability +```kotlin +// ✅ Use @JvmStatic for Java compatibility +object Snowplow { + @JvmStatic + fun createTracker(...): TrackerController { } +} + +// ✅ Use @JvmOverloads for optional parameters +@JvmOverloads +fun createTracker( + context: Context, + namespace: String, + endpoint: String, + method: HttpMethod = HttpMethod.POST +) +``` + +### Thread Safety +```kotlin +// ✅ Use synchronized for shared state +@Synchronized +private fun registerInstance(serviceProvider: ServiceProvider) { } + +// ✅ Use atomic operations +private val _dataCollection = AtomicBoolean(true) +``` + +## Quick Reference - Module Implementation + +### Adding New Event Type Checklist +- [ ] Create event class extending AbstractEvent/AbstractSelfDescribing/AbstractPrimitive +- [ ] Define schema (for self-describing events) +- [ ] Implement dataPayload property +- [ ] Add builder methods for fluent API +- [ ] Create corresponding test in androidTest + +### Adding New Configuration Checklist +- [ ] Create configuration class implementing Configuration +- [ ] Add configuration interface in core package +- [ ] Implement controller in core package +- [ ] Wire up in ServiceProvider +- [ ] Add to Snowplow.createTracker method + +### Testing New Features Checklist +- [ ] Create instrumentation test in androidTest +- [ ] Use MockEventStore for event verification +- [ ] Use MockNetworkConnection for network testing +- [ ] Test configuration changes +- [ ] Verify thread safety if applicable + +## Contributing to CLAUDE.md + +When adding or updating content in this document, please follow these guidelines: + +### File Size Limit +- **CLAUDE.md must not exceed 40KB** (currently ~19KB) +- Check file size after updates: `wc -c CLAUDE.md` +- Remove outdated content if approaching the limit + +### Code Examples +- Keep all code examples **4 lines or fewer** +- Focus on the essential pattern, not complete implementations +- Use `// ❌` and `// ✅` to clearly show wrong vs right approaches + +### Content Organization +- Add new patterns to existing sections when possible +- Create new sections sparingly to maintain structure +- Update the architectural principles section for major changes +- Ensure examples follow current codebase conventions + +### Quality Standards +- Test any new patterns in actual code before documenting +- Verify imports and syntax are correct for the codebase +- Keep language concise and actionable +- Focus on "what" and "how", minimize "why" explanations + +### Multiple CLAUDE.md Files +- **Directory-specific CLAUDE.md files** can be created for specialized modules +- Follow the same structure and guidelines as this root CLAUDE.md +- Keep them focused on directory-specific patterns and conventions +- Maximum 20KB per directory-specific CLAUDE.md file + +### Instructions for LLMs +When editing files in this repository, **always check for CLAUDE.md guidance**: + +1. **Look for CLAUDE.md in the same directory** as the file being edited +2. **If not found, check parent directories** recursively up to project root +3. **Follow the patterns and conventions** described in the applicable CLAUDE.md +4. **Prioritize directory-specific guidance** over root-level guidance when conflicts exist \ No newline at end of file