diff --git a/.changeset/v4-remove-legacy-queue-migration.md b/.changeset/v4-remove-legacy-queue-migration.md new file mode 100644 index 000000000..b517e957e --- /dev/null +++ b/.changeset/v4-remove-legacy-queue-migration.md @@ -0,0 +1,9 @@ +--- +'posthog-ios': major +--- + +**Breaking:** remove the migration of the event queue written by SDK 2.x (`posthog.queue.plist`). + +Events still queued on disk by a 2.x SDK are deleted instead of sent when the app updates straight from 2.x to 4.0. Distinct ID, anonymous ID, groups and cached feature flags are still carried over. Events created by a 3.x SDK are not affected. + +Queued events are no longer read in the 2.x format: a top-level `$set` and `message_id` are ignored. This includes 2.x events that 3.x imported and had not sent yet when the app updated to 4.0. diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 105f56817..c8ac8ab70 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ BF5E00000000000000000002 /* PHObjCExceptionCatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BF5E00000000000000000006 /* PHObjCExceptionCatcher.h */; settings = {ATTRIBUTES = (Private, ); }; }; BF5E00000000000000000003 /* PHObjCExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5E00000000000000000007 /* PHObjCExceptionCatcher.m */; }; D0E500012FA0000100000002 /* PostHogEventSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E500012FA0000100000001 /* PostHogEventSnapshotTests.swift */; }; + D0E500022FA0000100000002 /* PostHogEventTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E500022FA0000100000001 /* PostHogEventTest.swift */; }; D0E500012FA0000100000021 /* event-shapes-batch.json in Resources */ = {isa = PBXBuildFile; fileRef = D0E500012FA0000100000011 /* event-shapes-batch.json */; }; D0E500012FA0000100000022 /* feature-flags-request.json in Resources */ = {isa = PBXBuildFile; fileRef = D0E500012FA0000100000012 /* feature-flags-request.json */; }; D0E500012FA0000100000023 /* session-replay-request.json in Resources */ = {isa = PBXBuildFile; fileRef = D0E500012FA0000100000013 /* session-replay-request.json */; }; @@ -95,8 +96,6 @@ 6A11C0DE2026092800000011 /* PostHogSurveySheetDetentsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A11C0DE2026092800000012 /* PostHogSurveySheetDetentsTest.swift */; }; 6A11C0DE2026092800000003 /* PostHogHingeStatusTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A11C0DE2026092800000004 /* PostHogHingeStatusTest.swift */; }; 690FF0BF2AEFA97F00A0B06B /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0BE2AEFA97F00A0B06B /* FileUtils.swift */; }; - 690FF0C52AEFAE8200A0B06B /* PostHogLegacyQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0C42AEFAE8200A0B06B /* PostHogLegacyQueue.swift */; }; - 690FF0DF2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0DE2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift */; }; 690FF0E12AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0E02AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift */; }; 690FF0E32AEFD12900A0B06B /* PostHogConfigTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */; }; 6A0C1F2E3B4D5A6B7C8D9E02 /* PostHogOptOutPersistenceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0C1F2E3B4D5A6B7C8D9E01 /* PostHogOptOutPersistenceTest.swift */; }; @@ -864,6 +863,7 @@ BF5E00000000000000000006 /* PHObjCExceptionCatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PHObjCExceptionCatcher.h; sourceTree = ""; }; BF5E00000000000000000007 /* PHObjCExceptionCatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PHObjCExceptionCatcher.m; sourceTree = ""; }; D0E500012FA0000100000001 /* PostHogEventSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogEventSnapshotTests.swift; sourceTree = ""; }; + D0E500022FA0000100000001 /* PostHogEventTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogEventTest.swift; sourceTree = ""; }; D0E500012FA0000100000011 /* event-shapes-batch.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; path = "event-shapes-batch.json"; sourceTree = ""; }; D0E500012FA0000100000012 /* feature-flags-request.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; path = "feature-flags-request.json"; sourceTree = ""; }; D0E500012FA0000100000013 /* session-replay-request.json */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.json; path = "session-replay-request.json"; sourceTree = ""; }; @@ -958,8 +958,6 @@ 6A11C0DE2026092800000012 /* PostHogSurveySheetDetentsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSurveySheetDetentsTest.swift; sourceTree = ""; }; 6A11C0DE2026092800000004 /* PostHogHingeStatusTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogHingeStatusTest.swift; sourceTree = ""; }; 690FF0BE2AEFA97F00A0B06B /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; - 690FF0C42AEFAE8200A0B06B /* PostHogLegacyQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogLegacyQueue.swift; sourceTree = ""; }; - 690FF0DE2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogLegacyQueueTest.swift; sourceTree = ""; }; 690FF0E02AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogFileBackedQueueTest.swift; sourceTree = ""; }; 690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogConfigTest.swift; sourceTree = ""; }; 6A0C1F2E3B4D5A6B7C8D9E01 /* PostHogOptOutPersistenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogOptOutPersistenceTest.swift; sourceTree = ""; }; @@ -1808,7 +1806,6 @@ 6926DA8D2ADD2876005760D2 /* PostHogContext.swift */, 6A11C0DE2026092800000002 /* PostHogHingeStatusObserver.swift */, 69779BEB2AE68E6900D7A48E /* UIViewController.swift */, - 690FF0C42AEFAE8200A0B06B /* PostHogLegacyQueue.swift */, 69F518372BB2BA0100F52C14 /* PostHogSwizzler.swift */, 69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */, B5EB989F28A201A9F44026EE /* PostHogCompression.swift */, @@ -1847,6 +1844,7 @@ B74800000000000000000004 /* PostHogTests-Bridging-Header.h */, B74800000000000000000003 /* PostHogPushNotificationSwizzlingTest.swift */, D0E500012FA0000100000001 /* PostHogEventSnapshotTests.swift */, + D0E500022FA0000100000001 /* PostHogEventTest.swift */, DA27AEC82F56B3F3002A59CE /* PostHogSamplingTest.swift */, DA27AEF12F56B3F3002A59CE /* PostHogGeoIpTest.swift */, DB7100052F80000100000001 /* PostHogTracingHeadersTest.swift */, @@ -1879,7 +1877,6 @@ 6A11C0DE2026092300000002 /* PostHogContextScreenSizeTest.swift */, 6A11C0DE2026092800000012 /* PostHogSurveySheetDetentsTest.swift */, 6A11C0DE2026092800000004 /* PostHogHingeStatusTest.swift */, - 690FF0DE2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift */, DA4FFB142DA93C78006BAEEA /* PostHogSessionReplayTest.swift */, DA4FFB182DA93C78006BAEEA /* PostHogReplayScreenshotDedupTest.swift */, EA5700022DA93C78006BAEEA /* PostHogReplayCaptureTouchesTest.swift */, @@ -3623,7 +3620,6 @@ DA3BB5052EDF15320097A97A /* PostHogStackFrame.swift in Sources */, DACC08BB2D6E3C6D00115E46 /* PostHogIntegration.swift in Sources */, DAD5DD0C2CB6DEF30087387B /* PostHogMaskViewModifier.swift in Sources */, - 690FF0C52AEFAE8200A0B06B /* PostHogLegacyQueue.swift in Sources */, 3AE3FB332991388500AFFC18 /* PostHogQueue.swift in Sources */, DA1044882D0B1D2D00C4ACF3 /* View+PostHogLabel.swift in Sources */, DAA1B2F12F8C100000112233 /* View+PostHogNoRageClick.swift in Sources */, @@ -3751,6 +3747,7 @@ BF5E00000000000000000001 /* PHBeforeSendExceptionTestFixture.m in Sources */, B74800000000000000000002 /* PHNotificationDelegateTestFixture.m in Sources */, D0E500012FA0000100000002 /* PostHogEventSnapshotTests.swift in Sources */, + D0E500022FA0000100000002 /* PostHogEventTest.swift in Sources */, DA979D7B2CD370B700F56BAE /* PostHogAutocaptureEventTrackerSpec.swift in Sources */, DA0C944B2D54CDD000BFD9FB /* PostHogStorageMigrationTest.swift in Sources */, 690FF0F52AF0F06100A0B06B /* PostHogSDKTest.swift in Sources */, @@ -3809,7 +3806,6 @@ DADE7DFE2F7EA90D00C4650C /* PostHogDeepLinkIntegrationTests.swift in Sources */, DA52CAEF2F24319800305F3D /* PostHogSessionReplayPluginTest.swift in Sources */, DA52CAEF2F2431980030AA01 /* PostHogConsoleLogInterceptorTest.swift in Sources */, - 690FF0DF2AEFBC5700A0B06B /* PostHogLegacyQueueTest.swift in Sources */, DA6F24822D4A6CA100CA2777 /* PostHogApiTest.swift in Sources */, AE3C6A5B6DAB01EF554A0270 /* PostHogPushNotificationTest.swift in Sources */, D36F92424A13D7CE61B6E348 /* PostHogSwizzlerTest.swift in Sources */, diff --git a/PostHog/Models/PostHogEvent.swift b/PostHog/Models/PostHogEvent.swift index 6418b3c60..2f47b3a88 100644 --- a/PostHog/Models/PostHogEvent.swift +++ b/PostHog/Models/PostHogEvent.swift @@ -62,18 +62,11 @@ import Foundation let timestampDate = toISO8601Date(timestamp) ?? Date() - var properties = (json["properties"] as? [String: Any]) ?? [:] - - // back compatibility with v2 - let setProps = json["$set"] as? [String: Any] - if setProps != nil { - properties["$set"] = setProps - } + let properties = (json["properties"] as? [String: Any]) ?? [:] guard let distinctId = (json["distinct_id"] as? String) ?? (properties["distinct_id"] as? String) else { return nil } - // `message_id` is deprecated and only accepted for backwards compatibility with legacy persisted events. - let uuid = ((json["uuid"] as? String) ?? (json["message_id"] as? String)) ?? UUID.v7String() + let uuid = (json["uuid"] as? String) ?? UUID.v7String() let uuidObj = UUID(uuidString: uuid) ?? UUID.v7() // Wire field name remains api_key, but it carries the PostHog project token. diff --git a/PostHog/PostHogFileBackedQueue.swift b/PostHog/PostHogFileBackedQueue.swift index 5ea44b3af..9206ca0a0 100644 --- a/PostHog/PostHogFileBackedQueue.swift +++ b/PostHog/PostHogFileBackedQueue.swift @@ -43,7 +43,7 @@ class PostHogFileBackedQueue { migrateOldQueueFolder(queue: queue, oldQueueFolder: oldQueue) } else { // old plist file - migrateOldQueue(queue: queue, oldQueue: oldQueue) + deleteSafely(oldQueue) } } } diff --git a/PostHog/PostHogLegacyQueue.swift b/PostHog/PostHogLegacyQueue.swift deleted file mode 100644 index 8b7285259..000000000 --- a/PostHog/PostHogLegacyQueue.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// PostHogLegacyQueue.swift -// PostHog -// -// Created by Manoel Aranda Neto on 30.10.23. -// - -import Foundation - -// Migrates the Old Queue (v2) to the new Queue (v3) -func migrateOldQueue(queue: URL, oldQueue: URL) { - if !FileManager.default.fileExists(atPath: oldQueue.path) { - return - } - - defer { - deleteSafely(oldQueue) - } - - do { - let data = try Data(contentsOf: oldQueue) - guard let array = try JSONSerialization.jsonObject(with: data) as? [Any] else { - hedgeLog("Failed to migrate queue: invalid data format") - return - } - - for item in array { - guard let event = item as? [String: Any] else { - continue - } - let timestamp = event["timestamp"] as? String ?? toISO8601String(Date()) - - let timestampDate = toISO8601Date(timestamp) ?? Date() - - let filename = "\(timestampDate.timeIntervalSince1970)" - - let contents = try JSONSerialization.data(withJSONObject: event) - - try contents.write(to: queue.appendingPathComponent(filename)) - } - } catch { - hedgeLog("Failed to migrate queue \(error)") - } -} diff --git a/PostHogTests/PostHogEventTest.swift b/PostHogTests/PostHogEventTest.swift new file mode 100644 index 000000000..18ef37a43 --- /dev/null +++ b/PostHogTests/PostHogEventTest.swift @@ -0,0 +1,38 @@ +import Foundation +@testable import PostHog +import Testing + +@Suite("PostHogEvent JSON decoding") +struct PostHogEventTest { + @Test("ignores the 2.x top-level $set and message_id") + func ignoresV2Fields() throws { + let messageId = "5CE069F8-E967-4B47-9D89-207EF7519453" + let json: [String: Any] = [ + "event": "test", + "distinct_id": "user", + "properties": ["key": "value"], + "$set": ["plan": "pro"], + "message_id": messageId, + ] + + let event = try #require(PostHogEvent.fromJSON(json)) + + #expect(event.properties["$set"] == nil) + #expect(event.properties["key"] as? String == "value") + #expect(event.uuid.uuidString != messageId) + } + + @Test("reads uuid") + func readsUuid() throws { + let uuid = "019A0000-0000-7000-8000-000000000001" + let json: [String: Any] = [ + "event": "test", + "distinct_id": "user", + "uuid": uuid, + ] + + let event = try #require(PostHogEvent.fromJSON(json)) + + #expect(event.uuid.uuidString == uuid) + } +} diff --git a/PostHogTests/PostHogFileBackedQueueAlignmentTest.swift b/PostHogTests/PostHogFileBackedQueueAlignmentTest.swift index 8ad9e6726..a6b262957 100644 --- a/PostHogTests/PostHogFileBackedQueueAlignmentTest.swift +++ b/PostHogTests/PostHogFileBackedQueueAlignmentTest.swift @@ -31,6 +31,22 @@ struct PostHogFileBackedQueueAlignmentTest { data.map { String(data: $0, encoding: .utf8)! } } + @Test("deletes a v2 queue file without importing its events") + func deletesLegacyQueueFile() throws { + let dir = FileManager.default.temporaryDirectory + .appendingPathComponent("ph-queue-align-\(UUID().uuidString)") + let oldQueue = FileManager.default.temporaryDirectory + .appendingPathComponent("ph-queue-plist-\(UUID().uuidString)") + defer { try? FileManager.default.removeItem(at: dir) } + try Data(#"[{"event":"v2 event","distinct_id":"user","timestamp":"2023-10-25T14:14:04.407Z"}]"#.utf8) + .write(to: oldQueue) + + let queue = PostHogFileBackedQueue(queue: dir, oldQueues: [oldQueue]) + + #expect(!FileManager.default.fileExists(atPath: oldQueue.path)) + #expect(queue.depth == 0) + } + @Test("a failed write to a full queue preserves existing entries", .enabled(if: geteuid() != 0)) func failedWritePreservesFullQueue() throws { let (queue, dir) = makeQueue() diff --git a/PostHogTests/PostHogLegacyQueueTest.swift b/PostHogTests/PostHogLegacyQueueTest.swift deleted file mode 100644 index 9a91ce6a6..000000000 --- a/PostHogTests/PostHogLegacyQueueTest.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// PostHogLegacyQueueTest.swift -// PostHogTests -// -// Created by Manoel Aranda Neto on 30.10.23. -// - -import Foundation -import Nimble -@testable import PostHog -import Quick - -class PostHogLegacyQueueTest: QuickSpec { - override func spec() { - it("migrate old queue to new queue") { - let baseUrl = applicationSupportDirectoryURL() - try FileManager.default.createDirectory(atPath: baseUrl.path, withIntermediateDirectories: true) - - let newURL = baseUrl.appendingPathComponent("queue") - try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) - - let oldURL = baseUrl.appendingPathComponent("oldQueue") - - let eventsArray = - """ - [ - { - "properties": { - "$network_cellular": false, - "$groups": { - "some-group": "id:4" - }, - "$app_build": 1, - "$os_name": "iOS", - "$feature/multivariant": "payload", - "$screen_width": 852, - "$app_version": "1.0", - "$device_type": "Mobile", - "$active_feature_flags__0": "multivariant", - "$feature/4535-funnel-bar-viz": true, - "$network_wifi": true, - "$timezone": "Europe/Vienna", - "$device_id": "48DA3429-495A-4903-B347-F096FC31C3AB", - "$active_feature_flags": [ - "$feature/multivariant", - "$feature/testJson", - "$feature/4535-funnel-bar-viz", - "$feature/disabledFlag" - ], - "$active_feature_flags__3": "disabledFlag", - "$device_name": "iPhone", - "$app_name": "", - "$app_namespace": "com.posthog.CocoapodsExample", - "$locale": "en-US", - "$feature/disabledFlag": true, - "$active_feature_flags__1": "testJson", - "$screen_height": 393, - "$device_model": "arm64", - "$device_manufacturer": "Apple", - "$feature/testJson": "theInteger", - "$lib_version": "2.1.0", - "$os_version": "17.0.1", - "$lib": "posthog-ios", - "$active_feature_flags__2": "4535-funnel-bar-viz" - }, - "timestamp": "2023-10-25T14:14:04.407Z", - "message_id": "5CE069F8-E967-4B47-9D89-207EF7519453", - "event": "Cocoapods Example Button", - "distinct_id": "Prateek" - } - ] - """ - - let eventsData = eventsArray.data(using: .utf8)! - try eventsData.write(to: oldURL) - - expect(FileManager.default.fileExists(atPath: oldURL.path)) == true - - migrateOldQueue(queue: newURL, oldQueue: oldURL) - - expect(FileManager.default.fileExists(atPath: oldURL.path)) == false - - let items = try FileManager.default.contentsOfDirectory(atPath: newURL.path) - - let eventURL = newURL.appendingPathComponent(items[0]) - expect(FileManager.default.fileExists(atPath: eventURL.path)) == true - - let eventData = try Data(contentsOf: eventURL) - let eventObject = try JSONSerialization.jsonObject(with: eventData, options: .allowFragments) as? [String: Any] - - expect(eventObject!["distinct_id"] as? String) == "Prateek" - expect(eventObject!["event"] as? String) == "Cocoapods Example Button" - expect(eventObject!["message_id"] as? String) == "5CE069F8-E967-4B47-9D89-207EF7519453" - expect(eventObject!["timestamp"] as? String) == "2023-10-25T14:14:04.407Z" - expect(eventObject!["properties"] as? [String: Any]) != nil - - deleteSafely(oldURL) - deleteSafely(newURL) - } - - it("ignore and delete corrupted file") { - let baseUrl = applicationSupportDirectoryURL() - try FileManager.default.createDirectory(atPath: baseUrl.path, withIntermediateDirectories: true) - - let newURL = baseUrl.appendingPathComponent("queue") - try FileManager.default.createDirectory(atPath: newURL.path, withIntermediateDirectories: true) - - let oldURL = baseUrl.appendingPathComponent("oldQueue") - - let eventsArray = - """ - [ - i am broken - ] - """ - - let eventsData = eventsArray.data(using: .utf8)! - try eventsData.write(to: oldURL) - - expect(FileManager.default.fileExists(atPath: oldURL.path)) == true - - migrateOldQueue(queue: newURL, oldQueue: oldURL) - - expect(FileManager.default.fileExists(atPath: oldURL.path)) == false - - let items = try FileManager.default.contentsOfDirectory(atPath: newURL.path) - expect(items.isEmpty) == true - - deleteSafely(oldURL) - deleteSafely(newURL) - } - } -} diff --git a/PostHogTests/PostHogStorageTest.swift b/PostHogTests/PostHogStorageTest.swift index 9bbed3cb3..80cf1e943 100644 --- a/PostHogTests/PostHogStorageTest.swift +++ b/PostHogTests/PostHogStorageTest.swift @@ -124,6 +124,28 @@ class PostHogStorageTest { sut.reset() } + @Test("deletes a v2 queue file left in the legacy base folder without importing it") + func deletesLegacyV2QueueFileFromBaseFolder() throws { + let fileManager = FileManager.default + let baseUrl = applicationSupportDirectoryURL() + try fileManager.createDirectory(at: baseUrl, withIntermediateDirectories: true) + let legacyPlist = baseUrl.appendingPathComponent(PostHogStorage.StorageKey.oldQueuePlist.rawValue) + try Data(#"[{"event":"v2 event","distinct_id":"user","timestamp":"2023-10-25T14:14:04.407Z"}]"#.utf8) + .write(to: legacyPlist) + + let storage = PostHogStorage(PostHogConfig(projectToken: "v2_queue_\(UUID().uuidString)")) + defer { try? fileManager.removeItem(at: storage.appFolderUrl) } + // Same keys as the `/batch` endpoint. + let queue = PostHogFileBackedQueue( + queue: storage.url(forKey: .queue), + oldQueues: [.oldQueueFolder, .oldQueuePlist].map { storage.url(forKey: $0) } + ) + + #expect(!fileManager.fileExists(atPath: legacyPlist.path)) + #expect(!fileManager.fileExists(atPath: storage.url(forKey: .oldQueuePlist).path)) + #expect(queue.depth == 0) + } + @Test("writes to disk in a project token folder under application support directory") func writesToDiskInAProjectTokenFolderUnderApplicationSupportDirectory() { let config = PostHogConfig(projectToken: "test_project_token")