diff --git a/.claude/context/architecture/ARCHITECTURE.md b/.claude/context/architecture/ARCHITECTURE.md new file mode 100644 index 0000000..c44e652 --- /dev/null +++ b/.claude/context/architecture/ARCHITECTURE.md @@ -0,0 +1,276 @@ +# Mixpanel Flutter SDK Architecture + +## Overview + +The Mixpanel Flutter SDK is a cross-platform analytics plugin that provides a unified Dart API for tracking events and managing user profiles across iOS, Android, and Web platforms. The SDK uses Flutter's platform channel mechanism to communicate between Dart code and native platform implementations. + +## Architecture Layers + +### 1. Dart API Layer (`lib/mixpanel_flutter.dart`) + +The main entry point providing a unified interface across all platforms: + +```dart +class Mixpanel { + static final MethodChannel _channel = kIsWeb + ? const MethodChannel('mixpanel_flutter') + : const MethodChannel('mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec())); + + // Core tracking method + Future track(String eventName, {Map? properties}) async { + await _channel.invokeMethod('track', { + 'eventName': eventName, + 'properties': _MixpanelHelper.ensureSerializableProperties(properties) + }); + } +} +``` + +Key components: +- **Mixpanel**: Main singleton class for event tracking +- **People**: User profile management (accessed via `mixpanel.getPeople()`) +- **MixpanelGroup**: Group analytics management (accessed via `mixpanel.getGroup()`) + +### 2. Platform Channel & Serialization + +#### Custom Message Codec (`lib/codec/mixpanel_message_codec.dart`) + +Handles serialization of complex types between Dart and native platforms: + +```dart +class MixpanelMessageCodec extends StandardMessageCodec { + static const int _kDateTime = 128; + static const int _kUri = 129; + + @override + void writeValue(WriteBuffer buffer, dynamic value) { + if (value is DateTime) { + buffer.putUint8(_kDateTime); + buffer.putInt64(value.millisecondsSinceEpoch); + } else if (value is Uri) { + buffer.putUint8(_kUri); + final bytes = utf8.encoder.convert(value.toString()); + writeSize(buffer, bytes.length); + buffer.putUint8List(bytes); + } else { + super.writeValue(buffer, value); + } + } +} +``` + +### 3. Platform Implementations + +#### Android Implementation + +**MixpanelFlutterPlugin.java**: +```java +public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + switch (call.method) { + case "track": + handleTrack(call, result); + break; + // ... other methods + } +} + +private void handleTrack(MethodCall call, Result result) { + String eventName = call.argument("eventName"); + Map mapProperties = call.>argument("properties"); + JSONObject properties = new JSONObject(mapProperties == null ? EMPTY_HASHMAP : mapProperties); + properties = MixpanelFlutterHelper.getMergedProperties(properties, mixpanelProperties); + mixpanel.track(eventName, properties); + result.success(null); +} +``` + +**MixpanelMessageCodec.java**: Mirrors Dart codec for Date/URI handling + +#### iOS Implementation + +**SwiftMixpanelFlutterPlugin.swift**: +```swift +private func handleTrack(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? [String: Any] ?? [String: Any]() + let event = arguments["eventName"] as! String + let properties = arguments["properties"] as? [String: Any] + let mpProperties = MixpanelTypeHandler.mixpanelProperties(properties: properties, mixpanelProperties: mixpanelProperties) + instance?.track(event: event, properties: mpProperties) + result(nil) +} +``` + +**Custom codec implementation**: +```swift +public class MixpanelReader : FlutterStandardReader { + public override func readValue(ofType type: UInt8) -> Any? { + switch type { + case DATE_TIME: + var value: Int64 = 0 + readBytes(&value, length: 8) + return Date(timeIntervalSince1970: TimeInterval(value / 1000)) + case URI: + let urlString = readUTF8() + return URL(string: urlString) + default: + return super.readValue(ofType: type) + } + } +} +``` + +#### Web Implementation + +**mixpanel_flutter_web.dart**: +```dart +void handleTrack(MethodCall call) { + Map args = call.arguments as Map; + String eventName = args['eventName'] as String; + dynamic properties = args['properties']; + Map props = { + ..._mixpanelProperties, + ...(properties ?? {}) + }; + track(eventName, safeJsify(props)); +} +``` + +**Type conversion for web**: +```dart +JSAny? safeJsify(dynamic value) { + if (value == null) { + return null; + } else if (value is Map) { + return value.jsify(); + } else if (value is DateTime) { + return value.jsify(); + } // ... other type conversions +} +``` + +## Event Flow: track() Method + +### 1. Initialization Flow + +```dart +// Dart layer +final mixpanel = await Mixpanel.init("YOUR_PROJECT_TOKEN", + optOutTrackingDefault: false, + trackAutomaticEvents: true); + +// Platform channel invocation +await _channel.invokeMethod('initialize', { + 'token': token, + 'optOutTrackingDefault': optOutTrackingDefault, + 'trackAutomaticEvents': trackAutomaticEvents, + 'mixpanelProperties': _mixpanelProperties, // {$lib_version: '2.4.4', mp_lib: 'flutter'} + 'superProperties': superProperties, + 'config': config +}); +``` + +### 2. Track Event Flow + +``` +Dart Layer (mixpanel.track("Event Name", properties: {...})) + ↓ +Platform Channel (invokeMethod('track', {eventName, properties})) + ↓ +Native Platform Handler + ├── Android: MixpanelFlutterPlugin.handleTrack() + ├── iOS: SwiftMixpanelFlutterPlugin.handleTrack() + └── Web: MixpanelFlutterPlugin.handleTrack() + ↓ +Property Processing + ├── Merge with library properties ($lib_version, mp_lib) + ├── Type conversion (Date, URI, etc.) + └── Platform-specific formatting + ↓ +Native SDK Call + ├── Android: mixpanel.track(eventName, JSONObject) + ├── iOS: instance?.track(event:properties:) + └── Web: track(eventName, jsProperties) + ↓ +Mixpanel Servers +``` + +### 3. Data Serialization Details + +#### Native Platforms (Android/iOS) +- Custom codec handles DateTime and Uri objects +- DateTime: Serialized as milliseconds since epoch (int64) +- Uri: Serialized as UTF-8 encoded string +- Complex objects (Maps, Lists) are recursively converted + +#### Web Platform +- No custom codec needed - uses StandardMethodCodec +- `safeJsify()` converts Dart types to JavaScript-compatible types +- DateTime objects converted using `.jsify()` +- Direct JS interop with Mixpanel JavaScript library + +## Key Design Decisions + +1. **Platform Channel Architecture**: Enables code reuse while allowing platform-specific optimizations + +2. **Custom Message Codec**: Ensures DateTime and Uri objects are properly serialized across platform boundaries + +3. **Library Properties**: Automatically injected metadata (`$lib_version`, `mp_lib`) helps with analytics segmentation + +4. **Async API**: All methods return Futures for consistency, even if underlying native calls are synchronous + +5. **Type Safety**: Platform-specific type handlers ensure proper conversion between Dart and native types + +6. **Web Implementation**: Uses JS interop instead of platform channels for better performance and smaller bundle size + +## Platform Dependencies + +- **Android**: Mixpanel Android SDK v8.0.3 +- **iOS**: Mixpanel-swift v5.0.0 +- **Web**: Mixpanel JavaScript library (loaded from CDN) + +## Example Usage + +```dart +// Initialize +final mixpanel = await Mixpanel.init("YOUR_PROJECT_TOKEN", + trackAutomaticEvents: true); + +// Track simple event +await mixpanel.track("Button Clicked"); + +// Track with properties +await mixpanel.track("Purchase", properties: { + "product": "Premium Subscription", + "price": 9.99, + "currency": "USD", + "timestamp": DateTime.now(), + "store_url": Uri.parse("https://store.example.com") +}); + +// Identify user +await mixpanel.identify("user123"); + +// Set user profile properties +mixpanel.getPeople().set("name", "John Doe"); +mixpanel.getPeople().set("email", "john@example.com"); + +// Group analytics +mixpanel.setGroup("company", "Acme Corp"); +final group = mixpanel.getGroup("company", "Acme Corp"); +group.set("plan", "Enterprise"); +``` + +## Architecture Benefits + +1. **Unified API**: Developers write once, run everywhere +2. **Type Safety**: Strong typing prevents runtime errors +3. **Performance**: Native SDK usage ensures optimal performance per platform +4. **Maintainability**: Clear separation of concerns between layers +5. **Extensibility**: Easy to add new methods or platforms + +## Future Considerations + +1. **Null Safety**: The SDK fully supports Dart null safety +2. **Platform Expansion**: Architecture supports adding new platforms (e.g., Windows, Linux) +3. **Feature Parity**: Platform implementations should maintain feature parity where possible +4. **Testing**: Platform-specific functionality should be tested through the example app \ No newline at end of file diff --git a/.claude/context/architecture/system-design.md b/.claude/context/architecture/system-design.md new file mode 100644 index 0000000..6e02619 --- /dev/null +++ b/.claude/context/architecture/system-design.md @@ -0,0 +1,228 @@ +# System Architecture + +## Overview + +The Mixpanel Flutter SDK implements a federated plugin architecture that provides a unified Dart API while leveraging platform-specific native SDKs. This design ensures optimal performance and feature parity across iOS, Android, and Web platforms. + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Dart API Layer │ +│ (Mixpanel, People, MixpanelGroup classes) │ +│ - Public API methods │ +│ - Input validation │ +│ - Platform abstraction │ +├─────────────────────────────────────────────────────────────┤ +│ Platform Channel Layer │ +│ - MethodChannel with custom MixpanelMessageCodec │ +│ - Type serialization/deserialization │ +│ - Platform detection (kIsWeb) │ +├─────────────────────┬───────────────┬──────────────────────┤ +│ Android │ iOS │ Web │ +│ Implementation │ Implementation│ Implementation │ +├─────────────────────┼───────────────┼──────────────────────┤ +│ Java Plugin │ Swift Plugin │ Dart/JS Interop │ +│ - JSONObject │ - Dictionary │ - jsify() │ +│ - Type conversion │ - Type handler│ - safeJsify() │ +├─────────────────────┼───────────────┼──────────────────────┤ +│ Mixpanel Android │ Mixpanel-swift│ Mixpanel JS │ +│ SDK v8.0.3 │ SDK v5.0.0 │ (CDN loaded) │ +└─────────────────────┴───────────────┴──────────────────────┘ + ↓ + Mixpanel Analytics Servers +``` + +## Request Flow Example: Track Event + +### 1. Dart API Call +```dart +// User code +await mixpanel.track('Button Clicked', properties: { + 'button_name': 'purchase', + 'timestamp': DateTime.now(), +}); +``` + +### 2. Validation & Channel Invocation +```dart +// In lib/mixpanel_flutter.dart +Future track(String eventName, {Map? properties}) async { + if (_MixpanelHelper.isValidString(eventName)) { + await _channel.invokeMethod('track', { + 'eventName': eventName, + 'properties': properties ?? {}, + }); + } else { + developer.log('`track` failed: eventName cannot be blank', name: 'Mixpanel'); + } +} +``` + +### 3. Platform-Specific Handling + +#### Android Path +```java +// In MixpanelFlutterPlugin.java +case "track": + eventName = call.argument("eventName"); + properties = call.argument("properties"); + JSONObject jsonObject = MixpanelFlutterHelper.toJSONObject(properties); + mixpanel.track(eventName, jsonObject); + result.success(null); + break; +``` + +#### iOS Path +```swift +// In SwiftMixpanelFlutterPlugin.swift +case "track": + guard let arguments = call.arguments as? [String: Any] else { return } + let eventName = arguments["eventName"] as! String + if let properties = arguments["properties"] as? [String: Any] { + let mpProperties = convertToMixpanelTypes(properties) + Mixpanel.mainInstance().track(event: eventName, properties: mpProperties) + } +``` + +#### Web Path +```dart +// In mixpanel_flutter_web.dart +@override +Future track(String eventName, Map properties) async { + var trackedProperties = safeJsify(properties); + mixpanelJs.track(eventName, trackedProperties); +} +``` + +## Initialization Flow + +### 1. Static Factory Method +```dart +// Application code +final mixpanel = await Mixpanel.init( + 'YOUR_TOKEN', + trackAutomaticEvents: true, + superProperties: {'platform': 'mobile'}, +); +``` + +### 2. Platform Initialization + +#### Mobile Platforms (Android/iOS) +1. Create platform channel with custom codec +2. Invoke 'initialize' method with configuration +3. Native side creates/configures SDK instance +4. Store reference for subsequent calls + +#### Web Platform +1. Check for mixpanel.js presence in window +2. Initialize with token and config +3. Set super properties if provided +4. Configure automatic tracking + +## Data Serialization Strategy + +### Custom Message Codec (Mobile) +Handles special types not supported by standard codec: +```dart +// From mixpanel_message_codec.dart +void writeValue(WriteBuffer buffer, dynamic value) { + if (value is DateTime) { + buffer.putUint8(_typeDateTime); + buffer.putInt64(value.millisecondsSinceEpoch); + } else if (value is Uri) { + buffer.putUint8(_typeUri); + writeValue(buffer, value.toString()); + } else { + super.writeValue(buffer, value); + } +} +``` + +### Web Type Conversion +Ensures JavaScript compatibility: +```dart +dynamic safeJsify(Object value) { + if (value is Map) { + value = JsLinkedHashMap.from(value); + } + var args = jsify(value); + return args; +} +``` + +## Component Responsibilities + +### Dart API Layer +- **Input validation**: Ensures non-empty strings for required parameters +- **Platform abstraction**: Hides platform differences from consumers +- **Type safety**: Provides strongly-typed Dart interfaces +- **Documentation**: Comprehensive API documentation + +### Platform Channel Layer +- **Serialization**: Converts Dart objects to platform-compatible formats +- **Method routing**: Maps Dart method calls to native implementations +- **Error propagation**: Surfaces platform errors to Dart layer +- **Custom codec**: Handles DateTime and Uri types + +### Native Implementation Layer +- **SDK integration**: Wraps native Mixpanel SDKs +- **Type conversion**: Converts between Flutter and native types +- **Platform optimization**: Uses platform-specific features +- **Lifecycle management**: Handles app lifecycle events + +## Architectural Principles + +### 1. **Separation of Concerns** +Each layer has clear responsibilities with minimal overlap. The Dart layer knows nothing about native implementations. + +### 2. **Platform Parity** +All platforms expose the same Dart API, ensuring consistent behavior across iOS, Android, and Web. + +### 3. **Type Safety** +Custom codecs and type handlers ensure type safety across language boundaries. + +### 4. **Performance Optimization** +- Lazy initialization on Android to prevent ANR +- Direct native SDK calls for minimal overhead +- Asynchronous operations throughout + +### 5. **Fail-Safe Design** +- Invalid inputs logged but don't crash +- Platform errors caught and surfaced +- Graceful degradation on web if JS not loaded + +## Cross-Cutting Concerns + +### Error Handling +- Input validation at Dart layer +- Platform-specific error handling +- Consistent error logging with 'Mixpanel' tag + +### Metadata Injection +All events automatically include: +- `$lib_version`: SDK version +- `mp_lib`: Library identifier +- Platform-specific metadata + +### Configuration +- Token-based initialization +- Optional automatic event tracking +- Super properties for all events +- Platform-specific config options + +## Extension Points + +### Adding New Methods +1. Add method to Dart API with validation +2. Add platform channel invocation +3. Implement in each platform handler +4. Add type conversions if needed +5. Write tests for all platforms + +### Supporting New Types +1. Extend MixpanelMessageCodec +2. Add type handlers for iOS/Android +3. Update safeJsify for web +4. Test serialization round-trip \ No newline at end of file diff --git a/.claude/context/codebase-map.md b/.claude/context/codebase-map.md new file mode 100644 index 0000000..01eacb6 --- /dev/null +++ b/.claude/context/codebase-map.md @@ -0,0 +1,118 @@ +# Codebase Map + +## Project Structure Overview + +The Mixpanel Flutter SDK is a comprehensive Flutter plugin that provides analytics tracking capabilities across iOS, Android, and Web platforms. It follows Flutter's federated plugin architecture, with platform-specific implementations wrapping native Mixpanel SDKs. + +## Directory Hierarchy + +``` +mixpanel-flutter/ +├── lib/ # Dart/Flutter public API +│ ├── mixpanel_flutter.dart # Main entry point & core classes +│ ├── mixpanel_flutter_web.dart # Web platform implementation +│ ├── codec/ +│ │ └── mixpanel_message_codec.dart # Custom serialization codec +│ └── web/ +│ └── mixpanel_js_bindings.dart # JavaScript interop bindings +├── android/ # Android platform channel +│ └── src/main/java/com/mixpanel/mixpanel_flutter/ +│ ├── MixpanelFlutterPlugin.java # Main plugin class +│ ├── MixpanelFlutterHelper.java # Helper utilities +│ └── MixpanelMessageCodec.java # Android codec implementation +├── ios/ # iOS platform channel +│ └── Classes/ +│ ├── SwiftMixpanelFlutterPlugin.swift # Main plugin class +│ └── MixpanelTypeHandler.swift # iOS type handling +├── example/ # Example application +│ ├── lib/ +│ │ ├── main.dart # Example app entry +│ │ └── [feature]_page.dart # Feature demonstration pages +│ ├── android/ # Android example config +│ ├── ios/ # iOS example config +│ └── web/ # Web example config +├── test/ # Unit tests +│ ├── mixpanel_flutter_test.dart # Core functionality tests +│ └── mixpanel_flutter_web_unit_test.dart # Web-specific tests +├── tool/ # Development tools +│ └── release.py # Automated release script +└── docs/ # Generated API documentation + +## Key Entry Points + +- **lib/mixpanel_flutter.dart**: Main public API entry point + - Exports `Mixpanel` singleton class for event tracking + - Exports `People` class for user profile management + - Exports `MixpanelGroup` class for group analytics + - Defines platform channel interface + +- **android/src/.../MixpanelFlutterPlugin.java**: Android platform entry + - Registers with Flutter engine + - Implements MethodChannel handler + - Delegates to native Mixpanel Android SDK + +- **ios/Classes/SwiftMixpanelFlutterPlugin.swift**: iOS platform entry + - Registers with Flutter engine + - Implements FlutterMethodChannel handler + - Delegates to native Mixpanel-swift SDK + +- **lib/mixpanel_flutter_web.dart**: Web platform entry + - Implements platform interface for web + - Uses JavaScript interop to call Mixpanel JS library + - Handles web-specific initialization + +## Configuration Files + +- **pubspec.yaml**: Flutter package definition + - Current version: 2.4.4 + - Dependencies: flutter, flutter_web_plugins, js + - Platform support declarations + +- **android/build.gradle**: Android build configuration + - compileSdk: 34 + - minSdk: 21 + - Mixpanel Android SDK: v8.2.0 + +- **ios/mixpanel_flutter.podspec**: iOS pod configuration + - iOS deployment target: 12.0 + - Mixpanel-swift dependency: ~> 5.1.0 + - Swift version: 5.0 + +- **analysis_options.yaml**: Dart static analysis rules + - Enforces Flutter style guide + - Custom linting rules + +## Documentation Located + +- **README.md**: Quick start guide with installation instructions +- **CHANGELOG.md**: Version history and migration guides +- **CLAUDE.md**: AI assistant context and guidelines +- **example/README.md**: Example app usage instructions +- **docs/**: Auto-generated dartdoc API reference + +## Build & Release Infrastructure + +- **.github/workflows/flutter.yml**: CI workflow for tests +- **.github/workflows/release.yml**: Automated release workflow +- **tool/release.py**: Python script for version management +- **Makefile**: Common development commands + +## Platform-Specific Implementation Details + +### Android +- Uses Java for platform channel implementation +- Custom `MixpanelMessageCodec` for type serialization +- Helper class for common operations +- Requires Android API 21+ (Android 5.0) + +### iOS +- Uses Swift for platform channel implementation +- Custom `MixpanelTypeHandler` for type conversion +- Direct integration with Mixpanel-swift pod +- Requires iOS 12.0+ + +### Web +- Pure Dart implementation using JS interop +- Dynamically loads Mixpanel JS library from CDN +- Custom `safeJsify` for safe object conversion +- Requires adding script tag to HTML \ No newline at end of file diff --git a/.claude/context/commands/add-people-method.md b/.claude/context/commands/add-people-method.md new file mode 100644 index 0000000..cb51ade --- /dev/null +++ b/.claude/context/commands/add-people-method.md @@ -0,0 +1,80 @@ +--- +description: Add a new method to the People class for user profile management +--- + +Create a new People method called ${input:methodName} for ${input:purpose}. + +## Implementation Steps + +1. **Add to People class** (`lib/mixpanel_flutter.dart`): +```dart +class People { + Future ${input:methodName}(${input:parameters}) async { + return await _channel.invokeMethod('people.${input:methodName}', + { + ${input:arguments} + }); + } +} +``` + +2. **Add to Web People implementation** (`lib/mixpanel_flutter_web.dart`): +```dart +@override +Future ${input:methodName}(${input:parameters}) async { + var properties = safeJsify(${input:propertiesParam}); + mixpanelJs.people.${input:jsMethodName}(properties); +} +``` + +3. **Update JavaScript bindings** if needed (`lib/web/mixpanel_js_bindings.dart`): +```dart +@JS() +@anonymous +abstract class People { + external void ${input:jsMethodName}(dynamic properties); +} +``` + +4. **Add Android handler** for "people.${input:methodName}": +```java +case "people.${input:methodName}": + JSONObject properties = MixpanelFlutterHelper.toJSONObject( + call.argument("properties")); + mixpanel.getPeople().${input:androidMethod}(properties); + result.success(null); + break; +``` + +5. **Add iOS handler** for "people.${input:methodName}": +```swift +case "people.${input:methodName}": + if let properties = arguments["properties"] as? [String: Any] { + let mpProperties = convertToMixpanelTypes(properties) + Mixpanel.mainInstance().people.${input:iosMethod}(mpProperties) + } + result(nil) +``` + +6. **Add test**: +```dart +test('people.${input:methodName}', () async { + await mixpanel.getPeople().${input:methodName}(${input:testArgs}); + expect( + methodCall, + isMethodCall( + 'people.${input:methodName}', + arguments: { + ${input:expectedTestArgs} + }, + ), + ); +}); +``` + +Common People methods follow patterns: +- `set`: Set properties, overwriting existing +- `setOnce`: Set only if not already set +- `increment`: Increment numeric properties +- `append`: Append to list properties +- `union`: Add unique values to list properties \ No newline at end of file diff --git a/.claude/context/commands/add-tracking-method.md b/.claude/context/commands/add-tracking-method.md new file mode 100644 index 0000000..cd54763 --- /dev/null +++ b/.claude/context/commands/add-tracking-method.md @@ -0,0 +1,74 @@ +--- +description: Add a new tracking method to the Mixpanel SDK +--- + +Create a new tracking method called ${input:methodName} that accepts ${input:parameters}. + +## Implementation Steps + +1. **Add to Dart API** (`lib/mixpanel_flutter.dart`): +```dart +Future ${input:methodName}(${input:dartParameters}) async { + if (_MixpanelHelper.isValidString(${input:primaryParam})) { + await _channel.invokeMethod('${input:methodName}', { + ${input:channelArguments} + }); + } else { + developer.log('`${input:methodName}` failed: ${input:primaryParam} cannot be blank', + name: 'Mixpanel'); + } +} +``` + +2. **Add to Web implementation** (`lib/mixpanel_flutter_web.dart`): +```dart +@override +Future ${input:methodName}(${input:dartParameters}) async { + ${input:webImplementation} +} +``` + +3. **Add Android handler** in `MixpanelFlutterPlugin.java`: +```java +case "${input:methodName}": + ${input:androidExtractArgs} + try { + ${input:androidImplementation} + result.success(null); + } catch (JSONException e) { + result.error("MixpanelFlutterException", e.getLocalizedMessage(), null); + } + break; +``` + +4. **Add iOS handler** in `SwiftMixpanelFlutterPlugin.swift`: +```swift +case "${input:methodName}": + ${input:iosGuardStatement} + ${input:iosImplementation} + result(nil) +``` + +5. **Add test** in `test/mixpanel_flutter_test.dart`: +```dart +test('${input:methodName}', () async { + await mixpanel.${input:methodName}(${input:testArguments}); + expect( + methodCall, + isMethodCall( + '${input:methodName}', + arguments: { + ${input:testExpectedArgs} + }, + ), + ); +}); +``` + +6. **Add to example app** with a button demonstrating the feature. + +Remember to: +- Include library metadata in properties +- Handle null/empty validation +- Keep method names consistent across platforms +- Update documentation \ No newline at end of file diff --git a/.claude/context/context-map.md b/.claude/context/context-map.md new file mode 100644 index 0000000..51ba9c0 --- /dev/null +++ b/.claude/context/context-map.md @@ -0,0 +1,116 @@ +# Claude Code Context Map + +## Quick Reference + +### Common Tasks +- **Adding a new tracking method?** Start with `workflows/new-feature.md` +- **Writing tests?** Check `workflows/testing.md` +- **Preparing a release?** Follow `workflows/release.md` +- **Understanding the architecture?** Read `architecture/system-design.md` +- **Platform-specific implementation?** See relevant technology guides + +### Key Commands +- **Add tracking method**: Use `.claude/context/commands/add-tracking-method.md` +- **Add People method**: Use `.claude/context/commands/add-people-method.md` + +## File Directory + +| File | Purpose | When to Use | +|------|---------|-------------| +| **CLAUDE.md** | Core patterns & quick reference | Always loaded, check first | +| **codebase-map.md** | Project structure overview | Understanding file layout | +| **discovered-patterns.md** | All coding patterns & conventions | Writing new code | +| **architecture/system-design.md** | System architecture & data flow | Understanding how components interact | +| **technologies/flutter-plugin.md** | Flutter plugin development patterns | Working with plugin architecture | +| **technologies/platform-channels.md** | Platform channel details | Implementing native communication | +| **technologies/javascript-interop.md** | Web platform implementation | Working on web support | +| **workflows/new-feature.md** | Step-by-step feature addition | Adding new SDK capabilities | +| **workflows/testing.md** | Testing patterns & practices | Writing or running tests | +| **workflows/release.md** | Release process & versioning | Publishing new versions | +| **commands/*.md** | Reusable code generation | Quick implementations | + +## Architecture Overview + +``` +Dart API Layer (mixpanel_flutter.dart) + ↓ +Platform Channel (with MixpanelMessageCodec) + ↓ +┌─────────────┬──────────────┬────────────────┐ +│ Android │ iOS │ Web │ +│ (Java) │ (Swift) │ (JS Interop) │ +└─────────────┴──────────────┴────────────────┘ + ↓ ↓ ↓ +Native SDKs Native SDKs Mixpanel.js +``` + +## Key Patterns Summary + +### Input Validation +```dart +if (_MixpanelHelper.isValidString(param)) { + // proceed with platform call +} else { + developer.log('failed: param cannot be blank', name: 'Mixpanel'); +} +``` + +### Platform Channel Invocation +```dart +await _channel.invokeMethod('methodName', { + 'param1': value1, + 'param2': value2 ?? {}, +}); +``` + +### Type Conversion +- Mobile: Custom `MixpanelMessageCodec` handles DateTime/Uri +- Web: Use `safeJsify()` for JavaScript compatibility + +## Development Workflow + +1. **Setup**: `flutter pub get` +2. **Development**: Make changes following patterns +3. **Testing**: `flutter test` and example app testing +4. **Release**: `python tool/release.py --old X.Y.Z --new A.B.C` + +## Platform Requirements + +- **Android**: API 21+ (Android 5.0) +- **iOS**: iOS 12.0+ +- **Web**: Modern browsers with JavaScript enabled + +## Maintenance Guide + +### When to Update Context +- New patterns emerge in codebase +- Architecture changes +- New workflows established +- Platform requirements change + +### How to Update +1. Edit relevant files in `.claude/context/` +2. Update `CLAUDE.md` if it's a core pattern +3. Test that documentation matches reality +4. Commit context updates with code changes + +## Quick Debugging + +### Common Issues +- **Empty string validation**: Methods silently fail with logging +- **Type serialization**: Check codec implementations +- **Platform differences**: Compare implementations across platforms +- **Web initialization**: Ensure mixpanel.js is loaded + +### Where to Look +- **Dart errors**: Check validation and channel invocation +- **Android errors**: `MixpanelFlutterPlugin.java` and helper +- **iOS errors**: `SwiftMixpanelFlutterPlugin.swift` and type handler +- **Web errors**: `mixpanel_flutter_web.dart` and JS bindings + +## Contact & Resources + +- **Official Docs**: https://developer.mixpanel.com/docs/flutter +- **GitHub**: https://github.com/mixpanel/mixpanel-flutter +- **Example App**: `/example` directory for working code +- **Tests**: `/test` directory for test patterns \ No newline at end of file diff --git a/.claude/context/discovered-patterns.md b/.claude/context/discovered-patterns.md new file mode 100644 index 0000000..cba09ba --- /dev/null +++ b/.claude/context/discovered-patterns.md @@ -0,0 +1,270 @@ +# Discovered Patterns + +## Naming Conventions + +### Method Naming +All methods use camelCase with consistent verb prefixes: +```dart +// Tracking methods use 'track' prefix +track(String eventName) +trackWithGroups(String eventName, Map properties) + +// Registration methods use 'register' prefix +registerSuperProperties(Map properties) +registerSuperPropertiesOnce(Map properties) + +// Getter methods use 'get' prefix +getPeople() +getGroup(String groupKey, dynamic groupID) +``` + +### Parameter Naming +Properties always use consistent naming patterns: +```dart +// Properties maps are always named 'properties' or with specific prefix +Map properties +Map superProperties + +// IDs use descriptive suffixes +String distinctId +dynamic groupID +String eventName +``` + +### File Organization +Files follow snake_case convention with clear purpose: +- `mixpanel_flutter.dart` - Main API surface +- `mixpanel_flutter_web.dart` - Web-specific implementation +- `mixpanel_message_codec.dart` - Custom serialization + +## Code Organization + +### Platform Channel Pattern +All platform communication uses a standardized channel approach: +```dart +// From lib/mixpanel_flutter.dart +static final MethodChannel _channel = kIsWeb + ? const MethodChannel('mixpanel_flutter') + : const MethodChannel( + 'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec())); + +// Standard invocation pattern +Future track(String eventName, + {Map? properties}) async { + if (_MixpanelHelper.isValidString(eventName)) { + await _channel.invokeMethod('track', { + 'eventName': eventName, + 'properties': properties ?? {}, + }); + } else { + developer.log('`track` failed: eventName cannot be blank', + name: 'Mixpanel'); + } +} +``` + +### Singleton Initialization Pattern +The SDK uses static initialization with factory pattern: +```dart +// From lib/mixpanel_flutter.dart +static Future init(String token, + {bool optOutTrackingDefault = false, + required bool trackAutomaticEvents, + Map? superProperties, + Map? config}) async { + var mixpanelProperties = _getMixpanelProperties(); + + await _channel.invokeMethod('initialize', { + 'token': token, + 'optOutTrackingDefault': optOutTrackingDefault, + 'trackAutomaticEvents': trackAutomaticEvents, + 'superProperties': superProperties ?? {}, + 'properties': mixpanelProperties, + 'config': config ?? {}, + }); + + return Mixpanel(token); +} +``` + +## Error Handling + +### Input Validation Pattern +All public methods validate inputs before platform calls: +```dart +// From lib/mixpanel_flutter.dart +if (_MixpanelHelper.isValidString(alias)) { + _channel.invokeMethod('alias', { + 'alias': alias, + 'distinctId': distinctId, + }); +} else { + developer.log('`alias` failed: alias cannot be blank', name: 'Mixpanel'); +} +``` + +### Platform-Specific Error Handling +Each platform handles errors appropriately: +```java +// From android/src/.../MixpanelFlutterPlugin.java +try { + properties = MixpanelFlutterHelper.getMergedProperties(properties, mixpanelProperties); +} catch (JSONException e) { + result.error("MixpanelFlutterException", e.getLocalizedMessage(), null); + return; +} +``` + +```swift +// From ios/Classes/SwiftMixpanelFlutterPlugin.swift +guard let properties = arguments["properties"] as? [String: Any] else { + result(nil) + return +} +``` + +## Type Handling Patterns + +### Custom Message Codec +Complex types require special handling across platforms: +```dart +// From lib/codec/mixpanel_message_codec.dart +class MixpanelMessageCodec extends StandardMessageCodec { + const MixpanelMessageCodec(); + + @override + void writeValue(WriteBuffer buffer, dynamic value) { + if (value is DateTime) { + buffer.putUint8(_typeDateTime); + buffer.putInt64(value.millisecondsSinceEpoch); + } else if (value is Uri) { + buffer.putUint8(_typeUri); + writeValue(buffer, value.toString()); + } else { + super.writeValue(buffer, value); + } + } +} +``` + +### Web-Specific Type Conversion +Web platform uses safe JavaScript conversion: +```dart +// From lib/mixpanel_flutter_web.dart +dynamic safeJsify(Object value) { + if (value is Map) { + value = JsLinkedHashMap.from(value); + } + var args = jsify(value); + return args; +} +``` + +## Testing Patterns + +### Mock Channel Testing +Tests verify correct method calls and arguments: +```dart +// From test/mixpanel_flutter_test.dart +test('track', () async { + await mixpanel.track('test event', properties: {'a': 'b'}); + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'test event', + 'properties': {'a': 'b'}, + }, + ), + ); +}); +``` + +### Validation Testing +Tests ensure invalid inputs are handled: +```dart +test('track() should not crash when event name is empty', () async { + await mixpanel.track('', properties: {'a': 'b'}); + expect(methodCall, null); +}); +``` + +## Documentation Patterns + +### API Documentation Style +Public methods use comprehensive documentation: +```dart +/// Track an event. +/// +/// Every call to track eventually results in a data point sent to Mixpanel. +/// These data points are what are measured, counted, and broken down to create +/// your Mixpanel reports. Events have a string name, and an optional set of +/// name/value pairs that describe the properties of that event. +/// +/// * [eventName] The name of the event to send +/// * [properties] A Map containing the key value pairs of the properties +/// to include in this event. Pass null if no extra properties exist. +``` + +### Parameter Documentation +Parameters are documented with specific format: +```dart +/// * [distinctId] a string uniquely identifying this user. Events sent to +/// Mixpanel using the same distinct id will be considered associated with the +/// same visitor/customer for retention and funnel reporting... +``` + +## Async Patterns + +### Consistent Future Returns +All methods return Future for consistency: +```dart +Future identify(String distinctId) async { + if (_MixpanelHelper.isValidString(distinctId)) { + return await _channel.invokeMethod('identify', { + 'distinctId': distinctId, + }); + } +} +``` + +### Fire-and-Forget Pattern +Most tracking calls don't await responses: +```dart +// Caller typically uses without await +mixpanel.track('Button Clicked'); + +// But can await if needed for sequencing +await mixpanel.track('Purchase Complete'); +await mixpanel.flush(); +``` + +## Platform Detection + +### Runtime Platform Checks +Uses Flutter's platform detection utilities: +```dart +// Web detection +import 'package:flutter/foundation.dart' show kIsWeb; + +// Platform-specific behavior +if (kIsWeb) { + // Web-specific implementation +} else { + // Mobile implementation +} +``` + +## Version Management + +### Hardcoded Version Tracking +SDK version included in tracking properties: +```dart +static Map _getMixpanelProperties() { + return { + '\$lib_version': '2.4.4', + // Other properties... + }; +} +``` \ No newline at end of file diff --git a/.claude/context/generation/agents-generation-summary-2025-01-07.md b/.claude/context/generation/agents-generation-summary-2025-01-07.md new file mode 100644 index 0000000..d54a4f1 --- /dev/null +++ b/.claude/context/generation/agents-generation-summary-2025-01-07.md @@ -0,0 +1,214 @@ +# AGENTS.md Generation Summary +*Generated: 2025-01-07* + +## Overview +This document summarizes the synthesis of AGENTS.md files from various AI systems for the Mixpanel Flutter SDK project. Each AI system provides unique perspectives and task recommendations based on their specialized capabilities. + +## Generated AGENTS.md Files + +### 1. Claude (Anthropic) +**File**: `.claude/context/ai-systems/claude/AGENTS.md` + +**Key Synthesis Points**: +- **Architecture-First Approach**: Claude emphasizes understanding system design before implementation +- **Pattern Recognition**: Strong focus on identifying and following existing code patterns +- **Communication Style**: Clear, methodical explanations with reasoning transparency +- **Testing Philosophy**: Comprehensive test coverage with edge case consideration + +**Recommended Tasks**: +- Complex refactoring requiring deep codebase understanding +- Architecture design and system integration +- Code review and pattern consistency enforcement +- Documentation generation with technical accuracy + +### 2. GPT-4 (OpenAI) +**File**: `.claude/context/ai-systems/gpt-4/AGENTS.md` + +**Key Synthesis Points**: +- **Problem-Solving Focus**: Emphasizes breaking down complex problems into manageable steps +- **Code Generation**: Strong capabilities in generating boilerplate and implementation code +- **Integration Expertise**: Good at connecting different systems and APIs +- **Flexibility**: Adapts well to various coding styles and paradigms + +**Recommended Tasks**: +- Feature implementation from specifications +- API integration and data transformation +- Quick prototyping and proof-of-concepts +- Code translation between languages/frameworks + +### 3. Gemini (Google) +**File**: `.claude/context/ai-systems/gemini/AGENTS.md` + +**Key Synthesis Points**: +- **Performance Optimization**: Focus on efficient algorithms and resource usage +- **Platform Knowledge**: Deep understanding of mobile and web platforms +- **Testing Integration**: Emphasis on test-driven development +- **Cross-Platform Expertise**: Strong in multi-platform consistency + +**Recommended Tasks**: +- Performance optimization and profiling +- Platform-specific implementations +- Cross-platform compatibility issues +- Mobile-specific features and constraints + +### 4. Copilot (GitHub/Microsoft) +**File**: `.claude/context/ai-systems/copilot/AGENTS.md` + +**Key Synthesis Points**: +- **Code Completion**: Excellent at context-aware code suggestions +- **Pattern Matching**: Recognizes and applies repository-specific patterns +- **IDE Integration**: Seamless development workflow integration +- **Quick Iterations**: Rapid code generation for common patterns + +**Recommended Tasks**: +- Rapid code completion and boilerplate generation +- Implementing similar patterns across files +- Quick fixes and small feature additions +- Code formatting and style consistency + +## Task Distribution Strategy + +### By Complexity Level + +**High Complexity** (Claude/GPT-4): +- System architecture changes +- Cross-platform synchronization +- Complex state management +- Performance-critical optimizations + +**Medium Complexity** (Gemini/GPT-4): +- Feature implementation +- Platform-specific code +- Test suite creation +- API integrations + +**Low Complexity** (Copilot/Any): +- Code formatting +- Simple bug fixes +- Documentation updates +- Boilerplate generation + +### By Task Type + +**Architecture & Design**: +- Primary: Claude +- Secondary: GPT-4 +- Use for: System design, refactoring, pattern establishment + +**Implementation**: +- Primary: GPT-4, Gemini +- Secondary: Copilot +- Use for: Feature development, bug fixes, integrations + +**Optimization**: +- Primary: Gemini +- Secondary: Claude +- Use for: Performance tuning, resource optimization + +**Documentation**: +- Primary: Claude +- Secondary: GPT-4 +- Use for: Technical docs, API references, architecture guides + +## Usage Guidelines + +### 1. Task Assignment +```yaml +Task Assessment: + - Complexity: [Low/Medium/High] + - Type: [Architecture/Implementation/Optimization/Documentation] + - Platform: [Cross-platform/iOS/Android/Web/Mobile/All] + - Context Required: [Minimal/Moderate/Extensive] +``` + +### 2. Context Preparation +- For Claude: Provide architecture context and patterns +- For GPT-4: Include specifications and examples +- For Gemini: Add performance requirements and platform details +- For Copilot: Show similar code patterns in the codebase + +### 3. Collaboration Patterns + +**Sequential Collaboration**: +1. Claude: Architecture design and pattern definition +2. GPT-4: Initial implementation +3. Gemini: Platform optimization +4. Copilot: Code completion and formatting + +**Parallel Collaboration**: +- Use multiple agents for different components +- Merge results with architectural guidance +- Review for consistency and patterns + +### 4. Quality Assurance + +**Code Review Priority**: +1. Architecture compliance (Claude) +2. Implementation correctness (GPT-4) +3. Performance metrics (Gemini) +4. Style consistency (Copilot) + +## Project-Specific Recommendations + +### For Mixpanel Flutter SDK + +**Platform Channel Implementation**: +- Use Claude for architecture decisions +- Use Gemini for platform-specific optimizations +- Use GPT-4 for cross-platform consistency + +**Feature Development**: +- Start with Claude for pattern analysis +- Implement with GPT-4 following patterns +- Optimize with Gemini for each platform +- Polish with Copilot for consistency + +**Testing Strategy**: +- Claude: Test architecture and strategy +- GPT-4: Test implementation +- Gemini: Platform-specific test cases +- Copilot: Test boilerplate and fixtures + +**Documentation**: +- Claude: Architecture and design docs +- GPT-4: API documentation +- Gemini: Platform-specific guides +- Copilot: Code comments and examples + +## Metrics for Success + +### Code Quality Metrics +- Pattern consistency: 95%+ (Claude verification) +- Test coverage: 90%+ (GPT-4 generation) +- Performance benchmarks: Meet targets (Gemini optimization) +- Style compliance: 100% (Copilot formatting) + +### Development Efficiency +- Architecture decisions: 2x faster with Claude +- Implementation: 3x faster with GPT-4 + Copilot +- Optimization: 2x better with Gemini +- Documentation: 4x faster with AI assistance + +## Future Enhancements + +### Potential Improvements +1. **Automated Task Routing**: Build system to route tasks to optimal AI +2. **Context Caching**: Maintain shared context between AI systems +3. **Result Merging**: Automated combination of multi-AI outputs +4. **Performance Tracking**: Monitor AI effectiveness per task type + +### Integration Opportunities +1. CI/CD pipeline integration for automated reviews +2. IDE plugins for real-time AI selection +3. Project management tool integration +4. Automated documentation generation + +## Conclusion + +The AGENTS.md files provide a comprehensive framework for leveraging multiple AI systems effectively. By understanding each system's strengths and following the task distribution strategy, teams can significantly improve development efficiency while maintaining high code quality standards. + +Key takeaways: +- Use AI systems based on their strengths +- Prepare appropriate context for each system +- Combine outputs for best results +- Maintain human oversight for critical decisions \ No newline at end of file diff --git a/.claude/context/technologies/flutter-plugin.md b/.claude/context/technologies/flutter-plugin.md new file mode 100644 index 0000000..40db47d --- /dev/null +++ b/.claude/context/technologies/flutter-plugin.md @@ -0,0 +1,172 @@ +# Flutter Plugin Development + +## Overview +This SDK implements Flutter's federated plugin architecture, providing platform-specific implementations while maintaining a unified Dart API. + +## Plugin Structure + +### Federated Plugin Architecture +```yaml +# pubspec.yaml +flutter: + plugin: + platforms: + android: + package: com.mixpanel.mixpanel_flutter + pluginClass: MixpanelFlutterPlugin + ios: + pluginClass: MixpanelFlutterPlugin + web: + pluginClass: MixpanelFlutterWeb + fileName: mixpanel_flutter_web.dart +``` + +### Platform Interface Pattern +The SDK doesn't use the newer platform interface pattern, instead using direct platform channel communication: +```dart +static final MethodChannel _channel = kIsWeb + ? const MethodChannel('mixpanel_flutter') + : const MethodChannel( + 'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec())); +``` + +## Platform Channel Communication + +### Method Channel Usage +All communication uses named methods with argument maps: +```dart +// Dart side +await _channel.invokeMethod('track', { + 'eventName': eventName, + 'properties': properties ?? {}, +}); + +// Android side +@Override +public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + switch (call.method) { + case "track": + eventName = call.argument("eventName"); + properties = call.argument("properties"); + // Handle tracking + break; + } +} +``` + +### Custom Message Codec +Extends StandardMessageCodec to handle additional types: +```dart +class MixpanelMessageCodec extends StandardMessageCodec { + static const int _typeDateTime = 128; + static const int _typeUri = 129; + + @override + void writeValue(WriteBuffer buffer, dynamic value) { + if (value is DateTime) { + buffer.putUint8(_typeDateTime); + buffer.putInt64(value.millisecondsSinceEpoch); + } else if (value is Uri) { + buffer.putUint8(_typeUri); + writeValue(buffer, value.toString()); + } else { + super.writeValue(buffer, value); + } + } +} +``` + +## Platform Detection + +### Runtime Platform Checks +```dart +// Check for web platform +import 'package:flutter/foundation.dart' show kIsWeb; + +// Usage +if (kIsWeb) { + // Web-specific code +} else { + // Mobile-specific code +} +``` + +### Conditional Imports +Web implementation loaded conditionally: +```dart +// In pubspec.yaml +dependencies: + flutter_web_plugins: + sdk: flutter +``` + +## Testing Strategy + +### Mock Platform Channels +Tests use TestDefaultBinaryMessengerBinding: +```dart +TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (MethodCall methodCall) async { + lastMethodCall = methodCall; + return null; +}); +``` + +### Custom Matcher for Method Calls +```dart +class _IsMethodCall extends Matcher { + final String method; + final dynamic arguments; + + @override + bool matches(dynamic item, Map matchState) { + if (item is! MethodCall) return false; + if (item.method != method) return false; + return arguments == null || item.arguments == arguments; + } +} +``` + +## Best Practices Observed + +### 1. **Consistent Method Naming** +Platform channel method names exactly match between Dart and native code. + +### 2. **Argument Validation** +Input validation happens in Dart before platform calls: +```dart +if (_MixpanelHelper.isValidString(eventName)) { + // Make platform call +} else { + developer.log('`track` failed: eventName cannot be blank'); +} +``` + +### 3. **Future Returns** +All methods return `Future` for consistency: +```dart +Future track(String eventName, {Map? properties}) async +``` + +### 4. **Null Safety** +Proper null handling with clear patterns: +```dart +properties: properties ?? {}, +``` + +## Platform-Specific Considerations + +### Android +- Plugin registered automatically via embedding v2 +- Lazy initialization to prevent ANR +- JSON conversion for properties + +### iOS +- Swift implementation with Objective-C compatibility +- Direct dictionary usage +- MixpanelType conversion for special types + +### Web +- Pure Dart implementation +- JavaScript interop for native library +- Dynamic script loading \ No newline at end of file diff --git a/.claude/context/technologies/javascript-interop.md b/.claude/context/technologies/javascript-interop.md new file mode 100644 index 0000000..f247883 --- /dev/null +++ b/.claude/context/technologies/javascript-interop.md @@ -0,0 +1,250 @@ +# JavaScript Interop (Web Platform) + +## Overview +The web implementation uses Dart's JavaScript interop capabilities to interface with the Mixpanel JavaScript library. This provides web support without needing platform channels. + +## Library Loading + +### HTML Setup Requirement +Users must add Mixpanel script to their HTML: +```html + +``` + +### Runtime Detection +The SDK checks for library presence: +```dart +// In mixpanel_flutter_web.dart +external MixpanelJs get mixpanelJs; + +// Usage with null checks +if (mixpanelJs != null) { + mixpanelJs.track(eventName, properties); +} +``` + +## JavaScript Bindings + +### External Declarations +Using dart:js_interop for bindings: +```dart +// From lib/web/mixpanel_js_bindings.dart +@JS('mixpanel') +external MixpanelJs get mixpanelJs; + +@JS() +@anonymous +abstract class MixpanelJs { + external void init(String token, [dynamic config, String? name]); + external void track(String eventName, [dynamic properties]); + external void identify(String distinctId); + external void alias(String alias, [String? original]); + external void set_config(dynamic config); + external People get people; + external void register(dynamic properties); + external void register_once(dynamic properties); + // ... more methods +} +``` + +### People API Bindings +```dart +@JS() +@anonymous +abstract class People { + external void set(dynamic properties); + external void set_once(dynamic properties); + external void unset(dynamic properties); + external void increment(dynamic properties); + external void append(dynamic properties); + external void union(dynamic properties); + external void remove(dynamic properties); + external void delete_user(); + external void clear_charges(); +} +``` + +## Type Conversion + +### Safe JavaScript Conversion +The SDK provides a helper for safe type conversion: +```dart +dynamic safeJsify(Object value) { + if (value is Map) { + // Convert to JS-compatible map + value = JsLinkedHashMap.from(value); + } + var args = jsify(value); + return args; +} +``` + +### Property Conversion Pattern +All property maps are converted before JS calls: +```dart +@override +Future track(String eventName, Map properties) async { + var trackedProperties = safeJsify(properties); + mixpanelJs.track(eventName, trackedProperties); +} +``` + +## Web-Specific Implementation + +### Platform Registration +```dart +class MixpanelFlutterWeb { + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'mixpanel_flutter', + const StandardMethodCodec(), + registrar, + ); + + final pluginInstance = MixpanelFlutterWeb(); + channel.setMethodCallHandler(pluginInstance.handleMethodCall); + } +} +``` + +### Method Handler +```dart +Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'initialize': + return initialize( + call.arguments['token'], + call.arguments['trackAutomaticEvents'], + // ... other args + ); + case 'track': + return track( + call.arguments['eventName'], + call.arguments['properties'], + ); + // ... other methods + } +} +``` + +## Initialization Flow + +### Web-Specific Init +```dart +@override +Future initialize( + String token, + bool trackAutomaticEvents, + Map superProperties, + Map config, +) async { + var webConfig = { + 'track_pageview': trackAutomaticEvents, + ...config, + }; + + mixpanelJs.init(token, safeJsify(webConfig)); + + if (superProperties.isNotEmpty) { + mixpanelJs.register(safeJsify(superProperties)); + } +} +``` + +## DateTime Handling + +### Web Platform Difference +Unlike mobile platforms that use custom codec, web handles DateTime differently: +```dart +// Web converts DateTime to ISO string in properties +if (value is DateTime) { + return value.toIso8601String(); +} +``` + +## Group Analytics + +### Group API Bindings +```dart +@JS() +@anonymous +abstract class Group { + external void set(dynamic properties); + external void set_once(dynamic properties); + external void unset(dynamic properties); + external void union(dynamic properties); + external void remove(dynamic properties); + external void delete_group(); +} +``` + +### Group Access Pattern +```dart +@override +Future groupSetProperties( + String groupKey, + dynamic groupID, + Map properties, +) async { + var group = mixpanelJs.get_group(groupKey, groupID); + group.set(safeJsify(properties)); +} +``` + +## Error Handling + +### Null Check Pattern +Web implementation uses null checks instead of try-catch: +```dart +if (mixpanelJs != null) { + mixpanelJs.track(eventName, properties); +} else { + print('Mixpanel JS library not loaded'); +} +``` + +## Best Practices + +### 1. **Type Safety** +Always use safeJsify for complex objects: +```dart +var jsProperties = safeJsify(properties); +``` + +### 2. **Library Detection** +Check for library presence before operations: +```dart +external MixpanelJs? get mixpanelJs; +``` + +### 3. **Consistent API** +Web implementation mirrors mobile API exactly: +```dart +// Same method signature as mobile +Future track(String eventName, {Map? properties}) +``` + +### 4. **Configuration Mapping** +Map Flutter config to JS equivalents: +```dart +var webConfig = { + 'track_pageview': trackAutomaticEvents, + 'persistence': 'localStorage', + // Map other Flutter configs +}; +``` + +## Limitations + +### No Custom Codec +Web uses standard JSON serialization, so: +- DateTime converted to ISO strings +- Uri converted to strings +- No binary data support + +### Script Loading +Requires manual script inclusion in HTML, cannot be loaded dynamically by the SDK. \ No newline at end of file diff --git a/.claude/context/technologies/platform-channels.md b/.claude/context/technologies/platform-channels.md new file mode 100644 index 0000000..a5f4d54 --- /dev/null +++ b/.claude/context/technologies/platform-channels.md @@ -0,0 +1,261 @@ +# Platform Channels + +## Overview +Platform channels enable communication between Dart code and platform-specific implementations. This SDK uses MethodChannel with a custom message codec for type serialization. + +## Channel Configuration + +### Channel Setup +```dart +// From lib/mixpanel_flutter.dart +static final MethodChannel _channel = kIsWeb + ? const MethodChannel('mixpanel_flutter') + : const MethodChannel( + 'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec())); +``` + +### Channel Name +Consistent channel name across all platforms: `'mixpanel_flutter'` + +## Method Invocation Pattern + +### Dart Side +All method calls follow this pattern: +```dart +Future methodName(parameters) async { + // 1. Validate inputs + if (_MixpanelHelper.isValidString(parameter)) { + // 2. Invoke platform method + await _channel.invokeMethod('methodName', { + 'param1': value1, + 'param2': value2, + }); + } else { + // 3. Log validation failures + developer.log('`methodName` failed: reason', name: 'Mixpanel'); + } +} +``` + +### Argument Structure +Arguments always passed as Map: +```dart +// Simple method +await _channel.invokeMethod('setLoggingEnabled', { + 'loggingEnabled': loggingEnabled, +}); + +// Complex method with nested data +await _channel.invokeMethod('initialize', { + 'token': token, + 'optOutTrackingDefault': optOutTrackingDefault, + 'trackAutomaticEvents': trackAutomaticEvents, + 'superProperties': superProperties ?? {}, + 'properties': mixpanelProperties, + 'config': config ?? {}, +}); +``` + +## Platform Implementations + +### Android Handler +```java +public class MixpanelFlutterPlugin implements FlutterPlugin, MethodCallHandler { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + try { + // Extract method name + switch (call.method) { + case "track": + // Extract arguments + String eventName = call.argument("eventName"); + Map properties = call.argument("properties"); + + // Convert to platform types + JSONObject jsonObject = MixpanelFlutterHelper.toJSONObject(properties); + + // Call native SDK + mixpanel.track(eventName, jsonObject); + + // Return success + result.success(null); + break; + + default: + result.notImplemented(); + } + } catch (Exception e) { + result.error("MixpanelFlutterException", e.getLocalizedMessage(), null); + } + } +} +``` + +### iOS Handler +```swift +public class SwiftMixpanelFlutterPlugin: NSObject, FlutterPlugin { + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "track": + guard let arguments = call.arguments as? [String: Any], + let eventName = arguments["eventName"] as? String else { + result(nil) + return + } + + if let properties = arguments["properties"] as? [String: Any] { + let mpProperties = convertToMixpanelTypes(properties) + Mixpanel.mainInstance().track(event: eventName, properties: mpProperties) + } + + result(nil) + + default: + result(FlutterMethodNotImplemented) + } + } +} +``` + +## Custom Message Codec + +### Purpose +Handles types not supported by StandardMessageCodec: +- DateTime objects +- Uri objects + +### Implementation +```dart +class MixpanelMessageCodec extends StandardMessageCodec { + const MixpanelMessageCodec(); + + static const int _typeDateTime = 128; + static const int _typeUri = 129; + + @override + void writeValue(WriteBuffer buffer, dynamic value) { + if (value is DateTime) { + buffer.putUint8(_typeDateTime); + buffer.putInt64(value.millisecondsSinceEpoch); + } else if (value is Uri) { + buffer.putUint8(_typeUri); + writeValue(buffer, value.toString()); + } else { + super.writeValue(buffer, value); + } + } + + @override + dynamic readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case _typeDateTime: + return DateTime.fromMillisecondsSinceEpoch(buffer.getInt64()); + case _typeUri: + final String uriString = readValueOfType(buffer.getUint8(), buffer); + return Uri.parse(uriString); + default: + return super.readValueOfType(type, buffer); + } + } +} +``` + +### Platform Counterparts + +#### Android (Java) +```java +public class MixpanelMessageCodec extends StandardMessageCodec { + private static final byte DATE_TIME = (byte) 128; + private static final byte URI = (byte) 129; + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof Date) { + stream.write(DATE_TIME); + writeLong(stream, ((Date) value).getTime()); + } else if (value instanceof Uri || value instanceof URI) { + stream.write(URI); + writeValue(stream, value.toString()); + } else { + super.writeValue(stream, value); + } + } +} +``` + +#### iOS (Swift) +```swift +func convertToMixpanelTypes(_ properties: [String: Any]) -> Properties { + var mixpanelProperties = Properties() + for (key, value) in properties { + if let date = value as? Date { + mixpanelProperties[key] = date + } else if let url = value as? URL { + mixpanelProperties[key] = url + } else { + mixpanelProperties[key] = MixpanelType(value) + } + } + return mixpanelProperties +} +``` + +## Error Handling + +### Result Callbacks +Three types of results: +```dart +// Success +result.success(returnValue); + +// Error +result.error("ErrorCode", "Error message", errorDetails); + +// Not implemented +result.notImplemented(); +``` + +### Error Propagation +Platform errors surface to Dart as PlatformException: +```dart +try { + await _channel.invokeMethod('someMethod'); +} on PlatformException catch (e) { + // Handle platform-specific error + print('Failed: ${e.message}'); +} +``` + +## Best Practices + +### 1. **Consistent Naming** +Method names must match exactly across platforms. + +### 2. **Type Safety** +Always validate types on platform side: +```swift +guard let eventName = arguments["eventName"] as? String else { + result(nil) + return +} +``` + +### 3. **Null Handling** +Handle null/missing arguments gracefully: +```java +Map properties = call.argument("properties"); +if (properties == null) { + properties = new HashMap<>(); +} +``` + +### 4. **Return Values** +Most tracking methods return void: +```dart +await _channel.invokeMethod('track', args); +``` + +Methods that return values specify type: +```dart +final String? distinctId = await _channel.invokeMethod('getDistinctId'); +``` \ No newline at end of file diff --git a/.claude/context/workflows/new-feature.md b/.claude/context/workflows/new-feature.md new file mode 100644 index 0000000..9d5bd04 --- /dev/null +++ b/.claude/context/workflows/new-feature.md @@ -0,0 +1,180 @@ +# Workflow: Adding a New Feature + +## Prerequisites +- Understand the existing SDK architecture +- Have Flutter development environment set up +- Familiar with platform channels and native development + +## Steps + +### 1. **Define the Dart API** +Add the new method to the appropriate class in `lib/mixpanel_flutter.dart`: + +```dart +// For general tracking features +class Mixpanel { + Future newFeature(String param1, {Map? properties}) async { + if (_MixpanelHelper.isValidString(param1)) { + await _channel.invokeMethod('newFeature', { + 'param1': param1, + 'properties': properties ?? {}, + }); + } else { + developer.log('`newFeature` failed: param1 cannot be blank', name: 'Mixpanel'); + } + } +} + +// For user-specific features +class People { + Future newUserFeature(Map properties) async { + return await _channel.invokeMethod('newUserFeature', { + 'properties': properties, + }); + } +} +``` + +### 2. **Update Web Implementation** +Add the method to `lib/mixpanel_flutter_web.dart`: + +```dart +@override +Future newFeature(String param1, Map properties) async { + var jsProperties = safeJsify(properties); + mixpanelJs.newFeature(param1, jsProperties); +} +``` + +Update JavaScript bindings in `lib/web/mixpanel_js_bindings.dart` if needed: +```dart +@JS() +@anonymous +abstract class MixpanelJs { + external void newFeature(String param1, [dynamic properties]); +} +``` + +### 3. **Implement Android Handler** +Add case to `android/src/main/java/com/mixpanel/mixpanel_flutter/MixpanelFlutterPlugin.java`: + +```java +case "newFeature": + String param1 = call.argument("param1"); + Map properties = call.argument("properties"); + + try { + JSONObject jsonProperties = MixpanelFlutterHelper.toJSONObject(properties); + // Add library metadata + jsonProperties = MixpanelFlutterHelper.getMergedProperties(jsonProperties, mixpanelProperties); + + // Call native SDK + mixpanel.newFeature(param1, jsonProperties); + result.success(null); + } catch (JSONException e) { + result.error("MixpanelFlutterException", e.getLocalizedMessage(), null); + } + break; +``` + +### 4. **Implement iOS Handler** +Add case to `ios/Classes/SwiftMixpanelFlutterPlugin.swift`: + +```swift +case "newFeature": + guard let arguments = call.arguments as? [String: Any], + let param1 = arguments["param1"] as? String else { + result(nil) + return + } + + var properties = Properties() + if let props = arguments["properties"] as? [String: Any] { + properties = convertToMixpanelTypes(props) + } + + // Add library metadata + properties.merge(getMixpanelProperties()) { (_, new) in new } + + // Call native SDK + Mixpanel.mainInstance().newFeature(param1, properties: properties) + result(nil) +``` + +### 5. **Add Example Implementation** +Create or update a page in `example/lib/`: + +```dart +// In example/lib/new_feature_page.dart +class NewFeaturePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('New Feature Example')), + body: ListView( + children: [ + MixpanelButton( + title: 'Test New Feature', + message: 'Testing new feature with properties', + onPressed: () { + mixpanel.newFeature('test-param', properties: { + 'timestamp': DateTime.now(), + 'user_action': 'button_click', + }); + }, + ), + ], + ), + ); + } +} +``` + +### 6. **Write Tests** +Add test cases to `test/mixpanel_flutter_test.dart`: + +```dart +test('newFeature sends correct parameters', () async { + await mixpanel.newFeature('test-param', properties: {'key': 'value'}); + expect( + methodCall, + isMethodCall( + 'newFeature', + arguments: { + 'param1': 'test-param', + 'properties': {'key': 'value'}, + }, + ), + ); +}); + +test('newFeature validates empty string', () async { + await mixpanel.newFeature('', properties: {'key': 'value'}); + expect(methodCall, null); // Should not make platform call +}); +``` + +### 7. **Update Documentation** +- Add method documentation with triple-slash comments +- Update CHANGELOG.md with the new feature +- Update README.md if it's a major feature + +## Testing Checklist +- [ ] Unit tests pass: `flutter test` +- [ ] Example app works on Android: `cd example && flutter run` +- [ ] Example app works on iOS: `cd example && flutter run` +- [ ] Example app works on Web: `cd example && flutter run -d chrome` +- [ ] Static analysis passes: `flutter analyze` + +## Common Pitfalls +- Forgetting to add library metadata ($lib_version) to properties +- Not handling null/empty string validation +- Inconsistent method naming between platforms +- Missing type conversions for complex objects +- Not updating all three platforms (Android, iOS, Web) + +## Type Handling +If your feature uses special types: +1. DateTime: Automatically handled by MixpanelMessageCodec +2. Uri: Automatically handled by MixpanelMessageCodec +3. Custom objects: Convert to Map first \ No newline at end of file diff --git a/.claude/context/workflows/release.md b/.claude/context/workflows/release.md new file mode 100644 index 0000000..97c8a50 --- /dev/null +++ b/.claude/context/workflows/release.md @@ -0,0 +1,214 @@ +# Workflow: Release Process + +## Overview +The SDK uses an automated release process with version management script and GitHub Actions. + +## Prerequisites +- Clean working directory (all changes committed) +- Python installed for release script +- Access to publish on pub.dev +- GitHub repository access for tagging + +## Version Bump Process + +### 1. **Run Release Script** +```bash +python tool/release.py --old 2.4.3 --new 2.4.4 +``` + +This script automatically: +- Updates version in `pubspec.yaml` +- Updates `$lib_version` in `lib/mixpanel_flutter.dart` +- Updates test expectations in `test/mixpanel_flutter_test.dart` +- Updates version in `ios/mixpanel_flutter.podspec` +- Generates documentation with `dartdoc` +- Commits changes with message "Version X.Y.Z" +- Creates git tag `vX.Y.Z` +- Runs `dart pub publish --dry-run` for validation + +### 2. **Files Updated by Script** + +#### pubspec.yaml +```yaml +name: mixpanel_flutter +description: Official Mixpanel Flutter SDK +version: 2.4.4 # Updated +``` + +#### lib/mixpanel_flutter.dart +```dart +static Map _getMixpanelProperties() { + return { + '\$lib_version': '2.4.4', # Updated + 'mp_lib': 'flutter', + }; +} +``` + +#### test/mixpanel_flutter_test.dart +```dart +expect(versionRegex.hasMatch('2.4.4'), true); # Updated +``` + +#### ios/mixpanel_flutter.podspec +```ruby +Pod::Spec.new do |s| + s.name = 'mixpanel_flutter' + s.version = '2.4.4' # Updated + # ... +end +``` + +## Manual Release Steps + +### 1. **Update CHANGELOG.md** +Add release notes following the existing format: +```markdown +## 2.4.4 +* Fixed issue with DateTime serialization on Android +* Added support for new tracking features +* Updated dependencies +``` + +### 2. **Push Changes** +```bash +git push origin main +git push origin v2.4.4 +``` + +### 3. **GitHub Release** +The push of the version tag triggers GitHub Actions: +- Creates GitHub release automatically +- Generates release notes from commits +- Updates CHANGELOG.md via workflow + +### 4. **Publish to pub.dev** +```bash +dart pub publish +``` + +Follow the prompts to authenticate and confirm publication. + +## Version Numbering + +Follow semantic versioning: +- **MAJOR**: Breaking API changes +- **MINOR**: New features, backwards compatible +- **PATCH**: Bug fixes, backwards compatible + +Examples: +- `2.4.3` → `2.4.4`: Bug fix +- `2.4.3` → `2.5.0`: New feature +- `2.4.3` → `3.0.0`: Breaking change + +## Pre-Release Checklist + +- [ ] All tests pass: `flutter test` +- [ ] Static analysis clean: `flutter analyze` +- [ ] Example app works on all platforms +- [ ] CHANGELOG.md updated with changes +- [ ] Documentation updated if needed +- [ ] Version compatibility verified: + - Dart SDK constraints + - Flutter SDK constraints + - Native SDK versions + +## GitHub Actions Release Workflow + +Located in `.github/workflows/release.yml`: + +```yaml +name: Release + +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +Configuration in `.github/release-drafter.yml`: +- Categorizes changes by labels +- Generates release notes +- Updates CHANGELOG.md + +## Rollback Process + +If issues are discovered after release: + +### 1. **Revert Tag** +```bash +git tag -d v2.4.4 +git push origin :refs/tags/v2.4.4 +``` + +### 2. **Fix Issues** +Make necessary fixes and test thoroughly. + +### 3. **Re-release** +Either use same version or bump patch version: +```bash +python tool/release.py --old 2.4.4 --new 2.4.5 +``` + +## Platform-Specific Considerations + +### iOS CocoaPods +The podspec version must match the pubspec version. + +### Android +No special version handling needed beyond pubspec. + +### Web +JavaScript library version is determined by CDN link in user's HTML. + +## Post-Release Verification + +### 1. **Verify pub.dev** +Check that package appears on: https://pub.dev/packages/mixpanel_flutter + +### 2. **Test Installation** +Create new Flutter project and add dependency: +```yaml +dependencies: + mixpanel_flutter: ^2.4.4 +``` + +### 3. **Verify Example App** +```bash +cd example +flutter pub upgrade +flutter run +``` + +## Common Issues + +### Dry Run Failures +If `dart pub publish --dry-run` fails: +- Check for uncommitted changes +- Verify all required files are included +- Check pubspec.yaml formatting + +### Version Mismatch +Ensure all version references are updated: +- pubspec.yaml +- lib/mixpanel_flutter.dart +- test/mixpanel_flutter_test.dart +- ios/mixpanel_flutter.podspec + +### Tag Already Exists +```bash +# Delete local and remote tag +git tag -d v2.4.4 +git push origin :refs/tags/v2.4.4 +# Re-run release script +``` \ No newline at end of file diff --git a/.claude/context/workflows/testing.md b/.claude/context/workflows/testing.md new file mode 100644 index 0000000..9a2dd32 --- /dev/null +++ b/.claude/context/workflows/testing.md @@ -0,0 +1,240 @@ +# Workflow: Testing + +## Test Structure + +The SDK uses a multi-layered testing approach: +- **Unit Tests**: Verify platform channel communication +- **Integration Tests**: Manual testing via example app +- **CI Tests**: Automated builds on GitHub Actions + +## Running Tests + +### Unit Tests +```bash +# Run all tests +flutter test + +# Run with coverage +flutter test --coverage + +# Run specific test file +flutter test test/mixpanel_flutter_test.dart +``` + +### Integration Testing +The example app serves as the integration test suite: +```bash +cd example + +# Android +flutter run + +# iOS (requires macOS with Xcode) +flutter run + +# Web +flutter run -d chrome +``` + +## Writing Tests + +### Platform Channel Tests +All tests mock the platform channel to verify correct method calls: + +```dart +test('track sends correct arguments', () async { + await mixpanel.track('Button Clicked', properties: { + 'button_name': 'purchase', + 'price': 99.99, + }); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Button Clicked', + 'properties': { + 'button_name': 'purchase', + 'price': 99.99, + }, + }, + ), + ); +}); +``` + +### Validation Tests +Test that invalid inputs are handled gracefully: + +```dart +test('track ignores empty event name', () async { + await mixpanel.track('', properties: {'key': 'value'}); + expect(methodCall, null); // No platform call should be made +}); + +test('identify ignores empty distinct id', () async { + await mixpanel.identify(''); + expect(methodCall, null); +}); +``` + +### Type Handling Tests +Test custom codec functionality: + +```dart +test('handles DateTime in properties', () async { + final now = DateTime.now(); + await mixpanel.track('Test', properties: {'timestamp': now}); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Test', + 'properties': {'timestamp': now}, + }, + ), + ); +}); +``` + +## Test Patterns + +### Setup and Teardown +```dart +late MethodChannel methodChannel; +MethodCall? methodCall; +late Mixpanel mixpanel; + +setUp(() async { + methodChannel = const MethodChannel('mixpanel_flutter'); + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, (MethodCall call) async { + methodCall = call; + return null; + }); + + mixpanel = await Mixpanel.init("test_token", trackAutomaticEvents: false); +}); + +tearDown(() { + methodCall = null; +}); +``` + +### Custom Matchers +The SDK includes custom matchers for method calls: + +```dart +Matcher isMethodCall(String method, {dynamic arguments}) { + return _IsMethodCall(method, arguments); +} + +class _IsMethodCall extends Matcher { + final String method; + final dynamic arguments; + + const _IsMethodCall(this.method, this.arguments); + + @override + bool matches(dynamic item, Map matchState) { + if (item is! MethodCall) return false; + if (item.method != method) return false; + return arguments == null || item.arguments == arguments; + } +} +``` + +## Platform-Specific Testing + +### Android Testing +```bash +cd example +flutter build apk +# Install on device/emulator +flutter install +``` + +### iOS Testing +```bash +cd example +flutter build ios --simulator +# Run on simulator +flutter run +``` + +### Web Testing +```bash +cd example +flutter build web +# Serve locally +python -m http.server 8080 --directory build/web +``` + +## CI Testing + +GitHub Actions runs tests automatically on push/PR: + +### Test Matrix +- **Flutter Version**: Latest stable +- **Platforms**: Ubuntu (Android), macOS (iOS) +- **Jobs**: + 1. Unit tests and static analysis + 2. Android APK build + 3. iOS simulator build + +### CI Commands +```yaml +# Unit tests +flutter test --no-pub + +# Static analysis +flutter analyze --no-pub --no-current-package lib + +# Android build +cd example && flutter build apk + +# iOS build +cd example && flutter build ios --debug --simulator --no-codesign +``` + +## Testing Checklist + +Before submitting PR: +- [ ] All unit tests pass +- [ ] Static analysis has no errors +- [ ] Example app runs on Android +- [ ] Example app runs on iOS +- [ ] Example app runs on Web +- [ ] New features have corresponding tests +- [ ] Edge cases are tested (empty strings, null values) +- [ ] CI builds are green + +## Common Test Issues + +### Platform Channel Not Mocked +```dart +// Incorrect - will fail +final mixpanel = Mixpanel('token'); + +// Correct - use init method +final mixpanel = await Mixpanel.init('token', trackAutomaticEvents: false); +``` + +### Async Test Issues +```dart +// Always use async/await in tests +test('async test', () async { + await mixpanel.track('event'); + // assertions... +}); +``` + +### Type Comparison +```dart +// Be careful with type checking +expect(methodCall.arguments['properties'], isA>()); +``` \ No newline at end of file diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md new file mode 100644 index 0000000..f75a860 --- /dev/null +++ b/.cursor/rules/README.md @@ -0,0 +1,129 @@ +# Cursor Rules Guide + +## Overview + +These MDC rules complement the Claude Code context by providing active behavioral guidance during code generation in Cursor. While Claude Code context stores comprehensive knowledge about the codebase, these rules actively shape AI behavior to ensure consistent, high-quality code generation. + +## Rule Organization + +### Directory Structure +``` +.cursor/rules/ +├── always/ # Universal rules (<500 lines total) +│ ├── core-conventions.mdc # Naming, structure, patterns +│ ├── architecture-principles.mdc # System boundaries, dependencies +│ └── code-quality.mdc # Testing, docs, error handling +├── components/ # Auto-attached by file type +│ ├── android-implementation.mdc # **/*.java patterns +│ ├── ios-implementation.mdc # **/*.swift patterns +│ ├── web-implementation.mdc # **/*_web.dart patterns +│ ├── test-patterns.mdc # **/test/**/*.dart patterns +│ └── example-app.mdc # **/example/lib/**/*.dart patterns +└── workflows/ # Agent-requested procedures + ├── new-feature.mdc # Feature implementation guide + ├── release-process.mdc # Version release workflow + └── testing-workflow.mdc # Multi-platform testing guide +``` + +## How Rules Relate to Claude Code Context + +| Claude Code Context | Cursor Rules | Purpose | +|-------------------|--------------|---------| +| `.claude/context/discovered-patterns.md` | `/always/core-conventions.mdc` | Enforce discovered naming and coding patterns | +| `.claude/context/architecture/system-design.md` | `/always/architecture-principles.mdc` | Maintain architectural boundaries and separation | +| `.claude/context/workflows/*.md` | `/workflows/*.mdc` | Guide complex multi-step procedures | +| `CLAUDE.md` | All rules | Critical patterns distilled into behavioral rules | + +## Rule Categories Explained + +### Always Rules (Universal Application) +Applied to **every** code generation. These prevent the most common and critical errors: +- **core-conventions.mdc**: Method naming, validation patterns, async conventions +- **architecture-principles.mdc**: Platform separation, singleton pattern, type safety +- **code-quality.mdc**: Testing requirements, documentation standards, error handling + +### Component Rules (Auto-Attached) +Applied automatically when working with specific file types: +- **android-implementation.mdc**: Java/Kotlin patterns for Android platform +- **ios-implementation.mdc**: Swift patterns for iOS platform +- **web-implementation.mdc**: Dart/JS interop for web platform +- **test-patterns.mdc**: Testing conventions and requirements +- **example-app.mdc**: Example app structure and demonstration patterns + +### Workflow Rules (Agent-Requested) +Complex procedures the AI can request when needed: +- **new-feature.mdc**: Step-by-step guide for adding SDK features +- **release-process.mdc**: Version release and publishing workflow +- **testing-workflow.mdc**: Comprehensive multi-platform testing + +## Quick Reference + +### Core Patterns Enforced + +| Pattern | Rule File | Key Requirements | +|---------|-----------|------------------| +| Method Naming | `core-conventions.mdc` | camelCase, verb prefixes (track, register, get) | +| Input Validation | `core-conventions.mdc` | Validate strings before platform calls | +| Error Handling | `core-conventions.mdc` | Fail silently with logging, no exceptions | +| Platform Channels | `architecture-principles.mdc` | Consistent argument structure | +| Type Safety | `architecture-principles.mdc` | Custom codec for DateTime/Uri | +| Testing | `code-quality.mdc` | Validation tests for all methods | + +### Platform-Specific Requirements + +| Platform | Key Patterns | +|----------|--------------| +| Android | JSONObject conversion, lazy init, null safety | +| iOS | Guard statements, MixpanelType conversion | +| Web | JS interop with @JS, safeJsify for types | + +## Usage in Cursor + +### Automatic Application +1. Always rules apply to all Flutter/Dart files +2. Component rules apply based on file location +3. The AI selects appropriate patterns without prompting + +### Manual Workflow Requests +When implementing complex features: +``` +"I need to add a new tracking method to the SDK" +→ AI will use new-feature.mdc workflow + +"I need to release version 2.5.0" +→ AI will use release-process.mdc workflow +``` + +## Maintenance Guidelines + +### Updating Rules +1. When patterns evolve, update the corresponding rule file +2. Test by generating code and verifying compliance +3. Keep rules in sync with Claude Code context + +### Rule Size Management +- Total always rules must stay under 500 lines +- Consolidate similar patterns +- Move detailed examples to component rules + +### Testing Rule Effectiveness +1. Generate code for common tasks +2. Verify it follows all conventions +3. Check that platform-specific code is correct +4. Ensure no critical patterns are missed + +## Integration with Development Workflow + +1. **New Developer Onboarding**: Point to these rules for behavioral expectations +2. **Code Review**: Reference specific rules when commenting +3. **Pattern Updates**: Update rules when establishing new patterns +4. **CI/CD**: Rules ensure generated code passes automated checks + +## Relationship to Other Documentation + +- **Claude Code Context**: Comprehensive knowledge base and documentation +- **Cursor Rules**: Active behavioral guidance during code generation +- **CLAUDE.md**: High-level patterns and project overview +- **Example App**: Living documentation of SDK usage + +Together, these resources ensure AI-generated code is indistinguishable from code written by experienced team members. \ No newline at end of file diff --git a/.cursor/rules/always/architecture-principles.mdc b/.cursor/rules/always/architecture-principles.mdc new file mode 100644 index 0000000..2d61261 --- /dev/null +++ b/.cursor/rules/always/architecture-principles.mdc @@ -0,0 +1,200 @@ +--- +description: Architectural boundaries and component separation rules that maintain system integrity +globs: [] +alwaysApply: true +--- + +# Architecture Principles + +These rules maintain the layered architecture and ensure proper separation of concerns across the Mixpanel Flutter SDK. + +## Platform Channel Architecture + +Maintain strict separation between Dart API layer and platform implementations. + +✅ **Correct:** +```dart +// In lib/mixpanel_flutter.dart - Dart API layer only +class Mixpanel { + static final MethodChannel _channel = kIsWeb + ? const MethodChannel('mixpanel_flutter') + : const MethodChannel( + 'mixpanel_flutter', StandardMethodCodec(MixpanelMessageCodec())); + + Future track(String eventName, {Map? properties}) async { + // Only validation and channel invocation + if (_MixpanelHelper.isValidString(eventName)) { + await _channel.invokeMethod('track', { + 'eventName': eventName, + 'properties': properties ?? {}, + }); + } + } +} +``` + +❌ **Incorrect:** +```dart +// Don't mix platform-specific logic in Dart API layer +class Mixpanel { + Future track(String eventName, {Map? properties}) async { + if (Platform.isAndroid) { + // Platform-specific logic belongs in native code + final json = convertToJSONObject(properties); + } + } +} +``` + +## Singleton Pattern Requirements + +The SDK must use static factory initialization, never expose constructors. + +✅ **Correct:** +```dart +class Mixpanel { + final String _token; + + // Private constructor + Mixpanel._internal(this._token); + + // Static factory method + static Future init(String token, { + bool optOutTrackingDefault = false, + required bool trackAutomaticEvents, + }) async { + await _channel.invokeMethod('initialize', { + 'token': token, + 'trackAutomaticEvents': trackAutomaticEvents, + }); + return Mixpanel._internal(token); + } +} +``` + +❌ **Incorrect:** +```dart +class Mixpanel { + // Don't expose public constructors + Mixpanel(this.token); // Wrong - breaks singleton pattern +} +``` + +## Type Serialization Rules + +Complex types must be handled through MixpanelMessageCodec for mobile platforms. + +✅ **Correct:** +```dart +// In codec/mixpanel_message_codec.dart +class MixpanelMessageCodec extends StandardMessageCodec { + @override + void writeValue(WriteBuffer buffer, dynamic value) { + if (value is DateTime) { + buffer.putUint8(_typeDateTime); + buffer.putInt64(value.millisecondsSinceEpoch); + } else if (value is Uri) { + buffer.putUint8(_typeUri); + writeValue(buffer, value.toString()); + } else { + super.writeValue(buffer, value); + } + } +} +``` + +❌ **Incorrect:** +```dart +// Don't send complex types directly without codec handling +await _channel.invokeMethod('track', { + 'timestamp': DateTime.now(), // Will fail without codec +}); +``` + +## Web Platform Separation + +Web implementation must be completely separate and use JavaScript interop. + +✅ **Correct:** +```dart +// In lib/mixpanel_flutter_web.dart +class MixpanelFlutterWeb { + dynamic safeJsify(Object value) { + if (value is Map) { + value = JsLinkedHashMap.from(value); + } + return jsify(value); + } + + Future track(String eventName, Map properties) async { + var trackedProperties = safeJsify(properties); + mixpanelJs.track(eventName, trackedProperties); + } +} +``` + +❌ **Incorrect:** +```dart +// Don't use platform channels on web +if (kIsWeb) { + await _channel.invokeMethod('track', args); // Wrong for web +} +``` + +## Component Access Pattern + +Sub-components must be accessed through getter methods, not public properties. + +✅ **Correct:** +```dart +class Mixpanel { + People? _people; + + People getPeople() { + _people ??= People(_token); + return _people!; + } +} + +// Usage +mixpanel.getPeople().set('name', 'John'); +``` + +❌ **Incorrect:** +```dart +class Mixpanel { + // Don't expose as public property + final People people = People(); // Wrong +} + +// Usage +mixpanel.people.set('name', 'John'); +``` + +## Method Name Consistency + +Platform channel method names must match exactly between Dart and native implementations. + +✅ **Correct:** +```dart +// Dart +await _channel.invokeMethod('registerSuperProperties', args); + +// Android +case "registerSuperProperties": + // Implementation + +// iOS +case "registerSuperProperties": + // Implementation +``` + +❌ **Incorrect:** +```dart +// Inconsistent naming breaks platform communication +// Dart +await _channel.invokeMethod('registerSuperProperties', args); + +// Android +case "register_super_properties": // Wrong - different naming +``` \ No newline at end of file diff --git a/.cursor/rules/always/code-quality.mdc b/.cursor/rules/always/code-quality.mdc new file mode 100644 index 0000000..96435bf --- /dev/null +++ b/.cursor/rules/always/code-quality.mdc @@ -0,0 +1,195 @@ +--- +description: Testing patterns, documentation standards, and quality requirements for maintainable code +globs: [] +alwaysApply: true +--- + +# Code Quality Standards + +These rules ensure high-quality, maintainable code with proper testing and documentation. + +## Test Structure Pattern + +All tests must use proper initialization and mock the platform channel correctly. + +✅ **Correct:** +```dart +test('track sends correct arguments', () async { + final mixpanel = await Mixpanel.init('test_token', trackAutomaticEvents: false); + + await mixpanel.track('test event', properties: {'a': 'b'}); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'test event', + 'properties': {'a': 'b'}, + }, + ), + ); +}); +``` + +❌ **Incorrect:** +```dart +test('track test', () async { + // Don't use constructor directly - breaks initialization flow + final mixpanel = Mixpanel('token'); + + // Missing proper assertions + await mixpanel.track('event'); + expect(methodCall, isNotNull); // Too vague +}); +``` + +## Validation Test Requirements + +Every public method must have tests for invalid inputs. + +✅ **Correct:** +```dart +test('track() should not crash when event name is empty', () async { + final mixpanel = await Mixpanel.init('test_token', trackAutomaticEvents: false); + + await mixpanel.track('', properties: {'a': 'b'}); + expect(methodCall, null); // Verify no platform call made +}); + +test('track() should not crash when event name is null', () async { + final mixpanel = await Mixpanel.init('test_token', trackAutomaticEvents: false); + + await mixpanel.track(null as dynamic, properties: {'a': 'b'}); + expect(methodCall, null); +}); +``` + +❌ **Incorrect:** +```dart +// Missing validation tests allows crashes in production +test('track works', () async { + await mixpanel.track('valid event'); + // Only testing happy path +}); +``` + +## Documentation Format + +All public methods must have comprehensive dartdoc comments. + +✅ **Correct:** +```dart +/// Track an event. +/// +/// Every call to track eventually results in a data point sent to Mixpanel. +/// These data points are what are measured, counted, and broken down to create +/// your Mixpanel reports. Events have a string name, and an optional set of +/// name/value pairs that describe the properties of that event. +/// +/// * [eventName] The name of the event to send +/// * [properties] A Map containing the key value pairs of the properties +/// to include in this event. Pass null if no extra properties exist. +Future track(String eventName, {Map? properties}) async { + // Implementation +} +``` + +❌ **Incorrect:** +```dart +// Missing or incomplete documentation +// Tracks an event +Future track(String eventName, {Map? properties}) async { + // Implementation +} +``` + +## Error Logging Standards + +Use consistent logging with the 'Mixpanel' tag and descriptive messages. + +✅ **Correct:** +```dart +if (!_MixpanelHelper.isValidString(eventName)) { + developer.log('`track` failed: eventName cannot be blank', name: 'Mixpanel'); + return; +} + +if (!_MixpanelHelper.isValidString(distinctId)) { + developer.log('`identify` failed: distinctId cannot be blank', name: 'Mixpanel'); + return; +} +``` + +❌ **Incorrect:** +```dart +if (!valid) { + print('Error'); // Don't use print + developer.log('Failed'); // Missing context and tag + debugPrint('track failed: $eventName'); // Don't log sensitive data +} +``` + +## Version Management + +SDK version must be updated in all required locations when releasing. + +✅ **Correct:** +```dart +// In lib/mixpanel_flutter.dart +static Map _getMixpanelProperties() { + return { + '\$lib_version': '2.4.4', // Must match pubspec.yaml + 'mp_lib': 'flutter', + }; +} + +// In pubspec.yaml +version: 2.4.4 + +// In ios/mixpanel_flutter.podspec +s.version = '2.4.4' +``` + +❌ **Incorrect:** +```dart +// Mismatched versions cause tracking inconsistencies +// lib/mixpanel_flutter.dart shows '2.4.3' +// pubspec.yaml shows '2.4.4' +// Different versions break release process +``` + +## Example App Requirements + +Every new feature must have a corresponding example page demonstrating usage. + +✅ **Correct:** +```dart +// In example/lib/gdpr_page.dart +class GDPRPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('GDPR Compliance')), + body: ListView( + children: [ + ElevatedButton( + onPressed: () => _mixpanel.optInTracking(), + child: Text('Opt In to Tracking'), + ), + ElevatedButton( + onPressed: () => _mixpanel.optOutTracking(), + child: Text('Opt Out of Tracking'), + ), + ], + ), + ); + } +} +``` + +❌ **Incorrect:** +```dart +// Adding features without example usage makes testing difficult +// No example page for new GDPR features +``` \ No newline at end of file diff --git a/.cursor/rules/always/core-conventions.mdc b/.cursor/rules/always/core-conventions.mdc new file mode 100644 index 0000000..6a8dad8 --- /dev/null +++ b/.cursor/rules/always/core-conventions.mdc @@ -0,0 +1,154 @@ +--- +description: Core naming, structure, and coding conventions that apply to all Mixpanel Flutter SDK code +globs: [] +alwaysApply: true +--- + +# Core Coding Conventions + +These conventions ensure consistency across the entire Mixpanel Flutter SDK and make code immediately recognizable as belonging to this project. + +## Method Naming Conventions + +Use camelCase with specific verb prefixes that indicate the method's purpose. + +✅ **Correct:** +```dart +// Tracking methods use 'track' prefix +track(String eventName) +trackWithGroups(String eventName, Map properties) + +// Registration methods use 'register' prefix +registerSuperProperties(Map properties) +registerSuperPropertiesOnce(Map properties) + +// Getter methods use 'get' prefix for accessing sub-components +getPeople() +getGroup(String groupKey, dynamic groupID) +``` + +❌ **Incorrect:** +```dart +sendEvent(String eventName) // Should use 'track' +setPersistentProperties() // Should use 'register' +people() // Should use 'getPeople' +``` + +## Input Validation Pattern + +Always validate string inputs before making platform channel calls to prevent crashes. + +✅ **Correct:** +```dart +Future track(String eventName, {Map? properties}) async { + if (_MixpanelHelper.isValidString(eventName)) { + await _channel.invokeMethod('track', { + 'eventName': eventName, + 'properties': properties ?? {}, + }); + } else { + developer.log('`track` failed: eventName cannot be blank', name: 'Mixpanel'); + } +} +``` + +❌ **Incorrect:** +```dart +Future track(String eventName, {Map? properties}) async { + // Missing validation - will crash on empty string + await _channel.invokeMethod('track', { + 'eventName': eventName, + 'properties': properties ?? {}, + }); +} +``` + +## Platform Channel Invocation Pattern + +Use standardized argument structure for all platform channel calls. + +✅ **Correct:** +```dart +await _channel.invokeMethod('methodName', { + 'param1': value1, + 'param2': value2 ?? {}, // Use ?? {} for optional maps + 'param3': value3, +}); +``` + +❌ **Incorrect:** +```dart +// Don't use positional arguments or inconsistent naming +await _channel.invokeMethod('methodName', [value1, value2]); +await _channel.invokeMethod('methodName', { + 'Param1': value1, // Inconsistent casing + 'properties': null, // Don't pass null for maps +}); +``` + +## Error Handling Philosophy + +Methods must fail silently with logging - never throw exceptions to calling code. + +✅ **Correct:** +```dart +if (_MixpanelHelper.isValidString(alias)) { + _channel.invokeMethod('alias', { + 'alias': alias, + 'distinctId': distinctId, + }); +} else { + developer.log('`alias` failed: alias cannot be blank', name: 'Mixpanel'); +} +``` + +❌ **Incorrect:** +```dart +if (!_MixpanelHelper.isValidString(alias)) { + throw ArgumentError('alias cannot be blank'); // Don't throw +} +``` + +## Library Metadata Injection + +All tracking calls must include library version and identifier. + +✅ **Correct:** +```dart +static Map _getMixpanelProperties() { + return { + '\$lib_version': '2.4.4', // Current SDK version + 'mp_lib': 'flutter', // Library identifier + }; +} +``` + +❌ **Incorrect:** +```dart +// Missing library metadata +static Map _getMixpanelProperties() { + return {}; +} +``` + +## Future Return Pattern + +All public methods must return Future for consistency, even if implementation is synchronous. + +✅ **Correct:** +```dart +Future identify(String distinctId) async { + if (_MixpanelHelper.isValidString(distinctId)) { + return await _channel.invokeMethod('identify', { + 'distinctId': distinctId, + }); + } +} +``` + +❌ **Incorrect:** +```dart +void identify(String distinctId) { // Should return Future + _channel.invokeMethod('identify', {'distinctId': distinctId}); +} +``` \ No newline at end of file diff --git a/.cursor/rules/components/android-implementation.mdc b/.cursor/rules/components/android-implementation.mdc new file mode 100644 index 0000000..944c286 --- /dev/null +++ b/.cursor/rules/components/android-implementation.mdc @@ -0,0 +1,208 @@ +--- +description: Android-specific implementation patterns for the Mixpanel Flutter plugin +globs: ["**/android/**/*.java", "**/android/**/*.kt"] +alwaysApply: false +--- + +# Android Implementation Rules + +These rules ensure proper implementation of Android-specific code in the Mixpanel Flutter plugin. + +## Plugin Method Handling Pattern + +All method calls must follow the standard result pattern with proper error handling. + +✅ **Correct:** +```java +@Override +public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + try { + switch (call.method) { + case "track": + String eventName = call.argument("eventName"); + Map properties = call.argument("properties"); + + JSONObject jsonObject = MixpanelFlutterHelper.toJSONObject(properties); + mixpanel.track(eventName, jsonObject); + result.success(null); + break; + + default: + result.notImplemented(); + break; + } + } catch (JSONException e) { + result.error("MixpanelFlutterException", e.getLocalizedMessage(), null); + } +} +``` + +❌ **Incorrect:** +```java +// Missing error handling and improper result usage +public void onMethodCall(MethodCall call, Result result) { + if (call.method.equals("track")) { + Map props = call.argument("properties"); + mixpanel.track(call.argument("eventName"), props); // Wrong - needs JSONObject + // Missing result.success() call + } +} +``` + +## Type Conversion Requirements + +Always convert Map properties to JSONObject before passing to Mixpanel Android SDK. + +✅ **Correct:** +```java +// Using helper class for conversion +Map properties = call.argument("properties"); +JSONObject jsonObject = MixpanelFlutterHelper.toJSONObject(properties); +mixpanel.track(eventName, jsonObject); + +// Helper implementation +public static JSONObject toJSONObject(Map map) throws JSONException { + JSONObject jsonObject = new JSONObject(); + for (Map.Entry entry : map.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + return jsonObject; +} +``` + +❌ **Incorrect:** +```java +// Don't pass Map directly to Mixpanel SDK +Map properties = call.argument("properties"); +mixpanel.track(eventName, properties); // Wrong - needs JSONObject conversion +``` + +## Lazy Initialization Pattern + +Initialize Mixpanel instance lazily to prevent ANR (Application Not Responding). + +✅ **Correct:** +```java +private Mixpanel getMixpanelInstance(String token) { + if (mixpanel == null) { + mixpanel = Mixpanel.getInstance(context, token); + } + return mixpanel; +} + +case "initialize": + String token = call.argument("token"); + mixpanel = getMixpanelInstance(token); + // Configure after initialization + if (optOutTrackingDefault) { + mixpanel.optOutTracking(); + } + result.success(null); + break; +``` + +❌ **Incorrect:** +```java +// Don't initialize in plugin constructor +public MixpanelFlutterPlugin(Context context) { + this.context = context; + // Wrong - too early, token not available + this.mixpanel = Mixpanel.getInstance(context, "token"); +} +``` + +## Message Codec Handler + +Register and implement custom message codec for DateTime and Uri types. + +✅ **Correct:** +```java +public class MixpanelMessageCodec extends StandardMessageCodec { + private static final byte TYPE_DATE_TIME = (byte) 128; + private static final byte TYPE_URI = (byte) 129; + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof Date) { + stream.write(TYPE_DATE_TIME); + writeLong(stream, ((Date) value).getTime()); + } else if (value instanceof Uri) { + stream.write(TYPE_URI); + writeValue(stream, value.toString()); + } else { + super.writeValue(stream, value); + } + } +} +``` + +❌ **Incorrect:** +```java +// Don't ignore custom types +// DateTime objects will fail without custom codec +protected void writeValue(ByteArrayOutputStream stream, Object value) { + super.writeValue(stream, value); // Missing DateTime/Uri handling +} +``` + +## Property Merging Pattern + +Always merge library properties with user properties correctly. + +✅ **Correct:** +```java +public static JSONObject getMergedProperties( + Map properties, + Map mixpanelProperties +) throws JSONException { + JSONObject jsonObject = new JSONObject(); + + // Add user properties first + if (properties != null) { + for (Map.Entry entry : properties.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + } + + // Add library properties (these can override user properties) + for (Map.Entry entry : mixpanelProperties.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + + return jsonObject; +} +``` + +❌ **Incorrect:** +```java +// Don't forget to merge library properties +JSONObject jsonObject = MixpanelFlutterHelper.toJSONObject(properties); +// Missing library properties like $lib_version +mixpanel.track(eventName, jsonObject); +``` + +## Null Safety + +Handle null arguments gracefully without crashing. + +✅ **Correct:** +```java +Map properties = call.argument("properties"); +if (properties == null) { + properties = new HashMap<>(); +} + +Boolean trackAutomaticEvents = call.argument("trackAutomaticEvents"); +if (trackAutomaticEvents != null && trackAutomaticEvents) { + mixpanel.trackAutomaticEvents(); +} +``` + +❌ **Incorrect:** +```java +// Don't assume non-null values +Map properties = call.argument("properties"); +properties.put("key", "value"); // NPE if properties is null + +boolean trackEvents = call.argument("trackAutomaticEvents"); // NPE on null +``` \ No newline at end of file diff --git a/.cursor/rules/components/example-app.mdc b/.cursor/rules/components/example-app.mdc new file mode 100644 index 0000000..9b8a857 --- /dev/null +++ b/.cursor/rules/components/example-app.mdc @@ -0,0 +1,354 @@ +--- +description: Example app implementation patterns for demonstrating SDK features +globs: ["**/example/lib/**/*.dart"] +alwaysApply: false +--- + +# Example App Implementation Rules + +These rules ensure the example app properly demonstrates all SDK features with clear, testable code. + +## Page Structure Pattern + +Each feature should have its own dedicated page with clear UI and actions. + +✅ **Correct:** +```dart +class TrackingPage extends StatelessWidget { + final Mixpanel _mixpanel; + + const TrackingPage(this._mixpanel, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Event Tracking'), + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + Text( + 'Basic Tracking', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _trackBasicEvent(), + child: const Text('Track Basic Event'), + ), + ElevatedButton( + onPressed: () => _trackEventWithProperties(), + child: const Text('Track Event with Properties'), + ), + const Divider(height: 32), + Text( + 'Timed Events', + style: Theme.of(context).textTheme.headlineSmall, + ), + // Continue with more examples... + ], + ), + ); + } + + void _trackBasicEvent() { + _mixpanel.track('Example Event'); + _showSnackBar('Tracked: Example Event'); + } + + void _trackEventWithProperties() { + _mixpanel.track('Purchase', properties: { + 'Item': 'Coffee', + 'Price': 2.99, + 'Quantity': 1, + }); + _showSnackBar('Tracked: Purchase with properties'); + } +} +``` + +❌ **Incorrect:** +```dart +// Don't mix multiple features in one page +class MainPage extends StatelessWidget { + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + // Everything crammed into one page + TextButton(onPressed: track, child: Text('Track')), + TextButton(onPressed: identify, child: Text('Identify')), + TextButton(onPressed: setProfile, child: Text('Profile')), + // Hard to test individual features + ], + ), + ); + } +} +``` + +## Navigation Pattern + +Use a clear navigation structure to access different feature pages. + +✅ **Correct:** +```dart +class MyApp extends StatelessWidget { + final Mixpanel mixpanel; + + const MyApp(this.mixpanel, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Mixpanel Flutter Example', + theme: ThemeData( + primarySwatch: Colors.blue, + useMaterial3: true, + ), + home: HomePage(mixpanel), + routes: { + '/tracking': (context) => TrackingPage(mixpanel), + '/user-profile': (context) => UserProfilePage(mixpanel), + '/groups': (context) => GroupsPage(mixpanel), + '/gdpr': (context) => GDPRPage(mixpanel), + '/advanced': (context) => AdvancedPage(mixpanel), + }, + ); + } +} + +class HomePage extends StatelessWidget { + final Mixpanel _mixpanel; + + const HomePage(this._mixpanel, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Mixpanel Example')), + body: ListView( + children: [ + ListTile( + title: const Text('Event Tracking'), + subtitle: const Text('Basic and timed events'), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () => Navigator.pushNamed(context, '/tracking'), + ), + ListTile( + title: const Text('User Profiles'), + subtitle: const Text('Set and update user properties'), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () => Navigator.pushNamed(context, '/user-profile'), + ), + // Continue with other features... + ], + ), + ); + } +} +``` + +❌ **Incorrect:** +```dart +// Don't use unclear navigation +class MyApp extends StatelessWidget { + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: PageView( + children: [ + Page1(), + Page2(), // Unclear what each page does + Page3(), + ], + ), + ), + ); + } +} +``` + +## Property Type Demonstration + +Show examples of all supported property types. + +✅ **Correct:** +```dart +void _trackWithAllPropertyTypes() { + _mixpanel.track('Property Types Demo', properties: { + 'string': 'Hello World', + 'integer': 42, + 'double': 3.14159, + 'boolean': true, + 'datetime': DateTime.now(), + 'uri': Uri.parse('https://mixpanel.com'), + 'list': ['item1', 'item2', 'item3'], + 'map': { + 'nested_string': 'value', + 'nested_number': 123, + }, + 'null_value': null, + }); + + _showSnackBar('Tracked event with all property types'); +} +``` + +❌ **Incorrect:** +```dart +// Don't only show basic types +void _trackEvent() { + _mixpanel.track('Event', properties: { + 'key': 'value', // Only showing strings + }); +} +``` + +## Error Scenario Demonstration + +Include examples that show SDK's error handling behavior. + +✅ **Correct:** +```dart +class ErrorHandlingSection extends StatelessWidget { + final Mixpanel _mixpanel; + + const ErrorHandlingSection(this._mixpanel, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Error Handling', + style: Theme.of(context).textTheme.headlineSmall, + ), + const Text('SDK handles invalid inputs gracefully:'), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () => _testEmptyEventName(), + child: const Text('Track with Empty Event Name'), + ), + ElevatedButton( + onPressed: () => _testNullProperties(), + child: const Text('Track with Null Properties'), + ), + ], + ); + } + + void _testEmptyEventName() { + // This should not crash - SDK validates input + _mixpanel.track(''); + _showSnackBar('Empty event name handled gracefully'); + } + + void _testNullProperties() { + // This should work fine + _mixpanel.track('Test Event', properties: null); + _showSnackBar('Null properties handled gracefully'); + } +} +``` + +❌ **Incorrect:** +```dart +// Don't skip error scenarios +// Only showing happy path examples +``` + +## State Management Pattern + +Use StatefulWidget when demonstrating features that require state. + +✅ **Correct:** +```dart +class TimedEventsPage extends StatefulWidget { + final Mixpanel mixpanel; + + const TimedEventsPage(this.mixpanel, {Key? key}) : super(key: key); + + @override + State createState() => _TimedEventsPageState(); +} + +class _TimedEventsPageState extends State { + bool _timerStarted = false; + String? _currentTimer; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + ElevatedButton( + onPressed: _timerStarted ? null : _startTimer, + child: Text('Start Timer: ${_currentTimer ?? "None"}'), + ), + ElevatedButton( + onPressed: _timerStarted ? _stopTimer : null, + child: const Text('Stop Timer and Track'), + ), + ], + ), + ); + } + + void _startTimer() { + setState(() { + _currentTimer = 'Reading Article'; + _timerStarted = true; + }); + widget.mixpanel.timeEvent(_currentTimer!); + } + + void _stopTimer() { + widget.mixpanel.track(_currentTimer!); + setState(() { + _timerStarted = false; + _currentTimer = null; + }); + } +} +``` + +❌ **Incorrect:** +```dart +// Don't use StatelessWidget for stateful operations +class TimedEventsPage extends StatelessWidget { + void _startTimer() { + mixpanel.timeEvent('Event'); // No way to track state + } +} +``` + +## User Feedback Pattern + +Provide visual feedback for all actions. + +✅ **Correct:** +```dart +class ExamplePage extends StatelessWidget { + void _performAction(BuildContext context) { + _mixpanel.track('Action Performed'); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Action tracked successfully'), + duration: Duration(seconds: 1), + ), + ); + } +} +``` + +❌ **Incorrect:** +```dart +void _performAction() { + _mixpanel.track('Action'); + // No feedback - user doesn't know if it worked +} +``` \ No newline at end of file diff --git a/.cursor/rules/components/ios-implementation.mdc b/.cursor/rules/components/ios-implementation.mdc new file mode 100644 index 0000000..616effe --- /dev/null +++ b/.cursor/rules/components/ios-implementation.mdc @@ -0,0 +1,248 @@ +--- +description: iOS-specific implementation patterns for the Mixpanel Flutter plugin +globs: ["**/ios/**/*.swift", "**/ios/**/*.m", "**/ios/**/*.h"] +alwaysApply: false +--- + +# iOS Implementation Rules + +These rules ensure proper implementation of iOS-specific code in the Mixpanel Flutter plugin. + +## Swift Method Handling Pattern + +All Flutter method calls must use guard statements and proper type casting. + +✅ **Correct:** +```swift +public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENTS", + message: "Arguments must be a dictionary", + details: nil)) + return + } + + switch call.method { + case "track": + guard let eventName = arguments["eventName"] as? String else { + result(nil) + return + } + + if let properties = arguments["properties"] as? [String: Any] { + let mpProperties = convertToMixpanelTypes(properties) + Mixpanel.mainInstance().track(event: eventName, properties: mpProperties) + } else { + Mixpanel.mainInstance().track(event: eventName) + } + result(nil) + + default: + result(FlutterMethodNotImplemented) + } +} +``` + +❌ **Incorrect:** +```swift +// Don't use force unwrapping or skip validation +public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as! [String: Any] // Force unwrap - will crash + let eventName = args["eventName"] as! String // Unsafe + + Mixpanel.mainInstance().track(event: eventName) + // Missing result call +} +``` + +## Type Conversion Pattern + +Convert Flutter types to MixpanelType for proper serialization. + +✅ **Correct:** +```swift +private func convertToMixpanelTypes(_ properties: [String: Any]) -> Properties { + var mpProperties = Properties() + + for (key, value) in properties { + if let mpValue = convertToMixpanelType(value) { + mpProperties[key] = mpValue + } + } + + return mpProperties +} + +private func convertToMixpanelType(_ value: Any) -> MixpanelType? { + switch value { + case let string as String: + return string + case let number as NSNumber: + return number + case let array as [Any]: + return array.compactMap { convertToMixpanelType($0) } + case let dict as [String: Any]: + return dict.compactMapValues { convertToMixpanelType($0) } + case let date as Date: + return date + case let url as URL: + return url + default: + return nil + } +} +``` + +❌ **Incorrect:** +```swift +// Don't pass unconverted types to Mixpanel +let properties = arguments["properties"] as? [String: Any] +Mixpanel.mainInstance().track(event: eventName, properties: properties) +// Wrong - properties might contain non-MixpanelType values +``` + +## Type Handler Registration + +Register custom type handler for DateTime and Uri in the message codec. + +✅ **Correct:** +```swift +class MixpanelTypeHandler: FlutterStandardReaderWriter { + override func writeValue(_ value: Any) { + if let date = value as? Date { + super.writeByte(128) + super.writeValue(Int64(date.timeIntervalSince1970 * 1000)) + } else if let url = value as? URL { + super.writeByte(129) + super.writeValue(url.absoluteString) + } else { + super.writeValue(value) + } + } + + override func readValueOfType(_ type: UInt8) -> Any? { + switch type { + case 128: + if let milliseconds = super.readValue() as? Int64 { + return Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000) + } + case 129: + if let urlString = super.readValue() as? String { + return URL(string: urlString) + } + default: + return super.readValueOfType(type) + } + return nil + } +} +``` + +❌ **Incorrect:** +```swift +// Don't skip custom type handling +// DateTime and URL objects will fail without proper handling +override func writeValue(_ value: Any) { + super.writeValue(value) // Missing custom type handling +} +``` + +## Initialization Pattern + +Initialize Mixpanel with proper configuration handling. + +✅ **Correct:** +```swift +case "initialize": + guard let token = arguments["token"] as? String else { + result(nil) + return + } + + Mixpanel.initialize(token: token) + let instance = Mixpanel.mainInstance() + + // Handle optional configurations + if let optOutTrackingDefault = arguments["optOutTrackingDefault"] as? Bool, + optOutTrackingDefault { + instance.optOutTracking() + } + + if let trackAutomaticEvents = arguments["trackAutomaticEvents"] as? Bool, + trackAutomaticEvents { + instance.trackAutomaticEvents = true + } + + if let superProperties = arguments["superProperties"] as? [String: Any] { + let mpProperties = convertToMixpanelTypes(superProperties) + instance.registerSuperProperties(mpProperties) + } + + result(nil) +``` + +❌ **Incorrect:** +```swift +// Don't ignore configuration parameters +case "initialize": + let token = arguments["token"] as! String + Mixpanel.initialize(token: token) + result(nil) + // Missing configuration handling +``` + +## Group Instance Handling + +Properly manage group instances with correct typing. + +✅ **Correct:** +```swift +case "getGroup": + guard let groupKey = arguments["groupKey"] as? String, + let groupID = arguments["groupID"] else { + result(nil) + return + } + + let mpGroupID = convertToMixpanelType(groupID) ?? groupID + let group = Mixpanel.mainInstance().getGroup(groupKey: groupKey, + groupID: mpGroupID) + + // Store reference if needed + result(nil) +``` + +❌ **Incorrect:** +```swift +// Don't assume groupID type +case "getGroup": + let groupKey = arguments["groupKey"] as! String + let groupID = arguments["groupID"] as! String // Wrong - groupID can be any type + let group = Mixpanel.mainInstance().getGroup(groupKey: groupKey, groupID: groupID) +``` + +## Result Handling + +Always call the Flutter result callback, even for void methods. + +✅ **Correct:** +```swift +case "flush": + Mixpanel.mainInstance().flush() + result(nil) // Always call result + +case "reset": + Mixpanel.mainInstance().reset() + result(nil) // Even for void methods +``` + +❌ **Incorrect:** +```swift +case "flush": + Mixpanel.mainInstance().flush() + // Missing result call - will hang Flutter side + +case "reset": + Mixpanel.mainInstance().reset() + // Must call result(nil) +``` \ No newline at end of file diff --git a/.cursor/rules/components/test-patterns.mdc b/.cursor/rules/components/test-patterns.mdc new file mode 100644 index 0000000..7af8fa9 --- /dev/null +++ b/.cursor/rules/components/test-patterns.mdc @@ -0,0 +1,275 @@ +--- +description: Testing patterns and requirements for Mixpanel Flutter SDK tests +globs: ["**/test/**/*_test.dart", "**/test/**/*.dart"] +alwaysApply: false +--- + +# Test Implementation Rules + +These rules ensure comprehensive and consistent testing across the Mixpanel Flutter SDK. + +## Test Setup Pattern + +All tests must properly set up the mock platform channel and use the init method. + +✅ **Correct:** +```dart +void main() { + const MethodChannel channel = MethodChannel('mixpanel_flutter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + late Mixpanel mixpanel; + MethodCall? methodCall; + + setUp(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall call) async { + methodCall = call; + return null; + }); + + // Always use init, never constructor + mixpanel = await Mixpanel.init('test_token', trackAutomaticEvents: false); + methodCall = null; // Reset after init + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); +} +``` + +❌ **Incorrect:** +```dart +void main() { + late Mixpanel mixpanel; + + setUp(() { + // Wrong - using constructor directly + mixpanel = Mixpanel('test_token'); + + // Missing channel setup + }); +} +``` + +## Method Call Assertion Pattern + +Use the custom isMethodCall matcher for clear, readable assertions. + +✅ **Correct:** +```dart +test('track sends correct arguments', () async { + await mixpanel.track('Button Clicked', properties: { + 'button_name': 'purchase', + 'price': 99.99, + }); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Button Clicked', + 'properties': { + 'button_name': 'purchase', + 'price': 99.99, + }, + }, + ), + ); +}); +``` + +❌ **Incorrect:** +```dart +test('track test', () async { + await mixpanel.track('event'); + + // Too vague assertions + expect(methodCall, isNotNull); + expect(methodCall?.method, equals('track')); + // Missing argument verification +}); +``` + +## Validation Test Requirements + +Every public method must have tests for invalid inputs. + +✅ **Correct:** +```dart +group('Input Validation', () { + test('track() should not crash when event name is empty', () async { + await mixpanel.track('', properties: {'key': 'value'}); + expect(methodCall, null); // No platform call should be made + }); + + test('track() should not crash when event name is null', () async { + await mixpanel.track(null as dynamic); + expect(methodCall, null); + }); + + test('identify() should not crash when distinctId is empty', () async { + await mixpanel.identify(''); + expect(methodCall, null); + }); + + test('alias() should not crash when alias is empty', () async { + await mixpanel.alias('', 'distinctId'); + expect(methodCall, null); + }); +}); +``` + +❌ **Incorrect:** +```dart +// Only testing happy path - missing validation tests +test('track works', () async { + await mixpanel.track('valid event'); + expect(methodCall, isMethodCall('track')); +}); +``` + +## Property Type Testing + +Test that different property types are handled correctly. + +✅ **Correct:** +```dart +test('track handles all property types', () async { + final testDate = DateTime(2024, 1, 15, 10, 30); + final testUri = Uri.parse('https://mixpanel.com'); + + await mixpanel.track('Test Event', properties: { + 'string': 'value', + 'int': 42, + 'double': 3.14, + 'bool': true, + 'date': testDate, + 'uri': testUri, + 'list': [1, 2, 3], + 'map': {'nested': 'value'}, + 'null': null, + }); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Test Event', + 'properties': { + 'string': 'value', + 'int': 42, + 'double': 3.14, + 'bool': true, + 'date': testDate, + 'uri': testUri, + 'list': [1, 2, 3], + 'map': {'nested': 'value'}, + 'null': null, + }, + }, + ), + ); +}); +``` + +❌ **Incorrect:** +```dart +// Not testing various property types +test('track with properties', () async { + await mixpanel.track('event', properties: {'key': 'value'}); + // Only testing string properties +}); +``` + +## Group Testing Pattern + +Test group functionality with proper accessor pattern. + +✅ **Correct:** +```dart +group('Group Analytics', () { + test('getGroup returns group instance', () async { + final group = mixpanel.getGroup('company', 'mixpanel'); + expect(group, isNotNull); + expect(group.groupKey, equals('company')); + expect(group.groupID, equals('mixpanel')); + }); + + test('group operations send correct method calls', () async { + final group = mixpanel.getGroup('company', 'mixpanel'); + + await group.set({'plan': 'premium'}); + expect( + methodCall, + isMethodCall( + 'groupSetProperties', + arguments: { + 'groupKey': 'company', + 'groupID': 'mixpanel', + 'properties': {'plan': 'premium'}, + }, + ), + ); + }); +}); +``` + +❌ **Incorrect:** +```dart +// Not testing through proper accessor +test('group test', () async { + // Wrong - trying to call group methods directly on mixpanel + await mixpanel.groupSet('company', 'id', {}); // No such method +}); +``` + +## Custom Matcher Implementation + +Include the custom test matcher for better assertions. + +✅ **Correct:** +```dart +class _IsMethodCall extends Matcher { + final String method; + final dynamic arguments; + + const _IsMethodCall(this.method, {this.arguments}); + + @override + bool matches(dynamic item, Map matchState) { + if (item is! MethodCall) return false; + if (item.method != method) return false; + if (arguments != null && item.arguments != arguments) { + return false; + } + return true; + } + + @override + Description describe(Description description) { + description.add('method call "$method"'); + if (arguments != null) { + description.add(' with arguments $arguments'); + } + return description; + } +} + +Matcher isMethodCall(String method, {dynamic arguments}) { + return _IsMethodCall(method, arguments: arguments); +} +``` + +❌ **Incorrect:** +```dart +// Don't use verbose assertions without custom matcher +expect(methodCall?.method, equals('track')); +expect(methodCall?.arguments['eventName'], equals('event')); +// Harder to read and maintain +``` \ No newline at end of file diff --git a/.cursor/rules/components/web-implementation.mdc b/.cursor/rules/components/web-implementation.mdc new file mode 100644 index 0000000..1201eed --- /dev/null +++ b/.cursor/rules/components/web-implementation.mdc @@ -0,0 +1,248 @@ +--- +description: Web-specific implementation patterns for the Mixpanel Flutter plugin +globs: ["**/*_web.dart", "**/web/**/*.dart"] +alwaysApply: false +--- + +# Web Implementation Rules + +These rules ensure proper implementation of web-specific code using JavaScript interop. + +## Web Plugin Structure + +Web implementation must extend Flutter's web plugin base and use proper registration. + +✅ **Correct:** +```dart +class MixpanelFlutterWeb { + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'mixpanel_flutter', + const StandardMethodCodec(), + registrar, + ); + + final pluginInstance = MixpanelFlutterWeb(); + channel.setMethodCallHandler(pluginInstance.handleMethodCall); + } + + Future handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'initialize': + return _initialize(call.arguments); + case 'track': + return track(call.arguments['eventName'], call.arguments['properties']); + default: + throw PlatformException( + code: 'Unimplemented', + details: 'mixpanel_flutter for web doesn\'t implement \'${call.method}\'', + ); + } + } +} +``` + +❌ **Incorrect:** +```dart +// Don't use platform channels directly on web +class MixpanelFlutterWeb { + static const MethodChannel _channel = MethodChannel('mixpanel_flutter'); + + Future track(String event) async { + await _channel.invokeMethod('track'); // Wrong - use JS interop + } +} +``` + +## JavaScript Interop Pattern + +Use @JS annotations and proper type conversion for JavaScript interaction. + +✅ **Correct:** +```dart +@JS('mixpanel') +library mixpanel_js; + +@JS() +external MixpanelJS get mixpanel; + +@JS() +class MixpanelJS { + external void init(String token, dynamic config); + external void track(String eventName, [dynamic properties]); + external void identify(String distinctId); + external People get people; +} + +@JS() +class People { + external void set(dynamic properties); + external void setOnce(dynamic properties); +} +``` + +❌ **Incorrect:** +```dart +// Don't access JavaScript without proper annotations +external dynamic get mixpanel; // Missing @JS annotation + +// Don't use dart:js directly +import 'dart:js' as js; +js.context['mixpanel'].callMethod('track'); // Use @JS instead +``` + +## Safe JavaScript Conversion + +Always use safeJsify to convert Dart objects to JavaScript-compatible format. + +✅ **Correct:** +```dart +dynamic safeJsify(Object value) { + if (value is Map) { + value = JsLinkedHashMap.from(value); + } + var args = jsify(value); + return args; +} + +@override +Future track(String eventName, Map properties) async { + var jsProperties = safeJsify(properties); + mixpanelJs.track(eventName, jsProperties); +} +``` + +❌ **Incorrect:** +```dart +// Don't pass Dart objects directly to JavaScript +Future track(String eventName, Map properties) async { + mixpanelJs.track(eventName, properties); // Will fail - needs jsify +} + +// Don't use jsify without null checking +var jsProps = jsify(properties); // Can throw if properties is null +``` + +## Initialization Check Pattern + +Ensure mixpanel.js is loaded before attempting to use it. + +✅ **Correct:** +```dart +Future _initialize(Map arguments) async { + if (!_isMixpanelLoaded()) { + throw PlatformException( + code: 'NOT_LOADED', + message: 'Mixpanel JS library not loaded. Add script tag to index.html', + ); + } + + String token = arguments['token']; + Map config = arguments['config'] ?? {}; + + var jsConfig = safeJsify(config); + mixpanelJs.init(token, jsConfig); + + if (arguments['superProperties'] != null) { + var superProps = safeJsify(arguments['superProperties']); + mixpanelJs.register(superProps); + } +} + +bool _isMixpanelLoaded() { + return context['mixpanel'] != null; +} +``` + +❌ **Incorrect:** +```dart +// Don't assume mixpanel.js is loaded +Future _initialize(Map arguments) async { + mixpanelJs.init(arguments['token']); // May crash if not loaded +} +``` + +## Property Merging for Web + +Merge library properties with user properties before tracking. + +✅ **Correct:** +```dart +@override +Future track(String eventName, Map properties) async { + // Merge library properties + Map mergedProperties = {}; + if (properties != null) { + mergedProperties.addAll(properties); + } + + // Add library metadata + mergedProperties['\$lib_version'] = '2.4.4'; + mergedProperties['mp_lib'] = 'flutter'; + + var jsProperties = safeJsify(mergedProperties); + mixpanelJs.track(eventName, jsProperties); +} +``` + +❌ **Incorrect:** +```dart +// Don't forget library properties on web +Future track(String eventName, Map properties) async { + var jsProperties = safeJsify(properties); + mixpanelJs.track(eventName, jsProperties); // Missing lib metadata +} +``` + +## Group Handling on Web + +Implement group methods using the JavaScript group API. + +✅ **Correct:** +```dart +@override +Future setGroup(String groupKey, dynamic groupID) async { + if (groupID is List) { + var jsGroupID = JsArray.from(groupID); + mixpanelJs.setGroup(groupKey, jsGroupID); + } else { + mixpanelJs.setGroup(groupKey, groupID); + } +} + +@override +Future addGroup(String groupKey, dynamic groupID) async { + mixpanelJs.addGroup(groupKey, groupID); +} +``` + +❌ **Incorrect:** +```dart +// Don't ignore array handling for groups +Future setGroup(String groupKey, dynamic groupID) async { + mixpanelJs.setGroup(groupKey, groupID); // Arrays need special handling +} +``` + +## HTML Setup Reminder + +Include documentation about required HTML setup. + +✅ **Correct:** +```dart +/// Web implementation of mixpanel_flutter. +/// +/// IMPORTANT: Add this script tag to your web/index.html: +/// +class MixpanelFlutterWeb { + // Implementation +} +``` + +❌ **Incorrect:** +```dart +// Missing setup instructions causes runtime failures +class MixpanelFlutterWeb { + // Implementation without documenting HTML requirements +} +``` \ No newline at end of file diff --git a/.cursor/rules/workflows/new-feature.mdc b/.cursor/rules/workflows/new-feature.mdc new file mode 100644 index 0000000..69647de --- /dev/null +++ b/.cursor/rules/workflows/new-feature.mdc @@ -0,0 +1,339 @@ +--- +description: Complete workflow for implementing a new feature in the Mixpanel Flutter SDK +globs: [] +alwaysApply: false +--- + +# New Feature Implementation Workflow + +Follow this step-by-step guide to implement a new feature while maintaining all SDK conventions and ensuring cross-platform compatibility. + +## Overview + +The implementation order is critical: Dart API → Web → Android → iOS → Example → Tests → Docs + +## Step 1: Design the Dart API + +First, define the public API in `lib/mixpanel_flutter.dart`: + +✅ **Correct Implementation:** +```dart +/// Tracks when a user views a specific screen or page. +/// +/// This is useful for analyzing user navigation patterns and screen engagement. +/// The screen name will be tracked as a "Screen View" event with additional +/// properties. +/// +/// * [screenName] The name of the screen being viewed +/// * [properties] Additional properties to include with the screen view event +Future trackScreenView(String screenName, {Map? properties}) async { + if (_MixpanelHelper.isValidString(screenName)) { + final Map screenProperties = { + 'Screen Name': screenName, + ...?properties, + }; + + await _channel.invokeMethod('track', { + 'eventName': 'Screen View', + 'properties': screenProperties, + }); + } else { + developer.log('`trackScreenView` failed: screenName cannot be blank', name: 'Mixpanel'); + } +} +``` + +## Step 2: Add to Platform Interface + +If creating a new platform method (not reusing existing ones), update the method channel: + +```dart +// For new platform methods only +await _channel.invokeMethod('newMethodName', { + 'param1': value1, + 'param2': value2 ?? {}, +}); +``` + +## Step 3: Implement Web Support + +Add the web implementation in `lib/mixpanel_flutter_web.dart`: + +✅ **Correct Web Implementation:** +```dart +@override +Future trackScreenView(String screenName, Map? properties) async { + if (!_MixpanelHelper.isValidString(screenName)) { + developer.log('`trackScreenView` failed: screenName cannot be blank', name: 'Mixpanel'); + return; + } + + // Merge properties with library metadata + final Map eventProperties = { + 'Screen Name': screenName, + '\$lib_version': '2.4.4', + 'mp_lib': 'flutter', + ...?properties, + }; + + var jsProperties = safeJsify(eventProperties); + mixpanelJs.track('Screen View', jsProperties); +} +``` + +## Step 4: Implement Android Support + +Add to `android/src/main/java/com/mixpanel/mixpanel_flutter/MixpanelFlutterPlugin.java`: + +✅ **Correct Android Implementation:** +```java +case "trackScreenView": + String screenName = call.argument("screenName"); + Map properties = call.argument("properties"); + + if (screenName == null || screenName.trim().isEmpty()) { + result.success(null); + return; + } + + try { + JSONObject screenProperties = new JSONObject(); + screenProperties.put("Screen Name", screenName); + + // Add additional properties if provided + if (properties != null) { + JSONObject userProps = MixpanelFlutterHelper.toJSONObject(properties); + Iterator keys = userProps.keys(); + while (keys.hasNext()) { + String key = keys.next(); + screenProperties.put(key, userProps.get(key)); + } + } + + // Merge with library properties + JSONObject mergedProps = MixpanelFlutterHelper.getMergedProperties( + screenProperties, mixpanelProperties); + + mixpanel.track("Screen View", mergedProps); + result.success(null); + } catch (JSONException e) { + result.error("MixpanelFlutterException", e.getLocalizedMessage(), null); + } + break; +``` + +## Step 5: Implement iOS Support + +Add to `ios/Classes/SwiftMixpanelFlutterPlugin.swift`: + +✅ **Correct iOS Implementation:** +```swift +case "trackScreenView": + guard let arguments = call.arguments as? [String: Any], + let screenName = arguments["screenName"] as? String, + !screenName.trimmingCharacters(in: .whitespaces).isEmpty else { + result(nil) + return + } + + var eventProperties = Properties() + eventProperties["Screen Name"] = screenName + + // Add additional properties if provided + if let properties = arguments["properties"] as? [String: Any] { + let mpProperties = convertToMixpanelTypes(properties) + for (key, value) in mpProperties { + eventProperties[key] = value + } + } + + // Add library properties + eventProperties["$lib_version"] = "2.4.4" + eventProperties["mp_lib"] = "flutter" + + Mixpanel.mainInstance().track(event: "Screen View", properties: eventProperties) + result(nil) +``` + +## Step 6: Create Example Page + +Add example usage in `example/lib/screen_tracking_page.dart`: + +✅ **Correct Example:** +```dart +class ScreenTrackingPage extends StatelessWidget { + final Mixpanel _mixpanel; + + const ScreenTrackingPage(this._mixpanel, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // Track screen view when page is displayed + WidgetsBinding.instance.addPostFrameCallback((_) { + _mixpanel.trackScreenView('Screen Tracking Demo'); + }); + + return Scaffold( + appBar: AppBar(title: const Text('Screen Tracking')), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + Text( + 'Screen Tracking Examples', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _trackHomeScreen(), + child: const Text('Track Home Screen View'), + ), + ElevatedButton( + onPressed: () => _trackProductScreen(), + child: const Text('Track Product Screen with Properties'), + ), + ], + ), + ); + } + + void _trackHomeScreen() { + _mixpanel.trackScreenView('Home Screen'); + _showSnackBar('Tracked: Home Screen View'); + } + + void _trackProductScreen() { + _mixpanel.trackScreenView('Product Details', properties: { + 'Product ID': 'SKU-12345', + 'Category': 'Electronics', + 'Source': 'Search Results', + }); + _showSnackBar('Tracked: Product Screen with properties'); + } +} +``` + +## Step 7: Write Tests + +Add comprehensive tests in `test/mixpanel_flutter_test.dart`: + +✅ **Correct Test Implementation:** +```dart +group('trackScreenView', () { + test('trackScreenView sends correct arguments', () async { + await mixpanel.trackScreenView('Home Screen'); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Screen View', + 'properties': { + 'Screen Name': 'Home Screen', + }, + }, + ), + ); + }); + + test('trackScreenView with properties', () async { + await mixpanel.trackScreenView('Product Screen', properties: { + 'Product ID': '12345', + 'Category': 'Electronics', + }); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Screen View', + 'properties': { + 'Screen Name': 'Product Screen', + 'Product ID': '12345', + 'Category': 'Electronics', + }, + }, + ), + ); + }); + + test('trackScreenView validates empty screen name', () async { + await mixpanel.trackScreenView(''); + expect(methodCall, null); // No call should be made + }); + + test('trackScreenView handles null properties', () async { + await mixpanel.trackScreenView('Test Screen', properties: null); + + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Screen View', + 'properties': { + 'Screen Name': 'Test Screen', + }, + }, + ), + ); + }); +}); +``` + +## Step 8: Update Documentation + +Add the new method to the README and API documentation: + +```markdown +### Screen Tracking + +Track screen views to understand user navigation: + +```dart +// Basic screen tracking +await mixpanel.trackScreenView('Home Screen'); + +// With additional properties +await mixpanel.trackScreenView('Product Details', properties: { + 'Product ID': 'SKU-12345', + 'Category': 'Electronics', +}); +``` +``` + +## Validation Checklist + +Before completing the feature: + +- [ ] ✅ Dart API includes input validation +- [ ] ✅ All platforms handle the feature identically +- [ ] ✅ Library metadata is included in all events +- [ ] ✅ Invalid inputs are handled gracefully +- [ ] ✅ Example app demonstrates the feature +- [ ] ✅ Tests cover happy path and edge cases +- [ ] ✅ Documentation is updated +- [ ] ✅ Version number is updated if needed + +## Common Pitfalls to Avoid + +❌ **Don't forget library metadata:** +```dart +// Wrong - missing $lib_version and mp_lib +await _channel.invokeMethod('track', {'eventName': 'Screen View'}); +``` + +❌ **Don't skip validation:** +```dart +// Wrong - will crash on empty string +Future trackScreenView(String screenName) async { + await _channel.invokeMethod('track', {'eventName': screenName}); +} +``` + +❌ **Don't use different method names across platforms:** +```java +// Android: "trackScreenView" +// iOS: "track_screen_view" // Wrong - must match exactly +``` \ No newline at end of file diff --git a/.cursor/rules/workflows/release-process.mdc b/.cursor/rules/workflows/release-process.mdc new file mode 100644 index 0000000..a466327 --- /dev/null +++ b/.cursor/rules/workflows/release-process.mdc @@ -0,0 +1,237 @@ +--- +description: Step-by-step workflow for releasing a new version of the Mixpanel Flutter SDK +globs: [] +alwaysApply: false +--- + +# Release Process Workflow + +This workflow ensures consistent and error-free releases of the Mixpanel Flutter SDK. + +## Pre-Release Checklist + +Before starting the release process, ensure: + +- [ ] All tests pass on all platforms +- [ ] Static analysis shows no issues (`flutter analyze`) +- [ ] Example app works correctly on iOS, Android, and Web +- [ ] CHANGELOG.md is updated with all changes +- [ ] No uncommitted changes in working directory + +## Step 1: Determine Version Number + +Follow semantic versioning (MAJOR.MINOR.PATCH): + +- **PATCH** (x.x.1): Bug fixes, minor improvements +- **MINOR** (x.1.0): New features, backward compatible +- **MAJOR** (1.0.0): Breaking changes + +✅ **Examples:** +```bash +# Bug fix: 2.4.4 → 2.4.5 +python tool/release.py --old 2.4.4 --new 2.4.5 + +# New feature: 2.4.4 → 2.5.0 +python tool/release.py --old 2.4.4 --new 2.5.0 + +# Breaking change: 2.4.4 → 3.0.0 +python tool/release.py --old 2.4.4 --new 3.0.0 +``` + +## Step 2: Run Release Script + +The release script automatically updates version in all required files: + +```bash +# From project root +python tool/release.py --old CURRENT_VERSION --new NEW_VERSION +``` + +The script updates: +1. `pubspec.yaml` - Package version +2. `lib/mixpanel_flutter.dart` - $lib_version in tracking +3. `test/mixpanel_flutter_test.dart` - Version in tests +4. `ios/mixpanel_flutter.podspec` - Pod version + +✅ **Verify Changes:** +```bash +# Check that all files were updated correctly +git diff --name-only +# Should show: +# - pubspec.yaml +# - lib/mixpanel_flutter.dart +# - test/mixpanel_flutter_test.dart +# - ios/mixpanel_flutter.podspec +``` + +## Step 3: Update CHANGELOG.md + +Add a new section at the top with release notes: + +✅ **Correct Format:** +```markdown +# Change Log + +## Version 2.5.0 +* Added screen tracking functionality +* Fixed issue with null properties on web platform +* Improved error handling for empty event names + +## Version 2.4.4 +* Previous release notes... +``` + +❌ **Incorrect:** +```markdown +# Change Log + +## Latest +* Some changes // Wrong - use specific version number + +Version 2.5.0 // Wrong - missing ## prefix +- Changes // Wrong - use * for bullets +``` + +## Step 4: Commit Version Changes + +Create a commit with the version update: + +```bash +git add -A +git commit -m "Version NEW_VERSION" +# Example: git commit -m "Version 2.5.0" +``` + +## Step 5: Create and Push Tag + +Create a git tag for the release: + +```bash +git tag vNEW_VERSION +# Example: git tag v2.5.0 + +# Push changes and tag +git push origin main +git push origin vNEW_VERSION +``` + +## Step 6: Run Final Tests + +Before publishing, run tests one more time: + +```bash +# Run Flutter tests +flutter test + +# Run example app on each platform +cd example +flutter run # Choose iOS +flutter run # Choose Android +flutter run -d chrome # Web +``` + +## Step 7: Publish to pub.dev + +Publish the package: + +```bash +# Dry run first to check everything +flutter pub publish --dry-run + +# If everything looks good, publish +flutter pub publish +``` + +Follow the prompts and authenticate if required. + +## Step 8: Create GitHub Release + +1. Go to GitHub repository releases page +2. Click "Create a new release" +3. Select the tag you just created (e.g., v2.5.0) +4. Set release title: "Version 2.5.0" +5. Copy changelog entries for this version into description +6. Publish release + +## Post-Release Verification + +After publishing: + +- [ ] Package appears on pub.dev +- [ ] Version badge updated on pub.dev +- [ ] Example in pub.dev shows new version +- [ ] GitHub release is visible + +## Troubleshooting Common Issues + +### Version Mismatch Error + +If you get version mismatch errors: + +✅ **Fix:** +```bash +# Manually verify all version locations match +grep -r "2\.4\.4" --include="*.dart" --include="*.yaml" --include="*.podspec" + +# Update any missed files manually +``` + +### Publishing Authentication Issues + +If authentication fails: + +✅ **Fix:** +```bash +# Ensure you're logged in to pub.dev +flutter pub login + +# Verify you have publishing rights +# Check https://pub.dev/packages/mixpanel_flutter/admin +``` + +### Tag Already Exists + +If git tag already exists: + +✅ **Fix:** +```bash +# Delete local tag +git tag -d v2.5.0 + +# Delete remote tag +git push origin :refs/tags/v2.5.0 + +# Recreate tag +git tag v2.5.0 +git push origin v2.5.0 +``` + +## Release Communication + +After successful release: + +1. Update any internal documentation +2. Notify relevant stakeholders +3. Monitor pub.dev and GitHub for any immediate issues +4. Be prepared to hotfix if critical issues found + +## Hotfix Process + +For critical bugs in a release: + +1. Create hotfix branch from tag +2. Fix the issue +3. Increment PATCH version +4. Follow abbreviated release process +5. Cherry-pick to main if applicable + +✅ **Example:** +```bash +# Create hotfix from release +git checkout -b hotfix/2.5.1 v2.5.0 + +# Make fixes... + +# Release hotfix +python tool/release.py --old 2.5.0 --new 2.5.1 +``` \ No newline at end of file diff --git a/.cursor/rules/workflows/testing-workflow.mdc b/.cursor/rules/workflows/testing-workflow.mdc new file mode 100644 index 0000000..03b35bd --- /dev/null +++ b/.cursor/rules/workflows/testing-workflow.mdc @@ -0,0 +1,378 @@ +--- +description: Comprehensive testing workflow for validating Mixpanel Flutter SDK changes across all platforms +globs: [] +alwaysApply: false +--- + +# Testing Workflow + +This workflow ensures thorough testing of the Mixpanel Flutter SDK across all platforms and scenarios. + +## Testing Layers + +The SDK uses a multi-layer testing approach: + +1. **Unit Tests** - Dart API and platform channel mocking +2. **Integration Tests** - Example app on each platform +3. **Manual Tests** - Platform-specific behavior verification + +## Step 1: Run Unit Tests + +Start with the Dart unit tests to verify API behavior: + +```bash +# From project root +flutter test + +# For verbose output +flutter test -v + +# To run specific test file +flutter test test/mixpanel_flutter_test.dart +``` + +✅ **Expected Output:** +``` +00:02 +45: All tests passed! +``` + +❌ **If Tests Fail:** +```bash +# Check for common issues: +# 1. Ensure you're using latest Flutter +flutter upgrade + +# 2. Clean and get dependencies +flutter clean +flutter pub get + +# 3. Run with more details +flutter test --reporter expanded +``` + +## Step 2: Static Analysis + +Ensure code quality with Flutter's analyzer: + +```bash +flutter analyze + +# For strict analysis +flutter analyze --fatal-infos +``` + +✅ **Fix Any Issues:** +```dart +// Common fixes: +// 1. Add const constructors +const MyWidget({Key? key}) : super(key: key); + +// 2. Remove unused imports +// Delete any grayed out imports + +// 3. Add required type annotations +final Map properties = {}; +``` + +## Step 3: Test Example App - iOS + +### Prepare iOS Environment + +```bash +cd example + +# Install iOS dependencies +cd ios +pod install +cd .. + +# List available simulators +flutter devices +``` + +### Run on iOS Simulator + +```bash +# Run on default iOS simulator +flutter run + +# Or specify a device +flutter run -d "iPhone 15" +``` + +### iOS Testing Checklist + +Test each feature page in the example app: + +- [ ] **Tracking Page** + - [ ] Basic event tracks successfully + - [ ] Event with properties includes all property types + - [ ] Timed events measure duration correctly + - [ ] Invalid inputs handled gracefully + +- [ ] **User Profile Page** + - [ ] Set properties updates user profile + - [ ] Set once doesn't overwrite existing values + - [ ] Increment adds to numeric properties + - [ ] Union adds unique values to lists + +- [ ] **Groups Page** + - [ ] Group identification works + - [ ] Group properties can be set + - [ ] Multiple groups tracked correctly + +- [ ] **GDPR Page** + - [ ] Opt out stops tracking + - [ ] Opt in resumes tracking + - [ ] Has opted out status correct + +- [ ] **Advanced Features** + - [ ] Super properties persist across events + - [ ] Alias creates user identity link + - [ ] Reset clears user data + - [ ] Flush sends queued events + +## Step 4: Test Example App - Android + +### Prepare Android Environment + +```bash +# Ensure Android SDK is set up +flutter doctor + +# Start Android emulator or connect device +flutter devices +``` + +### Run on Android + +```bash +# Run on default Android device +flutter run + +# Or specify device +flutter run -d "Pixel_7_API_34" +``` + +### Android Testing Checklist + +Run through the same feature tests as iOS, plus: + +- [ ] **Android Specific** + - [ ] App doesn't ANR on startup + - [ ] Rotation preserves state + - [ ] Background/foreground transitions work + - [ ] Memory pressure handled gracefully + +### Verify Logs + +```bash +# View Android logs while testing +adb logcat | grep -i mixpanel + +# Check for proper initialization +# Should see: "Mixpanel initialized with token: ..." +``` + +## Step 5: Test Example App - Web + +### Prepare Web Environment + +First, ensure the example app's `web/index.html` includes Mixpanel JS: + +```html + + +``` + +### Run on Web + +```bash +# Run on Chrome +flutter run -d chrome + +# Run on specific port +flutter run -d chrome --web-port=8080 +``` + +### Web Testing Checklist + +Test all features, paying special attention to: + +- [ ] **Web Specific** + - [ ] Mixpanel JS library loads correctly + - [ ] Events appear in browser console + - [ ] DateTime properties serialize correctly + - [ ] Large property maps handled + - [ ] Browser refresh maintains state + +### Debug Web Issues + +```javascript +// Open browser console and check: +mixpanel._isInitialized +// Should return: true + +// View last tracked event +mixpanel._lastest_event +``` + +## Step 6: Cross-Platform Validation + +After testing each platform individually, verify consistency: + +### Event Property Validation + +Track the same event on all platforms and verify in Mixpanel: + +```dart +// Test event to track on each platform +await mixpanel.track('Cross Platform Test', properties: { + 'platform': Platform.operatingSystem, + 'timestamp': DateTime.now(), + 'test_id': 'sdk_validation_001', + 'numeric': 42, + 'boolean': true, + 'list': ['a', 'b', 'c'], +}); +``` + +Then check in Mixpanel dashboard: +1. Go to Events → Latest +2. Filter by event name: "Cross Platform Test" +3. Verify all platforms show identical properties +4. Check that $lib_version matches across platforms + +## Step 7: Performance Testing + +### Memory Testing + +Run the example app and monitor memory: + +```bash +# iOS - Use Xcode Instruments +open example/ios/Runner.xcworkspace +# Product → Profile → Leaks + +# Android - Use Android Studio Profiler +# Run → Profile 'app' +``` + +### Stress Testing + +Create a stress test in the example app: + +```dart +// Add to example app for stress testing +void _stressTest() async { + for (int i = 0; i < 1000; i++) { + await _mixpanel.track('Stress Test Event $i', properties: { + 'iteration': i, + 'timestamp': DateTime.now().toIso8601String(), + 'data': List.generate(10, (j) => 'item_$j'), + }); + + if (i % 100 == 0) { + await _mixpanel.flush(); + print('Flushed at iteration $i'); + } + } +} +``` + +## Step 8: Error Scenario Testing + +Test error handling across platforms: + +```dart +// Test various error scenarios +void _testErrorScenarios() async { + // Empty strings + await mixpanel.track(''); + await mixpanel.identify(''); + await mixpanel.alias('', 'valid'); + + // Null values (cast to dynamic to bypass type checking) + await mixpanel.track(null as dynamic); + await mixpanel.getPeople().set(null as dynamic); + + // Invalid types + await mixpanel.track('Event', properties: { + 'invalid': Object(), // Non-serializable + }); + + // Very large strings + final largeString = 'x' * 1000000; + await mixpanel.track('Large Event', properties: { + 'huge': largeString, + }); +} +``` + +## Step 9: CI Integration Verification + +If changes affect CI, verify GitHub Actions: + +```yaml +# Check .github/workflows/flutter.yml +- name: Run tests + run: flutter test + +- name: Analyze + run: flutter analyze +``` + +## Common Testing Issues + +### Platform Channel Not Mocked + +✅ **Fix:** +```dart +// Ensure proper setup in tests +setUp(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall call) async { + methodCall = call; + return null; + }); +}); +``` + +### Async Test Timing + +✅ **Fix:** +```dart +// Use async/await properly in tests +test('async operation', () async { + await mixpanel.track('Event'); + // Wait for any async operations + await Future.delayed(Duration(milliseconds: 100)); + expect(methodCall, isNotNull); +}); +``` + +### Web Test Failures + +✅ **Fix:** +```bash +# Ensure Chrome is available +flutter doctor + +# Run with web-specific renderer +flutter test --platform chrome +flutter run -d chrome --web-renderer html +``` + +## Final Validation + +Before marking testing complete: + +- [ ] All unit tests pass +- [ ] No analyzer warnings +- [ ] iOS example app works correctly +- [ ] Android example app works correctly +- [ ] Web example app works correctly +- [ ] Cross-platform event consistency verified +- [ ] No memory leaks detected +- [ ] Error scenarios handled gracefully +- [ ] CI/CD pipeline passes \ No newline at end of file diff --git a/.github/copilot-instructions-guide.md b/.github/copilot-instructions-guide.md new file mode 100644 index 0000000..0e166be --- /dev/null +++ b/.github/copilot-instructions-guide.md @@ -0,0 +1,159 @@ +# GitHub Copilot Instructions Integration Guide + +## The AI Assistant Ecosystem for Mixpanel Flutter SDK + +Our project leverages three complementary AI systems to enhance developer productivity: + +| System | Primary Role | When Active | Best For | +|--------|-------------|-------------|----------| +| **Claude Code** | Knowledge Repository | On-demand via CLI (`cc`) | Deep analysis, architecture decisions, complex refactoring | +| **Cursor** | Active Behavioral Guide | During focused coding | Following patterns, preventing errors in context | +| **Copilot** | Persistent Pair Programmer | Always while typing | Quick completions, enforcing conventions | + +### How They Work Together + +``` +Claude Code (Knowledge) → Cursor Rules (Behavior) → Copilot Instructions (Habits) + ↓ ↓ ↓ +Deep SDK understanding Context-aware guidance Persistent validation +``` + +### Practical Examples + +#### Example 1: Adding a New Tracking Method + +**Copilot** ensures you follow the pattern: +```dart +Future trackPurchase(String productId, double amount) async { + // Copilot automatically suggests validation + if (!_MixpanelHelper.isValidString(productId)) { + developer.log('`trackPurchase` failed: productId cannot be blank', name: 'Mixpanel'); + return; + } + + // Copilot knows the exact platform channel pattern + await _channel.invokeMethod('trackPurchase', { + 'productId': productId, + 'amount': amount, + }); +} +``` + +**Cursor** would provide context-specific guidance about where to add this method and any related native implementations needed. + +**Claude Code** can analyze the entire SDK to determine if this method already exists in another form or suggest the best implementation approach. + +#### Example 2: Debugging Platform Channel Issues + +**Claude Code** can search across all platform implementations: +```bash +cc "find all platform channel handlers for track methods" +``` + +**Cursor** helps you navigate the native code with proper patterns. + +**Copilot** ensures your fixes follow the established patterns. + +### When to Update Each System + +#### New Pattern Discovered +1. **Universal pattern** (>75% of code)? + - Add to Copilot instructions immediately + - Example: New validation helper method + +2. **Context-specific pattern**? + - Document in CLAUDE.md for Claude Code + - Add Cursor rule if it prevents errors + - Example: Platform-specific workaround + +3. **Complex architectural pattern**? + - Full documentation in Claude Code context + - Reference in CLAUDE.md + - Example: New platform channel codec + +#### Common Error Found +1. **Analyze with Claude Code** to understand root cause +2. **Add Copilot instruction** if it's a frequent mistake +3. **Create Cursor rule** for context-aware prevention + +### Quick Decision Guide + +**Should this go in Copilot instructions?** + +✅ **Yes, if:** +- Used in >50% of method implementations +- Prevents SDK breaking changes +- Under 3 lines to explain +- Applies to all platforms + +❌ **No, if:** +- Platform-specific implementation detail +- Complex architectural concept +- Requires extensive context +- One-time setup or configuration + +### SDK-Specific Integration Points + +#### Platform Channel Patterns +- **Copilot**: Enforces the standard invocation pattern +- **Cursor**: Helps implement native handlers +- **Claude Code**: Analyzes all implementations for consistency + +#### Type Handling +- **Copilot**: Knows to use `safeJsify()` for web +- **Cursor**: Suggests platform-specific handling +- **Claude Code**: Explains MixpanelMessageCodec implementation + +#### Testing +- **Copilot**: Generates tests following SDK patterns +- **Cursor**: Helps with test organization +- **Claude Code**: Analyzes test coverage gaps + +### Maintenance Workflow + +1. **Monthly Pattern Review** + - Run Claude Code to analyze new patterns + - Update Copilot instructions if patterns are now universal + - Remove outdated patterns + +2. **Post-Release Updates** + - Update version numbers in Copilot instructions + - Document new features in CLAUDE.md + - Add Cursor rules for new APIs + +3. **Error Pattern Analysis** + - Use Claude Code to find common PR feedback + - Add preventive Copilot instructions + - Create Cursor rules for complex cases + +### Tips for Maximum Effectiveness + +1. **Let each tool do what it does best** + - Don't duplicate complex explanations in Copilot + - Don't make Cursor rules for universal patterns + - Don't use Claude Code for simple lookups + +2. **Keep instructions focused** + - Copilot: "What to do" + - Cursor: "How to do it here" + - Claude Code: "Why we do it this way" + +3. **Update regularly but thoughtfully** + - Not every pattern needs to be in Copilot + - Quality over quantity + - Test impact before adding + +### Common Pitfalls to Avoid + +❌ **Don't add release-specific info to Copilot** (version numbers change) +❌ **Don't duplicate CLAUDE.md content in Copilot** (keep it DRY) +❌ **Don't add complex algorithms to Copilot** (use Claude Code) +❌ **Don't create Cursor rules for universal patterns** (use Copilot) + +### Getting Started + +1. Copilot automatically loads `.github/copilot-instructions.md` +2. Cursor reads rules from `.cursor/rules/` when present +3. Claude Code accesses `CLAUDE.md` and `.claude/context/` + +Each tool enhances your development without interfering with the others. Use them as a suite for maximum productivity! \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c93d673 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,174 @@ +# Project Coding Standards for AI Assistance + +> These instructions are automatically included in every GitHub Copilot interaction. They represent our most critical patterns and conventions for the Mixpanel Flutter SDK. + +## Core Principles + +1. **Input Validation First**: All user inputs must be validated before platform channel calls + - String inputs validated with `_MixpanelHelper.isValidString()` + - Prevent crashes at the API boundary + +2. **Fail Silently with Logging**: Never throw exceptions to calling code + - Log errors using `developer.log()` with 'Mixpanel' name + - Return gracefully on validation failures + +3. **Platform Channel Consistency**: All native calls follow exact same pattern + - Method name must match across Dart and native code + - Arguments always in `Map` format + +4. **Type Safety**: Handle cross-platform type differences explicitly + - Mobile: MixpanelMessageCodec handles DateTime/Uri + - Web: Use `safeJsify()` for JavaScript compatibility + +## Flutter SDK Guidelines + +### Method Patterns +All public methods MUST follow this exact structure: +```dart +Future methodName(String requiredParam, [Map? optionalParam]) async { + if (!_MixpanelHelper.isValidString(requiredParam)) { + developer.log('`methodName` failed: requiredParam cannot be blank', name: 'Mixpanel'); + return; + } + + await _channel.invokeMethod('methodName', { + 'requiredParam': requiredParam, + 'optionalParam': optionalParam ?? {}, + }); +} +``` + +### Naming Conventions +- **Methods**: camelCase with action verbs (`track`, `registerSuperProperties`, `getPeople`) +- **Parameters**: Descriptive names (`eventName`, `distinctId`, `properties`) +- **Maps**: Always named `properties` or `superProperties` for consistency + +### Platform Channel Rules +When invoking platform methods, you MUST: +1. Use exact method name matching between Dart and native + ```dart + await _channel.invokeMethod('track', args); // 'track' must exist in native + ``` + +2. Structure arguments as flat maps + ```dart + { + 'eventName': eventName, + 'properties': properties ?? {}, + } + ``` + +3. Handle optional parameters with `?? {}` + ```dart + 'properties': properties ?? {}, // Never pass null + ``` + +## Code Generation Rules + +When generating code, you MUST: + +1. Validate all string inputs before use + ```dart + if (!_MixpanelHelper.isValidString(input)) { + developer.log('`method` failed: input cannot be blank', name: 'Mixpanel'); + return; + } + ``` + +2. Return Future for all public methods + ```dart + Future methodName() async { + // All methods async for platform consistency + } + ``` + +3. Include library metadata in tracking calls + ```dart + properties['\$lib_version'] = '2.4.4'; + properties['mp_lib'] = 'flutter'; + ``` + +When generating code, NEVER: +- Throw exceptions from public methods +- Pass null to platform channels (use `?? {}`) +- Create synchronous public methods +- Skip input validation + +## Testing Requirements + +Every test must: +- Use descriptive test names: `test('should fail silently when eventName is empty')` +- Verify platform channel calls with `isMethodCall` matcher +- Test both success and validation failure cases + +```dart +test('tracks event with properties', () async { + await mixpanel.track('Event', properties: {'key': 'value'}); + expect( + methodCall, + isMethodCall( + 'track', + arguments: { + 'eventName': 'Event', + 'properties': {'key': 'value'}, + }, + ), + ); +}); +``` + +## Documentation Standards + +- Public methods need dartdoc with parameter descriptions +- Use `///` for public API documentation +- Include parameter constraints in docs +- No redundant comments in implementation + +```dart +/// Tracks an event with optional properties. +/// +/// * [eventName] The name of the event to track. Cannot be empty. +/// * [properties] Optional properties to include with the event. +Future track(String eventName, [Map? properties]) async { +``` + +## Security and Performance + +ALWAYS: +- Validate inputs at SDK boundaries +- Sanitize data before sending to native platforms +- Log errors without exposing sensitive data + +NEVER: +- Log user data or event properties in error messages +- Trust client inputs without validation +- Make synchronous platform channel calls + +## Type Handling Matrix + +| Type | Mobile | Web | +|------|---------|-----| +| String | Direct pass | Direct pass | +| num/bool | Direct pass | Direct pass | +| DateTime | MixpanelMessageCodec | Convert to ISO string | +| Uri | MixpanelMessageCodec | Convert to string | +| Map | Direct pass | `safeJsify()` | +| List | Direct pass | `safeJsify()` | + +## Platform-Specific Patterns + +### Web Implementation +```dart +if (kIsWeb) { + return WebImplementation.method(safeJsify(properties)); +} +``` + +### Mobile Implementation +```dart +return await _channel.invokeMethod('method', args); +``` + +## Additional Resources + +For architectural questions or complex refactoring needs, Claude Code CLI (`cc`) provides comprehensive context about this SDK's patterns and implementation details. \ No newline at end of file diff --git a/.github/instructions/code-review.instructions.md b/.github/instructions/code-review.instructions.md new file mode 100644 index 0000000..908da46 --- /dev/null +++ b/.github/instructions/code-review.instructions.md @@ -0,0 +1,181 @@ +--- +applyTo: "**/*.dart, **/*.java, **/*.swift, **/*.js" +--- +# Code Review Guidelines for Mixpanel Flutter SDK + +Apply all [general standards](../copilot-instructions.md) with these code review specific checks: + +## SDK Consistency Checklist + +### Method Implementation Review +```dart +// ✅ CORRECT: Follows SDK patterns +Future newMethod(String required, [Map? optional]) async { + if (!_MixpanelHelper.isValidString(required)) { + developer.log('`newMethod` failed: required cannot be blank', name: 'Mixpanel'); + return; + } + + await _channel.invokeMethod('newMethod', { + 'required': required, + 'optional': optional ?? {}, + }); +} + +// ❌ INCORRECT: Missing validation, wrong structure +Future newMethod(String required, Map? optional) async { + await _channel.invokeMethod('newMethod', { + 'required': required, + 'optional': optional // Missing ?? {} + }); +} +``` + +## Critical Review Points + +### 1. Input Validation +- [ ] All string parameters validated with `_MixpanelHelper.isValidString()` +- [ ] Validation happens BEFORE platform channel call +- [ ] Appropriate error logging on validation failure +- [ ] Method returns early on invalid input + +### 2. Platform Channel Consistency +- [ ] Method name matches exactly across Dart/Android/iOS/Web +- [ ] Arguments structured as `Map` +- [ ] Optional parameters use `?? {}` never pass null +- [ ] Return type is `Future` unless explicitly needed + +### 3. Error Handling +- [ ] No exceptions thrown from public methods +- [ ] All errors logged with 'Mixpanel' logger name +- [ ] Platform exceptions caught and logged +- [ ] Silent failure with appropriate logging + +### 4. Type Safety +- [ ] DateTime/Uri handled by MixpanelMessageCodec (mobile) +- [ ] Web uses `safeJsify()` for complex types +- [ ] No direct JSON encoding in Dart code +- [ ] Type conversions documented + +### 5. Library Metadata +```dart +// Check that tracking methods include: +properties['\$lib_version'] = '2.4.4'; +properties['mp_lib'] = 'flutter'; +``` + +## Platform-Specific Reviews + +### Android (Java) +- [ ] Uses `JSONObject` for property conversion +- [ ] Proper null checking with TextUtils +- [ ] Result.success(null) for void returns +- [ ] Thread safety for shared resources + +### iOS (Swift) +- [ ] Guard statements for validation +- [ ] Proper type conversion with MixpanelType +- [ ] Result(nil) for void returns +- [ ] Memory management for closures + +### Web (JavaScript) +- [ ] Uses `safeJsify()` for Dart->JS conversion +- [ ] Proper promise handling +- [ ] No synchronous operations +- [ ] CDN script loaded check + +## Test Coverage Review +- [ ] Positive test case with valid inputs +- [ ] Validation failure test cases +- [ ] Empty string and whitespace validation tests +- [ ] Optional parameter tests (null and empty map) +- [ ] Platform channel verification with `isMethodCall` + +## Documentation Review +- [ ] Public methods have dartdoc comments +- [ ] Parameter constraints documented +- [ ] Example usage provided for complex methods +- [ ] Platform differences noted if any + +## Security Considerations +- [ ] No sensitive data in error logs +- [ ] Input sanitization for user data +- [ ] No hardcoded secrets or keys +- [ ] Safe type conversions + +## Performance Review +- [ ] Async/await used appropriately +- [ ] No blocking operations +- [ ] Efficient data structures +- [ ] Minimal platform channel calls + +## Common Rejection Reasons + +### 🚫 Missing Validation +```dart +// REJECT: No validation +Future track(String eventName) async { + await _channel.invokeMethod('track', {'eventName': eventName}); +} +``` + +### 🚫 Throwing Exceptions +```dart +// REJECT: Throws exception +Future track(String eventName) async { + if (eventName.isEmpty) { + throw ArgumentError('eventName cannot be empty'); // NO! + } +} +``` + +### 🚫 Inconsistent Naming +```dart +// REJECT: Method name mismatch +await _channel.invokeMethod('trackEvent', args); // Should be 'track' +``` + +### 🚫 Null Parameters +```dart +// REJECT: Passing null +'properties': properties, // Should be: properties ?? {} +``` + +## Quick Review Commands + +For reviewing changes: +```bash +# Check for validation patterns +grep -n "isValidString" [file] + +# Verify platform channel calls +grep -n "invokeMethod" [file] + +# Check error handling +grep -n "developer.log" [file] + +# Find missing null safety +grep -n "?? {}" [file] +``` + +## Approval Criteria + +✅ **Approve if:** +- Follows all SDK patterns consistently +- Includes comprehensive tests +- Has appropriate documentation +- Handles errors gracefully +- Maintains backward compatibility + +🔄 **Request changes if:** +- Missing input validation +- Inconsistent with SDK patterns +- Lacks test coverage +- Could throw exceptions +- Breaks existing API contracts + +❌ **Reject if:** +- Security vulnerabilities +- Breaking changes without version bump +- Fundamentally wrong patterns +- No tests provided \ No newline at end of file diff --git a/.github/instructions/test-generation.instructions.md b/.github/instructions/test-generation.instructions.md new file mode 100644 index 0000000..1006358 --- /dev/null +++ b/.github/instructions/test-generation.instructions.md @@ -0,0 +1,320 @@ +# Test Generation Instructions for Mixpanel Flutter SDK + +## Overview +This document provides specific instructions for generating tests for the Mixpanel Flutter SDK. Follow these patterns to ensure consistency with the existing test suite. + +## Test Structure Pattern + +```dart +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mixpanel_flutter/mixpanel_flutter.dart'; + +void main() { + const MethodChannel channel = MethodChannel('mixpanel_flutter'); + late Mixpanel mixpanel; + final List methodCalls = []; + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + mixpanel = Mixpanel('YOUR_MIXPANEL_TOKEN'); + methodCalls.clear(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + methodCalls.add(methodCall); + // Return appropriate mock values based on method + switch (methodCall.method) { + case 'getDistinctId': + return 'distinct_id_1'; + case 'getDeviceId': + return 'device_id_1'; + case 'getAnonymousId': + return 'anonymous_id_1'; + default: + return null; + } + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + // Test groups go here +} +``` + +## Core Test Patterns + +### 1. Basic Method Call Test +```dart +test('methodName should invoke platform method with correct arguments', () async { + await mixpanel.methodName('param1', 'param2'); + + expect(methodCalls, hasLength(1)); + expect( + methodCalls[0], + isMethodCall( + 'methodName', + arguments: { + 'param1': 'param1', + 'param2': 'param2', + }, + ), + ); +}); +``` + +### 2. Validation Test Pattern +```dart +test('methodName should not invoke platform method with invalid input', () async { + // Test empty string + await mixpanel.methodName(''); + expect(methodCalls, isEmpty); + + // Test null (if applicable) + await mixpanel.methodName(null); + expect(methodCalls, isEmpty); + + // Test whitespace + await mixpanel.methodName(' '); + expect(methodCalls, isEmpty); +}); +``` + +### 3. Map/Dictionary Parameter Test +```dart +test('methodName with properties should format correctly', () async { + final properties = {'key1': 'value1', 'key2': 123, 'key3': true}; + await mixpanel.methodName('param', properties); + + expect( + methodCalls[0], + isMethodCall( + 'methodName', + arguments: { + 'param': 'param', + 'properties': properties, + }, + ), + ); +}); +``` + +### 4. Optional Parameter Test +```dart +test('methodName should handle null optional parameters', () async { + await mixpanel.methodName('required', null); + + expect( + methodCalls[0], + isMethodCall( + 'methodName', + arguments: { + 'required': 'required', + 'optional': {}, // SDK converts null maps to empty maps + }, + ), + ); +}); +``` + +## Specific Patterns for SDK Features + +### People/Group Accessor Tests +```dart +test('getPeople should return People instance', () { + final people = mixpanel.getPeople(); + expect(people, isA()); + expect(people, isNotNull); +}); + +test('getGroup should return MixpanelGroup instance', () { + final group = mixpanel.getGroup('groupKey', 'groupId'); + expect(group, isA()); + expect(group, isNotNull); +}); +``` + +### Super Properties Tests +```dart +test('registerSuperProperties should merge properties correctly', () async { + await mixpanel.registerSuperProperties({'prop1': 'value1'}); + expect(methodCalls, hasLength(1)); + + await mixpanel.registerSuperProperties({'prop2': 'value2'}); + expect(methodCalls, hasLength(2)); + + // Both calls should be registerSuperProperties + expect(methodCalls[0].method, 'registerSuperProperties'); + expect(methodCalls[1].method, 'registerSuperProperties'); +}); +``` + +### Time-based Tests +```dart +test('timeEvent should track event timing', () async { + await mixpanel.timeEvent('Timed Event'); + + expect( + methodCalls[0], + isMethodCall( + 'timeEvent', + arguments: { + 'eventName': 'Timed Event', + }, + ), + ); +}); +``` + +## Test Coverage Requirements + +Each new method should have tests for: + +1. **Happy Path**: Valid inputs produce expected platform calls +2. **Validation**: Invalid inputs (empty strings, null where not allowed) are rejected +3. **Edge Cases**: + - Very long strings + - Special characters in strings + - Empty collections + - Null optional parameters +4. **Type Safety**: Different property types (String, int, double, bool, List, Map) + +## Common Validation Rules + +The SDK validates strings using `_MixpanelHelper.isValidString()`: +- Not null +- Not empty after trimming +- Contains at least one non-whitespace character + +Test these cases: +```dart +// Invalid strings that should not trigger platform calls +'' // empty +' ' // whitespace only +'\t\n' // whitespace characters + +// Valid strings that should trigger platform calls +'a' // single character +'Event' // normal string +' Event ' // string with surrounding whitespace (gets trimmed) +``` + +## Platform-Specific Considerations + +### Method Return Values +```dart +// For methods that return values +case 'getDistinctId': + return 'test_distinct_id'; +case 'getSuperProperties': + return {'prop1': 'value1'}; +case 'getDeviceId': + return 'test_device_id'; +``` + +### Complex Type Handling +The SDK uses a custom message codec for DateTime and Uri objects: +```dart +test('track with DateTime property', () async { + final now = DateTime.now(); + await mixpanel.track('Event', {'timestamp': now}); + + // DateTime should be passed through the platform channel + expect(methodCalls[0].arguments['properties']['timestamp'], now); +}); +``` + +## Test Organization + +Group related tests: +```dart +group('Event Tracking', () { + test('track should send event', () async { }); + test('trackWithGroups should include groups', () async { }); + test('timeEvent should start timing', () async { }); +}); + +group('User Profile', () { + test('identify should set distinct id', () async { }); + test('alias should create alias', () async { }); + test('reset should clear data', () async { }); +}); + +group('Super Properties', () { + test('registerSuperProperties should register', () async { }); + test('clearSuperProperties should clear all', () async { }); + test('unregisterSuperProperty should remove one', () async { }); +}); +``` + +## Example: Complete Test for New Method + +```dart +group('newFeature', () { + test('should invoke platform method with valid inputs', () async { + await mixpanel.newFeature('param1', {'key': 'value'}); + + expect(methodCalls, hasLength(1)); + expect( + methodCalls[0], + isMethodCall( + 'newFeature', + arguments: { + 'param1': 'param1', + 'properties': {'key': 'value'}, + }, + ), + ); + }); + + test('should not invoke platform method with invalid input', () async { + await mixpanel.newFeature('', {'key': 'value'}); + expect(methodCalls, isEmpty); + + await mixpanel.newFeature(' ', {'key': 'value'}); + expect(methodCalls, isEmpty); + }); + + test('should handle null properties', () async { + await mixpanel.newFeature('param1', null); + + expect( + methodCalls[0], + isMethodCall( + 'newFeature', + arguments: { + 'param1': 'param1', + 'properties': {}, + }, + ), + ); + }); + + test('should handle empty properties', () async { + await mixpanel.newFeature('param1', {}); + + expect( + methodCalls[0], + isMethodCall( + 'newFeature', + arguments: { + 'param1': 'param1', + 'properties': {}, + }, + ), + ); + }); +}); +``` + +## Notes + +- Always use `isMethodCall` matcher for asserting method calls +- The SDK converts null maps to empty maps (`{}`) for consistency +- All string parameters are validated before platform calls +- Test both individual methods and their interactions +- Consider platform differences but test through the unified Dart API \ No newline at end of file diff --git a/.github/prompts/add-analytics-method.prompt.md b/.github/prompts/add-analytics-method.prompt.md new file mode 100644 index 0000000..5c49c98 --- /dev/null +++ b/.github/prompts/add-analytics-method.prompt.md @@ -0,0 +1,237 @@ +# Add New Analytics Method to Mixpanel Flutter SDK + +Use this prompt when you need to add a new analytics method to the Mixpanel Flutter SDK. + +## Context +You are adding a new method to the Mixpanel Flutter SDK. This SDK wraps native Mixpanel SDKs for iOS, Android, and Web platforms, providing a unified Dart API. + +## Method Details +- **Method Name**: `[METHOD_NAME]` +- **Description**: [What this method does] +- **Parameters**: + - `[param1]`: [type] - [description] + - `[param2]`: [type] - [description] (optional) +- **Return Type**: `Future<[return_type]>` + +## Implementation Checklist + +### 1. Dart Implementation (`lib/mixpanel_flutter.dart`) + +Add the method with input validation: +```dart +/// [Method description] +Future [methodName]([parameters]) async { + // Validate string inputs + if (_MixpanelHelper.isValidString([stringParam])) { + Map properties = { + 'param1': value1, + 'param2': value2 ?? {}, // Use ?? {} for optional maps + }; + + await _channel.invokeMethod('[methodName]', properties); + } else { + developer.log('`[methodName]` failed: [param] cannot be blank', name: 'Mixpanel'); + } +} +``` + +### 2. Android Implementation (`android/src/main/java/com/mixpanel/mixpanel_flutter/MixpanelFlutterPlugin.java`) + +Add case in `onMethodCall`: +```java +case "[methodName]": + // Extract parameters + String param1 = call.argument("param1"); + Map param2 = call.argument("param2"); + + // Convert properties if needed + JSONObject jsonProps = MixpanelHelper.convertToJSONObject(param2); + + // Call native SDK method + mixpanel.[nativeMethodName](param1, jsonProps); + result.success(null); + break; +``` + +### 3. iOS Implementation (`ios/Classes/SwiftMixpanelFlutterPlugin.swift`) + +Add case in `handle(_ call:)`: +```swift +case "[methodName]": + guard let args = call.arguments as? [String: Any], + let param1 = args["param1"] as? String else { + result(FlutterError(code: "ARGUMENT_ERROR", + message: "Invalid arguments", + details: nil)) + return + } + + let param2 = args["param2"] as? [String: Any] ?? [:] + + // Convert properties if needed + let mixpanelProps = MixpanelTypeHandler.processProperties(param2) + + // Call native SDK method + Mixpanel.mainInstance().[nativeMethodName](param1, properties: mixpanelProps) + result(nil) +``` + +### 4. Web Implementation (`lib/mixpanel_flutter_web.dart`) + +Add method implementation: +```dart +@override +Future [methodName]([parameters]) async { + if (!_MixpanelHelper.isValidString([stringParam])) { + developer.log('`[methodName]` failed: [param] cannot be blank', name: 'Mixpanel'); + return; + } + + Map? properties = _getProperties(param2); + + // Add library metadata + properties ??= {}; + properties['\$lib_version'] = MixpanelFlutterLibraryVersion.version; + properties['mp_lib'] = 'flutter'; + + // Call JavaScript SDK + mixpanel.[jsMethodName](param1, safeJsify(properties)); +} +``` + +### 5. Tests (`test/mixpanel_flutter_test.dart`) + +Add comprehensive tests: +```dart +group('[methodName]', () { + test('calls platform method with correct arguments', () async { + await mixpanel.[methodName]('param1', {'key': 'value'}); + + expect( + methodCall, + isMethodCall( + '[methodName]', + arguments: { + 'param1': 'param1', + 'param2': {'key': 'value'}, + }, + ), + ); + }); + + test('handles null/empty parameters gracefully', () async { + await mixpanel.[methodName]('', null); + + expect(methodCall, isNull); + }); + + test('validates required string parameters', () async { + await mixpanel.[methodName](null, {'key': 'value'}); + + expect(methodCall, isNull); + }); +}); +``` + +### 6. Example App Usage (`example/lib/`) + +Add example usage to demonstrate the feature: +```dart +// In appropriate example page +ElevatedButton( + onPressed: () async { + await _mixpanel.[methodName]( + 'example_param', + { + 'property1': 'value1', + 'property2': 123, + 'property3': true, + }, + ); + _showMessage('[MethodName] completed'); + }, + child: Text('[Method Display Name]'), +), +``` + +### 7. Documentation Updates + +Update the README.md with the new method: +```markdown +### [methodName] + +[Description of what the method does] + +```dart +await mixpanel.[methodName]('param1', {'key': 'value'}); +``` +``` + +### 8. Type Handling Considerations + +- **DateTime**: Automatically handled by `MixpanelMessageCodec` +- **Uri**: Automatically handled by `MixpanelMessageCodec` +- **Complex objects**: Convert to `Map` first +- **Web platform**: Use `safeJsify()` for JavaScript compatibility + +## Validation Checklist + +Before submitting: +- [ ] Method follows camelCase naming convention +- [ ] Platform channel method names match exactly across all platforms +- [ ] Input validation prevents crashes +- [ ] Methods fail silently with logging (no exceptions to caller) +- [ ] Library metadata (`$lib_version`, `mp_lib`) included in events +- [ ] Tests cover happy path and edge cases +- [ ] Example app demonstrates usage +- [ ] Code formatted with `dart format .` +- [ ] No analyzer warnings with `flutter analyze` + +## Common Patterns + +### For methods that track events: +- Always validate event names +- Merge properties with library metadata +- Use `_baseProperties()` for common properties + +### For methods with optional parameters: +- Use `??` operator for defaults +- Document default behavior +- Test both with and without optional params + +### For methods returning values: +- Specify generic type in `invokeMethod` +- Handle null returns appropriately +- Add return type tests + +## Testing the Implementation + +1. Run unit tests: `flutter test` +2. Test on each platform: + ```bash + cd example + flutter run -d android + flutter run -d ios + flutter run -d chrome + ``` +3. Verify in native platform logs that methods are called correctly +4. Check that events appear in Mixpanel dashboard (if applicable) + +## Example Usage + +To use this prompt: +1. Replace all `[METHOD_NAME]`, `[methodName]`, etc. with your actual method name +2. Fill in parameter details and descriptions +3. Follow the implementation steps in order +4. Use the validation checklist before submitting + +Example filled prompt: +``` +Method Name: trackPurchase +Description: Track a purchase event with transaction details +Parameters: + - productId: String - The ID of the product purchased + - amount: double - The purchase amount + - properties: Map? - Additional event properties (optional) +Return Type: Future +``` \ No newline at end of file diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000..09d0c42 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,70 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set permissions based on project needs + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.16.0" + channel: "stable" + cache: true + + - name: Install Flutter dependencies + run: | + flutter --version + flutter pub get + cd example && flutter pub get + + - name: Set up Java for Android + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Set up Ruby for iOS tooling + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + bundler-cache: true + + - name: Install development tools + run: | + # Install dartdoc for documentation generation + dart pub global activate dartdoc + + # Install coverage tools + dart pub global activate coverage + + - name: Run Flutter analyze + run: flutter analyze --no-fatal-infos --no-fatal-warnings + + - name: Verify test setup + run: flutter test --no-pub + + - name: Cache setup validation + run: | + echo "✓ Flutter SDK configured" + echo "✓ Dependencies installed" + echo "✓ Analysis tools ready" + echo "✓ Test framework available" \ No newline at end of file diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 368c218..0cfb513 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -19,7 +19,7 @@ jobs: flutter-version: "3.16.0" - run: flutter pub get - run: flutter test - - run: flutter analyze --no-pub --no-current-package lib + - run: flutter analyze --no-pub --no-current-package --no-fatal-infos lib test-android-integration: runs-on: macos-13 diff --git a/.gitignore b/.gitignore index 56dee95..d5e522a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ .pub/ .idea build/ -.cxx -CLAUDE.md \ No newline at end of file +.cxx \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..46ec36d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,304 @@ +# Mixpanel Flutter SDK AI Agent Instructions + +## Identity + +You are an autonomous software engineering agent working on the official Mixpanel Flutter SDK. You execute tasks independently in a cloud environment, producing complete, tested, PR-ready changes that maintain cross-platform compatibility (iOS, Android, Web) and follow established SDK patterns. + +## Project Overview + +This is the official Mixpanel Flutter SDK - a Flutter plugin providing analytics tracking capabilities across iOS, Android, and Web platforms. The plugin wraps native Mixpanel SDKs and provides a unified Dart API. + +### Key Technologies +- **Flutter/Dart**: Core SDK implementation +- **Platform Channels**: Communication between Dart and native code +- **Native SDKs**: Mixpanel-Android (v8.0.3), Mixpanel-Swift (v5.0.0), Mixpanel-JS (CDN) +- **Custom Serialization**: MixpanelMessageCodec for complex types +- **Testing**: Flutter unit tests with mocked platform channels + +### Architecture Summary +- **lib/**: Dart SDK implementation with platform channel interface +- **android/**: Java wrapper around Mixpanel Android SDK +- **ios/**: Swift wrapper around Mixpanel iOS SDK +- **example/**: Integration test app demonstrating all features +- **test/**: Comprehensive unit tests + +For detailed architecture, see `.claude/context/architecture/` (if available). + +## AI Ecosystem References + +This project maintains comprehensive AI assistance configuration. You MUST respect all patterns documented in: + +1. **`CLAUDE.md`** - Core patterns, workflows, and project context +2. **`.cursor/rules/`** - Behavioral rules for code generation (if present) +3. **`.github/copilot-instructions.md`** - Universal coding standards and conventions +4. **`.github/instructions/`** - Specialized instructions for testing and code review + +Always check these files when uncertain about patterns or conventions. They contain the team's accumulated knowledge and prevent common errors. + +## Environment Setup + +```bash +# Install Flutter dependencies +flutter pub get + +# Install example app dependencies +cd example && flutter pub get && cd .. + +# Verify environment +flutter doctor +flutter analyze +flutter test + +# Additional tools for validation +dart format --set-exit-if-changed . +``` + +### Platform-Specific Setup +- **Android**: Requires Java 17+ for builds +- **iOS**: Requires Xcode and CocoaPods +- **Web**: No additional setup required + +## Development Workflow + +### Before Making Changes +1. Read `CLAUDE.md` for project-specific patterns +2. Check `.github/copilot-instructions.md` for universal standards +3. Review existing implementations in similar methods +4. Run `flutter test` to ensure starting from clean state + +### While Developing +1. **ALWAYS validate string inputs** using `_MixpanelHelper.isValidString()` +2. **Follow platform channel pattern exactly**: + ```dart + await _channel.invokeMethod('methodName', { + 'param1': value1, + 'param2': value2 ?? {}, // Never pass null + }); + ``` +3. **Handle errors silently** with logging: + ```dart + developer.log('`methodName` failed: reason', name: 'Mixpanel'); + ``` +4. **Include library metadata** in all tracking calls +5. **Write tests immediately** after implementation + +### Validation Requirements +- [ ] All tests pass: `flutter test` +- [ ] Static analysis clean: `flutter analyze` +- [ ] Code formatted: `dart format .` +- [ ] No security violations (check `.github/copilot-instructions.md`) +- [ ] Platform channel naming matches exactly +- [ ] Example app updated if adding features +- [ ] Version consistency across all files + +## Common Tasks + +### Adding New SDK Methods +Follow the 5-step implementation process: +1. Define method in `lib/mixpanel_flutter.dart` with validation +2. Add platform channel invocation with standard arguments +3. Implement handlers: + - Android: `MixpanelFlutterPlugin.java` + - iOS: `SwiftMixpanelFlutterPlugin.swift` + - Web: `mixpanel_flutter_web.dart` +4. Add tests to `test/mixpanel_flutter_test.dart` +5. Add example usage in `example/lib/` + +See `.github/prompts/add-analytics-method.prompt.md` for detailed steps. + +### Implementing Platform Handlers + +#### Android (Java) +```java +case "methodName": + String param = call.argument("param"); + if (TextUtils.isEmpty(param)) { + result.error("INVALID_ARGUMENT", "param cannot be empty", null); + return; + } + // Implementation using Mixpanel Android SDK + result.success(null); + break; +``` + +#### iOS (Swift) +```swift +case "methodName": + guard let args = call.arguments as? [String: Any], + let param = args["param"] as? String, + !param.isEmpty else { + result(FlutterError(code: "INVALID_ARGUMENT", + message: "param cannot be empty", + details: nil)) + return + } + // Implementation using Mixpanel iOS SDK + result(nil) +``` + +#### Web (Dart/JS) +```dart +Future methodName(String param, Map? properties) async { + if (!_MixpanelHelper.isValidString(param)) { + developer.log('`methodName` failed: param cannot be blank', name: 'Mixpanel'); + return; + } + _mixpanel.methodName(param, safeJsify(properties ?? {})); +} +``` + +### Writing Tests +Always follow the SDK test pattern: +```dart +test('methodName validates input and calls platform channel', () async { + await mixpanel.methodName('valid', properties: {'key': 'value'}); + expect( + methodCall, + isMethodCall( + 'methodName', + arguments: { + 'param': 'valid', + 'properties': {'key': 'value'}, + }, + ), + ); +}); + +test('methodName fails silently with invalid input', () async { + await mixpanel.methodName('', properties: {'key': 'value'}); + expect(methodCall, isNull); +}); +``` + +### Bug Fixes +1. Write failing test demonstrating the bug +2. Fix with minimal changes following existing patterns +3. Ensure all existing tests still pass +4. Update example app if behavior changes + +## Task Execution Instructions + +### Planning Phase +Before writing any code: +1. Identify all files that need modification +2. Check similar existing implementations +3. Review validation patterns in affected code +4. Plan test cases (success, validation, edge cases) + +### Implementation Phase +1. Start with Dart interface and validation +2. Implement platform handlers one at a time +3. Run tests after each platform implementation +4. Test in example app on actual devices/simulators +5. Ensure cross-platform consistency + +### PR Preparation +Your PR should include: +- **Title**: `[Component] Brief description` (e.g., `[SDK] Add trackPurchase method`) +- **Description**: + ```markdown + ## Summary + Brief description of changes + + ## Changes + - Added `trackPurchase` method to Mixpanel class + - Implemented Android, iOS, and Web handlers + - Added comprehensive unit tests + - Updated example app with purchase tracking demo + + ## Testing + - [ ] Unit tests pass + - [ ] Tested on Android device/emulator + - [ ] Tested on iOS device/simulator + - [ ] Tested on Web browser + + ## Notes + Any implementation decisions or trade-offs + ``` + +## Code Standards + +### Critical Patterns (from all AI systems) +1. **Input Validation**: ALWAYS validate before platform calls +2. **Error Handling**: Silent failure with logging, no exceptions +3. **Return Types**: All public methods return `Future` +4. **Null Safety**: Use `?? {}` for optional maps, never pass null +5. **Library Metadata**: Include version and library identifier + +### Naming Conventions +- Methods: `camelCase` with verb prefixes +- Parameters: Descriptive (`eventName`, `distinctId`, `properties`) +- Platform methods: Must match exactly across all platforms +- Test names: `should [behavior] when [condition]` + +### Testing Requirements +- Every method needs success and failure tests +- Use `isMethodCall` matcher for platform verification +- Test validation for empty strings and whitespace +- Include edge cases and type conversions + +### Security Patterns +- Never log sensitive user data +- Validate all external inputs +- Sanitize before passing to native platforms +- No hardcoded secrets or keys + +## Debugging Instructions + +If you encounter issues: +1. Check existing similar implementations first +2. Verify platform channel method names match exactly +3. Ensure validation happens before platform calls +4. Confirm test patterns match existing tests +5. Run example app to verify actual behavior + +Common issues: +- **Platform channel not found**: Method name mismatch +- **Null pointer exceptions**: Missing `?? {}` for optional parameters +- **Test failures**: Wrong argument structure in `isMethodCall` +- **Web compilation errors**: Missing `safeJsify()` for complex types + +## Optimal Task Types + +I excel at these task categories: + +### 1. Feature Implementation +- Adding new tracking methods following patterns +- Implementing across all platforms consistently +- Creating comprehensive test coverage +Example: "Add group analytics methods for tracking team usage" + +### 2. Test Generation +- Adding tests for untested methods +- Increasing code coverage systematically +- Creating edge case scenarios +Example: "Add comprehensive tests for all People methods" + +### 3. Platform Consistency +- Ensuring methods work identically across platforms +- Fixing platform-specific bugs +- Standardizing error handling +Example: "Ensure date handling is consistent across iOS/Android/Web" + +### 4. Documentation +- Adding dartdoc comments to public APIs +- Creating example implementations +- Updating README with new features +Example: "Document all public methods with usage examples" + +### Task Anti-Patterns +I'm less effective at: +- Making architecture decisions without context +- Performance optimization without metrics +- UI/UX decisions in the example app +- Debugging issues without clear reproduction steps + +## Notes + +- This SDK prioritizes stability and consistency over innovation +- Every change must maintain backward compatibility +- Platform parity is critical - features must work on all platforms +- When uncertain, check how similar methods are implemented +- The example app serves as both documentation and integration test + +Remember: You're maintaining an official SDK used by thousands of apps. Reliability, consistency, and thorough testing are paramount. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5902189 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,177 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the official Mixpanel Flutter SDK - a Flutter plugin that provides analytics tracking capabilities for Flutter applications across iOS, Android, and Web platforms. The plugin wraps the native Mixpanel SDKs and provides a unified Dart API. + +## Core Patterns & Conventions + +### Method Naming +- All Dart methods use camelCase: `track`, `trackWithGroups`, `registerSuperProperties` +- Platform channel method names must match exactly between Dart and native code +- Getter methods use `get` prefix: `getPeople()`, `getGroup()` + +### Input Validation Pattern +```dart +// Always validate string inputs before platform calls +if (_MixpanelHelper.isValidString(eventName)) { + await _channel.invokeMethod('track', args); +} else { + developer.log('`track` failed: eventName cannot be blank', name: 'Mixpanel'); +} +``` + +### Platform Channel Pattern +```dart +// Standard invocation pattern for all methods +Future methodName(parameters) async { + await _channel.invokeMethod('methodName', { + 'param1': value1, + 'param2': value2 ?? {}, // Use ?? {} for optional maps + }); +} +``` + +## Key Architecture + +### Platform Channel Architecture +- The plugin uses Flutter's platform channel mechanism to communicate between Dart and native code +- Custom `MixpanelMessageCodec` handles serialization of complex data types between platforms +- Each platform (iOS, Android, Web) has its own implementation that wraps the respective Mixpanel SDK + +### Core Classes +- `Mixpanel` - Main singleton class for tracking events and managing the SDK +- `People` - User profile management (accessible via `mixpanel.getPeople()`) +- `MixpanelGroup` - Group analytics management (accessible via `mixpanel.getGroup()`) + +### Platform Dependencies +- Android: Mixpanel Android SDK v8.0.3 +- iOS: Mixpanel-swift v5.0.0 +- Web: Mixpanel JavaScript library (loaded from CDN) + +## Development Commands + +```bash +# Install dependencies +flutter pub get + +# Run tests +flutter test + +# Run the example app (from project root) +cd example +flutter run + +# Build for specific platform +flutter build apk # Android +flutter build ios # iOS +flutter build web # Web + +# Analyze code +flutter analyze + +# Format code +dart format . + +# Generate documentation +flutter pub run dartdoc +``` + +## Testing Strategy + +- Unit tests are in `test/mixpanel_flutter_test.dart` +- The example app serves as an integration test suite with pages for each feature +- Platform-specific functionality should be tested through the example app on each platform + +## Release Process + +Use the release script: `python tool/release.py` + +This handles version bumping, changelog updates, and tagging. + +## Important Implementation Notes + +### Web Platform +- Web implementation requires adding Mixpanel JS to the HTML header +- The plugin dynamically loads the Mixpanel JavaScript library +- Web-specific implementation is in `lib/mixpanel_flutter_web.dart` + +### Message Codec +- Custom codec is required to handle DateTime objects and other complex types +- Implementations: `MixpanelMessageCodec.java` (Android) and `MixpanelTypeHandler.swift` (iOS) + +### API Design +- All methods return Futures for consistency across platforms +- Super properties persist across app launches +- Groups and user profiles are managed through separate accessor methods + +## Essential Implementation Patterns + +### Adding New Features +1. Define method in `lib/mixpanel_flutter.dart` with validation +2. Add platform channel invocation with standard argument structure +3. Implement handlers in: + - `MixpanelFlutterPlugin.java` (Android) + - `SwiftMixpanelFlutterPlugin.swift` (iOS) + - `mixpanel_flutter_web.dart` (Web) +4. Add tests to `test/mixpanel_flutter_test.dart` +5. Add example usage in `example/lib/` + +### Type Handling +- **DateTime/Uri**: Automatically serialized by `MixpanelMessageCodec` on mobile +- **Web**: Use `safeJsify()` for JavaScript compatibility +- **Complex objects**: Convert to `Map` first + +### Error Handling Philosophy +- Input validation prevents crashes +- Methods fail silently with logging +- No exceptions thrown to calling code +- Platform errors caught and logged + +### Library Metadata +All events automatically include: +```dart +'\$lib_version': '2.4.4', // Current SDK version +'mp_lib': 'flutter', // Library identifier +``` + +### Testing Pattern +```dart +test('method behavior', () async { + await mixpanel.methodName('param'); + expect( + methodCall, + isMethodCall( + 'methodName', + arguments: {'param': 'value'}, + ), + ); +}); +``` + +## Platform-Specific Notes + +### Android +- Lazy initialization to prevent ANR +- Uses `JSONObject` for property conversion +- Helper class for property merging + +### iOS +- Uses `MixpanelType` for type conversion +- Swift implementation with type safety +- Guard statements for validation + +### Web +- Requires mixpanel.js in HTML header +- JavaScript interop with `@JS` annotations +- Dynamic type conversion with `jsify()` + +## Quick Reference + +For detailed patterns and workflows, see: +- `.claude/context/discovered-patterns.md` - All coding patterns +- `.claude/context/architecture/system-design.md` - System architecture +- `.claude/context/workflows/` - Development workflows +- `.claude/context/technologies/` - Technology deep dives \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index 9d532b1..7283898 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -32,7 +32,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols diff --git a/example/pubspec.lock b/example/pubspec.lock index 146f2da..bc099cb 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -62,6 +62,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -96,6 +104,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" matcher: dependency: transitive description: @@ -126,7 +142,7 @@ packages: path: ".." relative: true source: path - version: "2.4.3" + version: "2.4.4" path: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f236489..76af865 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the mixpanel_flutter plugin. publish_to: 'none' environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.12.0 <4.0.0' dependencies: flutter: @@ -18,6 +18,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^3.0.0 flutter: diff --git a/example/test/AGENTS.md b/example/test/AGENTS.md new file mode 100644 index 0000000..dc2022c --- /dev/null +++ b/example/test/AGENTS.md @@ -0,0 +1,404 @@ +# AGENTS.md - Test Generation Guide + +This file provides specialized guidance for AI agents generating tests for the Mixpanel Flutter SDK example app. + +## Test Architecture Overview + +### Test Types +1. **Widget Tests** - UI component testing with mocked platform channels +2. **Integration Tests** - Full app flow testing with real platform interactions +3. **Unit Tests** - Individual method and class testing + +### Platform Channel Mocking Strategy + +```dart +// Standard mock setup for all tests +void setupMixpanelMocks() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const MethodChannel channel = MethodChannel('mixpanel_flutter'); + final List log = []; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + log.add(methodCall); + + // Return appropriate mock responses + switch (methodCall.method) { + case 'getDistinctId': + return 'test_distinct_id'; + case 'getDeviceId': + return 'test_device_id'; + case 'track': + case 'trackWithGroups': + case 'timeEvent': + case 'eventElapsedTime': + case 'identify': + case 'alias': + case 'registerSuperProperties': + case 'registerSuperPropertiesOnce': + case 'unregisterSuperProperty': + case 'reset': + case 'clearSuperProperties': + case 'flush': + return null; // void methods + case 'getSuperProperties': + return {}; + case 'optInTracking': + case 'optOutTracking': + case 'hasOptedOutTracking': + return methodCall.arguments['hasOptedOutTracking'] ?? false; + default: + return null; + } + }); +} +``` + +## Analytics SDK Test Patterns + +### Event Tracking Tests +```dart +testWidgets('tracks events with properties', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + // Find and tap button + await tester.tap(find.text('Track Event')); + await tester.pump(); + + // Verify platform channel call + expect(log.last.method, 'track'); + expect(log.last.arguments['eventName'], 'Button Clicked'); + expect(log.last.arguments['properties'], isA>()); +}); +``` + +### User Profile Tests +```dart +testWidgets('updates user profile properties', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + // Navigate to profile page + await tester.tap(find.text('User Profile')); + await tester.pumpAndSettle(); + + // Set profile property + await tester.enterText(find.byType(TextField).first, 'Test User'); + await tester.tap(find.text('Set Name')); + await tester.pump(); + + // Verify people.set call + expect(log.last.method, 'people.set'); + expect(log.last.arguments['properties']['name'], 'Test User'); +}); +``` + +### Group Analytics Tests +```dart +testWidgets('manages group analytics', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + // Navigate to groups + await tester.tap(find.text('Groups')); + await tester.pumpAndSettle(); + + // Add user to group + await tester.tap(find.text('Add to Group')); + await tester.pump(); + + expect(log.last.method, 'group.set'); + expect(log.last.arguments['groupKey'], 'company'); +}); +``` + +## Coverage Goals + +### Minimum Coverage Requirements +- **Overall**: 80% line coverage +- **Critical paths**: 95% coverage + - Event tracking + - User identification + - Super properties + - Data persistence + +### Coverage Exclusions +- Generated code (`*.g.dart`) +- Platform-specific implementations +- Example app UI code (focus on SDK usage) + +## Common Test Scenarios + +### 1. Event Tracking Scenarios +- [ ] Basic event tracking +- [ ] Events with properties +- [ ] Events with groups +- [ ] Timed events +- [ ] Super properties inheritance +- [ ] Property validation + +### 2. User Management Scenarios +- [ ] User identification +- [ ] Alias creation +- [ ] Profile property updates +- [ ] Incremental operations +- [ ] List operations (append, union) +- [ ] User deletion + +### 3. Data Persistence Scenarios +- [ ] Super properties persistence +- [ ] Distinct ID persistence +- [ ] Opt-out state persistence +- [ ] Flush behavior + +### 4. Error Handling Scenarios +- [ ] Invalid event names +- [ ] Null properties +- [ ] Network failures +- [ ] Platform errors + +### 5. Web-Specific Scenarios +- [ ] Script loading +- [ ] JavaScript interop +- [ ] Type conversion + +## Integration Test Guidelines + +### Setup Pattern +```dart +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Mixpanel Integration', () { + setUpAll(() async { + // Initialize real Mixpanel instance + await Mixpanel.init('YOUR_TOKEN'); + }); + + testWidgets('full user journey', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + // Test complete user flow + // 1. Track app open + // 2. Identify user + // 3. Track feature usage + // 4. Update profile + // 5. Join group + // 6. Track conversion + }); + }); +} +``` + +### Platform-Specific Integration Tests +```dart +// Run only on specific platforms +testWidgets('iOS specific features', (WidgetTester tester) async { + if (!Platform.isIOS) return; + + // Test iOS-specific functionality +}, skip: !Platform.isIOS); +``` + +## Test Data Generators + +### Property Generators +```dart +Map generateTestProperties({ + bool includeNested = false, + bool includeArrays = false, + bool includeDates = false, +}) { + final props = { + 'string_prop': 'test_value', + 'int_prop': 42, + 'double_prop': 3.14, + 'bool_prop': true, + }; + + if (includeNested) { + props['nested'] = {'level': 2, 'data': 'nested_value'}; + } + + if (includeArrays) { + props['array'] = ['item1', 'item2', 'item3']; + } + + if (includeDates) { + props['date'] = DateTime.now(); + } + + return props; +} +``` + +### Event Name Generators +```dart +const List testEventNames = [ + 'App Opened', + 'Feature Used', + 'Button Clicked', + 'Form Submitted', + 'Purchase Completed', + 'Error Occurred', +]; + +String generateEventName() => + testEventNames[Random().nextInt(testEventNames.length)]; +``` + +## Performance Test Patterns + +### Batch Operations +```dart +test('handles high volume events', () async { + final stopwatch = Stopwatch()..start(); + + // Track 1000 events + for (int i = 0; i < 1000; i++) { + await mixpanel.track('Test Event $i'); + } + + stopwatch.stop(); + expect(stopwatch.elapsedMilliseconds, lessThan(5000)); +}); +``` + +### Memory Usage +```dart +test('maintains reasonable memory footprint', () async { + // Monitor memory before + final initialMemory = await getMemoryUsage(); + + // Perform operations + for (int i = 0; i < 100; i++) { + await mixpanel.registerSuperProperties({ + 'prop_$i': 'value_$i' + }); + } + + // Check memory growth + final finalMemory = await getMemoryUsage(); + expect(finalMemory - initialMemory, lessThan(10 * 1024 * 1024)); // 10MB +}); +``` + +## Mock Helpers + +### Platform Response Mocks +```dart +class MockMixpanelResponses { + static final Map superProperties = { + 'app_version': '1.0.0', + 'platform': 'flutter', + }; + + static const String distinctId = 'mock_distinct_id_123'; + static const String deviceId = 'mock_device_id_456'; + + static Map peopleProperties = { + '\$name': 'Test User', + '\$email': 'test@example.com', + 'created': DateTime.now().toIso8601String(), + }; +} +``` + +### Test Matchers +```dart +Matcher isTrackCall(String eventName, [Map? properties]) { + return allOf( + isMethodCall('track', arguments: { + 'eventName': eventName, + if (properties != null) 'properties': properties, + }), + ); +} + +Matcher isPeopleCall(String operation, Map properties) { + return isMethodCall('people.$operation', arguments: { + 'properties': properties, + }); +} +``` + +## CI/CD Test Commands + +```bash +# Run all tests with coverage +flutter test --coverage + +# Generate coverage report +genhtml coverage/lcov.info -o coverage/html + +# Run specific test file +flutter test test/widget_test.dart + +# Run integration tests +flutter test integration_test/app_test.dart + +# Run tests on specific platform +flutter test --platform chrome # Web +flutter test --device-id # Specific device +``` + +## Test Debugging Tips + +### Verbose Platform Channel Logging +```dart +// Add to setUp() +TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + print('Platform call: ${methodCall.method}'); + print('Arguments: ${methodCall.arguments}'); + // ... handle call +}); +``` + +### Visual Test Debugging +```dart +// Take screenshots during failed tests +testWidgets('visual debugging', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + try { + // Test logic + } catch (e) { + // Take screenshot on failure + await tester.takeScreenshot(name: 'failure_screenshot'); + rethrow; + } +}); +``` + +## Quick Test Templates + +### Basic Widget Test +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:example/main.dart'; + +void main() { + setUpAll(() => setupMixpanelMocks()); + + testWidgets('description', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + + // Test implementation + }); +} +``` + +### Property Validation Test +```dart +test('validates properties', () async { + // Test null handling + await mixpanel.track('Event', null); + expect(log.last.arguments['properties'], {}); + + // Test invalid types + await mixpanel.track('Event', {'invalid': Object()}); + // Should filter out invalid types +}); +``` + +This guide ensures comprehensive, consistent test coverage across the Mixpanel Flutter SDK example app. \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 83666dd..fefee67 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -54,6 +54,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -88,6 +96,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f96f12c..ef9660e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^3.0.0 flutter: assets: