|
| 1 | +# CLAUDE.md - Snowplow Flutter Tracker Documentation |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +The Snowplow Flutter Tracker is a cross-platform analytics SDK that enables Flutter applications to send events to Snowplow collectors. It wraps native iOS, Android, and JavaScript trackers to provide a unified Flutter API for comprehensive event tracking and analytics. |
| 6 | + |
| 7 | +**Core Technologies:** |
| 8 | +- Flutter/Dart for cross-platform API |
| 9 | +- Platform channels for native communication |
| 10 | +- Kotlin for Android implementation |
| 11 | +- Swift for iOS implementation |
| 12 | +- JavaScript interop for Web support |
| 13 | + |
| 14 | +## Development Commands |
| 15 | + |
| 16 | +```bash |
| 17 | +# Build and run |
| 18 | +flutter pub get # Install dependencies |
| 19 | +flutter analyze # Run static analysis |
| 20 | +flutter test # Run unit tests |
| 21 | +flutter run --dart-define=ENDPOINT=http://localhost:9090 # Run example app |
| 22 | + |
| 23 | +# Integration testing |
| 24 | +cd example && flutter test integration_test --dart-define=ENDPOINT=http://192.168.0.20:9090 |
| 25 | + |
| 26 | +# Format code |
| 27 | +dart format lib test example/lib |
| 28 | + |
| 29 | +# Check package score |
| 30 | +flutter pub publish --dry-run |
| 31 | +``` |
| 32 | + |
| 33 | +## Architecture |
| 34 | + |
| 35 | +### System Design |
| 36 | + |
| 37 | +The tracker follows a **Plugin Architecture** with platform-specific implementations: |
| 38 | + |
| 39 | +``` |
| 40 | +┌─────────────────────────────────────────┐ |
| 41 | +│ Flutter/Dart API Layer │ |
| 42 | +│ (lib/snowplow.dart, tracker.dart) │ |
| 43 | +└─────────────┬───────────────────────────┘ |
| 44 | + │ MethodChannel |
| 45 | + ┌─────────┴──────────┬──────────────┐ |
| 46 | + ▼ ▼ ▼ |
| 47 | +┌──────────┐ ┌──────────┐ ┌──────────┐ |
| 48 | +│ Android │ │ iOS │ │ Web │ |
| 49 | +│ (Kotlin)│ │ (Swift) │ │ (JS) │ |
| 50 | +└──────────┘ └──────────┘ └──────────┘ |
| 51 | +``` |
| 52 | + |
| 53 | +### Module Organization |
| 54 | + |
| 55 | +- **`lib/`**: Core Flutter/Dart API |
| 56 | + - **`configurations/`**: Configuration classes for tracker setup |
| 57 | + - **`events/`**: Event type definitions |
| 58 | + - **`entities/`**: Data entities (media tracking) |
| 59 | + - **`src/web/`**: Web-specific implementations |
| 60 | +- **`android/`**: Android platform implementation |
| 61 | +- **`ios/`**: iOS platform implementation |
| 62 | +- **`example/`**: Demo application and integration tests |
| 63 | +- **`test/`**: Unit tests |
| 64 | + |
| 65 | +## Core Architectural Principles |
| 66 | + |
| 67 | +### 1. Immutable Event Pattern |
| 68 | +All events and configurations are immutable with `@immutable` annotation: |
| 69 | +```dart |
| 70 | +// ✅ Correct: Immutable event |
| 71 | +@immutable |
| 72 | +class ScreenView implements Event { |
| 73 | + final String name; |
| 74 | + const ScreenView({required this.name}); |
| 75 | +} |
| 76 | +
|
| 77 | +// ❌ Wrong: Mutable event |
| 78 | +class ScreenView implements Event { |
| 79 | + String name; // Mutable field |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +### 2. Platform Channel Communication |
| 84 | +Use consistent message passing through MethodChannel: |
| 85 | +```dart |
| 86 | +// ✅ Correct: Clean method channel usage |
| 87 | +await _channel.invokeMethod('trackScreenView', event.toMap()); |
| 88 | +
|
| 89 | +// ❌ Wrong: Direct platform access |
| 90 | +// Attempting to bypass the channel abstraction |
| 91 | +``` |
| 92 | + |
| 93 | +### 3. Null-Safe Map Serialization |
| 94 | +Always remove null values from serialization maps: |
| 95 | +```dart |
| 96 | +// ✅ Correct: Remove null values |
| 97 | +Map<String, Object?> toMap() { |
| 98 | + final map = {'field': value}; |
| 99 | + map.removeWhere((key, value) => value == null); |
| 100 | + return map; |
| 101 | +} |
| 102 | +
|
| 103 | +// ❌ Wrong: Including null values |
| 104 | +Map<String, Object?> toMap() => {'field': value}; // May include nulls |
| 105 | +``` |
| 106 | + |
| 107 | +### 4. Factory Constructor Pattern for Deserialization |
| 108 | +Use named factory constructors for map deserialization: |
| 109 | +```dart |
| 110 | +// ✅ Correct: Factory constructor |
| 111 | +ScreenView.fromMap(Map<String, Object?> map) |
| 112 | + : name = map['name'] as String; |
| 113 | +
|
| 114 | +// ❌ Wrong: Static method |
| 115 | +static ScreenView fromMap(Map map) { } // Inconsistent pattern |
| 116 | +``` |
| 117 | + |
| 118 | +## Layer Organization & Responsibilities |
| 119 | + |
| 120 | +### API Layer (lib/) |
| 121 | +- **Responsibility**: Public Flutter API, event definitions, configurations |
| 122 | +- **Key Classes**: `Snowplow`, `SnowplowTracker`, `Event` implementations |
| 123 | +- **Pattern**: Immutable data classes with `toMap()` serialization |
| 124 | + |
| 125 | +### Platform Layer (android/, ios/, web/) |
| 126 | +- **Responsibility**: Native implementation and platform-specific features |
| 127 | +- **Key Classes**: `SnowplowTrackerPlugin`, platform readers |
| 128 | +- **Pattern**: Message readers for deserialization, controller for business logic |
| 129 | + |
| 130 | +### Configuration Layer |
| 131 | +- **Responsibility**: Tracker initialization and feature configuration |
| 132 | +- **Key Classes**: `Configuration`, `TrackerConfiguration`, `NetworkConfiguration` |
| 133 | +- **Pattern**: Builder-like immutable configuration objects |
| 134 | + |
| 135 | +## Critical Import Patterns |
| 136 | + |
| 137 | +### Event Imports |
| 138 | +```dart |
| 139 | +// ✅ Correct: Import from snowplow_tracker package |
| 140 | +import 'package:snowplow_tracker/snowplow_tracker.dart'; |
| 141 | +import 'package:snowplow_tracker/events/screen_view.dart'; |
| 142 | +
|
| 143 | +// ❌ Wrong: Direct file imports |
| 144 | +import '../events/screen_view.dart'; // Use package imports |
| 145 | +``` |
| 146 | + |
| 147 | +### Platform-Specific Code |
| 148 | +```dart |
| 149 | +// ✅ Correct: Use kIsWeb for platform checks |
| 150 | +import 'package:flutter/foundation.dart'; |
| 151 | +if (kIsWeb) { /* web specific */ } |
| 152 | +
|
| 153 | +// ❌ Wrong: Using Platform.isAndroid on web |
| 154 | +import 'dart:io'; |
| 155 | +if (Platform.isAndroid) { } // Crashes on web |
| 156 | +``` |
| 157 | + |
| 158 | +## Essential Library Patterns |
| 159 | + |
| 160 | +### Tracker Initialization |
| 161 | +```dart |
| 162 | +// ✅ Correct: Comprehensive tracker setup |
| 163 | +final tracker = await Snowplow.createTracker( |
| 164 | + namespace: 'ns1', |
| 165 | + endpoint: 'https://collector.example.com', |
| 166 | + trackerConfig: TrackerConfiguration(appId: 'app'), |
| 167 | +); |
| 168 | +
|
| 169 | +// ❌ Wrong: Missing required configuration |
| 170 | +final tracker = await Snowplow.createTracker(); // Missing params |
| 171 | +``` |
| 172 | + |
| 173 | +### Event Tracking |
| 174 | +```dart |
| 175 | +// ✅ Correct: Track with contexts |
| 176 | +await tracker.track( |
| 177 | + ScreenView(name: 'home'), |
| 178 | + contexts: [SelfDescribing(schema: 'iglu:...', data: {})], |
| 179 | +); |
| 180 | +
|
| 181 | +// ❌ Wrong: Incorrect event structure |
| 182 | +await tracker.track({'type': 'screen'}); // Not an Event object |
| 183 | +``` |
| 184 | + |
| 185 | +### Media Tracking |
| 186 | +```dart |
| 187 | +// ✅ Correct: Start media tracking with configuration |
| 188 | +final media = await tracker.startMediaTracking( |
| 189 | + MediaTrackingConfiguration(id: 'video-1'), |
| 190 | +); |
| 191 | +
|
| 192 | +// ❌ Wrong: Missing required ID |
| 193 | +final media = await tracker.startMediaTracking( |
| 194 | + MediaTrackingConfiguration(), // Missing id |
| 195 | +); |
| 196 | +``` |
| 197 | + |
| 198 | +## Model Organization Pattern |
| 199 | + |
| 200 | +### Event Hierarchy |
| 201 | +```dart |
| 202 | +abstract class Event { |
| 203 | + String endpoint(); |
| 204 | + Map<String, Object?> toMap(); |
| 205 | +} |
| 206 | +
|
| 207 | +// Concrete implementations |
| 208 | +class ScreenView implements Event { } |
| 209 | +class Structured implements Event { } |
| 210 | +class SelfDescribing implements Event { } |
| 211 | +``` |
| 212 | + |
| 213 | +### Configuration Pattern |
| 214 | +```dart |
| 215 | +@immutable |
| 216 | +class Configuration { |
| 217 | + final String namespace; |
| 218 | + final NetworkConfiguration networkConfig; |
| 219 | + // Optional configs |
| 220 | + final TrackerConfiguration? trackerConfig; |
| 221 | + |
| 222 | + Map<String, Object?> toMap() { |
| 223 | + // Serialize and remove nulls |
| 224 | + } |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +## Common Pitfalls & Solutions |
| 229 | + |
| 230 | +### 1. WebView Integration |
| 231 | +```dart |
| 232 | +// ❌ Wrong: Forgetting to register JavaScript channel |
| 233 | +webView.loadUrl('https://example.com'); |
| 234 | +
|
| 235 | +// ✅ Correct: Register channel before loading |
| 236 | +tracker.registerWebViewJavaScriptChannel( |
| 237 | + webViewController: controller, |
| 238 | +); |
| 239 | +webView.loadUrl('https://example.com'); |
| 240 | +``` |
| 241 | + |
| 242 | +### 2. Navigator Observer |
| 243 | +```dart |
| 244 | +// ❌ Wrong: Creating observer without tracker |
| 245 | +MaterialApp(navigatorObservers: [SnowplowObserver()]); |
| 246 | +
|
| 247 | +// ✅ Correct: Use tracker's observer |
| 248 | +MaterialApp( |
| 249 | + navigatorObservers: [tracker.getObserver()], |
| 250 | +); |
| 251 | +``` |
| 252 | + |
| 253 | +### 3. Platform Context Properties |
| 254 | +```dart |
| 255 | +// ❌ Wrong: Setting platform properties on Web |
| 256 | +TrackerConfiguration( |
| 257 | + platformContextProperties: properties, // Not supported on Web |
| 258 | +); |
| 259 | +
|
| 260 | +// ✅ Correct: Check platform first |
| 261 | +TrackerConfiguration( |
| 262 | + platformContextProperties: kIsWeb ? null : properties, |
| 263 | +); |
| 264 | +``` |
| 265 | + |
| 266 | +### 4. Async Initialization |
| 267 | +```dart |
| 268 | +// ❌ Wrong: Not awaiting tracker creation |
| 269 | +final tracker = Snowplow.createTracker(...); // Returns Future |
| 270 | +
|
| 271 | +// ✅ Correct: Await initialization |
| 272 | +final tracker = await Snowplow.createTracker(...); |
| 273 | +``` |
| 274 | + |
| 275 | +## File Structure Template |
| 276 | + |
| 277 | +``` |
| 278 | +lib/ |
| 279 | +├── snowplow_tracker.dart # Package exports |
| 280 | +├── snowplow.dart # Main API class |
| 281 | +├── tracker.dart # Tracker instance |
| 282 | +├── configurations/ |
| 283 | +│ ├── configuration.dart # Base configuration |
| 284 | +│ ├── tracker_configuration.dart |
| 285 | +│ └── network_configuration.dart |
| 286 | +├── events/ |
| 287 | +│ ├── event.dart # Event interface |
| 288 | +│ ├── screen_view.dart |
| 289 | +│ └── self_describing.dart |
| 290 | +└── entities/ |
| 291 | + └── media_player_entity.dart |
| 292 | +``` |
| 293 | + |
| 294 | +## Testing Patterns |
| 295 | + |
| 296 | +### Unit Test Structure |
| 297 | +```dart |
| 298 | +void main() { |
| 299 | + setUp(() async { |
| 300 | + // Mock method channel |
| 301 | + TestDefaultBinaryMessengerBinding.instance |
| 302 | + .defaultBinaryMessenger |
| 303 | + .setMockMethodCallHandler(channel, handler); |
| 304 | + }); |
| 305 | +
|
| 306 | + test('tracks event', () async { |
| 307 | + await tracker.track(ScreenView(name: 'test')); |
| 308 | + expect(capturedMethod, 'trackScreenView'); |
| 309 | + }); |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +### Integration Test Pattern |
| 314 | +```dart |
| 315 | +testWidgets('end-to-end tracking', (tester) async { |
| 316 | + final events = await getMicroEvents(endpoint); |
| 317 | + expect(events.any((e) => e['eventType'] == 'struct'), true); |
| 318 | +}); |
| 319 | +``` |
| 320 | + |
| 321 | +## Quick Reference |
| 322 | + |
| 323 | +### Event Type Checklist |
| 324 | +- [ ] Implements `Event` interface |
| 325 | +- [ ] Has `@immutable` annotation |
| 326 | +- [ ] Implements `endpoint()` method |
| 327 | +- [ ] Implements `toMap()` with null removal |
| 328 | +- [ ] Has factory constructor `.fromMap()` for deserialization |
| 329 | +- [ ] All fields are `final` |
| 330 | + |
| 331 | +### Configuration Checklist |
| 332 | +- [ ] All fields are `final` and nullable (optional) |
| 333 | +- [ ] Has `toMap()` method with null removal |
| 334 | +- [ ] Uses `@immutable` annotation |
| 335 | +- [ ] Documents platform-specific features |
| 336 | + |
| 337 | +### Platform Implementation Checklist |
| 338 | +- [ ] Has reader class for deserialization |
| 339 | +- [ ] Handles null values appropriately |
| 340 | +- [ ] Maps to native tracker methods |
| 341 | +- [ ] Consistent error handling |
| 342 | + |
| 343 | +## Contributing to CLAUDE.md |
| 344 | + |
| 345 | +When adding or updating content in this document, please follow these guidelines: |
| 346 | + |
| 347 | +### File Size Limit |
| 348 | +- **CLAUDE.md must not exceed 40KB** (currently ~19KB) |
| 349 | +- Check file size after updates: `wc -c CLAUDE.md` |
| 350 | +- Remove outdated content if approaching the limit |
| 351 | + |
| 352 | +### Code Examples |
| 353 | +- Keep all code examples **4 lines or fewer** |
| 354 | +- Focus on the essential pattern, not complete implementations |
| 355 | +- Use `// ❌` and `// ✅` to clearly show wrong vs right approaches |
| 356 | + |
| 357 | +### Content Organization |
| 358 | +- Add new patterns to existing sections when possible |
| 359 | +- Create new sections sparingly to maintain structure |
| 360 | +- Update the architectural principles section for major changes |
| 361 | +- Ensure examples follow current codebase conventions |
| 362 | + |
| 363 | +### Quality Standards |
| 364 | +- Test any new patterns in actual code before documenting |
| 365 | +- Verify imports and syntax are correct for the codebase |
| 366 | +- Keep language concise and actionable |
| 367 | +- Focus on "what" and "how", minimize "why" explanations |
| 368 | + |
| 369 | +### Multiple CLAUDE.md Files |
| 370 | +- **Directory-specific CLAUDE.md files** can be created for specialized modules |
| 371 | +- Follow the same structure and guidelines as this root CLAUDE.md |
| 372 | +- Keep them focused on directory-specific patterns and conventions |
| 373 | +- Maximum 20KB per directory-specific CLAUDE.md file |
| 374 | + |
| 375 | +### Instructions for LLMs |
| 376 | +When editing files in this repository, **always check for CLAUDE.md guidance**: |
| 377 | + |
| 378 | +1. **Look for CLAUDE.md in the same directory** as the file being edited |
| 379 | +2. **If not found, check parent directories** recursively up to project root |
| 380 | +3. **Follow the patterns and conventions** described in the applicable CLAUDE.md |
| 381 | +4. **Prioritize directory-specific guidance** over root-level guidance when conflicts exist |
0 commit comments