From c71cacd0eb14321c07c35d5d32ebc6f2d995f2ee Mon Sep 17 00:00:00 2001 From: Jared McFarland Date: Wed, 15 Oct 2025 14:47:47 -0700 Subject: [PATCH] add AI assistants --- .claude/commands/add-property.md | 58 +++ .claude/commands/debug-issue.md | 202 ++++++++ .claude/commands/fix-thread-safety.md | 135 +++++ .claude/commands/migrate-db.md | 233 +++++++++ .claude/commands/new-api.md | 102 ++++ .claude/commands/write-test.md | 210 ++++++++ .claude/quick-reference.md | 81 +++ .cursor/rules/api-design.mdc | 94 ++++ .cursor/rules/architecture.mdc | 54 ++ .cursor/rules/commands.mdc | 136 +++++ .cursor/rules/common-workflows.mdc | 232 +++++++++ .cursor/rules/networking.mdc | 146 ++++++ .cursor/rules/persistence.mdc | 110 ++++ .cursor/rules/platform-support.mdc | 166 ++++++ .cursor/rules/project-overview.mdc | 35 ++ .cursor/rules/property-types.mdc | 72 +++ .cursor/rules/release-process.mdc | 142 ++++++ .cursor/rules/testing.mdc | 113 +++++ .cursor/rules/thread-safety.mdc | 65 +++ .devcontainer/Dockerfile | 144 ++++++ .devcontainer/devcontainer.json | 95 ++++ .devcontainer/init-firewall.sh | 126 +++++ .devcontainer/setup-shell.sh | 110 ++++ .github/copilot-instructions.md | 101 ++++ .../instructions/api-design.instructions.md | 53 ++ .../instructions/networking.instructions.md | 54 ++ .../instructions/persistence.instructions.md | 49 ++ .../property-types.instructions.md | 41 ++ .github/instructions/testing.instructions.md | 54 ++ .../thread-safety.instructions.md | 43 ++ .github/prompts/add-event-property.prompt.md | 37 ++ .../prompts/add-platform-support.prompt.md | 52 ++ .github/prompts/database-migration.prompt.md | 55 ++ .github/prompts/debug-issue.prompt.md | 61 +++ .github/prompts/fix-thread-safety.prompt.md | 44 ++ .github/prompts/implement-new-api.prompt.md | 53 ++ .../prompts/optimize-performance.prompt.md | 60 +++ .github/prompts/write-unit-test.prompt.md | 57 +++ .github/workflows/claude-code-agent.yaml | 78 +++ claude/architecture/persistence-layer.md | 408 +++++++++++++++ claude/architecture/threading-model.md | 334 ++++++++++++ claude/context-map.md | 135 +++++ claude/migration-guide.md | 165 ++++++ claude/patterns/type-safety-patterns.md | 429 ++++++++++++++++ claude/technologies/swift-features.md | 478 ++++++++++++++++++ claude/workflows/release-process.md | 309 +++++++++++ 46 files changed, 6011 insertions(+) create mode 100644 .claude/commands/add-property.md create mode 100644 .claude/commands/debug-issue.md create mode 100644 .claude/commands/fix-thread-safety.md create mode 100644 .claude/commands/migrate-db.md create mode 100644 .claude/commands/new-api.md create mode 100644 .claude/commands/write-test.md create mode 100644 .claude/quick-reference.md create mode 100644 .cursor/rules/api-design.mdc create mode 100644 .cursor/rules/architecture.mdc create mode 100644 .cursor/rules/commands.mdc create mode 100644 .cursor/rules/common-workflows.mdc create mode 100644 .cursor/rules/networking.mdc create mode 100644 .cursor/rules/persistence.mdc create mode 100644 .cursor/rules/platform-support.mdc create mode 100644 .cursor/rules/project-overview.mdc create mode 100644 .cursor/rules/property-types.mdc create mode 100644 .cursor/rules/release-process.mdc create mode 100644 .cursor/rules/testing.mdc create mode 100644 .cursor/rules/thread-safety.mdc create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/init-firewall.sh create mode 100644 .devcontainer/setup-shell.sh create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/api-design.instructions.md create mode 100644 .github/instructions/networking.instructions.md create mode 100644 .github/instructions/persistence.instructions.md create mode 100644 .github/instructions/property-types.instructions.md create mode 100644 .github/instructions/testing.instructions.md create mode 100644 .github/instructions/thread-safety.instructions.md create mode 100644 .github/prompts/add-event-property.prompt.md create mode 100644 .github/prompts/add-platform-support.prompt.md create mode 100644 .github/prompts/database-migration.prompt.md create mode 100644 .github/prompts/debug-issue.prompt.md create mode 100644 .github/prompts/fix-thread-safety.prompt.md create mode 100644 .github/prompts/implement-new-api.prompt.md create mode 100644 .github/prompts/optimize-performance.prompt.md create mode 100644 .github/prompts/write-unit-test.prompt.md create mode 100644 .github/workflows/claude-code-agent.yaml create mode 100644 claude/architecture/persistence-layer.md create mode 100644 claude/architecture/threading-model.md create mode 100644 claude/context-map.md create mode 100644 claude/migration-guide.md create mode 100644 claude/patterns/type-safety-patterns.md create mode 100644 claude/technologies/swift-features.md create mode 100644 claude/workflows/release-process.md diff --git a/.claude/commands/add-property.md b/.claude/commands/add-property.md new file mode 100644 index 00000000..14ee7fce --- /dev/null +++ b/.claude/commands/add-property.md @@ -0,0 +1,58 @@ +# /add-property + +Add a new automatic property to all tracked events in the Mixpanel SDK. + +## Usage +``` +/add-property $app_build_number +``` + +## Steps I'll Execute + +1. **Update `Sources/AutomaticProperties.swift`** + - Add property to the appropriate collection method + - Handle platform-specific availability with `#if os()` + - Follow existing patterns (look at `$app_version` implementation) + +2. **Add Constants** (if needed) + - Update `Sources/Constants.swift` with new property key + - Follow naming convention: `$property_name` + +3. **Platform Compatibility** + ```swift + #if os(iOS) + properties["$app_build_number"] = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + #endif + ``` + +4. **Write Tests** + - Add test to `MixpanelDemoTests/MixpanelAutomaticEventsTests.swift` + - Verify property appears in tracked events + - Test across all platforms + +5. **Update Documentation** + - Add entry to `CHANGELOG.md` + - Document property meaning and platform availability + +## Example Implementation +```swift +// In AutomaticProperties.swift +func collectAutomaticProperties() -> InternalProperties { + var properties: InternalProperties = [ + // Existing properties... + ] + + #if os(iOS) || os(tvOS) + if let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { + properties["$app_build_number"] = buildNumber + } + #endif + + return properties +} +``` + +## Validation +- Property value must conform to `MixpanelType` +- Test on iOS, macOS, tvOS, watchOS +- Verify in event payload: `instance.eventsQueue.first?["$app_build_number"]` \ No newline at end of file diff --git a/.claude/commands/debug-issue.md b/.claude/commands/debug-issue.md new file mode 100644 index 00000000..b39396f2 --- /dev/null +++ b/.claude/commands/debug-issue.md @@ -0,0 +1,202 @@ +# /debug-issue + +Debug and diagnose issues in the Mixpanel SDK. + +## Usage +``` +/debug-issue Events not being tracked +/debug-issue App crashes when tracking +/debug-issue Memory leak in People operations +``` + +## Debugging Steps + +### 1. Enable Verbose Logging +```swift +// In AppDelegate or initialization +Mixpanel.mainInstance().logger = MixpanelLogger(level: .debug) + +// Custom logger for more control +class CustomLogger: MixpanelLogging { + func log(_ message: MixpanelLogMessage) { + print("[\(message.level)] \(message.text)") + // Can also write to file, send to crash reporter, etc. + } +} +Mixpanel.mainInstance().logger = CustomLogger() +``` + +### 2. Common Issues & Solutions + +#### Events Not Tracking +```swift +// Check 1: Verify token +print("Token: \(instance.apiToken)") + +// Check 2: Inspect queue +print("Events in queue: \(instance.eventsQueue.count)") +instance.eventsQueue.forEach { event in + print("Event: \(event)") +} + +// Check 3: Force synchronous check +instance.trackingQueue.sync { + print("Internal queue count: \(instance._eventsQueue.count)") +} + +// Check 4: Verify persistence +let saved = instance.persistence.loadEntitiesInBatch(type: .events) +print("Persisted events: \(saved.count)") + +// Check 5: Test network +instance.flush { + print("Flush completed") +} +``` + +#### Thread Safety Issues +```swift +// Enable Thread Sanitizer in Xcode: +// Edit Scheme → Run → Diagnostics → ✓ Thread Sanitizer + +// Add debug logging to ReadWriteLock operations +extension ReadWriteLock { + func debugRead(_ block: () -> T) -> T { + print("🔵 Read lock acquired: \(label)") + defer { print("🔵 Read lock released: \(label)") } + return read(block) + } +} +``` + +#### Memory Leaks +```swift +// Check for retain cycles +// 1. Use Instruments → Leaks +// 2. Add weak self checks: + +// Look for missing [weak self]: +trackingQueue.async { + self.doSomething() // RETAIN CYCLE! +} + +// Should be: +trackingQueue.async { [weak self] in + self?.doSomething() +} + +// Debug deallocations: +deinit { + print("✅ \(type(of: self)) deallocated") +} +``` + +### 3. Database Debugging + +```bash +# Find SQLite database +find ~/Library/Developer/CoreSimulator -name "mixpanel-*.sqlite" -ls + +# Inspect database +sqlite3 /path/to/mixpanel-TOKEN.sqlite + +# Useful queries +.tables +SELECT COUNT(*) FROM events; +SELECT * FROM events LIMIT 5; +SELECT * FROM events WHERE json_extract(data, '$.event') = 'Your Event'; +.schema events +``` + +### 4. Network Debugging + +```swift +// Use proxy to inspect requests +// 1. Set up Charles Proxy or similar +// 2. Configure in SDK: + +let serverURL = "https://your-proxy.com/mixpanel" +instance.setServerURL(serverURL: serverURL) + +// Log all network requests: +class DebugNetwork: Network { + override func sendRequest(_ request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void) { + print("📡 Request: \(request.url?.absoluteString ?? "")") + if let body = request.httpBody { + print("📡 Body: \(String(data: body, encoding: .utf8) ?? "")") + } + + super.sendRequest(request) { data, response, error in + if let httpResponse = response as? HTTPURLResponse { + print("📡 Response: \(httpResponse.statusCode)") + } + if let error = error { + print("📡 Error: \(error)") + } + completion(data, response, error) + } + } +} +``` + +### 5. Performance Profiling + +```swift +// Measure operation time +let start = CFAbsoluteTimeGetCurrent() +// Operation +let elapsed = CFAbsoluteTimeGetCurrent() - start +print("Operation took \(elapsed)s") + +// Profile with Instruments: +// 1. Time Profiler - Find slow methods +// 2. Allocations - Track memory usage +// 3. System Trace - Queue behavior +``` + +### 6. Debugging Checklist + +#### For "Events Not Appearing": +- [ ] Check API token is correct +- [ ] Verify network connectivity +- [ ] Check flush is being called +- [ ] Inspect server response for errors +- [ ] Verify property types are valid +- [ ] Check for opt-out status + +#### For Crashes: +- [ ] Check crash logs for stack trace +- [ ] Look for force unwraps (!) +- [ ] Verify thread safety +- [ ] Check for nil delegate/closure calls +- [ ] Test with Guard Malloc + +#### For Performance Issues: +- [ ] Profile with Instruments +- [ ] Check batch sizes +- [ ] Monitor queue lengths +- [ ] Verify SQLite query performance +- [ ] Check for main thread blocking + +### 7. Test in Isolation + +```swift +// Minimal reproduction case +let instance = MixpanelInstance(apiToken: "YOUR_TOKEN") +instance.logger = MixpanelLogger(level: .debug) +instance.track(event: "Test") +instance.flush { + print("Done") +} +``` + +## Quick Debug Properties + +```swift +// Add to track calls for debugging +let debugProps: Properties = [ + "debug_timestamp": Date().timeIntervalSince1970, + "debug_thread": Thread.current.isMainThread ? "main" : "background", + "debug_queue_size": instance.eventsQueue.count +] +``` \ No newline at end of file diff --git a/.claude/commands/fix-thread-safety.md b/.claude/commands/fix-thread-safety.md new file mode 100644 index 00000000..c00332b2 --- /dev/null +++ b/.claude/commands/fix-thread-safety.md @@ -0,0 +1,135 @@ +# /fix-thread-safety + +Fix thread safety issues in selected code by adding proper synchronization. + +## Usage +``` +/fix-thread-safety +``` + +## Analysis Checklist + +1. **Identify Shared State** + - Instance variables accessed from multiple threads + - Collections that are modified + - Properties without synchronization + +2. **Common Issues to Fix** + - Direct property access without locks + - Race conditions in read-modify-write operations + - Missing `[weak self]` in async closures + - Incorrect queue usage + +## Fix Patterns + +### Basic Property Protection +```swift +// BEFORE (Unsafe) +class Component { + var sharedData: [String: Any] = [:] + + func updateData(key: String, value: Any) { + sharedData[key] = value // RACE CONDITION! + } +} + +// AFTER (Thread-safe) +class Component { + private let readWriteLock = ReadWriteLock(label: "com.mixpanel.component") + private var _sharedData: [String: Any] = [:] + + var sharedData: [String: Any] { + return readWriteLock.read { _sharedData } + } + + func updateData(key: String, value: Any) { + readWriteLock.write { + _sharedData[key] = value + } + } +} +``` + +### Collection Operations +```swift +// BEFORE +func addItem(_ item: String) { + items.append(item) // UNSAFE! +} + +// AFTER +func addItem(_ item: String) { + readWriteLock.write { + _items.append(item) + } +} +``` + +### Async Operations +```swift +// BEFORE +trackingQueue.async { + self.processEvent() // RETAIN CYCLE! +} + +// AFTER +trackingQueue.async { [weak self] in + guard let self = self else { return } + self.processEvent() +} +``` + +### Read-Modify-Write +```swift +// BEFORE +var counter: Int { + get { _counter } + set { _counter = newValue } +} + +func increment() { + counter += 1 // NOT ATOMIC! +} + +// AFTER +func increment() { + readWriteLock.write { + _counter += 1 + } +} +``` + +## Queue Usage Rules +- `trackingQueue`: All event tracking operations +- `networkQueue`: All network requests +- `flushQueue`: Batch processing operations +- Main queue: UI updates only + +## Testing Thread Safety +```swift +func testConcurrentAccess() { + let iterations = 1000 + let expectation = XCTestExpectation(description: "Concurrent access") + expectation.expectedFulfillmentCount = iterations * 2 + + DispatchQueue.concurrentPerform(iterations: iterations) { i in + // Write operation + component.updateData(key: "key\(i)", value: i) + expectation.fulfill() + + // Read operation + _ = component.sharedData + expectation.fulfill() + } + + wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(component.sharedData.count, iterations) +} +``` + +## Common Mistakes to Avoid +- ❌ Calling `write` from within `read` block (deadlock) +- ❌ Forgetting `[weak self]` in closures +- ❌ Using wrong queue for operation type +- ❌ Synchronous dispatch from same queue +- ❌ Multiple locks in different order \ No newline at end of file diff --git a/.claude/commands/migrate-db.md b/.claude/commands/migrate-db.md new file mode 100644 index 00000000..9f56eb97 --- /dev/null +++ b/.claude/commands/migrate-db.md @@ -0,0 +1,233 @@ +# /migrate-db + +Implement database schema migrations for the Mixpanel SDK. + +## Usage +``` +/migrate-db Add retry_count column to events table +/migrate-db Create indexes for performance +``` + +## Migration Implementation Guide + +### 1. Update Database Version +```swift +// In MPDB.swift +private let CURRENT_DB_VERSION = 3 // Increment this +``` + +### 2. Add Migration Logic + +```swift +// In MPDB.swift, add to appropriate migration method +func migrateDatabase() { + let oldVersion = getCurrentVersion() + + if oldVersion < 2 { + migrateToV2() + } + + if oldVersion < 3 { + migrateToV3() + } + + updateVersion(CURRENT_DB_VERSION) +} + +private func migrateToV3() { + do { + // Start transaction + try db.execute("BEGIN TRANSACTION") + + // Add new column + try db.execute(""" + ALTER TABLE events + ADD COLUMN retry_count INTEGER DEFAULT 0 + """) + + // Add index for performance + try db.execute(""" + CREATE INDEX IF NOT EXISTS idx_events_retry + ON events(retry_count, created_at) + """) + + // Update metadata + try db.execute(""" + INSERT OR REPLACE INTO metadata (key, value) + VALUES ('migration_v3_date', ?) + """, Date().timeIntervalSince1970) + + // Commit + try db.execute("COMMIT") + + Logger.info("Successfully migrated to v3") + + } catch { + // Rollback on error + try? db.execute("ROLLBACK") + Logger.error("Migration to v3 failed: \(error)") + throw error + } +} +``` + +### 3. Common Migration Patterns + +#### Add Column with Default +```swift +// Safe for existing data +ALTER TABLE events ADD COLUMN status TEXT DEFAULT 'pending' +``` + +#### Create New Table +```swift +CREATE TABLE IF NOT EXISTS event_metadata ( + event_id TEXT PRIMARY KEY, + retry_count INTEGER DEFAULT 0, + last_retry_at REAL, + error_message TEXT, + FOREIGN KEY (event_id) REFERENCES events(uuid) +) +``` + +#### Add Index for Performance +```swift +// For frequently queried columns +CREATE INDEX idx_events_token_created +ON events(token, created_at) + +// For WHERE clauses +CREATE INDEX idx_events_status +ON events(status) +WHERE status != 'sent' +``` + +#### Data Migration +```swift +// Transform existing data +UPDATE events +SET new_column = CASE + WHEN old_column = 'value1' THEN 'new_value1' + WHEN old_column = 'value2' THEN 'new_value2' + ELSE 'default' +END +``` + +### 4. Safety Measures + +```swift +private func safelyMigrate(from: Int, to: Int) throws { + // Backup critical data first + let backupPath = databasePath + ".backup.\(from)" + try FileManager.default.copyItem( + atPath: databasePath, + toPath: backupPath + ) + + do { + try performMigration(from: from, to: to) + } catch { + // Restore backup on failure + Logger.error("Migration failed, restoring backup") + try? FileManager.default.removeItem(atPath: databasePath) + try? FileManager.default.moveItem( + atPath: backupPath, + toPath: databasePath + ) + throw error + } + + // Clean up backup after success + try? FileManager.default.removeItem(atPath: backupPath) +} +``` + +### 5. Testing Migrations + +```swift +func testMigrationFromV2ToV3() { + // Create v2 database + let v2db = createV2Database() + + // Insert test data + v2db.insert(testEvents) + + // Close and reopen with new version + v2db.close() + let v3db = MPDB(path: v2db.path) + + // Verify migration + XCTAssertTrue(v3db.columnExists("retry_count", in: "events")) + XCTAssertEqual(v3db.version, 3) + + // Verify data integrity + let events = v3db.loadEvents() + XCTAssertEqual(events.count, testEvents.count) +} +``` + +### 6. Migration Checklist + +- [ ] Increment `CURRENT_DB_VERSION` +- [ ] Add migration method `migrateToVX()` +- [ ] Use transactions for atomicity +- [ ] Handle errors gracefully +- [ ] Log migration progress +- [ ] Test upgrade paths from all versions +- [ ] Verify data integrity +- [ ] Add rollback capability +- [ ] Document schema changes + +### 7. Schema Best Practices + +```sql +-- Use appropriate data types +CREATE TABLE events ( + uuid TEXT PRIMARY KEY, + token TEXT NOT NULL, + data BLOB NOT NULL, -- JSON data + created_at REAL DEFAULT (datetime('now')), + retry_count INTEGER DEFAULT 0 +); + +-- Add constraints +CREATE TABLE people ( + uuid TEXT PRIMARY KEY, + token TEXT NOT NULL, + data BLOB NOT NULL, + UNIQUE(token, uuid) +); + +-- Optimize with indexes +CREATE INDEX idx_events_flush +ON events(token, created_at) +WHERE retry_count < 3; +``` + +### 8. Performance Considerations + +```swift +// Run VACUUM periodically +func optimizeDatabase() { + do { + try db.execute("VACUUM") + try db.execute("ANALYZE") + } catch { + Logger.warning("Database optimization failed: \(error)") + } +} + +// Monitor database size +func getDatabaseSize() -> Int64 { + let attr = try? FileManager.default.attributesOfItem(atPath: databasePath) + return attr?[.size] as? Int64 ?? 0 +} +``` + +## Common Migration Scenarios + +1. **Adding retry logic**: Add retry_count, last_retry columns +2. **Performance optimization**: Add indexes on frequently queried columns +3. **New features**: Add tables for new data types +4. **Data cleanup**: Remove obsolete columns/tables +5. **Schema normalization**: Split large tables \ No newline at end of file diff --git a/.claude/commands/new-api.md b/.claude/commands/new-api.md new file mode 100644 index 00000000..d1d837e7 --- /dev/null +++ b/.claude/commands/new-api.md @@ -0,0 +1,102 @@ +# /new-api + +Implement a new public API method for the Mixpanel SDK. + +## Usage +``` +/new-api trackPurchase(amount: Double, currency: String, properties: Properties?) +``` + +## Implementation Steps + +1. **Add to `Mixpanel.swift`** (static interface) + ```swift + @discardableResult + public static func trackPurchase(amount: Double, + currency: String = "USD", + properties: Properties? = nil) -> MixpanelInstance { + return mainInstance().trackPurchase(amount: amount, + currency: currency, + properties: properties) + } + ``` + +2. **Implement in `MixpanelInstance.swift`** + ```swift + @discardableResult + public func trackPurchase(amount: Double, + currency: String = "USD", + properties: Properties? = nil) -> MixpanelInstance { + var purchaseProps = properties ?? [:] + purchaseProps["$amount"] = amount + purchaseProps["$currency"] = currency + + // Thread-safe implementation + trackingQueue.async { [weak self, purchaseProps] in + self?.track(event: "$purchase", properties: purchaseProps) + } + + return self + } + ``` + +3. **Add Objective-C Support** (if needed) + ```swift + @objc public func trackPurchase(amount: NSNumber, + currency: String, + properties: [String: Any]?) { + let validProps = properties?.compactMapValues { $0 as? MixpanelType } ?? [:] + _ = trackPurchase(amount: amount.doubleValue, + currency: currency, + properties: validProps) + } + ``` + +4. **Documentation** + ```swift + /// Tracks a purchase event with amount and currency. + /// + /// - Parameters: + /// - amount: The purchase amount + /// - currency: ISO 4217 currency code (default: "USD") + /// - properties: Additional event properties + /// - Returns: This instance for method chaining + /// + /// Example: + /// ```swift + /// Mixpanel.mainInstance() + /// .trackPurchase(amount: 29.99, currency: "EUR", properties: [ + /// "product_id": "sku_123", + /// "product_name": "Premium Subscription" + /// ]) + /// ``` + ``` + +5. **Write Tests** + ```swift + func testTrackPurchase() { + let amount = 99.99 + let currency = "GBP" + let props: Properties = ["product": "widget"] + + instance.trackPurchase(amount: amount, currency: currency, properties: props) + waitForTrackingQueue(instance) + + XCTAssertEqual(instance.eventsQueue.count, 1) + let event = instance.eventsQueue.first + XCTAssertEqual(event?["event"] as? String, "$purchase") + XCTAssertEqual(event?["properties"]?["$amount"] as? Double, amount) + XCTAssertEqual(event?["properties"]?["$currency"] as? String, currency) + XCTAssertEqual(event?["properties"]?["product"] as? String, "widget") + } + ``` + +## Checklist +- [ ] Thread safety with ReadWriteLock +- [ ] Property type validation +- [ ] Default parameter values +- [ ] Return self for chaining +- [ ] Comprehensive documentation +- [ ] Unit tests with edge cases +- [ ] Objective-C compatibility +- [ ] Update CHANGELOG.md \ No newline at end of file diff --git a/.claude/commands/write-test.md b/.claude/commands/write-test.md new file mode 100644 index 00000000..361f8e65 --- /dev/null +++ b/.claude/commands/write-test.md @@ -0,0 +1,210 @@ +# /write-test + +Write comprehensive unit tests for Mixpanel SDK components. + +## Usage +``` +/write-test People.set +/write-test trackingQueue synchronization +``` + +## Test Structure Template + +```swift +import XCTest +@testable import Mixpanel + +class ComponentNameTests: MixpanelBaseTests { + + var instance: MixpanelInstance! + + override func setUp() { + super.setUp() + instance = MixpanelInstance( + apiToken: testToken, + flushInterval: 0, // Disable auto-flush + trackAutomaticEvents: false + ) + } + + override func tearDown() { + instance.reset() + super.tearDown() + } + + // Test methods here +} +``` + +## Test Categories + +### 1. Basic Functionality Test +```swift +func testBasicOperation() { + // Arrange + let testData = "test value" + + // Act + instance.performOperation(testData) + waitForTrackingQueue(instance) + + // Assert + XCTAssertEqual(instance.someProperty, expectedValue) +} +``` + +### 2. Edge Cases Test +```swift +func testEdgeCases() { + // Empty input + instance.track(event: "") + waitForTrackingQueue(instance) + XCTAssertEqual(instance.eventsQueue.count, 0, "Empty events should be rejected") + + // Nil values + let props: Properties = ["key": NSNull()] + instance.track(event: "Test", properties: props) + waitForTrackingQueue(instance) + XCTAssertNotNil(instance.eventsQueue.first?["properties"]?["key"]) + + // Very long strings + let longString = String(repeating: "a", count: 10000) + instance.track(event: longString) + waitForTrackingQueue(instance) + // Verify truncation or handling +} +``` + +### 3. Thread Safety Test +```swift +func testConcurrentAccess() { + let iterations = 100 + let expectation = XCTestExpectation(description: "Concurrent operations") + expectation.expectedFulfillmentCount = iterations + + DispatchQueue.concurrentPerform(iterations: iterations) { index in + instance.track(event: "Event \(index)") + expectation.fulfill() + } + + wait(for: [expectation], timeout: 5.0) + waitForTrackingQueue(instance) + + XCTAssertEqual(instance.eventsQueue.count, iterations) +} +``` + +### 4. Persistence Test +```swift +func testPersistence() { + // Track events + instance.track(event: "Test Event", properties: ["key": "value"]) + waitForTrackingQueue(instance) + + // Verify persisted + let saved = instance.persistence.loadEntitiesInBatch(type: .events) + XCTAssertEqual(saved.count, 1) + + // Create new instance (simulating app restart) + let newInstance = MixpanelInstance(apiToken: testToken) + waitForTrackingQueue(newInstance) + + // Verify events restored + XCTAssertEqual(newInstance.eventsQueue.count, 1) +} +``` + +### 5. Network/Flush Test +```swift +func testFlush() { + // Track multiple events + for i in 0..<5 { + instance.track(event: "Event \(i)") + } + + let expectation = XCTestExpectation(description: "Flush complete") + + instance.flush { + expectation.fulfill() + } + + wait(for: [expectation], timeout: 10.0) + + // Verify queue cleared + XCTAssertEqual(instance.eventsQueue.count, 0) +} +``` + +### 6. Platform-Specific Test +```swift +#if os(iOS) +func testIOSSpecificFeature() { + // Test automatic events + XCTAssertTrue(instance.trackAutomaticEvents) + // iOS-specific assertions +} +#else +func testNonIOSBehavior() { + // Test that automatic events are disabled + XCTAssertFalse(instance.trackAutomaticEvents) +} +#endif +``` + +## Test Utilities + +### Wait for Async Operations +```swift +waitForTrackingQueue(instance) +flushAndWaitForTrackingQueue(instance) +``` + +### Generate Test Data +```swift +let testId = randomId() +let testProps: Properties = [ + "test_id": testId, + "timestamp": Date(), + "count": 42, + "active": true +] +``` + +### Custom Assertions +```swift +func assertEventTracked(name: String, properties: Properties? = nil) { + let event = instance.eventsQueue.first { $0["event"] as? String == name } + XCTAssertNotNil(event, "Event '\(name)' not found") + + if let properties = properties { + for (key, value) in properties { + XCTAssertEqual( + event?["properties"]?[key] as? MixpanelType, + value, + "Property '\(key)' mismatch" + ) + } + } +} +``` + +## Performance Test Template +```swift +func testPerformance() { + measure { + for i in 0..<1000 { + instance.track(event: "Performance Test \(i)") + } + flushAndWaitForTrackingQueue(instance) + } +} +``` + +## Test Checklist +- [ ] Happy path scenarios +- [ ] Edge cases (nil, empty, very large) +- [ ] Error conditions +- [ ] Thread safety +- [ ] Memory leaks (use Instruments) +- [ ] Platform-specific behavior +- [ ] Performance benchmarks \ No newline at end of file diff --git a/.claude/quick-reference.md b/.claude/quick-reference.md new file mode 100644 index 00000000..20ead7d8 --- /dev/null +++ b/.claude/quick-reference.md @@ -0,0 +1,81 @@ +# Claude Code Quick Reference for Mixpanel Swift SDK + +## Available Slash Commands + +| Command | Purpose | Example | +|---------|---------|---------| +| `/add-property` | Add automatic event property | `/add-property $app_build_number` | +| `/new-api` | Create public API method | `/new-api trackBatch(events: [Event])` | +| `/fix-thread-safety` | Fix concurrency issues | `/fix-thread-safety` | +| `/write-test` | Generate unit tests | `/write-test People.set` | +| `/debug-issue` | Debug SDK problems | `/debug-issue Events not tracking` | +| `/migrate-db` | Database schema changes | `/migrate-db Add retry_count column` | + +## Knowledge Base Structure + +``` +@claude/architecture/threading-model.md # Concurrency deep dive +@claude/architecture/persistence-layer.md # Database architecture +@claude/patterns/type-safety-patterns.md # Type system patterns +@claude/technologies/swift-features.md # Swift language usage +@claude/workflows/release-process.md # Release procedures +``` + +## Most Common Operations + +### Track an Event +```swift +Mixpanel.mainInstance().track(event: "Purchase", properties: [ + "amount": 29.99, + "currency": "USD" +]) +``` + +### Identify User +```swift +Mixpanel.mainInstance().identify(distinctId: "user_123") +Mixpanel.mainInstance().people.set(properties: [ + "name": "John Doe", + "email": "john@example.com" +]) +``` + +### Thread-Safe Property +```swift +private var _value: String = "" +var value: String { + get { readWriteLock.read { _value } } + set { readWriteLock.write { _value = newValue } } +} +``` + +### Test Pattern +```swift +func testFeature() { + instance.track(event: "Test") + waitForTrackingQueue(instance) + XCTAssertEqual(instance.eventsQueue.count, 1) +} +``` + +## Critical Rules (Always Remember) +1. **Always use ReadWriteLock** for shared state +2. **All properties must be MixpanelType** +3. **Never break API compatibility** +4. **Test on all platforms** +5. **Use `[weak self]` in async closures** + +## Platform Quick Check +- iOS 11.0+ ✓ Automatic Events +- macOS 10.13+ ✗ No Automatic Events +- tvOS 11.0+ ✗ Limited Background +- watchOS 4.0+ ✗ Limited Storage + +## Debug Commands +```swift +// Enable logging +Mixpanel.mainInstance().logger = MixpanelLogger(level: .debug) + +// Check queues +print("Events: \(instance.eventsQueue.count)") +``` \ No newline at end of file diff --git a/.cursor/rules/api-design.mdc b/.cursor/rules/api-design.mdc new file mode 100644 index 00000000..214a1ecc --- /dev/null +++ b/.cursor/rules/api-design.mdc @@ -0,0 +1,94 @@ +--- +description: Public API design guidelines and patterns +globs: ["**/Mixpanel.swift", "**/MixpanelInstance.swift", "**/People.swift", "**/Group.swift"] +alwaysApply: false +--- + +# Public API Design Guidelines + +## Method Design Principles +```swift +// Return self or @discardableResult for chaining +@discardableResult +public func track(event: String, properties: Properties? = nil) -> Self { + // Implementation + return self +} + +// Provide sensible defaults +public func identify(distinctId: String, + usePeople: Bool = true, + completion: (() -> Void)? = nil) + +// Clear parameter names and types +public func setServerURL(serverURL: String) // Good +public func setURL(_ u: String) // Bad +``` + +## Backward Compatibility +- NEVER remove or rename public methods +- Use @available for deprecation: +```swift +@available(*, deprecated, message: "Use track(event:properties:) instead") +public func trackEvent(_ event: String) { + track(event: event) +} +``` + +## Documentation Pattern +```swift +/// Tracks an event with optional properties. +/// +/// - Parameters: +/// - event: The name of the event to track +/// - properties: Optional properties dictionary conforming to MixpanelType +/// - Returns: The current instance for method chaining +/// +/// - Note: Event names are case-sensitive and should follow your naming convention +/// +/// Example: +/// ```swift +/// Mixpanel.mainInstance().track(event: "Sign Up", properties: [ +/// "source": "invite", +/// "referring_user_id": 12345 +/// ]) +/// ``` +``` + +## Error Handling in Public API +- Never throw from public methods +- Log errors internally +- Provide completion handlers for async operations: +```swift +public func flush(completion: (() -> Void)? = nil) { + flushQueue.async { [weak self, completion] in + self?.flushEventsQueue() + self?.flushPeopleQueue() + DispatchQueue.main.async { + completion?() + } + } +} +``` + +## Objective-C Compatibility +```swift +// Mark compatible methods +@objc public func track(event: String) + +// Provide type-safe wrappers +@objc public func trackWithProperties(event: String, properties: [String: Any]) { + let validProps = properties.compactMapValues { $0 as? MixpanelType } + track(event: event, properties: validProps) +} +``` + +## Fluent Interface Pattern +Enable method chaining for better developer experience: +```swift +Mixpanel.mainInstance() + .identify(distinctId: "user123") + .people.set(properties: ["name": "John"]) + .track(event: "Profile Updated") + .flush() +``` \ No newline at end of file diff --git a/.cursor/rules/architecture.mdc b/.cursor/rules/architecture.mdc new file mode 100644 index 00000000..6e628597 --- /dev/null +++ b/.cursor/rules/architecture.mdc @@ -0,0 +1,54 @@ +--- +description: Core architecture and flow patterns for Mixpanel Swift SDK +alwaysApply: true +--- + +# Architecture Overview + +## Core Components Flow +``` +Mixpanel (static entry) → MixpanelManager (singleton) → MixpanelInstance → Core Components + ↓ + MixpanelPersistence (SQLite) + ↓ + Flush → Network +``` + +## Component Responsibilities +- **Mixpanel**: Static entry point, manages default instance +- **MixpanelManager**: Singleton managing multiple instances +- **MixpanelInstance**: Core SDK functionality per project token +- **Track**: Event tracking operations +- **People**: User profile operations +- **Groups**: Group analytics operations +- **Flush**: Batch processing and sending +- **Network**: API communication +- **MixpanelPersistence**: SQLite storage layer +- **FeatureFlags**: Feature flag management + +## Key Multi-File Patterns + +### Event Tracking Flow +1. User calls `track()` in `Mixpanel.swift` +2. Routes through `MixpanelInstance.swift` to `Track.swift` +3. Persisted via `MixpanelPersistence.swift` (SQLite operations) +4. Batched and sent by `Flush.swift` through `Network.swift` + +### Threading Model +- **ReadWriteLock**: Custom read-write lock using concurrent dispatch queue +- **trackingQueue**: Serial queue for event operations (QoS: .utility) +- **networkQueue**: Serial queue for network operations (QoS: .utility) +- All shared state protected by ReadWriteLock + +## Swift Conventions +- Use trailing closures for completion handlers +- Use guard statements for early returns +- Prefix internal properties with underscore (_) +- Use property observers (didSet) for reactive updates +- Mark completion handlers as @escaping when stored + +## Error Handling +- Log errors via MixpanelLogger, don't throw +- Use MPAssert for debug assertions +- Fail gracefully with default values +- Return discardable results for fluent API \ No newline at end of file diff --git a/.cursor/rules/commands.mdc b/.cursor/rules/commands.mdc new file mode 100644 index 00000000..d71dd8a3 --- /dev/null +++ b/.cursor/rules/commands.mdc @@ -0,0 +1,136 @@ +--- +description: Essential build, test, and development commands +alwaysApply: false +--- + +# Build & Development Commands + +## Building the SDK + +### All Platforms +```bash +# iOS +xcodebuild -scheme Mixpanel -destination 'generic/platform=iOS' + +# macOS +xcodebuild -scheme Mixpanel_macOS + +# tvOS +xcodebuild -scheme Mixpanel_tvOS -destination 'generic/platform=tvOS' + +# watchOS +xcodebuild -scheme Mixpanel_watchOS -destination 'generic/platform=watchOS' + +# Carthage build (all platforms) +./scripts/carthage.sh +``` + +### Swift Package Manager +```bash +swift build +swift test +``` + +## Running Tests + +### Platform-Specific Tests +```bash +# iOS Simulator +xcodebuild test -scheme MixpanelDemo -destination 'platform=iOS Simulator,name=iPhone 15' + +# macOS +xcodebuild test -scheme MixpanelDemoMac + +# tvOS Simulator +xcodebuild test -scheme MixpanelDemoTV -destination 'platform=tvOS Simulator,name=Apple TV' +``` + +### Specific Test Execution +```bash +# Run specific test class +xcodebuild test -scheme MixpanelDemo \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + -only-testing:MixpanelDemoTests/MixpanelPeopleTests + +# Run specific test method +xcodebuild test -scheme MixpanelDemo \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + -only-testing:MixpanelDemoTests/MixpanelPeopleTests/testPeopleSet + +# With code coverage +xcodebuild test -scheme MixpanelDemo \ + -destination 'platform=iOS Simulator,name=iPhone 15' \ + -enableCodeCoverage YES +``` + +## Code Quality + +### Linting +```bash +# SwiftLint (must be installed: brew install swiftlint) +swiftlint +swiftlint autocorrect + +# Swift-format (automated via pre-commit hook) +swift-format --recursive Sources/ --in-place +swift-format --recursive MixpanelDemo/ --in-place +``` + +### Documentation +```bash +# Generate API documentation with Jazzy +./scripts/generate_docs.sh + +# Preview docs locally +open docs/index.html +``` + +## Demo Apps +```bash +# Run iOS demo +open MixpanelDemo/MixpanelDemo.xcodeproj +# Select MixpanelDemo scheme and run + +# Run macOS demo +open MixpanelDemo/MixpanelDemo.xcodeproj +# Select MixpanelDemoMac scheme and run + +# Run tvOS demo +open MixpanelDemo/MixpanelDemo.xcodeproj +# Select MixpanelDemoTV scheme and run +``` + +## Dependency Management + +### CocoaPods +```bash +# Validate podspec +pod lib lint Mixpanel-swift.podspec + +# Install in demo +cd MixpanelDemo +pod install +``` + +### Carthage +```bash +# Build framework +carthage build --platform iOS,macOS,tvOS,watchOS + +# Update dependencies +carthage update --platform iOS +``` + +## Debugging +```bash +# Enable verbose logging +# Set in code: Mixpanel.mainInstance().loggingEnabled = true + +# View SQLite database (macOS) +sqlite3 ~/Library/Developer/CoreSimulator/Devices/[DEVICE_ID]/data/Containers/Data/Application/[APP_ID]/Library/mixpanel-[TOKEN].sqlite + +# Common queries +.tables +SELECT COUNT(*) FROM events; +SELECT * FROM events LIMIT 10; +``` \ No newline at end of file diff --git a/.cursor/rules/common-workflows.mdc b/.cursor/rules/common-workflows.mdc new file mode 100644 index 00000000..ad0adf6d --- /dev/null +++ b/.cursor/rules/common-workflows.mdc @@ -0,0 +1,232 @@ +--- +description: Common implementation patterns and workflows for SDK development +alwaysApply: false +--- + +# Common SDK Development Workflows + +## Adding a New Event Property + +### 1. Update Automatic Properties +```swift +// In AutomaticProperties.swift +func collectAutomaticProperties() -> InternalProperties { + var properties: InternalProperties = [ + // Existing properties... + "$new_property": getNewPropertyValue() // Add new property + ] + return properties +} + +private func getNewPropertyValue() -> MixpanelType { + // Implementation + return value +} +``` + +### 2. Add to Constants if Reserved +```swift +// In Constants.swift +struct ReservedProperties { + static let newProperty = "$new_property" +} +``` + +### 3. Add Validation +```swift +// In Track.swift +func validateEventProperties(_ properties: Properties) { + assertPropertyTypes(properties) + // Add specific validation if needed +} +``` + +### 4. Write Tests +```swift +func testNewProperty() { + let instance = MixpanelInstance(apiToken: testToken) + instance.track(event: "Test") + waitForTrackingQueue(instance) + + let event = instance.eventsQueue.first + XCTAssertNotNil(event?["$new_property"]) +} +``` + +## Implementing a New API Endpoint + +### 1. Add Endpoint Constant +```swift +// In Network.swift or Constants +struct APIConstants { + static let newEndpoint = "\(baseURL)/new_endpoint" +} +``` + +### 2. Create Request Builder +```swift +func buildNewEndpointRequest(data: [String: Any]) -> URLRequest? { + guard let url = URL(string: APIConstants.newEndpoint) else { return nil } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: data) + return request + } catch { + Logger.error(message: "Failed to serialize request: \(error)") + return nil + } +} +``` + +### 3. Add to Network Layer +```swift +func callNewEndpoint(data: [String: Any], completion: @escaping (Bool) -> Void) { + networkQueue.async { [weak self] in + guard let request = self?.buildNewEndpointRequest(data: data) else { + completion(false) + return + } + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + let success = self?.handleResponse(data, response, error) ?? false + completion(success) + } + task.resume() + } +} +``` + +## Fixing Thread Safety Issues + +### 1. Identify Shared State +```swift +// Before (unsafe) +class Component { + var sharedData: [String: Any] = [:] + + func updateData(key: String, value: Any) { + sharedData[key] = value // Race condition! + } +} +``` + +### 2. Add ReadWriteLock +```swift +// After (thread-safe) +class Component { + private let readWriteLock = ReadWriteLock(label: "com.mixpanel.component") + private var _sharedData: [String: Any] = [:] + + var sharedData: [String: Any] { + return readWriteLock.read { _sharedData } + } + + func updateData(key: String, value: Any) { + readWriteLock.write { + _sharedData[key] = value + } + } +} +``` + +### 3. Test Concurrent Access +```swift +func testThreadSafety() { + let expectation = XCTestExpectation(description: "Concurrent access") + expectation.expectedFulfillmentCount = 100 + + let component = Component() + + DispatchQueue.concurrentPerform(iterations: 100) { index in + if index % 2 == 0 { + component.updateData(key: "key\(index)", value: index) + } else { + _ = component.sharedData + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 5.0) +} +``` + +## Database Migration + +### 1. Check Current Version +```swift +func getDatabaseVersion() -> Int { + // Read from metadata table or pragma + return currentVersion +} +``` + +### 2. Implement Migration +```swift +func migrateDatabase(from oldVersion: Int, to newVersion: Int) { + if oldVersion < 2 { + migrateToV2() + } + if oldVersion < 3 { + migrateToV3() + } + updateDatabaseVersion(newVersion) +} + +private func migrateToV2() { + // Add new column + executeSQL("ALTER TABLE events ADD COLUMN retry_count INTEGER DEFAULT 0") +} +``` + +### 3. Handle Migration Errors +```swift +do { + try migrateDatabase(from: 1, to: 3) +} catch { + Logger.error(message: "Migration failed: \(error)") + // Fallback: recreate database + recreateDatabase() +} +``` + +## Performance Optimization + +### 1. Profile Current Performance +```swift +func measureEventTracking() { + measure { + for i in 0..<1000 { + instance.track(event: "Event \(i)") + } + flushAndWaitForTrackingQueue(instance) + } +} +``` + +### 2. Identify Bottlenecks +- Use Instruments for profiling +- Check Time Profiler for hot paths +- Monitor Allocations for memory usage + +### 3. Optimize Critical Paths +```swift +// Before: Creating many temporary objects +func processEvents(_ events: [Event]) { + for event in events { + let processed = processEvent(event) + let encoded = encode(processed) + save(encoded) + } +} + +// After: Batch processing +func processEvents(_ events: [Event]) { + let processed = events.map { processEvent($0) } + let encoded = batchEncode(processed) + batchSave(encoded) +} +``` \ No newline at end of file diff --git a/.cursor/rules/networking.mdc b/.cursor/rules/networking.mdc new file mode 100644 index 00000000..8dda7e4b --- /dev/null +++ b/.cursor/rules/networking.mdc @@ -0,0 +1,146 @@ +--- +description: Network layer patterns and API communication +globs: ["**/Network.swift", "**/Flush*.swift", "**/FlushRequest.swift"] +alwaysApply: false +--- + +# Networking Guidelines + +## API Endpoints +```swift +struct APIConstants { + static let trackEndpoint = "https://api.mixpanel.com/track" + static let engageEndpoint = "https://api.mixpanel.com/engage" + static let groupsEndpoint = "https://api.mixpanel.com/groups" + static let decideEndpoint = "https://api.mixpanel.com/decide" +} +``` + +## Request Construction +```swift +// Build form-encoded request +var request = URLRequest(url: endpoint) +request.httpMethod = "POST" +request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + +// Encode data +let jsonData = try JSONSerialization.data(withJSONObject: events) +let base64String = jsonData.base64EncodedString() + +// Add gzip if enabled +if useGzip { + let compressedData = try jsonData.gzipped() + let compressedBase64 = compressedData.base64EncodedString() + request.setValue("gzip", forHTTPHeaderField: "Content-Encoding") + request.httpBody = "data=\(compressedBase64)".data(using: .utf8) +} else { + request.httpBody = "data=\(base64String)".data(using: .utf8) +} +``` + +## Batch Processing +```swift +// Maximum 50 events per batch +let batchSize = min(events.count, 50) +let batch = Array(events.prefix(batchSize)) + +// Calculate payload size +let payloadSize = batch.reduce(0) { sum, event in + sum + (try? JSONSerialization.data(withJSONObject: event))?.count ?? 0 +} + +// Split if too large (>1MB recommended) +if payloadSize > 1_000_000 { + let midPoint = batch.count / 2 + sendBatch(Array(batch.prefix(midPoint))) + sendBatch(Array(batch.suffix(from: midPoint))) +} +``` + +## Network Queue Usage +```swift +networkQueue.async { [weak self] in + guard let self = self else { return } + + let request = self.buildRequest(for: batch) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + self.handleNetworkError(error, batch: batch) + } else if let httpResponse = response as? HTTPURLResponse { + self.handleResponse(httpResponse, data: data, batch: batch) + } + } + + task.resume() +} +``` + +## Error Handling & Retry Logic +```swift +func handleResponse(_ response: HTTPURLResponse, data: Data?, batch: [Event]) { + switch response.statusCode { + case 200: + // Success - remove from queue + removeFromQueue(batch) + + case 400: + // Client error - don't retry + Logger.error(message: "Invalid request: \(parseError(data))") + removeFromQueue(batch) + + case 500, 502, 503, 504: + // Server error - retry with backoff + retryWithBackoff(batch) + + default: + Logger.warning(message: "Unexpected status: \(response.statusCode)") + } +} + +func retryWithBackoff(_ batch: [Event]) { + let retryCount = getRetryCount(for: batch) + let delay = min(pow(2.0, Double(retryCount)), 60.0) // Max 60s + + DispatchQueue.global().asyncAfter(deadline: .now() + delay) { [weak self] in + self?.sendBatch(batch) + } +} +``` + +## Flush Behavior +```swift +// Automatic flush triggers +- Timer-based (default 60 seconds) +- App entering background (if flushOnBackground = true) +- Reaching batch size limit +- Manual flush() call + +// Flush implementation +func flush(completion: (() -> Void)? = nil) { + flushQueue.async { [weak self] in + self?.flushEventsQueue() + self?.flushPeopleQueue() + self?.flushGroupsQueue() + + DispatchQueue.main.async { + completion?() + } + } +} +``` + +## Response Parsing +```swift +// Parse JSON response +if let data = data, + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let status = json["status"] as? Int { + + if status == 1 { + // Success + } else if let error = json["error"] as? String { + Logger.error(message: "API error: \(error)") + } +} +``` \ No newline at end of file diff --git a/.cursor/rules/persistence.mdc b/.cursor/rules/persistence.mdc new file mode 100644 index 00000000..56787d77 --- /dev/null +++ b/.cursor/rules/persistence.mdc @@ -0,0 +1,110 @@ +--- +description: Database persistence and storage patterns +globs: ["**/MixpanelPersistence.swift", "**/MPDB.swift"] +alwaysApply: false +--- + +# Persistence Layer Guidelines + +## SQLite Operations +```swift +// Always use parameterized queries +let insertSQL = """ + INSERT INTO \(tableName) (TOKEN, DATA, UUID, METADATA) + VALUES (?, ?, ?, ?) +""" +sqlite3_bind_text(statement, 1, token, -1, SQLITE_TRANSIENT) + +// Handle errors gracefully +if sqlite3_prepare_v2(db, sql, -1, &statement, nil) != SQLITE_OK { + Logger.error(message: "Failed to prepare statement: \(errorMessage)") + return [] +} + +// Always finalize statements +defer { sqlite3_finalize(statement) } +``` + +## Entity Storage Patterns +```swift +// Saving entities +func saveEntity(_ entity: InternalProperties, + type: EntityType, + flag: Int32 = 0) { + let data = JSONHandler.encodeAPIData(entity.apiProperties()) + let metadata = MixpanelPersistenceMetadata( + flag: flag, + retryCount: 0 + ) + persistence.saveEntity(entity, type: type, data: data, metadata: metadata) +} + +// Loading entities +func loadEntitiesInBatch(type: EntityType, limit: Int = 50) -> [InternalProperties] { + let entities = persistence.loadEntitiesInBatch(type: type, limit: limit) + return entities.compactMap { data in + JSONHandler.decodeAPIData(data) as? InternalProperties + } +} +``` + +## UserDefaults Usage +Store lightweight metadata only: +```swift +// Identifiers +static let distinctId = "mixpanel-\(token)-distinctId" +static let anonymousId = "mixpanel-\(token)-anonymousId" +static let userId = "mixpanel-\(token)-userId" +static let alias = "mixpanel-\(token)-alias" + +// Collections +static let superProperties = "mixpanel-\(token)-superProperties" +static let timedEvents = "mixpanel-\(token)-timedEvents" +static let optOutStatus = "mixpanel-\(token)-optOutStatus" +``` + +## Batch Operations +```swift +// Process in batches of 50 +let batchSize = 50 +var processedCount = 0 + +while true { + let batch = loadEntitiesInBatch(type: .events, limit: batchSize) + if batch.isEmpty { break } + + processBatch(batch) + processedCount += batch.count +} +``` + +## Migration Support +```swift +// Check and migrate if needed +if !fileExists(at: databasePath) { + migrateFromArchiveIfNeeded() +} + +// Version-based migrations +let currentVersion = getDatabaseVersion() +if currentVersion < targetVersion { + performMigration(from: currentVersion, to: targetVersion) +} +``` + +## Performance Optimizations +- Use transactions for bulk operations +- Index on (TOKEN, UUID) columns +- Limit batch sizes to prevent memory issues +- Regular cleanup of old data: +```swift +let cutoffDate = Date().addingTimeInterval(-30 * 24 * 60 * 60) // 30 days +deleteEntitiesOlderThan(cutoffDate) +``` + +## Error Recovery +- Log but don't crash on SQLite errors +- Return empty results on read failures +- Skip corrupted entities +- Implement retry for transient errors +- Backup critical data before migrations \ No newline at end of file diff --git a/.cursor/rules/platform-support.mdc b/.cursor/rules/platform-support.mdc new file mode 100644 index 00000000..9d5b2d6a --- /dev/null +++ b/.cursor/rules/platform-support.mdc @@ -0,0 +1,166 @@ +--- +description: Platform-specific implementation patterns and considerations +alwaysApply: false +--- + +# Platform Support Guidelines + +## Supported Platforms +- iOS 11.0+ +- tvOS 11.0+ +- macOS 10.13+ +- watchOS 4.0+ + +## Platform Detection +```swift +#if os(iOS) + // iOS-specific code +#elseif os(tvOS) + // tvOS-specific code +#elseif os(OSX) + // macOS-specific code +#elseif os(watchOS) + // watchOS-specific code +#endif + +// Combined checks +#if !os(OSX) + // Mobile platforms (iOS, tvOS, watchOS) +#endif + +// App extension check +if Bundle.main.bundlePath.hasSuffix(".appex") { + // Running in app extension +} +``` + +## Automatic Events Support +```swift +// Only available on iOS +#if os(iOS) +if trackAutomaticEvents && !isiOSAppExtension() { + automaticEvents.initializeAutomaticEvents() +} +#endif + +// Automatic events include: +- $ae_first_open +- $ae_updated +- $ae_session +- $ae_session_length +``` + +## UI Framework Differences +```swift +#if os(iOS) || os(tvOS) +import UIKit +typealias MPColor = UIColor +typealias MPViewController = UIViewController +#elseif os(OSX) +import Cocoa +typealias MPColor = NSColor +typealias MPViewController = NSViewController +#endif +``` + +## App Lifecycle Handling +```swift +#if os(iOS) || os(tvOS) +// Background task handling +var backgroundTaskId = UIBackgroundTaskIdentifier.invalid + +func startBackgroundTask() { + backgroundTaskId = UIApplication.shared.beginBackgroundTask { + self.endBackgroundTask() + } +} +#endif + +// Notification observers +#if os(iOS) +NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil +) +#elseif os(OSX) +NotificationCenter.default.addObserver( + self, + selector: #selector(applicationWillTerminate), + name: NSApplication.willTerminateNotification, + object: nil +) +#endif +``` + +## File System Access +```swift +// iOS/tvOS/watchOS use sandboxed paths +#if os(iOS) || os(tvOS) || os(watchOS) +let documentsPath = NSSearchPathForDirectoriesInDomains( + .documentDirectory, + .userDomainMask, + true +).first! +#elseif os(OSX) +// macOS may have different permissions +let applicationSupport = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask +).first! +let appBundleID = Bundle.main.bundleIdentifier ?? "com.mixpanel" +let appDirectory = applicationSupport.appendingPathComponent(appBundleID) +#endif +``` + +## Network Configuration +```swift +// watchOS limitations +#if os(watchOS) +// Limited background networking +// Smaller payload sizes recommended +let maxBatchSize = 25 // vs 50 for other platforms +#endif + +// tvOS considerations +#if os(tvOS) +// No web views available +// Limited local storage +// Consider user attention patterns +#endif +``` + +## Testing Considerations +```swift +// Platform-specific test setup +#if os(iOS) +let testDestination = "platform=iOS Simulator,name=iPhone 15" +#elseif os(tvOS) +let testDestination = "platform=tvOS Simulator,name=Apple TV" +#elseif os(OSX) +let testDestination = "platform=macOS" +#endif + +// Skip tests on unsupported platforms +#if !os(iOS) +func testAutomaticEvents() { + XCTSkip("Automatic events only supported on iOS") +} +#endif +``` + +## Memory Management +```swift +#if os(watchOS) +// More aggressive memory limits +let maxQueueSize = 100 // vs 500 for other platforms +let flushInterval = 30.0 // vs 60.0 default +#endif +``` + +## Platform-Specific Features +- **iOS**: Full feature set including automatic events +- **macOS**: No automatic events by default, different UI lifecycle +- **tvOS**: Limited input methods, consider focus engine +- **watchOS**: Limited resources, shorter session times \ No newline at end of file diff --git a/.cursor/rules/project-overview.mdc b/.cursor/rules/project-overview.mdc new file mode 100644 index 00000000..3a5b61b9 --- /dev/null +++ b/.cursor/rules/project-overview.mdc @@ -0,0 +1,35 @@ +--- +description: Mixpanel Swift SDK project overview and key principles +alwaysApply: true +--- + +# Mixpanel Swift SDK + +Official Mixpanel SDK for iOS 11.0+, tvOS 11.0+, macOS 10.13+, watchOS 4.0+. + +## Core Principles +- **Thread Safety**: All shared state protected by ReadWriteLock +- **Type Safety**: MixpanelType protocol enforces valid property types +- **Backward Compatibility**: Never break existing public APIs +- **Performance**: Batch operations, async processing, SQLite persistence +- **Error Resilience**: Log errors, fail gracefully, provide defaults + +## Key Patterns +- Static `Mixpanel` class → `MixpanelManager` singleton → `MixpanelInstance` +- Serial queues for operations: trackingQueue, networkQueue (QoS: .utility) +- Properties must conform to MixpanelType (String, Int, Double, Bool, etc.) +- SQLite for persistence, UserDefaults for metadata +- Automatic batching and retry with exponential backoff + +## Quick Commands +- Test: `xcodebuild test -scheme MixpanelDemo -destination 'platform=iOS Simulator,name=iPhone 15'` +- Lint: `swiftlint` +- Docs: `./scripts/generate_docs.sh` +- Release: `python scripts/release.py --old X.Y.Z --new X.Y.Z` + +## When Making Changes +1. Check thread safety with ReadWriteLock +2. Validate property types with assertPropertyTypes() +3. Test on all platforms +4. Update CHANGELOG.md for user-facing changes +5. Maintain backward compatibility \ No newline at end of file diff --git a/.cursor/rules/property-types.mdc b/.cursor/rules/property-types.mdc new file mode 100644 index 00000000..d5349a52 --- /dev/null +++ b/.cursor/rules/property-types.mdc @@ -0,0 +1,72 @@ +--- +description: MixpanelType protocol and property validation rules +globs: ["**/Track.swift", "**/People.swift", "**/Group.swift", "**/*Properties*.swift"] +alwaysApply: false +--- + +# Property Type System + +## MixpanelType Protocol Conformance +Valid types that conform to MixpanelType: +- `String`, `Int`, `UInt`, `Double`, `Float`, `Bool` +- `[MixpanelType]` (arrays of valid types) +- `[String: MixpanelType]` (dictionaries with String keys) +- `Date`, `URL`, `NSNull` +- `Optional` (optionals of valid types) + +## Type Validation +```swift +// Always validate before use +assertPropertyTypes(properties) + +// Check individual values +if let validValue = value as? MixpanelType { + // Safe to use +} + +// Filter dictionary values +let validProperties = properties.filter { isValidNestedTypeAndValue($0.value) } +``` + +## Type Conversion Patterns +```swift +// Convert Any to MixpanelType +func convertToMixpanelType(_ value: Any) -> MixpanelType? { + if let mixpanelValue = value as? MixpanelType { + return mixpanelValue + } + // Handle special cases (e.g., NSNumber) + return nil +} + +// Properties typealias +typealias Properties = [String: MixpanelType] +``` + +## Reserved Properties +Never use these property names: +- Mixpanel internal: `mp_lib`, `$lib_version`, `$os`, `$os_version` +- Device info: `$manufacturer`, `$brand`, `$model` +- Core properties: `time`, `distinct_id`, `$device_id` + +## Property Naming Conventions +- Use snake_case for property names (Mixpanel standard) +- Prefix internal properties with `$` or `mp_` +- Keep names under 255 characters +- Use descriptive, meaningful names + +## Validation Example +```swift +func track(event: String, properties: Properties? = nil) { + var props = properties ?? [:] + + // Validate all properties + assertPropertyTypes(props) + + // Add automatic properties + props.merge(automaticProperties) { (_, new) in new } + + // Process event + processTrackEvent(event, properties: props) +} +``` \ No newline at end of file diff --git a/.cursor/rules/release-process.mdc b/.cursor/rules/release-process.mdc new file mode 100644 index 00000000..2f3cf59d --- /dev/null +++ b/.cursor/rules/release-process.mdc @@ -0,0 +1,142 @@ +--- +description: Release process and version management +alwaysApply: false +--- + +# Release Process + +## Version Update Locations +When updating the SDK version, modify these files: +1. `Mixpanel-swift.podspec` - Update `s.version` +2. `Info.plist` - Update `CFBundleShortVersionString` +3. `Sources/AutomaticProperties.swift` - Update `$lib_version` constant + +## Automated Release +```bash +# Full release process (recommended) +python scripts/release.py --old 5.0.0 --new 5.1.0 + +# This script will: +# 1. Update version in all required files +# 2. Generate updated documentation +# 3. Commit changes +# 4. Create git tag +# 5. Push to CocoaPods trunk +``` + +## Manual Release Steps + +### 1. Update Version Numbers +```swift +// In AutomaticProperties.swift +return [ + "$lib": "swift", + "$lib_version": "5.1.0", // Update this + "$os": currentOS(), + // ... +] +``` + +### 2. Update Changelog +```markdown +## [5.1.0] - 2024-01-15 +### Added +- New feature description + +### Changed +- Updated behavior description + +### Fixed +- Bug fix description +``` + +### 3. Generate Documentation +```bash +./scripts/generate_docs.sh +git add docs/ +git commit -m "Update documentation for v5.1.0" +``` + +### 4. Create Release Tag +```bash +git tag -a v5.1.0 -m "Version 5.1.0" +git push origin v5.1.0 +``` + +### 5. Publish to Package Managers + +#### CocoaPods +```bash +# Validate first +pod lib lint Mixpanel-swift.podspec + +# Push to trunk +pod trunk push Mixpanel-swift.podspec +``` + +#### Carthage +```bash +# Build and verify +carthage build --platform iOS,macOS,tvOS,watchOS --no-skip-current +carthage archive Mixpanel +``` + +#### Swift Package Manager +- No action needed, uses git tags + +## Pre-Release Checklist +- [ ] All tests pass on all platforms +- [ ] Documentation is updated +- [ ] CHANGELOG.md is updated +- [ ] Version numbers are consistent +- [ ] No compiler warnings +- [ ] Demo apps work correctly +- [ ] API compatibility maintained + +## Validation Commands +```bash +# Run all platform tests +for scheme in MixpanelDemo MixpanelDemoMac MixpanelDemoTV; do + xcodebuild test -scheme $scheme +done + +# Lint the code +swiftlint + +# Validate podspec +pod lib lint + +# Check for API breaks +swift api-digester -diagnose-sdk \ + -module Mixpanel \ + -o old-api.json \ + -new-swift-sdk new-api.json +``` + +## Semantic Versioning +Follow semver.org: +- **Major** (X.0.0): Breaking API changes +- **Minor** (0.X.0): New features, backward compatible +- **Patch** (0.0.X): Bug fixes only + +## Post-Release +1. Update GitHub releases page +2. Announce in changelog/blog +3. Update documentation site +4. Monitor for issues + +## Hotfix Process +```bash +# Create hotfix branch from tag +git checkout -b hotfix/5.1.1 v5.1.0 + +# Make fixes and test thoroughly +# Update version to 5.1.1 + +# Merge to main and tag +git checkout main +git merge hotfix/5.1.1 +git tag -a v5.1.1 -m "Hotfix 5.1.1" + +# Release as normal +``` \ No newline at end of file diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc new file mode 100644 index 00000000..fb2b3f28 --- /dev/null +++ b/.cursor/rules/testing.mdc @@ -0,0 +1,113 @@ +--- +description: Testing patterns and best practices +globs: ["**/*Tests.swift", "**/*Test.swift"] +alwaysApply: false +--- + +# Testing Guidelines + +## Test Class Structure +```swift +class MyFeatureTests: MixpanelBaseTests { + var instance: MixpanelInstance! + + override func setUp() { + super.setUp() + instance = MixpanelInstance( + apiToken: testToken, + flushInterval: 0, + trackAutomaticEvents: false + ) + } + + override func tearDown() { + instance.reset() + super.tearDown() + } +} +``` + +## Queue Synchronization +Always wait for async operations: +```swift +// Wait for tracking queue +waitForTrackingQueue(instance) + +// Flush and wait +flushAndWaitForTrackingQueue(instance) + +// With expectations +let expectation = XCTestExpectation(description: "flush complete") +instance.flush { + expectation.fulfill() +} +wait(for: [expectation], timeout: 10.0) +``` + +## Test Data Patterns +```swift +// Generate unique test data +let distinctId = randomId() +let eventName = "Test Event \(randomId())" +let properties: Properties = [ + "test_id": randomId(), + "timestamp": Date(), + "is_test": true +] + +// Use constants from TestConstants.swift +let token = testToken +``` + +## Common Test Assertions +```swift +// Event tracking +instance.track(event: "Test Event", properties: props) +waitForTrackingQueue(instance) +XCTAssertEqual(instance.eventsQueue.count, 1) +XCTAssertEqual(instance.eventsQueue.first?["event"] as? String, "Test Event") + +// People operations +instance.people.set(properties: ["name": "Test User"]) +waitForTrackingQueue(instance) +XCTAssertEqual(instance.peopleQueue.count, 1) + +// Persistence +let savedEvents = instance.persistence.loadEntitiesInBatch(type: .events) +XCTAssertEqual(savedEvents.count, 1) +``` + +## Platform-Specific Testing +```swift +#if os(iOS) +func testAutomaticEvents() { + // iOS-specific automatic events +} +#endif + +#if !os(OSX) +func testMobileSpecificFeature() { + // Mobile platform tests +} +#endif +``` + +## Performance Testing +```swift +func testLargeEventVolume() { + measure { + for i in 0..<1000 { + instance.track(event: "Event \(i)") + } + flushAndWaitForTrackingQueue(instance) + } +} +``` + +## Edge Case Testing +- Empty strings and nil values +- Very long property names (>255 chars) +- Concurrent access from multiple threads +- Network failures and retries +- App lifecycle transitions +- Memory pressure scenarios \ No newline at end of file diff --git a/.cursor/rules/thread-safety.mdc b/.cursor/rules/thread-safety.mdc new file mode 100644 index 00000000..45376a31 --- /dev/null +++ b/.cursor/rules/thread-safety.mdc @@ -0,0 +1,65 @@ +--- +description: Thread safety patterns and queue management +globs: ["**/*.swift"] +alwaysApply: false +--- + +# Thread Safety Requirements + +## Always use ReadWriteLock for shared state +```swift +// Reading a property +var value: String { + return readWriteLock.read { _value } +} + +// Writing a property +func setValue(_ newValue: String) { + readWriteLock.write { _value = newValue } +} + +// Updating with current value +readWriteLock.write { + _value = transform(_value) +} +``` + +## Queue Usage Patterns +```swift +// Event tracking operations +trackingQueue.async { [weak self] in + self?.performTrackingOperation() +} + +// Network operations +networkQueue.async { [weak self] in + self?.sendNetworkRequest() +} +``` + +## Queue Properties +- Both queues use QoS: .utility +- Both use autoreleaseFrequency: .workItem +- Serial queues to maintain operation order +- Use weak self in closures to prevent retain cycles + +## Deadlock Prevention +- NEVER call readWriteLock.write from within readWriteLock.read +- Order lock acquisition consistently +- Avoid nested locks when possible +- Use async dispatch for long operations + +## Common Thread-Safe Property Pattern +```swift +private var _property: Type +var property: Type { + get { readWriteLock.read { _property } } + set { readWriteLock.write { _property = newValue } } +} +``` + +## Testing Thread Safety +- Use `waitForTrackingQueue()` to ensure operations complete +- Test concurrent access scenarios +- Enable Thread Sanitizer in scheme +- Verify no data races or deadlocks \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..0ad6f168 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,144 @@ +# Swift Development Container for Mixpanel SDK +FROM swift:5.9-jammy + +ARG TZ +ENV TZ="$TZ" + +# Install essential development tools +RUN apt-get update && apt-get install -y \ + # Basic utilities + less \ + git \ + procps \ + sudo \ + fzf \ + zsh \ + man-db \ + unzip \ + gnupg2 \ + curl \ + iptables \ + ipset \ + wget \ + aggregate \ + jq \ + ripgrep \ + tmux \ + # Build essentials + build-essential \ + cmake \ + pkg-config \ + # Python for release scripts + python3 \ + python3-pip \ + # Ruby for CocoaPods + ruby-full \ + # Network tools for debugging + dnsutils \ + iproute2 \ + # iOS development dependencies + libssl-dev \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install GitHub CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ + && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && apt update \ + && apt install gh -y + +# Install SwiftLint +RUN git clone --depth 1 --branch 0.54.0 https://github.com/realm/SwiftLint.git /tmp/SwiftLint \ + && cd /tmp/SwiftLint \ + && swift build -c release --product swiftlint \ + && cp .build/release/swiftlint /usr/local/bin/ \ + && rm -rf /tmp/SwiftLint + +# Install swift-format +RUN git clone --depth 1 --branch 509.0.0 https://github.com/apple/swift-format.git /tmp/swift-format \ + && cd /tmp/swift-format \ + && swift build -c release --product swift-format \ + && cp .build/release/swift-format /usr/local/bin/ \ + && rm -rf /tmp/swift-format + +# Install CocoaPods +RUN gem install cocoapods + +# Create user for development +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + && echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +# Persist bash history +RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \ + && mkdir /commandhistory \ + && touch /commandhistory/.bash_history \ + && chown -R $USERNAME /commandhistory + +# Set DEVCONTAINER environment variable +ENV DEVCONTAINER=true + +# Create workspace and config directories +RUN mkdir -p /workspace /home/$USERNAME/.claude \ + && chown -R $USERNAME:$USERNAME /workspace /home/$USERNAME + +WORKDIR /workspace + +# Install delta for better git diffs +RUN ARCH=$(dpkg --print-architecture) && \ + wget "https://github.com/dandavison/delta/releases/download/0.18.2/git-delta_0.18.2_${ARCH}.deb" && \ + dpkg -i "git-delta_0.18.2_${ARCH}.deb" && \ + rm "git-delta_0.18.2_${ARCH}.deb" + +# Install Claude Squad CLI +RUN curl -fsSL https://raw.githubusercontent.com/smtg-ai/claude-squad/main/install.sh | bash && \ + mv /root/.local/bin/cs /usr/local/bin/cs && \ + chmod +x /usr/local/bin/cs + +# Copy scripts +COPY init-firewall.sh /usr/local/bin/ +COPY setup-shell.sh /home/$USERNAME/.local/bin/ + +USER root +RUN chmod +x /usr/local/bin/init-firewall.sh && \ + mkdir -p /home/$USERNAME/.local/bin && \ + chmod +x /home/$USERNAME/.local/bin/setup-shell.sh && \ + chown $USERNAME:$USERNAME /home/$USERNAME/.local/bin/setup-shell.sh + +# Switch to user +USER $USERNAME + +# Set up zsh with Starship prompt +RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended \ + && curl -sS https://starship.rs/install.sh | sh -s -- --yes + +# Configure shell +RUN echo 'eval "$(starship init zsh)"' >> ~/.zshrc \ + && echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc \ + && echo 'alias swift-test="swift test --enable-code-coverage"' >> ~/.zshrc \ + && echo 'alias xcode-test="xcodebuild test -scheme MixpanelDemo -destination \"platform=iOS Simulator,name=iPhone 15\""' >> ~/.zshrc && \ + echo 'source /usr/share/doc/fzf/examples/key-bindings.zsh' >> ~/.zshrc && \ + echo 'source /usr/share/doc/fzf/examples/completion.zsh' >> ~/.zshrc + +# Set default shell +ENV SHELL=/bin/zsh + +# Install Node.js (for Claude Code) +USER root +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g @anthropic-ai/claude-code + +USER $USERNAME + +# Install global packages +ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global +ENV PATH=$PATH:/usr/local/share/npm-global/bin + +USER $USERNAME \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..7612ae59 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,95 @@ +{ + "name": "Mixpanel Swift SDK Development", + "build": { + "dockerfile": "Dockerfile", + "args": { + "TZ": "${localEnv:TZ:America/Los_Angeles}" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--cap-add=NET_ADMIN", + "--cap-add=NET_RAW", + "--security-opt", "seccomp=unconfined" + ], + "customizations": { + "vscode": { + "extensions": [ + "swiftlang.swift-vscode", + "vknabel.vscode-swiftlint", + "vknabel.vscode-swiftformat", + "eamodio.gitlens", + "mhutchie.git-graph", + "donjayamanne.githistory", + "streetsidesoftware.code-spell-checker", + "GitHub.vscode-github-actions", + "redhat.vscode-yaml", + "ms-vscode.makefile-tools", + "hbenl.vscode-test-explorer", + "littlefoxteam.vscode-python" + ], + "settings": { + "swift.path": "/usr/bin", + "swift.buildArguments": [ + "-Xswiftc", "-target", "-Xswiftc", "aarch64-unknown-linux-gnu" + ], + "swift.testArguments": [ + "--enable-code-coverage" + ], + "swiftlint.enable": true, + "swiftlint.path": "/usr/local/bin/swiftlint", + "swiftlint.configFile": ".swiftlint.yml", + "swiftformat.enable": true, + "swiftformat.path": "/usr/local/bin/swift-format", + "editor.formatOnSave": true, + "editor.tabSize": 4, + "editor.insertSpaces": true, + "[swift]": { + "editor.defaultFormatter": "vknabel.vscode-swiftformat" + }, + "terminal.integrated.fontFamily": "MesloLGS NF, Menlo, Monaco, 'Courier New', monospace", + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "icon": "terminal-bash" + }, + "zsh": { + "path": "zsh" + } + }, + "files.associations": { + "*.swift": "swift", + "Package.resolved": "json", + "*.podspec": "ruby" + }, + "files.exclude": { + "**/.build": true, + "**/.swiftpm": true, + "**/DerivedData": true + } + } + } + }, + "remoteUser": "developer", + "mounts": [ + "source=mixpanel-swift-bashhistory,target=/commandhistory,type=volume", + "source=mixpanel-swift-config,target=/home/developer/.claude,type=volume", + "source=${localEnv:HOME}/.gitconfig,target=/home/developer/.gitconfig,type=bind,readonly", + "source=${localEnv:HOME}/.ssh,target=/home/developer/.ssh,type=bind,readonly" + ], + "remoteEnv": { + "SWIFT_VERSION": "5.9", + "CLAUDE_CONFIG_DIR": "/home/developer/.claude", + "STARSHIP_CONFIG": "/home/developer/.config/starship.toml", + "SWIFTPM_ENABLE_GLOBAL_CACHE": "true" + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", + "workspaceFolder": "/workspace", + "postCreateCommand": "sudo /usr/local/bin/init-firewall.sh && ~/.local/bin/setup-shell.sh", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest" + } + } +} \ No newline at end of file diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh new file mode 100644 index 00000000..8c9f8330 --- /dev/null +++ b/.devcontainer/init-firewall.sh @@ -0,0 +1,126 @@ +#!/bin/bash +set -euo pipefail # Exit on error, undefined vars, and pipeline failures +IFS=$'\n\t' # Stricter word splitting + +# Flush existing rules and delete existing ipsets +iptables -F +iptables -X +iptables -t nat -F +iptables -t nat -X +iptables -t mangle -F +iptables -t mangle -X +ipset destroy allowed-domains 2>/dev/null || true + +# First allow DNS and localhost before any restrictions +# Allow outbound DNS +iptables -A OUTPUT -p udp --dport 53 -j ACCEPT +# Allow inbound DNS responses +iptables -A INPUT -p udp --sport 53 -j ACCEPT +# Allow outbound SSH +iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT +# Allow inbound SSH responses +iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT +# Allow localhost +iptables -A INPUT -i lo -j ACCEPT +iptables -A OUTPUT -o lo -j ACCEPT + +# Create ipset with CIDR support +ipset create allowed-domains hash:net + +# Fetch GitHub meta information and aggregate + add their IP ranges +echo "Fetching GitHub IP ranges..." +gh_ranges=$(curl -s https://api.github.com/meta) +if [ -z "$gh_ranges" ]; then + echo "ERROR: Failed to fetch GitHub IP ranges" + exit 1 +fi + +if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then + echo "ERROR: GitHub API response missing required fields" + exit 1 +fi + +echo "Processing GitHub IPs..." +while read -r cidr; do + if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then + echo "ERROR: Invalid CIDR range from GitHub meta: $cidr" + exit 1 + fi + echo "Adding GitHub range $cidr" + ipset add allowed-domains "$cidr" +done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q) + +# Resolve and add other allowed domains +for domain in \ + "registry.npmjs.org" \ + "api.anthropic.com" \ + "sentry.io" \ + "statsig.anthropic.com" \ + "statsig.com" \ + "deepwiki.com"; do + echo "Resolving $domain..." + ips=$(dig +short A "$domain") + if [ -z "$ips" ]; then + echo "ERROR: Failed to resolve $domain" + exit 1 + fi + + while read -r ip; do + if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + echo "ERROR: Invalid IP from DNS for $domain: $ip" + exit 1 + fi + echo "Adding $ip for $domain" + ipset add allowed-domains "$ip" + done < <(echo "$ips") +done + +# Get host IP from default route +HOST_IP=$(ip route | grep default | cut -d" " -f3) +if [ -z "$HOST_IP" ]; then + echo "ERROR: Failed to detect host IP" + exit 1 +fi + +HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/") +echo "Host network detected as: $HOST_NETWORK" + +# Set up remaining iptables rules +iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT +iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT + +# Set default policies to DROP first +iptables -P INPUT DROP +iptables -P FORWARD DROP +iptables -P OUTPUT DROP + +# First allow established connections for already approved traffic +iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT +iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Then allow only specific outbound traffic to allowed domains +iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT + +echo "Firewall configuration complete" +echo "Verifying firewall rules..." +if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then + echo "ERROR: Firewall verification failed - was able to reach https://example.com" + exit 1 +else + echo "Firewall verification passed - unable to reach https://example.com as expected" +fi + +# Verify GitHub API access +if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then + echo "ERROR: Firewall verification failed - unable to reach https://api.github.com" + exit 1 +else + echo "Firewall verification passed - able to reach https://api.github.com as expected" +fi + +# Verify deepwiki.com access +if ! curl --connect-timeout 5 https://deepwiki.com >/dev/null 2>&1; then + echo "WARNING: Unable to reach https://deepwiki.com - it may be down or using different IPs" +else + echo "Firewall verification passed - able to reach https://deepwiki.com as expected" +fi \ No newline at end of file diff --git a/.devcontainer/setup-shell.sh b/.devcontainer/setup-shell.sh new file mode 100644 index 00000000..1bed489b --- /dev/null +++ b/.devcontainer/setup-shell.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# setup-shell.sh - Configure shell environment + +# Create Starship config directory +mkdir -p ~/.config + +# Configure Starship with a nice preset +cat << 'EOF' > ~/.config/starship.toml +# Starship configuration + +[container] +format = "[$symbol]($style) " +style = "bold blue" + +[directory] +truncation_length = 3 +truncate_to_repo = true + +[git_branch] +format = "[$symbol$branch]($style) " + +[git_status] +format = "([$all_status$ahead_behind]($style) )" + +[nodejs] +format = "[$symbol($version )]($style)" +symbol = "⬢ " + +[python] +format = "[$symbol($version )]($style)" + +[time] +disabled = false +format = "[$time]($style) " +time_format = "%H:%M" +style = "dimmed white" +EOF + +# Configure tmux +cat << 'EOF' > ~/.tmux.conf +# Enable mouse support +set -g mouse on + +# Set prefix to Ctrl-a (like screen) +unbind C-b +set-option -g prefix C-a +bind-key C-a send-prefix + +# Easy split pane commands +bind | split-window -h +bind - split-window -v +unbind '"' +unbind % + +# Easy pane navigation +bind h select-pane -L +bind j select-pane -D +bind k select-pane -U +bind l select-pane -R + +# Start windows and panes at 1, not 0 +set -g base-index 1 +setw -g pane-base-index 1 + +# Enable 256 colors +set -g default-terminal "screen-256color" + +# Increase scrollback buffer size +set -g history-limit 10000 + +# Status bar customization +set -g status-bg black +set -g status-fg white +set -g status-left '#[fg=green]#S ' +set -g status-right '#[fg=yellow]#(whoami)@#H' +EOF + +# Add aliases and shell configuration +cat << 'EOF' >> ~/.zshrc + +# Custom aliases +alias claude='claude --dangerously-skip-permissions' +alias ll='ls -la' +alias la='ls -A' +alias l='ls -CF' +alias gs='git status' +alias ga='git add' +alias gc='git commit' +alias gp='git push' +alias gl='git log --oneline --graph --decorate' +alias gd='git diff' +alias gco='git checkout' + +# Better history +export HISTSIZE=10000 +export SAVEHIST=10000 +setopt SHARE_HISTORY +setopt HIST_IGNORE_DUPS +setopt HIST_IGNORE_ALL_DUPS +setopt HIST_FIND_NO_DUPS + +# Editor +export EDITOR='vim' +export VISUAL='vim' + +# Git delta for better diffs +export GIT_PAGER='delta' +EOF + +echo "✅ Shell environment configured with Starship prompt" \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..e1ba2e96 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,101 @@ +# Mixpanel Swift SDK Copilot Instructions + +## Project Overview +This is the official Mixpanel Swift SDK supporting iOS 11.0+, tvOS 11.0+, macOS 10.13+, and watchOS 4.0+. + +## Architecture +- **Entry Point**: `Mixpanel` class (static) → `MixpanelManager` (singleton) → `MixpanelInstance` +- **Core Components**: Track, People, Groups, Flush, Network, FeatureFlags +- **Persistence**: SQLite via `MPDB` class, UserDefaults for metadata +- **Threading**: Custom `ReadWriteLock` with dedicated queues (trackingQueue, networkQueue) + +## Coding Standards + +### Swift Conventions +- Use trailing closures for completion handlers +- Use guard statements for early returns and validation +- Prefix internal properties with underscore (_) +- Use property observers (didSet) for reactive updates +- Mark completion handlers as @escaping when stored + +### Thread Safety +- Always use ReadWriteLock for shared state access +- Use `.read {}` for read operations, `.write {}` for mutations +- Dispatch async work to trackingQueue (QoS: .utility) +- Network operations go to networkQueue + +### Type System +- All property values must conform to MixpanelType protocol +- Supported types: String, Int, UInt, Double, Float, Bool, [MixpanelType], [String: MixpanelType], Date, URL, NSNull +- Always validate properties with `assertPropertyTypes()` +- Use `Properties` typealias for [String: MixpanelType] + +### Error Handling +- Log errors via MixpanelLogger, don't throw +- Use MPAssert for debug assertions +- Fail gracefully with default values +- Return discardable results for fluent API + +### Platform Support +- Use conditional compilation: #if os(iOS), #if !os(OSX) +- Check for app extensions with isiOSAppExtension() +- iOS supports automatic events, macOS doesn't by default +- Test on all platforms before making changes + +### API Design +- Methods should return self or @discardableResult +- Provide sensible defaults for optional parameters +- Use completion handlers for async operations +- Keep public API surface minimal and backward compatible + +### Testing +- Extend MixpanelBaseTests for new test classes +- Use waitForTrackingQueue() to ensure operations complete +- Use randomId() for test data generation +- Test on all supported platforms + +### Documentation +- Add comprehensive documentation comments for public APIs +- Include code examples in documentation +- Document platform-specific behavior +- Update CHANGELOG.md for user-facing changes + +## Common Operations + +### Adding a new event property +1. Update automatic properties in AutomaticProperties.swift +2. Add validation in Track.swift +3. Update persistence if needed +4. Add tests in MixpanelDemoTests + +### Modifying network behavior +1. Update Network.swift and FlushRequest.swift +2. Consider retry logic and error handling +3. Update APIConstants if endpoints change +4. Test with various network conditions + +### Creating new components +1. Follow existing component patterns (Track.swift, People.swift) +2. Implement thread safety with ReadWriteLock +3. Add to MixpanelInstance initialization +4. Create corresponding test file + +## Build and Test Commands +- Build all: `xcodebuild -scheme Mixpanel` +- Test iOS: `xcodebuild test -scheme MixpanelDemo -destination 'platform=iOS Simulator,name=iPhone 15'` +- Test macOS: `xcodebuild test -scheme MixpanelDemoMac` +- Carthage: `./scripts/carthage.sh` +- Documentation: `./scripts/generate_docs.sh` + +## Version Updates +When updating version, modify: +- Mixpanel-swift.podspec +- Info.plist +- Sources/AutomaticProperties.swift ($lib_version constant) + +## Important Notes +- NEVER commit secrets or API keys +- Always maintain backward compatibility +- Test memory usage with large event volumes +- Verify SQLite migrations work correctly +- Check performance on older devices \ No newline at end of file diff --git a/.github/instructions/api-design.instructions.md b/.github/instructions/api-design.instructions.md new file mode 100644 index 00000000..cdeabd04 --- /dev/null +++ b/.github/instructions/api-design.instructions.md @@ -0,0 +1,53 @@ +--- +applyTo: "**/Mixpanel.swift,**/MixpanelInstance.swift,**/People.swift,**/Group.swift" +--- +# API Design Instructions + +## Public API Guidelines +- Keep public surface minimal and focused +- Maintain backward compatibility +- Use @available for deprecation +- Document all public methods thoroughly + +## Method Design +- Return self or @discardableResult for chaining +- Use default parameters for optional values +- Provide both sync and async variants where appropriate +- Use completion handlers for async operations + +## Naming Conventions +- Use clear, descriptive method names +- Follow Swift API design guidelines +- Use verbs for actions (track, identify, flush) +- Use nouns for properties (distinctId, people) + +## Parameter Design +```swift +// Good: Clear parameters with defaults +public func track(event: String, + properties: Properties? = nil, + completion: (() -> Void)? = nil) + +// Bad: Unclear or too many parameters +public func track(_ e: String, _ p: [String: Any]?, _ c: Bool) +``` + +## Error Handling +- Never throw from public APIs +- Use completion handlers with Result type for errors +- Log errors internally +- Provide sensible fallbacks + +## Objective-C Compatibility +- Mark methods with @objc when needed +- Avoid Swift-only features in public API +- Provide type-safe wrappers + +## Documentation Pattern +```swift +/// Tracks an event with optional properties +/// - Parameters: +/// - event: The name of the event to track +/// - properties: Optional properties dictionary +/// - Returns: The current instance for chaining +``` \ No newline at end of file diff --git a/.github/instructions/networking.instructions.md b/.github/instructions/networking.instructions.md new file mode 100644 index 00000000..726b58d5 --- /dev/null +++ b/.github/instructions/networking.instructions.md @@ -0,0 +1,54 @@ +--- +applyTo: "**/Network.swift,**/Flush*.swift" +--- +# Networking Instructions + +## API Endpoints +- Track: https://api.mixpanel.com/track +- Engage: https://api.mixpanel.com/engage +- Groups: https://api.mixpanel.com/groups +- Decide: https://api.mixpanel.com/decide + +## Request Format +- Use POST with form-encoded data +- Include "data" parameter with base64-encoded JSON +- Support gzip compression when enabled +- Set proper Content-Type headers + +## Batching +- Maximum 50 events per batch +- Calculate batch size limits +- Split large batches automatically +- Maintain order of events + +## Network Queue Usage +```swift +networkQueue.async { [weak self] in + self?.sendRequest(request) { success in + // Handle response + } +} +``` + +## Error Handling +- Retry on network failures (500, 502, 503, 504) +- Don't retry on client errors (400) +- Implement exponential backoff +- Log all network errors + +## Flush Behavior +- Automatic flush on timer (default 60s) +- Flush on app background +- Manual flush via flush() method +- Respect flushOnBackground setting + +## Request Headers +- Content-Type: application/x-www-form-urlencoded +- Content-Encoding: gzip (when applicable) +- Accept: application/json + +## Response Handling +- Parse JSON responses +- Check "status" field (1 = success) +- Handle "error" field gracefully +- Update flush metrics \ No newline at end of file diff --git a/.github/instructions/persistence.instructions.md b/.github/instructions/persistence.instructions.md new file mode 100644 index 00000000..79943547 --- /dev/null +++ b/.github/instructions/persistence.instructions.md @@ -0,0 +1,49 @@ +--- +applyTo: "**/MixpanelPersistence.swift,**/MPDB.swift" +--- +# Persistence Layer Instructions + +## SQLite Usage +- All database operations go through MPDB class +- Use parameterized queries to prevent SQL injection +- Handle migrations gracefully with version checks +- Close database connections properly + +## Entity Storage Patterns +```swift +// Saving entities +let data = JSONHandler.encodeAPIData(entity.apiProperties()) +persistence.saveEntity(entity, type: .events, data: data, metadata: metadata) + +// Loading entities +let entities = persistence.loadEntitiesInBatch(type: .events, limit: 50) +``` + +## UserDefaults Storage +Used for lightweight metadata: +- distinctId, anonymousId, userId, alias +- superProperties, timedEvents +- optOutStatus + +## Data Types +- Events: InternalProperties with event data +- People: Set, unset, increment operations +- Groups: Group-specific operations + +## Performance Considerations +- Batch operations (limit 50 per batch) +- Use transactions for multiple operations +- Index on important columns (TOKEN, UUID) +- Regular cleanup of old data + +## Migration Support +- Check fileExists before migrations +- Migrate from archive files to SQLite +- Preserve data integrity during migration +- Log migration progress + +## Error Handling +- Log SQLite errors but don't crash +- Return empty results on read errors +- Skip corrupted entities +- Implement retry logic for transient errors \ No newline at end of file diff --git a/.github/instructions/property-types.instructions.md b/.github/instructions/property-types.instructions.md new file mode 100644 index 00000000..da2f9177 --- /dev/null +++ b/.github/instructions/property-types.instructions.md @@ -0,0 +1,41 @@ +--- +applyTo: "**/Track.swift,**/People.swift,**/Group.swift,**/*Properties*.swift" +--- +# Property Type System Instructions + +## MixpanelType Protocol +All property values MUST conform to MixpanelType protocol. Valid types: +- String, Int, UInt, Double, Float, Bool +- [MixpanelType] (arrays of valid types) +- [String: MixpanelType] (dictionaries with String keys) +- Date, URL, NSNull +- Optional (optionals of valid types) + +## Property Validation +Always validate properties before use: +```swift +assertPropertyTypes(properties) +``` + +## Type Conversion +Use the established conversion patterns: +```swift +// Convert Any to MixpanelType +if let validValue = value as? MixpanelType { + // Use validValue +} + +// For dictionaries +Properties.filterValues { isValidNestedTypeAndValue($0) } +``` + +## Reserved Properties +Never use these reserved property names: +- mp_lib, $lib_version, $os, $os_version +- $manufacturer, $brand, $model +- time, distinct_id, $device_id + +## Property Naming +- Use snake_case for property names (matching Mixpanel convention) +- Prefix Mixpanel internal properties with $ or mp_ +- Keep property names under 255 characters \ No newline at end of file diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md new file mode 100644 index 00000000..b9416152 --- /dev/null +++ b/.github/instructions/testing.instructions.md @@ -0,0 +1,54 @@ +--- +applyTo: "**/*Tests.swift,**/*Test.swift" +--- +# Testing Instructions for Mixpanel Swift SDK + +## Test Class Structure +- Extend MixpanelBaseTests for common utilities +- Use testToken constant from TestConstants.swift +- Override setUp() and call super.setUp() +- Clean up in tearDown() + +## Queue Management in Tests +Always wait for queues to complete: +```swift +waitForTrackingQueue(instance) +flushAndWaitForTrackingQueue(instance) +``` + +## Test Data Generation +- Use randomId() for unique identifiers +- Use Date() for timestamps +- Create realistic test properties + +## Common Test Patterns +```swift +// Test event tracking +instance.track(event: "Test Event", properties: ["key": "value"]) +waitForTrackingQueue(instance) +XCTAssertEqual(instance.eventsQueue.count, 1) + +// Test with completion handler +let expectation = XCTestExpectation(description: "flush complete") +instance.flush(completion: { + expectation.fulfill() +}) +wait(for: [expectation], timeout: 10.0) +``` + +## Platform-Specific Tests +- Use #if os(iOS) for iOS-specific tests +- Test automatic events only on iOS +- Verify platform-specific behavior + +## Performance Tests +- Test with large data sets (1000+ events) +- Measure memory usage +- Verify SQLite performance + +## Edge Cases to Test +- Empty strings and nil values +- Very long property names/values +- Concurrent access scenarios +- Network failures and retries +- App lifecycle transitions \ No newline at end of file diff --git a/.github/instructions/thread-safety.instructions.md b/.github/instructions/thread-safety.instructions.md new file mode 100644 index 00000000..bc425917 --- /dev/null +++ b/.github/instructions/thread-safety.instructions.md @@ -0,0 +1,43 @@ +--- +applyTo: "**/*.swift" +--- +# Thread Safety Instructions for Mixpanel Swift SDK + +## Always use ReadWriteLock for shared state +- Wrap read operations: `readWriteLock.read { /* read code */ }` +- Wrap write operations: `readWriteLock.write { /* write code */ }` +- Never access shared properties directly outside of lock protection + +## Queue Usage +- Use `trackingQueue` for all event tracking operations +- Use `networkQueue` for all network requests +- Both queues use QoS: .utility and autorelease: .workItem +- Dispatch async unless synchronous operation is explicitly needed + +## Common Thread-Safe Patterns +```swift +// Reading a property +var value: String { + return readWriteLock.read { _value } +} + +// Writing a property +func setValue(_ newValue: String) { + readWriteLock.write { _value = newValue } +} + +// Async operation +trackingQueue.async { [weak self] in + self?.performOperation() +} +``` + +## Avoid Deadlocks +- Never call readWriteLock.write from within readWriteLock.read +- Use weak self in async closures to prevent retain cycles +- Order lock acquisition consistently across the codebase + +## Testing Thread Safety +- Use waitForTrackingQueue() in tests to ensure operations complete +- Test concurrent access scenarios +- Verify no data races with Thread Sanitizer \ No newline at end of file diff --git a/.github/prompts/add-event-property.prompt.md b/.github/prompts/add-event-property.prompt.md new file mode 100644 index 00000000..4089fcb3 --- /dev/null +++ b/.github/prompts/add-event-property.prompt.md @@ -0,0 +1,37 @@ +--- +mode: agent +tools: [codebase, githubRepo] +description: Add a new automatic property to events +--- +# Add New Automatic Event Property + +I need to add a new automatic property named "${input:propertyName:Enter property name (e.g., app_build_number)}" to all tracked events. + +## Requirements + +1. **Update AutomaticProperties.swift**: + - Add the new property to the appropriate collection method + - Follow existing patterns for platform-specific properties + - Use proper MixpanelType conformance + +2. **Consider platform differences**: + - Check if property is available on all platforms (iOS, macOS, tvOS, watchOS) + - Use conditional compilation if needed + +3. **Update Constants if needed**: + - Add any new constant keys to Constants.swift + - Follow naming convention (e.g., `$app_build_number`) + +4. **Add tests**: + - Create test in MixpanelAutomaticEventsTests.swift + - Verify property is included in tracked events + - Test on all supported platforms + +5. **Documentation**: + - Update CHANGELOG.md with the new property + - Add inline documentation explaining the property + +## Implementation guidelines from [property-types instructions](../.github/instructions/property-types.instructions.md) + +## Example pattern to follow: +Look at how `$app_version` or `$os_version` are implemented in AutomaticProperties.swift \ No newline at end of file diff --git a/.github/prompts/add-platform-support.prompt.md b/.github/prompts/add-platform-support.prompt.md new file mode 100644 index 00000000..92e1207e --- /dev/null +++ b/.github/prompts/add-platform-support.prompt.md @@ -0,0 +1,52 @@ +--- +mode: agent +tools: [codebase] +description: Add platform-specific functionality +--- +# Add Platform-Specific Functionality + +Add platform-specific implementation for: ${input:feature:Describe the platform-specific feature} + +## Steps to implement: + +1. **Identify platform requirements**: + - Determine which platforms support this feature + - Check API availability (iOS 11.0+, tvOS 11.0+, macOS 10.13+, watchOS 4.0+) + +2. **Use conditional compilation**: + ```swift + #if os(iOS) + // iOS-specific code + #elseif os(macOS) + // macOS-specific code + #elseif os(tvOS) + // tvOS-specific code + #elseif os(watchOS) + // watchOS-specific code + #endif + ``` + +3. **Common patterns**: + - Check `!os(OSX)` for features not on macOS + - Use `targetEnvironment(macCatalyst)` for Mac Catalyst + - Check `isiOSAppExtension()` for app extensions + +4. **Update relevant files**: + - AutomaticEvents.swift for automatic tracking + - AutomaticProperties.swift for platform properties + - MixpanelInstance.swift for initialization + +5. **Test on all platforms**: + - Create platform-specific test targets + - Use appropriate simulators/devices + - Verify no compilation errors on unsupported platforms + +6. **Documentation**: + - Document platform availability + - Add platform badges to method documentation + - Update README if needed + +## Examples to reference: +- Automatic events (iOS only) +- Network activity indicator (iOS only) +- Screen properties (not on watchOS) \ No newline at end of file diff --git a/.github/prompts/database-migration.prompt.md b/.github/prompts/database-migration.prompt.md new file mode 100644 index 00000000..057e0497 --- /dev/null +++ b/.github/prompts/database-migration.prompt.md @@ -0,0 +1,55 @@ +--- +mode: agent +tools: [codebase] +description: Implement database schema migration +--- +# Implement Database Migration + +Create a database migration for: ${input:migrationDescription:Describe the schema change needed} + +## Migration Steps + +1. **Update MPDB.swift**: + - Increment database version + - Add migration logic in appropriate method + - Ensure backward compatibility + +2. **Migration pattern**: + ```swift + if oldVersion < newVersion { + // Perform migration + // Handle errors gracefully + // Log migration progress + } + ``` + +3. **Schema changes to handle**: + - Adding new columns (use ALTER TABLE) + - Creating new tables + - Adding indexes for performance + - Data transformation if needed + +4. **Safety requirements**: + - Never lose user data + - Handle partial migrations + - Test with corrupted databases + - Support rollback if possible + +5. **Follow existing patterns**: + - Reference existing migrations in MPDB + - Use SQLite best practices + - Maintain data integrity + +6. **Testing migration**: + - Test upgrade from all previous versions + - Test with large datasets + - Verify performance impact + - Test concurrent access during migration + +## Persistence patterns from [persistence instructions](../.github/instructions/persistence.instructions.md) + +## Important tables: +- EVENTS_TABLE: Event storage +- PEOPLE_TABLE: User profile updates +- GROUPS_TABLE: Group updates +- AUTOMATIC_EVENTS_TABLE: Automatic event tracking \ No newline at end of file diff --git a/.github/prompts/debug-issue.prompt.md b/.github/prompts/debug-issue.prompt.md new file mode 100644 index 00000000..c3f403f5 --- /dev/null +++ b/.github/prompts/debug-issue.prompt.md @@ -0,0 +1,61 @@ +--- +mode: agent +tools: [codebase, terminalLastCommand] +description: Debug and fix issues in Mixpanel SDK +--- +# Debug Mixpanel SDK Issue + +Debug issue: ${input:issueDescription:Describe the issue you're experiencing} + +## Debugging Steps + +1. **Enable verbose logging**: + ```swift + Mixpanel.mainInstance().logger = MixpanelLogger(level: .debug) + ``` + +2. **Check common issues**: + - **Events not tracking**: Verify token, check queue, ensure flush + - **Crashes**: Look for thread safety issues, nil unwrapping + - **Memory leaks**: Check retain cycles, use Instruments + - **Network failures**: Verify endpoints, check proxies + +3. **Debugging tools**: + - Set breakpoints in key methods + - Use `MPAssert` for validation + - Check SQLite database directly + - Monitor network traffic with proxy + +4. **Queue inspection**: + ```swift + // Check event queue + print("Events in queue: \(instance.eventsQueue.count)") + + // Force synchronous operation for debugging + instance.trackingQueue.sync { + // Inspect state + } + ``` + +5. **Common problem areas**: + - **Thread safety**: Race conditions, deadlocks + - **Type system**: Invalid property types + - **Persistence**: SQLite errors, migration issues + - **Network**: SSL errors, timeouts + - **Memory**: Retain cycles, large payloads + +6. **Platform-specific debugging**: + - iOS: Check for app extension limitations + - macOS: Verify sandboxing permissions + - Background modes and state restoration + +## Debug patterns: +- Add logging throughout code path +- Use guard statements to identify failure points +- Test with minimal reproduction case +- Check test files for similar issues + +## Related files to check: +- MixpanelLogger.swift for logging +- Error.swift for error definitions +- Test files for expected behavior \ No newline at end of file diff --git a/.github/prompts/fix-thread-safety.prompt.md b/.github/prompts/fix-thread-safety.prompt.md new file mode 100644 index 00000000..62a4012b --- /dev/null +++ b/.github/prompts/fix-thread-safety.prompt.md @@ -0,0 +1,44 @@ +--- +mode: edit +description: Fix thread safety issues in selected code +--- +# Fix Thread Safety Issues + +Review and fix thread safety issues in the selected code: + +1. **Identify shared state** that needs protection +2. **Add ReadWriteLock** protection where missing +3. **Use proper queue dispatch** for async operations +4. **Fix any potential race conditions** + +## Required changes: + +- Wrap all shared property reads in `readWriteLock.read { }` +- Wrap all shared property writes in `readWriteLock.write { }` +- Dispatch tracking operations to `trackingQueue` +- Dispatch network operations to `networkQueue` +- Use `[weak self]` in async closures to prevent retain cycles +- Never access instance variables directly outside of locks + +## Refer to: +- [Thread safety instructions](../.github/instructions/thread-safety.instructions.md) +- Existing patterns in Track.swift and MixpanelInstance.swift + +## Common patterns to apply: +```swift +// Thread-safe getter +var property: Type { + return readWriteLock.read { _property } +} + +// Thread-safe setter +func setProperty(_ value: Type) { + readWriteLock.write { _property = value } +} + +// Async operation +trackingQueue.async { [weak self] in + guard let self = self else { return } + // Operation code +} +``` \ No newline at end of file diff --git a/.github/prompts/implement-new-api.prompt.md b/.github/prompts/implement-new-api.prompt.md new file mode 100644 index 00000000..c1c71026 --- /dev/null +++ b/.github/prompts/implement-new-api.prompt.md @@ -0,0 +1,53 @@ +--- +mode: agent +tools: [codebase] +description: Implement a new public API method +--- +# Implement New Public API Method + +I need to implement a new public API method: ${input:methodSignature:Enter method signature (e.g., trackCustomEvent(name: String, metadata: [String: Any]))} + +## Implementation Steps + +1. **Add to Mixpanel.swift** (static interface): + - Add public static method + - Forward to mainInstance() + - Include proper documentation + +2. **Add to MixpanelInstance.swift**: + - Implement actual logic + - Use @objc if needed for Objective-C compatibility + - Make thread-safe with ReadWriteLock + - Return self or @discardableResult for fluent API + +3. **Follow patterns**: + - Reference similar methods like track() or identify() + - Use established queue patterns from [thread-safety instructions](../.github/instructions/thread-safety.instructions.md) + - Validate inputs using property type system + +4. **Error handling**: + - Use MixpanelLogger for errors + - Don't throw exceptions + - Fail gracefully with sensible defaults + +5. **Add tests**: + - Create comprehensive tests in appropriate test file + - Test edge cases and invalid inputs + - Test thread safety + +6. **Documentation**: + - Add comprehensive doc comments + - Include usage examples + - Document any platform-specific behavior + - Update public API documentation + +## Code Pattern Example: +```swift +@discardableResult +public func newMethod(param: String) -> MixpanelInstance { + readWriteLock.write { + // Implementation + } + return self +} +``` \ No newline at end of file diff --git a/.github/prompts/optimize-performance.prompt.md b/.github/prompts/optimize-performance.prompt.md new file mode 100644 index 00000000..d166b722 --- /dev/null +++ b/.github/prompts/optimize-performance.prompt.md @@ -0,0 +1,60 @@ +--- +mode: agent +tools: [codebase] +description: Optimize performance of Mixpanel operations +--- +# Performance Optimization Task + +Optimize performance for: ${input:component:Component or operation to optimize} + +## Performance Analysis Steps + +1. **Identify bottlenecks**: + - Profile with Instruments + - Check queue congestion + - Monitor memory usage + - Measure operation timing + +2. **Common optimization areas**: + - **Batch operations**: Process multiple items together + - **Queue management**: Optimize dispatch patterns + - **Memory usage**: Reduce allocations and copies + - **SQLite queries**: Add indexes, optimize queries + - **Network calls**: Batch requests, compress data + +3. **Optimization techniques**: + ```swift + // Batch processing + let batchSize = 50 + for batch in entities.chunked(into: batchSize) { + processBatch(batch) + } + + // Efficient queue usage + trackingQueue.async(flags: .barrier) { + // Exclusive write operation + } + ``` + +4. **Memory optimization**: + - Use autoreleasepool for loops + - Weak references where appropriate + - Clear caches when needed + - Profile for leaks + +5. **SQLite optimization**: + - Create appropriate indexes + - Use transactions for bulk operations + - Optimize query patterns + - Regular VACUUM operations + +6. **Testing performance**: + - Measure before and after + - Test with large datasets (10k+ events) + - Test on older devices + - Monitor battery impact + +## Performance patterns to follow: +- Reference Flush.swift for batch processing +- See MixpanelPersistence for SQLite optimization +- Check AutomaticEvents for efficient property collection \ No newline at end of file diff --git a/.github/prompts/write-unit-test.prompt.md b/.github/prompts/write-unit-test.prompt.md new file mode 100644 index 00000000..399e44a1 --- /dev/null +++ b/.github/prompts/write-unit-test.prompt.md @@ -0,0 +1,57 @@ +--- +mode: agent +tools: [codebase] +description: Write comprehensive unit tests for Mixpanel functionality +--- +# Write Unit Tests for ${input:component:Component or feature to test} + +## Test Requirements + +1. **Setup test class**: + - Extend `MixpanelBaseTests` + - Use `testToken` from TestConstants + - Set up fresh instance in `setUp()` + - Clean up in `tearDown()` + +2. **Test categories to cover**: + - Happy path functionality + - Edge cases and error conditions + - Thread safety (concurrent access) + - Platform-specific behavior + - Memory management + +3. **Use test utilities**: + - `waitForTrackingQueue()` - ensure async operations complete + - `flushAndWaitForTrackingQueue()` - flush and wait + - `randomId()` - generate test identifiers + - Direct queue access for verification + +4. **Test patterns from** [testing instructions](../.github/instructions/testing.instructions.md) + +5. **Assertions to include**: + - Verify queue counts + - Check property values + - Validate persistence + - Confirm network calls (using mocks if needed) + +## Example test structure: +```swift +func testFeatureName() { + // Arrange + let instance = MixpanelInstance(apiToken: testToken) + let testData = createTestData() + + // Act + instance.performAction(testData) + waitForTrackingQueue(instance) + + // Assert + XCTAssertEqual(instance.eventsQueue.count, 1) + // More assertions +} +``` + +## Platform-specific tests: +- Use `#if os(iOS)` for iOS-only features +- Skip tests on unsupported platforms +- Test automatic events only on iOS \ No newline at end of file diff --git a/.github/workflows/claude-code-agent.yaml b/.github/workflows/claude-code-agent.yaml new file mode 100644 index 00000000..9b857ee6 --- /dev/null +++ b/.github/workflows/claude-code-agent.yaml @@ -0,0 +1,78 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) + runs-on: ubuntu-24.04-mxpnl-large + timeout-minutes: 59 + + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + + env: + # Fix standalone module compatibility with GitHub Actions + ANALYTICS_DIR: ${{ github.workspace }}/.. + + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v2.1.4 + with: + # https://github.com/organizations/mixpanel/settings/apps/mixpanel-claude-code-agent + app-id: 2102903 + private-key: ${{ secrets.CLAUDE_CODE_AGENT_PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v5 + with: + token: ${{ steps.app-token.outputs.token }} + + - uses: ./.github/actions/setup-gcp + with: + # TODO: use claude-code-agent svc account + service_account: github-copilot-coding-agent@mixpanel-tools.iam.gserviceaccount.com + + # TODO: this can be removed if we get rid of this onboarding-components dependency + # https://github.com/mixpanel/analytics/blob/41baf08aceccf3ffa4a12df3d3c1d1f85ab8b276/package.json#L394 + - name: set git config to use token for npm + run: | + git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com".insteadOf "https://github.com" + - name: Enable tools/bin + run: echo "${GITHUB_WORKSPACE}/tools/bin" >> "$GITHUB_PATH" + + - uses: ./.github/actions/setup-node + - uses: ./.github/actions/setup-python + + - name: Generate OTEL resource attributes + id: otel-attrs + run: | + attrs=$(./sre/gcpdevbox/scripts/otel-user-attributes.sh) + echo "otel_attributes=$attrs" >> "$GITHUB_OUTPUT" + - uses: anthropics/claude-code-action@777ffcbfc9d2e2b07f3cfec41b7c7eadedd1f0dc # v1.0.12 + with: + github_token: ${{ steps.app-token.outputs.token }} + trigger_phrase: '@claude' + use_vertex: 'true' + claude_args: '--max-turns 10' + settings: '.claude/settings.json' + experimental_allowed_domains: >- + github.com + mixpanel.com + env: + ANTHROPIC_VERTEX_PROJECT_ID: mixpanel-claude-code + CLOUD_ML_REGION: us-east5 + OTEL_RESOURCE_ATTRIBUTES: ${{ steps.otel-attrs.outputs.otel_attributes }} diff --git a/claude/architecture/persistence-layer.md b/claude/architecture/persistence-layer.md new file mode 100644 index 00000000..9efe0323 --- /dev/null +++ b/claude/architecture/persistence-layer.md @@ -0,0 +1,408 @@ +# Persistence Layer Architecture + +## Overview +The Mixpanel SDK uses SQLite for persistent storage with a carefully designed schema to handle events, people updates, and groups data. The persistence layer ensures data survives app termination and provides efficient batch loading. + +## Database Schema + +### Core Tables + +```sql +-- Events table +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, + token TEXT NOT NULL, + data BLOB NOT NULL, + created_at REAL DEFAULT (datetime('now')), + retry_count INTEGER DEFAULT 0, + INDEX idx_token_created (token, created_at), + INDEX idx_uuid (uuid) +); + +-- People updates table +CREATE TABLE IF NOT EXISTS people ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, + token TEXT NOT NULL, + data BLOB NOT NULL, + created_at REAL DEFAULT (datetime('now')), + operation TEXT NOT NULL, -- 'set', 'unset', 'increment', etc. + retry_count INTEGER DEFAULT 0 +); + +-- Groups table +CREATE TABLE IF NOT EXISTS groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, + token TEXT NOT NULL, + group_key TEXT NOT NULL, + group_id TEXT NOT NULL, + data BLOB NOT NULL, + created_at REAL DEFAULT (datetime('now')), + operation TEXT NOT NULL, + UNIQUE(token, group_key, group_id) +); +``` + +### Metadata Storage +```sql +-- Configuration and metadata +CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +-- Version tracking +INSERT OR REPLACE INTO metadata (key, value) +VALUES ('db_version', '2'); +``` + +## Architecture Components + +### MPDB Class +Central database manager handling all SQLite operations. + +```swift +class MPDB { + private let dbPath: String + private var db: OpaquePointer? + private let dbQueue = DispatchQueue(label: "com.mixpanel.db", attributes: .concurrent) + + init(path: String) throws { + self.dbPath = path + try openDatabase() + try createTables() + try migrateIfNeeded() + } +} +``` + +### MixpanelPersistence Class +High-level interface for SDK components. + +```swift +class MixpanelPersistence { + private let mpdb: MPDB + private let fileManager = FileManager.default + + func saveEntity(_ entity: InternalProperties, + type: EntityType, + data: Data, + metadata: PersistenceMetadata) { + mpdb.insert( + table: type.tableName, + entity: entity, + data: data, + metadata: metadata + ) + } + + func loadEntitiesInBatch(type: EntityType, + limit: Int = 50) -> [Data] { + return mpdb.select( + from: type.tableName, + limit: limit, + orderBy: "created_at ASC" + ) + } +} +``` + +## Data Flow + +``` +Track Event → Encode to JSON → Compress (optional) → Store in SQLite + ↓ +Flush → Load Batch → Send to Network → On Success → Delete from SQLite + ↘ On Failure → Update retry_count +``` + +## Key Design Decisions + +### 1. BLOB Storage for JSON +Events are stored as BLOB (binary) data containing compressed JSON. + +**Why?** +- Flexibility: Schema can evolve without migrations +- Performance: Single column read vs. multiple joins +- Compression: Can store gzipped data directly +- Compatibility: Easy to send to API + +```swift +// Storing +let jsonData = try JSONSerialization.data(withJSONObject: eventDict) +let compressed = jsonData.gzipped() // Optional +mpdb.insert(data: compressed) + +// Loading +let compressed = mpdb.select(...) +let jsonData = compressed.gunzipped() +let event = try JSONSerialization.jsonObject(with: jsonData) +``` + +### 2. UUID for Deduplication +Each entity has a UUID to prevent duplicates. + +```swift +extension InternalProperties { + var uuid: String { + // Generate deterministic UUID from content + let data = try? JSONSerialization.data(withJSONObject: self) + return data?.sha256Hash() ?? UUID().uuidString + } +} +``` + +### 3. Batch Processing +Events are loaded in configurable batches for efficiency. + +```swift +let batchSize = 50 // Default batch size + +func flushBatch() { + while true { + // Load next batch + let batch = persistence.loadEntitiesInBatch( + type: .events, + limit: batchSize + ) + + guard !batch.isEmpty else { break } + + // Process batch + sendToNetwork(batch) { success in + if success { + // Delete successfully sent events + persistence.deleteEntities(batch.map { $0.uuid }) + } else { + // Update retry count + persistence.updateRetryCount(batch.map { $0.uuid }) + } + } + } +} +``` + +## Error Handling + +### 1. Database Corruption +```swift +func handleCorruption() { + Logger.error("Database corrupted, recreating...") + + // 1. Close current connection + sqlite3_close(db) + + // 2. Move corrupted file + let backupPath = dbPath + ".corrupted.\(Date().timeIntervalSince1970)" + try? FileManager.default.moveItem(atPath: dbPath, toPath: backupPath) + + // 3. Create new database + try? openDatabase() + try? createTables() +} +``` + +### 2. Disk Space +```swift +func checkDiskSpace() -> Bool { + let attributes = try? FileManager.default.attributesOfFileSystem( + forPath: NSHomeDirectory() + ) + + if let freeSpace = attributes?[.systemFreeSize] as? Int64 { + let minRequired: Int64 = 10 * 1024 * 1024 // 10MB + return freeSpace > minRequired + } + + return false +} +``` + +### 3. Migration Failures +```swift +func migrateWithFallback() { + do { + try performMigration() + } catch { + Logger.error("Migration failed: \(error)") + + // Option 1: Start fresh (data loss) + recreateDatabase() + + // Option 2: Continue with old schema (feature limited) + // Mark migration as skipped + } +} +``` + +## Performance Optimizations + +### 1. Indexes +Strategic indexes for common queries: + +```sql +-- For batch loading by token +CREATE INDEX idx_events_batch +ON events(token, created_at) +WHERE retry_count < 3; + +-- For UUID lookups (deduplication) +CREATE INDEX idx_events_uuid +ON events(uuid); + +-- For cleanup operations +CREATE INDEX idx_events_old +ON events(created_at) +WHERE created_at < datetime('now', '-30 days'); +``` + +### 2. Write-Ahead Logging (WAL) +```swift +// Enable WAL mode for better concurrency +sqlite3_exec(db, "PRAGMA journal_mode=WAL", nil, nil, nil) + +// Checkpoint periodically +func checkpoint() { + sqlite3_wal_checkpoint_v2(db, nil, SQLITE_CHECKPOINT_PASSIVE, nil, nil) +} +``` + +### 3. Prepared Statements +```swift +// Cache frequently used statements +class StatementCache { + private var statements: [String: OpaquePointer] = [:] + + func prepare(_ sql: String) -> OpaquePointer? { + if let cached = statements[sql] { + sqlite3_reset(cached) + return cached + } + + var statement: OpaquePointer? + if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { + statements[sql] = statement + return statement + } + + return nil + } +} +``` + +## Testing Strategies + +### 1. Database State Verification +```swift +func testPersistenceIntegrity() { + // Store events + let events = (0..<100).map { createTestEvent(id: $0) } + events.forEach { persistence.save($0) } + + // Verify count + let count = persistence.countEntities(type: .events) + XCTAssertEqual(count, 100) + + // Load and verify content + let loaded = persistence.loadAllEntities(type: .events) + XCTAssertEqual(loaded.count, 100) + + // Verify order preserved + let ids = loaded.compactMap { $0["id"] as? Int } + XCTAssertEqual(ids, Array(0..<100)) +} +``` + +### 2. Concurrent Access +```swift +func testConcurrentPersistence() { + let queues = (0..<10).map { + DispatchQueue(label: "test.\($0)") + } + + let expectation = XCTestExpectation(description: "Concurrent") + expectation.expectedFulfillmentCount = 1000 + + for i in 0..<1000 { + queues[i % 10].async { + self.persistence.save(createTestEvent(id: i)) + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: 10.0) + + // Verify all saved + let count = persistence.countEntities(type: .events) + XCTAssertEqual(count, 1000) +} +``` + +### 3. Corruption Recovery +```swift +func testCorruptionRecovery() { + // Corrupt database file + let dbPath = persistence.databasePath + try? "corrupted data".write(toFile: dbPath, atomically: true, encoding: .utf8) + + // Attempt to use + let newPersistence = MixpanelPersistence(token: testToken) + + // Should recover and work + newPersistence.save(createTestEvent()) + XCTAssertEqual(newPersistence.countEntities(type: .events), 1) +} +``` + +## Maintenance Operations + +### 1. Data Cleanup +```swift +// Remove old events +func cleanupOldData(daysToKeep: Int = 30) { + let cutoffDate = Date().addingTimeInterval( + -TimeInterval(daysToKeep * 24 * 60 * 60) + ) + + let sql = """ + DELETE FROM events + WHERE created_at < ? + AND retry_count >= 3 + """ + + mpdb.execute(sql, parameters: [cutoffDate.timeIntervalSince1970]) +} +``` + +### 2. Database Optimization +```swift +// Run periodically (e.g., on app launch) +func optimizeDatabase() { + // Reclaim space + sqlite3_exec(db, "VACUUM", nil, nil, nil) + + // Update statistics + sqlite3_exec(db, "ANALYZE", nil, nil, nil) + + // Checkpoint WAL + sqlite3_wal_checkpoint_v2(db, nil, SQLITE_CHECKPOINT_TRUNCATE, nil, nil) +} +``` + +### 3. Size Monitoring +```swift +func monitorDatabaseSize() { + let attrs = try? FileManager.default.attributesOfItem(atPath: dbPath) + let size = attrs?[.size] as? Int64 ?? 0 + + if size > 50_000_000 { // 50MB warning threshold + Logger.warning("Database size: \(size / 1_000_000)MB") + + // Trigger cleanup + cleanupOldData() + optimizeDatabase() + } +} +``` \ No newline at end of file diff --git a/claude/architecture/threading-model.md b/claude/architecture/threading-model.md new file mode 100644 index 00000000..ae5a8f9f --- /dev/null +++ b/claude/architecture/threading-model.md @@ -0,0 +1,334 @@ +# Threading Model Deep Dive + +## Overview +The Mixpanel SDK uses a sophisticated threading model to ensure thread safety while maintaining high performance. Understanding this model is crucial for making safe modifications. + +## Core Components + +### ReadWriteLock +Custom implementation using GCD's concurrent queue with barriers. + +```swift +public class ReadWriteLock { + private let concurrentQueue: DispatchQueue + + init(label: String) { + concurrentQueue = DispatchQueue( + label: label, + attributes: .concurrent + ) + } + + func read(_ block: () -> T) -> T { + return concurrentQueue.sync { block() } + } + + func write(_ block: () -> T) -> T { + return concurrentQueue.sync(flags: .barrier) { block() } + } +} +``` + +**Why this design?** +- Multiple readers can access simultaneously +- Writers get exclusive access via barrier +- Prevents reader-writer and writer-writer conflicts +- Better performance than serial queues for read-heavy workloads + +### Queue Architecture + +``` +Main Thread + ↓ +MixpanelInstance + ├─ trackingQueue (serial, QoS: .utility) + │ └─ Handles all event operations + ├─ networkQueue (serial, QoS: .utility) + │ └─ Manages network requests + └─ flushQueue (serial, internal to Flush) + └─ Coordinates batch processing +``` + +### Queue Responsibilities + +#### trackingQueue +- Event creation and validation +- Property collection +- Persistence operations +- Queue management + +```swift +trackingQueue.async { [weak self] in + guard let self = self else { return } + + // 1. Validate event + guard !eventName.isEmpty else { return } + + // 2. Collect properties + var allProperties = self.automaticProperties() + allProperties.merge(properties) { _, new in new } + + // 3. Create event + let event = [ + "event": eventName, + "properties": allProperties + ] + + // 4. Add to queue (thread-safe) + self.readWriteLock.write { + self._eventsQueue.append(event) + } + + // 5. Persist + self.persistence.save(event) +} +``` + +#### networkQueue +- HTTP request creation +- Response handling +- Retry logic +- Error management + +```swift +networkQueue.async { [weak self] in + guard let self = self else { return } + + let request = self.buildRequest(events) + + URLSession.shared.dataTask(with: request) { data, response, error in + self.networkQueue.async { [weak self] in + self?.handleResponse(data, response, error) + } + }.resume() +} +``` + +## Thread Safety Patterns + +### 1. Property Access Pattern +```swift +// Internal storage with underscore prefix +private var _distinctId: String = "" + +// Public access through ReadWriteLock +public var distinctId: String { + get { readWriteLock.read { _distinctId } } + set { + readWriteLock.write { + _distinctId = newValue + // Can trigger side effects safely here + updateSuperProperties() + } + } +} +``` + +### 2. Collection Modification Pattern +```swift +// NEVER do this: +eventsQueue.append(event) // RACE CONDITION! + +// ALWAYS do this: +readWriteLock.write { + _eventsQueue.append(event) +} + +// For complex operations: +readWriteLock.write { + _eventsQueue.removeAll { event in + shouldRemove(event) + } + _eventsQueue.append(contentsOf: newEvents) +} +``` + +### 3. Async Operation Pattern +```swift +func performAsyncOperation(completion: @escaping () -> Void) { + // Capture what you need + let currentValue = readWriteLock.read { _someValue } + + // Perform async work + trackingQueue.async { [weak self, currentValue] in + guard let self = self else { + completion() + return + } + + // Do work with captured value + let result = process(currentValue) + + // Update state + self.readWriteLock.write { + self._someValue = result + } + + // Call completion + DispatchQueue.main.async { + completion() + } + } +} +``` + +## Common Pitfalls + +### 1. Nested Lock Acquisition +```swift +// ❌ DEADLOCK RISK +readWriteLock.read { + let value = _property + readWriteLock.write { // DEADLOCK! + _property = transform(value) + } +} + +// ✅ CORRECT +let value = readWriteLock.read { _property } +readWriteLock.write { + _property = transform(value) +} +``` + +### 2. Synchronous Dispatch to Same Queue +```swift +// ❌ DEADLOCK +trackingQueue.sync { // If already on trackingQueue + performOperation() +} + +// ✅ CORRECT - Check current queue +if DispatchQueue.current == trackingQueue { + performOperation() +} else { + trackingQueue.sync { + performOperation() + } +} +``` + +### 3. Missing Weak Self +```swift +// ❌ RETAIN CYCLE +trackingQueue.async { + self.performOperation() // Self captured strongly +} + +// ✅ CORRECT +trackingQueue.async { [weak self] in + guard let self = self else { return } + self.performOperation() +} +``` + +## Testing Thread Safety + +### 1. Use Thread Sanitizer +Enable in Xcode: Edit Scheme → Run → Diagnostics → ✓ Thread Sanitizer + +### 2. Stress Test Pattern +```swift +func testConcurrentAccess() { + let iterations = 1000 + let queues = [ + DispatchQueue(label: "test.1", attributes: .concurrent), + DispatchQueue(label: "test.2", attributes: .concurrent), + DispatchQueue(label: "test.3", attributes: .concurrent) + ] + + let expectation = XCTestExpectation(description: "Concurrent") + expectation.expectedFulfillmentCount = iterations * 3 + + for i in 0.. Any + static func isValidType(_ value: Any) -> Bool +} +``` + +### Conforming Types +```swift +// Primitives +extension String: MixpanelType {} +extension Int: MixpanelType {} +extension UInt: MixpanelType {} +extension Double: MixpanelType {} +extension Float: MixpanelType {} +extension Bool: MixpanelType {} + +// Objects +extension Date: MixpanelType { + func toAPIObject() -> Any { + return DateFormatter.mixpanelDateFormatter.string(from: self) + } +} + +extension URL: MixpanelType { + func toAPIObject() -> Any { + return self.absoluteString + } +} + +extension NSNull: MixpanelType {} + +// Collections +extension Array: MixpanelType where Element: MixpanelType {} +extension Dictionary: MixpanelType where Key == String, Value: MixpanelType {} + +// Optional support +extension Optional: MixpanelType where Wrapped: MixpanelType { + func toAPIObject() -> Any { + switch self { + case .none: + return NSNull() + case .some(let value): + return value.toAPIObject() + } + } +} +``` + +## Type Validation Patterns + +### 1. Property Validation +```swift +public typealias Properties = [String: MixpanelType] + +func assertPropertyTypes(_ properties: Properties) { + #if DEBUG + for (key, value) in properties { + assert( + MixpanelType.isValidType(value), + "Property '\(key)' has invalid type: \(type(of: value))" + ) + + // Check nested types + if let array = value as? [Any] { + array.forEach { element in + assert( + MixpanelType.isValidType(element), + "Array element in '\(key)' has invalid type" + ) + } + } + + if let dict = value as? [String: Any] { + dict.forEach { (nestedKey, nestedValue) in + assert( + MixpanelType.isValidType(nestedValue), + "Nested property '\(key).\(nestedKey)' has invalid type" + ) + } + } + } + #endif +} +``` + +### 2. Safe Type Conversion +```swift +// Convert unknown types safely +func convertToMixpanelType(_ value: Any) -> MixpanelType? { + // Direct cast + if let mixpanelValue = value as? MixpanelType { + return mixpanelValue + } + + // Handle NSNumber (from Objective-C) + if let number = value as? NSNumber { + // Check for boolean + if CFBooleanGetTypeID() == CFGetTypeID(number) { + return number.boolValue + } + + // Check for integer + if let int = number as? Int { + return int + } + + // Default to Double + return number.doubleValue + } + + // Handle collections with type erasure + if let array = value as? [Any] { + let converted = array.compactMap { convertToMixpanelType($0) } + return converted.count == array.count ? converted : nil + } + + if let dict = value as? [String: Any] { + let converted = dict.compactMapValues { convertToMixpanelType($0) } + return converted.count == dict.count ? converted : nil + } + + return nil +} +``` + +### 3. Filtering Invalid Types +```swift +extension Dictionary where Key == String { + func filterValidProperties() -> Properties { + return self.compactMapValues { value in + if let mixpanelValue = value as? MixpanelType { + return mixpanelValue + } + + // Try conversion + return convertToMixpanelType(value) + } + } +} + +// Usage +let rawProperties: [String: Any] = [ + "valid_string": "hello", + "valid_number": 42, + "invalid_object": SomeCustomClass(), // Will be filtered out + "valid_date": Date() +] + +let validProperties = rawProperties.filterValidProperties() +// Only contains valid_string, valid_number, and valid_date +``` + +## Common Type Patterns + +### 1. Numeric Type Handling +```swift +// Problem: Swift has many numeric types +func handleNumericProperty(_ value: Any) -> MixpanelType? { + switch value { + case let int as Int: + return int + case let uint as UInt: + return uint + case let double as Double: + return double + case let float as Float: + return float + case let int8 as Int8: + return Int(int8) + case let int16 as Int16: + return Int(int16) + case let int32 as Int32: + return Int(int32) + case let int64 as Int64: + return Double(int64) // Prevent overflow + default: + return nil + } +} +``` + +### 2. Date Formatting +```swift +extension DateFormatter { + static let mixpanelDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + formatter.timeZone = TimeZone(abbreviation: "UTC") + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + }() +} + +// Consistent date handling +func formatDate(_ date: Date) -> String { + return DateFormatter.mixpanelDateFormatter.string(from: date) +} + +func parseDate(_ string: String) -> Date? { + return DateFormatter.mixpanelDateFormatter.date(from: string) +} +``` + +### 3. Collection Type Safety +```swift +// Ensure homogeneous arrays +func validateArray(_ array: [Any]) -> [MixpanelType]? { + let converted = array.compactMap { convertToMixpanelType($0) } + + // All elements must convert successfully + guard converted.count == array.count else { + Logger.warning("Array contains invalid types") + return nil + } + + // Optional: Check all elements are same type + if !array.isEmpty { + let firstType = type(of: array[0]) + let homogeneous = array.allSatisfy { type(of: $0) == firstType } + if !homogeneous { + Logger.debug("Array contains mixed types") + } + } + + return converted +} +``` + +## API Design for Type Safety + +### 1. Generic Constraints +```swift +// Type-safe property setter +func set(property: String, to value: T) { + properties[property] = value +} + +// Usage +people.set(property: "age", to: 25) // ✅ Int conforms +people.set(property: "name", to: "John") // ✅ String conforms +// people.set(property: "custom", to: MyClass()) // ❌ Compile error +``` + +### 2. Builder Pattern with Type Safety +```swift +class EventBuilder { + private var properties: Properties = [:] + + func with(_ key: String, value: T) -> Self { + properties[key] = value + return self + } + + func withOptional(_ key: String, value: T?) -> Self { + if let value = value { + properties[key] = value + } + return self + } + + func build() -> Properties { + return properties + } +} + +// Usage +let event = EventBuilder() + .with("product_id", value: 12345) + .with("product_name", value: "Widget") + .withOptional("coupon_code", value: userCoupon) + .build() +``` + +### 3. Result Type for Validation +```swift +enum ValidationResult { + case valid(T) + case invalid(reason: String) +} + +func validateProperties(_ raw: [String: Any]) -> ValidationResult { + var validated: Properties = [:] + + for (key, value) in raw { + if let validValue = convertToMixpanelType(value) { + validated[key] = validValue + } else { + return .invalid( + reason: "Property '\(key)' has invalid type: \(type(of: value))" + ) + } + } + + return .valid(validated) +} + +// Usage +switch validateProperties(userInput) { +case .valid(let properties): + track(event: "Purchase", properties: properties) +case .invalid(let reason): + Logger.error("Invalid properties: \(reason)") +} +``` + +## Objective-C Interoperability + +### 1. Type Bridging +```swift +@objc public class MixpanelObjC: NSObject { + @objc public static func track( + event: String, + properties: [String: Any]? + ) { + // Convert to type-safe properties + let validProperties = properties?.filterValidProperties() ?? [:] + Mixpanel.mainInstance().track( + event: event, + properties: validProperties + ) + } +} +``` + +### 2. Safe NSNumber Handling +```swift +extension NSNumber { + var mixpanelValue: MixpanelType { + // Check if boolean + if CFBooleanGetTypeID() == CFGetTypeID(self) { + return boolValue + } + + // Check encoding + let encoding = String(cString: objCType) + + switch encoding { + case "c", "C", "B": // char, unsigned char, bool + return boolValue + case "i", "s", "l", "q": // int types + return intValue + case "I", "S", "L", "Q": // unsigned int types + return uintValue + case "f": // float + return floatValue + case "d": // double + return doubleValue + default: + return doubleValue // Safe default + } + } +} +``` + +## Testing Type Safety + +### 1. Property Type Tests +```swift +func testValidPropertyTypes() { + let validProperties: Properties = [ + "string": "test", + "int": 42, + "double": 3.14, + "bool": true, + "date": Date(), + "url": URL(string: "https://mixpanel.com")!, + "array": [1, 2, 3], + "dict": ["nested": "value"], + "null": NSNull(), + "optional": Optional.none + ] + + // Should not crash + assertPropertyTypes(validProperties) + + // All should be valid + for (_, value) in validProperties { + XCTAssertTrue(MixpanelType.isValidType(value)) + } +} +``` + +### 2. Invalid Type Tests +```swift +func testInvalidPropertyTypes() { + let invalidValues: [Any] = [ + UIView(), // Custom object + NSObject(), // Foundation object + { print("closure") }, // Closure + Selector("test"), // Selector + ] + + for value in invalidValues { + XCTAssertFalse(MixpanelType.isValidType(value)) + XCTAssertNil(convertToMixpanelType(value)) + } +} +``` + +### 3. Conversion Tests +```swift +func testTypeConversion() { + // NSNumber conversions + XCTAssertEqual(convertToMixpanelType(NSNumber(value: true)) as? Bool, true) + XCTAssertEqual(convertToMixpanelType(NSNumber(value: 42)) as? Int, 42) + + // Collection conversions + let nsArray = NSArray(array: ["a", "b", "c"]) + let converted = convertToMixpanelType(nsArray) as? [String] + XCTAssertEqual(converted, ["a", "b", "c"]) +} +``` + +## Best Practices + +1. **Always validate at API boundaries** - Don't trust external input +2. **Use type aliases** - `Properties` is clearer than `[String: MixpanelType]` +3. **Fail gracefully** - Log and skip invalid properties rather than crash +4. **Document supported types** - Make it clear what types are accepted +5. **Test edge cases** - Empty strings, very large numbers, special characters +6. **Consider performance** - Type checking has overhead, cache when possible \ No newline at end of file diff --git a/claude/technologies/swift-features.md b/claude/technologies/swift-features.md new file mode 100644 index 00000000..45b66e06 --- /dev/null +++ b/claude/technologies/swift-features.md @@ -0,0 +1,478 @@ +# Swift Language Features in Mixpanel SDK + +## Property Wrappers (Future Enhancement) + +While not currently used, property wrappers could simplify thread-safe properties: + +```swift +@propertyWrapper +struct ThreadSafe { + private var value: T + private let lock = ReadWriteLock(label: "property.wrapper") + + init(wrappedValue: T) { + self.value = wrappedValue + } + + var wrappedValue: T { + get { lock.read { value } } + set { lock.write { value = newValue } } + } +} + +// Future usage: +class MixpanelInstance { + @ThreadSafe private var distinctId: String = "" + @ThreadSafe private var eventQueue: [Event] = [] +} +``` + +## Protocol-Oriented Design + +### 1. Protocol Composition +```swift +// Define capabilities through protocols +protocol EventTracking { + func track(event: String, properties: Properties?) +} + +protocol UserIdentification { + var distinctId: String { get set } + func identify(distinctId: String) +} + +protocol Flushable { + func flush(completion: (() -> Void)?) +} + +// Compose into main type +typealias MixpanelProtocol = EventTracking & UserIdentification & Flushable +``` + +### 2. Protocol Extensions +```swift +// Provide default implementations +extension EventTracking { + func track(event: String) { + track(event: event, properties: nil) + } +} + +// Conditional extensions +extension Collection where Element == Event { + func filterValid() -> [Event] { + return self.filter { event in + !event.name.isEmpty && + event.properties.allSatisfy { MixpanelType.isValidType($0.value) } + } + } +} +``` + +## Generics + +### 1. Type-Safe Storage +```swift +class TypedStorage { + private let key: String + private let defaults = UserDefaults.standard + + init(key: String) { + self.key = key + } + + var value: T? { + get { + guard let data = defaults.data(forKey: key) else { return nil } + return try? JSONDecoder().decode(T.self, from: data) + } + set { + guard let newValue = newValue else { + defaults.removeObject(forKey: key) + return + } + let data = try? JSONEncoder().encode(newValue) + defaults.set(data, forKey: key) + } + } +} + +// Usage +let superPropertiesStorage = TypedStorage( + key: "mixpanel.superProperties" +) +``` + +### 2. Generic Constraints +```swift +// Constrain to specific protocols +func updateCollection(_ collection: inout C, + with elements: C) + where C.Element: MixpanelType { + collection.removeAll() + collection.append(contentsOf: elements) +} +``` + +## Result Builders (Function Builders) + +Could be used for building complex events: + +```swift +@resultBuilder +struct EventBuilder { + static func buildBlock(_ components: Property...) -> Properties { + return components.reduce(into: [:]) { result, property in + result[property.key] = property.value + } + } + + static func buildOptional(_ component: Property?) -> Property { + return component ?? Property(key: "", value: NSNull()) + } + + static func buildEither(first component: Property) -> Property { + return component + } + + static func buildEither(second component: Property) -> Property { + return component + } +} + +struct Property { + let key: String + let value: MixpanelType +} + +// Usage +@EventBuilder +func buildPurchaseProperties(item: Item, user: User?) -> Properties { + Property(key: "item_id", value: item.id) + Property(key: "item_name", value: item.name) + Property(key: "price", value: item.price) + + if let user = user { + Property(key: "user_tier", value: user.tier) + } +} +``` + +## Enums with Associated Values + +### 1. Event Types +```swift +enum MixpanelEvent { + case standard(name: String, properties: Properties?) + case timed(name: String, properties: Properties?, startTime: Date) + case people(operation: PeopleOperation) + case group(key: String, id: String, operation: GroupOperation) + + var eventName: String { + switch self { + case .standard(let name, _), .timed(let name, _, _): + return name + case .people: + return "$people" + case .group: + return "$group" + } + } +} +``` + +### 2. Operation Types +```swift +enum PeopleOperation { + case set(properties: Properties) + case setOnce(properties: Properties) + case unset(keys: [String]) + case increment(properties: [String: Double]) + case append(properties: Properties) + case union(properties: [String: [MixpanelType]]) + case remove(properties: Properties) + case deleteUser + + var apiName: String { + switch self { + case .set: return "$set" + case .setOnce: return "$set_once" + case .unset: return "$unset" + case .increment: return "$add" + case .append: return "$append" + case .union: return "$union" + case .remove: return "$remove" + case .deleteUser: return "$delete" + } + } +} +``` + +## Closures and Functional Programming + +### 1. Higher-Order Functions +```swift +extension Array where Element == Event { + // Transform events + func mapProperties(_ transform: (Properties) -> Properties) -> [Event] { + return self.map { event in + var modified = event + modified.properties = transform(event.properties) + return modified + } + } + + // Filter by predicate + func whereProperty(_ key: String, + matches predicate: (MixpanelType) -> Bool) -> [Event] { + return self.filter { event in + guard let value = event.properties[key] else { return false } + return predicate(value) + } + } +} + +// Usage +let premiumEvents = events + .whereProperty("user_tier") { ($0 as? String) == "premium" } + .mapProperties { props in + var modified = props + modified["is_premium"] = true + return modified + } +``` + +### 2. Functional Composition +```swift +// Compose operations +typealias PropertyTransform = (Properties) -> Properties + +func compose(_ transforms: PropertyTransform...) -> PropertyTransform { + return { properties in + transforms.reduce(properties) { result, transform in + transform(result) + } + } +} + +// Predefined transforms +let addTimestamp: PropertyTransform = { props in + var modified = props + modified["timestamp"] = Date() + return modified +} + +let addDeviceInfo: PropertyTransform = { props in + var modified = props + modified["device_model"] = UIDevice.current.model + return modified +} + +// Usage +let enhanceProperties = compose(addTimestamp, addDeviceInfo) +let enhanced = enhanceProperties(baseProperties) +``` + +## Swift Concurrency (Future) + +Current SDK uses GCD, but could migrate to async/await: + +```swift +// Future async/await pattern +extension MixpanelInstance { + func track(event: String, properties: Properties?) async { + await withCheckedContinuation { continuation in + trackingQueue.async { [weak self] in + self?.performTracking(event, properties) + continuation.resume() + } + } + } + + func flush() async throws { + try await withCheckedThrowingContinuation { continuation in + networkQueue.async { [weak self] in + self?.performFlush { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } + } +} + +// Actor for thread safety +actor EventQueue { + private var events: [Event] = [] + + func add(_ event: Event) { + events.append(event) + } + + func drain() -> [Event] { + let current = events + events.removeAll() + return current + } +} +``` + +## KeyPaths + +### 1. Type-Safe Property Access +```swift +extension People { + func increment(_ keyPath: WritableKeyPath, + by amount: T) { + // Implementation using keyPath + } +} + +// Usage +people.increment(\.purchaseCount, by: 1) +people.increment(\.totalSpent, by: 29.99) +``` + +### 2. Dynamic Member Lookup +```swift +@dynamicMemberLookup +struct SuperProperties { + private var storage: Properties = [:] + + subscript(dynamicMember key: String) -> MixpanelType? { + get { storage[key] } + set { storage[key] = newValue } + } +} + +// Usage +var superProps = SuperProperties() +superProps.userId = "12345" // Dynamic property +superProps.plan = "premium" +``` + +## Codable + +### 1. Event Serialization +```swift +struct Event: Codable { + let event: String + let properties: Properties + + // Custom encoding for API format + enum CodingKeys: String, CodingKey { + case event + case properties + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(event, forKey: .event) + + // Convert properties to API format + let apiProperties = properties.mapValues { $0.toAPIObject() } + try container.encode(apiProperties, forKey: .properties) + } +} +``` + +### 2. Configuration Storage +```swift +struct MixpanelConfiguration: Codable { + let token: String + let flushInterval: TimeInterval + let trackAutomaticEvents: Bool + let useIPAddressForGeoLocation: Bool + + // Save to disk + func save() throws { + let encoder = JSONEncoder() + let data = try encoder.encode(self) + try data.write(to: configurationURL) + } + + // Load from disk + static func load() throws -> MixpanelConfiguration { + let data = try Data(contentsOf: configurationURL) + let decoder = JSONDecoder() + return try decoder.decode(MixpanelConfiguration.self, from: data) + } +} +``` + +## Memory Management + +### 1. Weak References +```swift +class NotificationObserver { + private weak var instance: MixpanelInstance? + + init(instance: MixpanelInstance) { + self.instance = instance + setupObservers() + } + + private func setupObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleNotification), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + } + + @objc private func handleNotification() { + instance?.flush() // Safe - won't retain instance + } +} +``` + +### 2. Capture Lists +```swift +// Always use capture lists in closures +trackingQueue.async { [weak self] in + guard let self = self else { return } + self.processEvents() +} + +// Capture specific values to avoid self +let currentToken = self.apiToken +networkQueue.async { [currentToken] in + // Use currentToken, not self.apiToken + sendRequest(token: currentToken) +} +``` + +## Attributes + +### 1. Availability +```swift +@available(iOS 13.0, *) +func trackWithCombine(event: String) -> AnyPublisher { + // Combine framework integration +} + +@available(*, deprecated, renamed: "track(event:properties:)") +func trackEvent(_ event: String) { + track(event: event, properties: nil) +} +``` + +### 2. Objective-C Bridging +```swift +@objc(MixpanelSDK) +public class Mixpanel: NSObject { + @objc public static func track(event: String) { + mainInstance().track(event: event) + } +} + +// Rename for Objective-C +@objc(trackEventWithName:properties:) +func track(event: String, properties: Properties?) { + // Implementation +} +``` \ No newline at end of file diff --git a/claude/workflows/release-process.md b/claude/workflows/release-process.md new file mode 100644 index 00000000..937e40ae --- /dev/null +++ b/claude/workflows/release-process.md @@ -0,0 +1,309 @@ +# Complete Release Process Workflow + +## Pre-Release Checklist + +### 1. Code Quality +- [ ] All tests passing on all platforms +- [ ] No SwiftLint warnings +- [ ] Code coverage above 80% +- [ ] No compiler warnings +- [ ] API documentation complete + +### 2. Testing Matrix +Run tests on: +- [ ] iOS 11.0 (minimum) on oldest device +- [ ] iOS 17.0 (latest) on newest device +- [ ] macOS 10.13 (minimum) +- [ ] macOS 14.0 (latest) +- [ ] tvOS 11.0 and latest +- [ ] watchOS 4.0 and latest + +### 3. Integration Testing +- [ ] CocoaPods integration works +- [ ] Carthage build succeeds +- [ ] Swift Package Manager resolves +- [ ] Demo apps run without issues + +## Release Steps + +### Step 1: Version Bump +Update version in THREE places: + +```bash +# 1. Podspec +vim Mixpanel-swift.podspec +# Change: s.version = "5.0.0" + +# 2. Info.plist +vim Info.plist +# Change: CFBundleShortVersionString +# 5.0.0 + +# 3. Source code +vim Sources/AutomaticProperties.swift +# Change: "$lib_version": "5.0.0" +``` + +### Step 2: Update CHANGELOG + +```markdown +## [5.0.0] - 2024-01-30 + +### Added +- New feature X with example usage +- Support for Y platform + +### Changed +- Improved performance of Z by 50% +- Updated minimum iOS version to 11.0 + +### Fixed +- Fixed crash when tracking events with nil properties (#123) +- Resolved memory leak in background flush (#456) + +### Deprecated +- `oldMethod()` - use `newMethod()` instead + +### Removed +- Removed support for iOS 10.0 +``` + +### Step 3: Final Testing + +```bash +# Clean build folder +rm -rf ~/Library/Developer/Xcode/DerivedData/Mixpanel-* + +# Test all platforms +xcodebuild test -scheme MixpanelDemo -destination 'platform=iOS Simulator,name=iPhone 15' +xcodebuild test -scheme MixpanelDemoMac +xcodebuild test -scheme MixpanelDemoTV -destination 'platform=tvOS Simulator,name=Apple TV' + +# Verify demo apps +open MixpanelDemo/MixpanelDemo.xcodeproj +# Build and run each target +``` + +### Step 4: Generate Documentation + +```bash +# Install jazzy if needed +gem install jazzy + +# Generate docs +./scripts/generate_docs.sh + +# Verify documentation +open docs/index.html + +# Check for undocumented symbols +cat docs/undocumented.json +``` + +### Step 5: Create Release Commit + +```bash +# Stage all changes +git add -A + +# Commit with version +git commit -m "Version 5.0.0 + +- Add feature X +- Improve performance +- Fix critical bugs + +See CHANGELOG.md for details" +``` + +### Step 6: Tag Release + +```bash +# Create annotated tag +git tag -a v5.0.0 -m "Version 5.0.0 + +Major release with: +- Feature X +- Performance improvements +- Bug fixes" + +# Verify tag +git show v5.0.0 +``` + +### Step 7: Push to GitHub + +```bash +# Push commits +git push origin main + +# Push tag +git push origin v5.0.0 +``` + +### Step 8: Create GitHub Release + +1. Go to https://github.com/mixpanel/mixpanel-swift/releases +2. Click "Draft a new release" +3. Select tag: v5.0.0 +4. Title: "Version 5.0.0" +5. Copy CHANGELOG entry to description +6. Add migration guide if breaking changes +7. Attach any binaries if needed +8. Click "Publish release" + +### Step 9: Publish to CocoaPods + +```bash +# Validate podspec +pod lib lint Mixpanel-swift.podspec + +# If validation passes, push to trunk +pod trunk push Mixpanel-swift.podspec + +# Verify on CocoaPods +open https://cocoapods.org/pods/Mixpanel-swift +``` + +### Step 10: Verify Carthage + +```bash +# In a test project +echo 'github "mixpanel/mixpanel-swift" ~> 5.0.0' > Cartfile +carthage update --platform iOS + +# Verify framework built +ls Carthage/Build/iOS/Mixpanel.framework +``` + +### Step 11: Verify Swift Package Manager + +```swift +// In Package.swift of test project +dependencies: [ + .package( + url: "https://github.com/mixpanel/mixpanel-swift.git", + from: "5.0.0" + ) +] + +// Then verify +swift package resolve +swift build +``` + +## Post-Release + +### 1. Monitor for Issues +- Watch GitHub issues for problems +- Monitor crash reporting services +- Check CocoaPods quality metrics + +### 2. Update Documentation Site +- Update version in documentation +- Add migration guide if needed +- Update code examples + +### 3. Announce Release +- Post in company Slack/communication channels +- Update internal documentation +- Notify major customers if breaking changes + +### 4. Plan Next Release +- Create milestone for next version +- Triage incoming issues +- Plan feature roadmap + +## Hotfix Process + +For critical bugs in released version: + +### 1. Create Hotfix Branch +```bash +# From the release tag +git checkout -b hotfix/5.0.1 v5.0.0 +``` + +### 2. Fix Issue +```bash +# Make minimal changes +vim Sources/BuggyFile.swift + +# Add test for the fix +vim MixpanelDemoTests/HotfixTests.swift + +# Verify fix +xcodebuild test -scheme MixpanelDemo +``` + +### 3. Update Version +```bash +# Bump patch version in all 3 places +# 5.0.0 → 5.0.1 +``` + +### 4. Release Hotfix +```bash +# Commit +git commit -am "Fix critical bug in event tracking" + +# Tag +git tag -a v5.0.1 -m "Hotfix 5.0.1: Fix critical bug" + +# Push +git push origin hotfix/5.0.1 +git push origin v5.0.1 + +# Merge back to main +git checkout main +git merge hotfix/5.0.1 +git push origin main +``` + +### 5. Fast-Track Publishing +- Skip beta testing for critical fixes +- Immediately push to CocoaPods +- Update GitHub release notes +- Notify affected users + +## Rollback Process + +If critical issue found after release: + +### 1. Immediate Actions +```bash +# Yank from CocoaPods (if within 24 hours) +pod trunk delete Mixpanel-swift 5.0.0 + +# Mark as pre-release on GitHub +# Edit release and check "This is a pre-release" +``` + +### 2. Fix Forward +- Create hotfix version 5.0.1 +- Include fix for the issue +- Add regression test +- Release as soon as possible + +### 3. Communication +- Post known issues in GitHub +- Email major customers +- Update status page if available + +## Automation Script + +The release script automates many steps: + +```bash +python scripts/release.py --old 4.9.0 --new 5.0.0 + +# What it does: +# 1. Updates version in all files +# 2. Generates documentation +# 3. Creates git commit +# 4. Tags release +# 5. Pushes to GitHub +# 6. Publishes to CocoaPods +``` + +Use manual process for major releases or when careful review needed. \ No newline at end of file