From 037e62350407325f23bef71618230d560b2fa332 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 17 Jun 2024 10:57:10 +0200 Subject: [PATCH 1/5] chore: migrate UUID from v4 to v7 --- CHANGELOG.md | 6 +- PostHog.xcodeproj/project.pbxproj | 8 +++ PostHog/Models/PostHogEvent.swift | 6 +- PostHog/PostHogSDK.swift | 2 +- PostHog/PostHogSessionManager.swift | 3 +- PostHog/Utils/UUIDUtils.swift | 59 ++++++++++++++++++++ PostHogTests/PostHogSessionManagerTest.swift | 4 +- PostHogTests/UUIDTest.swift | 35 ++++++++++++ 8 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 PostHog/Utils/UUIDUtils.swift create mode 100644 PostHogTests/UUIDTest.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cb6bb8e9..ac345bcfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,16 @@ ## Next +- chore: migrate UUID from v4 to v7 ([#142](https://github.com/PostHog/posthog-ios/pull/142)) + ## 3.5.1 - 2024-06-12 -- recording: fix `screenshotMode` typo ([#143](https://github.com/PostHog/posthog-android/pull/143)) +- recording: fix `screenshotMode` typo ([#143](https://github.com/PostHog/posthog-ios/pull/143)) ## 3.5.0 - 2024-06-11 - chore: change host to new address ([#139](https://github.com/PostHog/posthog-ios/pull/139)) - fix: rename groupProperties to groups for capture methods ([#140](https://github.com/PostHog/posthog-ios/pull/140)) -- recording: add `screenshotMode` option for session replay instead of wireframe ([#142](https://github.com/PostHog/posthog-android/pull/142)) +- recording: add `screenshotMode` option for session replay instead of wireframe ([#142](https://github.com/PostHog/posthog-ios/pull/142)) ## 3.4.0 - 2024-05-23 diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 6d1a5da44..27aa25850 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -81,6 +81,8 @@ 699991942AFE1B56000DCB78 /* PostHog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; }; 699991952AFE1B56000DCB78 /* PostHog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AC745B5296D6FE60025C109 /* PostHog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6999919A2AFE1BAB000DCB78 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699991992AFE1BAB000DCB78 /* AppDelegate.swift */; }; + 699C5FE62C20178E007DB818 /* UUIDUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699C5FE52C20178E007DB818 /* UUIDUtils.swift */; }; + 699C5FEF2C20242A007DB818 /* UUIDTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699C5FEE2C20242A007DB818 /* UUIDTest.swift */; }; 69BA38D72B888E8500AA69D6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */; }; 69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */; }; 69EE82BC2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */; }; @@ -326,6 +328,8 @@ 6999918E2AFE1B39000DCB78 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 699991902AFE1B39000DCB78 /* PostHogExampleMacOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PostHogExampleMacOS.entitlements; sourceTree = ""; }; 699991992AFE1BAB000DCB78 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 699C5FE52C20178E007DB818 /* UUIDUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDUtils.swift; sourceTree = ""; }; + 699C5FEE2C20242A007DB818 /* UUIDTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDTest.swift; sourceTree = ""; }; 69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogReplayIntegration.swift; sourceTree = ""; }; 69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionReplayConfig.swift; sourceTree = ""; }; @@ -470,6 +474,7 @@ 690FF0BE2AEFA97F00A0B06B /* FileUtils.swift */, 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */, 690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */, + 699C5FE52C20178E007DB818 /* UUIDUtils.swift */, ); path = Utils; sourceTree = ""; @@ -548,6 +553,7 @@ 690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */, 690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */, 690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */, + 699C5FEE2C20242A007DB818 /* UUIDTest.swift */, ); path = PostHogTests; sourceTree = ""; @@ -1081,6 +1087,7 @@ 69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */, 3AE3FB472992AB0000AFFC18 /* Hedgelog.swift in Sources */, 69261D132AD5685B00232EC7 /* PostHogFeatureFlags.swift in Sources */, + 699C5FE62C20178E007DB818 /* UUIDUtils.swift in Sources */, 69261D1D2AD967CD00232EC7 /* PostHogFileBackedQueue.swift in Sources */, 3AE3FB432992985A00AFFC18 /* Reachability.swift in Sources */, 69F518122BAC783300F52C14 /* CGColor+Util.swift in Sources */, @@ -1094,6 +1101,7 @@ 690FF0F52AF0F06100A0B06B /* PostHogSDKTest.swift in Sources */, 690FF0E12AEFC59100A0B06B /* PostHogFileBackedQueueTest.swift in Sources */, 3A62647529CB0168007E8C07 /* TestPostHog.swift in Sources */, + 699C5FEF2C20242A007DB818 /* UUIDTest.swift in Sources */, 3A62646A29C9E385007E8C07 /* MockPostHogServer.swift in Sources */, 690FF0BB2AEF8B8200A0B06B /* PostHogContextTest.swift in Sources */, 690FF0E32AEFD12900A0B06B /* PostHogConfigTest.swift in Sources */, diff --git a/PostHog/Models/PostHogEvent.swift b/PostHog/Models/PostHogEvent.swift index 3446cde87..2ed7bd611 100644 --- a/PostHog/Models/PostHogEvent.swift +++ b/PostHog/Models/PostHogEvent.swift @@ -25,7 +25,7 @@ public class PostHogEvent { case apiKey } - init(event: String, distinctId: String, properties: [String: Any]? = nil, timestamp: Date = Date(), uuid: UUID = .init(), apiKey: String? = nil) { + init(event: String, distinctId: String, properties: [String: Any]? = nil, timestamp: Date = Date(), uuid: UUID = UUID.v7(), apiKey: String? = nil) { self.event = event self.distinctId = distinctId self.properties = properties ?? [:] @@ -61,8 +61,8 @@ public class PostHogEvent { guard let distinctId = (json["distinct_id"] as? String) ?? (properties["distinct_id"] as? String) else { return nil } - let uuid = ((json["uuid"] as? String) ?? (json["message_id"] as? String)) ?? UUID().uuidString - let uuidObj = UUID(uuidString: uuid) ?? UUID() + let uuid = ((json["uuid"] as? String) ?? (json["message_id"] as? String)) ?? UUID.v7().uuidString + let uuidObj = UUID(uuidString: uuid) ?? UUID.v7() let apiKey = json["api_key"] as? String diff --git a/PostHog/PostHogSDK.swift b/PostHog/PostHogSDK.swift index 599cc9c5e..56009e92f 100644 --- a/PostHog/PostHogSDK.swift +++ b/PostHog/PostHogSDK.swift @@ -733,7 +733,7 @@ private let sessionChangeThreshold: TimeInterval = 60 * 30 } private func rotateSession() { - let newSessionId = UUID().uuidString + let newSessionId = UUID.v7().uuidString let newSessionLastTimestamp = now().timeIntervalSince1970 sessionLock.withLock { diff --git a/PostHog/PostHogSessionManager.swift b/PostHog/PostHogSessionManager.swift index 72ea396f2..67f76d67f 100644 --- a/PostHog/PostHogSessionManager.swift +++ b/PostHog/PostHogSessionManager.swift @@ -25,7 +25,8 @@ class PostHogSessionManager { anonymousId = storage.getString(forKey: .anonymousId) if anonymousId == nil { - anonymousId = idGen(UUID()).uuidString + let uuid = UUID.v7() + anonymousId = idGen(uuid).uuidString setAnonId(anonymousId ?? "") } } diff --git a/PostHog/Utils/UUIDUtils.swift b/PostHog/Utils/UUIDUtils.swift new file mode 100644 index 000000000..c7888eb9c --- /dev/null +++ b/PostHog/Utils/UUIDUtils.swift @@ -0,0 +1,59 @@ +// +// UUIDUtils.swift +// PostHog +// +// Created by Manoel Aranda Neto on 17.06.24. +// + +// Inspired and adapted from https://github.com/nthState/UUIDV7/blob/main/Sources/UUIDV7/UUIDV7.swift +// but using SecRandomCopyBytes + +import Foundation + +public extension UUID { + static func v7() -> Self { + let timestamp = Date().timeIntervalSince1970 + let unixTimeMilliseconds = UInt64(timestamp * 1000) + let timeBytes = unixTimeMilliseconds.bigEndianData.suffix(6) // First 6 bytes for the timestamp + + // Prepare the random part (10 bytes to complete the UUID) + var randomBytes = [UInt8](repeating: 0, count: 10) + let status = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes) + // If we can't generate secure random bytes, use a fallback + if status != errSecSuccess { + let randomData = (0 ..< 10).map { _ in UInt8.random(in: 0 ... 255) } + randomBytes = randomData + } + + // Combine parts + var uuidBytes = [UInt8]() + uuidBytes.append(contentsOf: timeBytes) + uuidBytes.append(contentsOf: randomBytes) + + // Set version (7) in the version byte + uuidBytes[6] = (uuidBytes[6] & 0x0F) | 0x70 + + // Set the UUID variant (10xx for standard UUIDs) + uuidBytes[8] = (uuidBytes[8] & 0x3F) | 0x80 + + // Ensure we have a total of 16 bytes + if uuidBytes.count == 16 { + let uuid = UUID(uuid: (uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], + uuidBytes[4], uuidBytes[5], uuidBytes[6], uuidBytes[7], + uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], + uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15])) + return uuid + } else { + // or fallback to UUID v4 + return UUID() + } + } +} + +extension UInt64 { + // Correctly generate Data representation in big endian format + var bigEndianData: Data { + var bigEndianValue = bigEndian + return Data(bytes: &bigEndianValue, count: MemoryLayout.size) + } +} diff --git a/PostHogTests/PostHogSessionManagerTest.swift b/PostHogTests/PostHogSessionManagerTest.swift index a445daebd..4c04a9189 100644 --- a/PostHogTests/PostHogSessionManagerTest.swift +++ b/PostHogTests/PostHogSessionManagerTest.swift @@ -35,7 +35,7 @@ class PostHogSessionManagerTest: QuickSpec { let distinctId = sut.getDistinctId() expect(distinctId) == anonymousId - let idToSet = UUID().uuidString + let idToSet = UUID.v7().uuidString sut.setDistinctId(idToSet) let newAnonymousId = sut.getAnonymousId() let newDistinctId = sut.getDistinctId() @@ -48,7 +48,7 @@ class PostHogSessionManagerTest: QuickSpec { it("Can can accept id customization via config") { let config = PostHogConfig(apiKey: "123") - let fixedUuid = UUID() + let fixedUuid = UUID.v7() config.getAnonymousId = { _ in fixedUuid } let sut = PostHogSessionManager(config) let anonymousId = sut.getAnonymousId() diff --git a/PostHogTests/UUIDTest.swift b/PostHogTests/UUIDTest.swift new file mode 100644 index 000000000..de7364ab2 --- /dev/null +++ b/PostHogTests/UUIDTest.swift @@ -0,0 +1,35 @@ +// +// UUIDTest.swift +// PostHogTests +// +// Created by Manoel Aranda Neto on 17.06.24. +// + +import Foundation + +import Foundation +import Nimble +@testable import PostHog +import Quick + +@available(iOS 17, *) +class UUIDTest: QuickSpec { + override func spec() { + it("test duplicated") { + let count = 10000 + + var created: [UUID] = [] + for _ in 0 ..< count { + created.append(UUID.v7()) + } + + var unique: Set = Set(minimumCapacity: count) + + for i in 0 ..< created.count { + if !unique.insert(created[i]).inserted { + fatalError("Duplicate at index \(i)") + } + } + } + } +} From 38fb27b4b9f4ba61218548405dedb6f5d4d10b77 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 17 Jun 2024 10:58:14 +0200 Subject: [PATCH 2/5] fix pr --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac345bcfb..e01cfae02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Next -- chore: migrate UUID from v4 to v7 ([#142](https://github.com/PostHog/posthog-ios/pull/142)) +- chore: migrate UUID from v4 to v7 ([#145](https://github.com/PostHog/posthog-ios/pull/145)) ## 3.5.1 - 2024-06-12 From 0bb0ea6aa7dca8c28700ba7f8502730c6bc007ce Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 17 Jun 2024 11:00:06 +0200 Subject: [PATCH 3/5] remove --- PostHogTests/UUIDTest.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/PostHogTests/UUIDTest.swift b/PostHogTests/UUIDTest.swift index de7364ab2..a811032cf 100644 --- a/PostHogTests/UUIDTest.swift +++ b/PostHogTests/UUIDTest.swift @@ -12,7 +12,6 @@ import Nimble @testable import PostHog import Quick -@available(iOS 17, *) class UUIDTest: QuickSpec { override func spec() { it("test duplicated") { From a6da4cece52f605b84f5fe868108f6073e26572d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 17 Jun 2024 14:29:36 +0200 Subject: [PATCH 4/5] add sort test --- PostHog.xcodeproj/project.pbxproj | 4 + PostHog/Utils/TimeBasedEpochGenerator.swift | 88 +++++++++++++++++++++ PostHog/Utils/UUIDUtils.swift | 44 +---------- PostHogTests/UUIDTest.swift | 69 +++++++++++++++- 4 files changed, 161 insertions(+), 44 deletions(-) create mode 100644 PostHog/Utils/TimeBasedEpochGenerator.swift diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 27aa25850..e05f76dd5 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 3AE3FB49299391DF00AFFC18 /* PostHogStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB48299391DF00AFFC18 /* PostHogStorage.swift */; }; 3AE3FB4B2993A68500AFFC18 /* PostHogStorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB4A2993A68500AFFC18 /* PostHogStorageTest.swift */; }; 3AE3FB4E2993D1D600AFFC18 /* PostHogSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE3FB4D2993D1D600AFFC18 /* PostHogSessionManager.swift */; }; + 690B2DF32C205B5600AE3B45 /* TimeBasedEpochGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690B2DF22C205B5600AE3B45 /* TimeBasedEpochGenerator.swift */; }; 690FF05F2AE7E2D400A0B06B /* Data+Gzip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF05E2AE7E2D400A0B06B /* Data+Gzip.swift */; }; 690FF0AF2AEB9C1400A0B06B /* DateUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */; }; 690FF0B52AEBBD3C00A0B06B /* DictUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */; }; @@ -278,6 +279,7 @@ 3AE3FB48299391DF00AFFC18 /* PostHogStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStorage.swift; sourceTree = ""; }; 3AE3FB4A2993A68500AFFC18 /* PostHogStorageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStorageTest.swift; sourceTree = ""; }; 3AE3FB4D2993D1D600AFFC18 /* PostHogSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionManager.swift; sourceTree = ""; }; + 690B2DF22C205B5600AE3B45 /* TimeBasedEpochGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeBasedEpochGenerator.swift; sourceTree = ""; }; 690FF02F2AE7C5BA00A0B06B /* PostHogExampleWithPods.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleWithPods.xcodeproj; path = PostHogExampleWithPods/PostHogExampleWithPods.xcodeproj; sourceTree = ""; }; 690FF0532AE7DB3700A0B06B /* PostHogExampleWithSPM.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleWithSPM.xcodeproj; path = PostHogExampleWithSPM/PostHogExampleWithSPM.xcodeproj; sourceTree = ""; }; 690FF05E2AE7E2D400A0B06B /* Data+Gzip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Gzip.swift"; sourceTree = ""; }; @@ -475,6 +477,7 @@ 690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */, 690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */, 699C5FE52C20178E007DB818 /* UUIDUtils.swift */, + 690B2DF22C205B5600AE3B45 /* TimeBasedEpochGenerator.swift */, ); path = Utils; sourceTree = ""; @@ -1088,6 +1091,7 @@ 3AE3FB472992AB0000AFFC18 /* Hedgelog.swift in Sources */, 69261D132AD5685B00232EC7 /* PostHogFeatureFlags.swift in Sources */, 699C5FE62C20178E007DB818 /* UUIDUtils.swift in Sources */, + 690B2DF32C205B5600AE3B45 /* TimeBasedEpochGenerator.swift in Sources */, 69261D1D2AD967CD00232EC7 /* PostHogFileBackedQueue.swift in Sources */, 3AE3FB432992985A00AFFC18 /* Reachability.swift in Sources */, 69F518122BAC783300F52C14 /* CGColor+Util.swift in Sources */, diff --git a/PostHog/Utils/TimeBasedEpochGenerator.swift b/PostHog/Utils/TimeBasedEpochGenerator.swift new file mode 100644 index 000000000..2fcf03129 --- /dev/null +++ b/PostHog/Utils/TimeBasedEpochGenerator.swift @@ -0,0 +1,88 @@ +// +// TimeBasedEpochGenerator.swift +// PostHog +// +// Created by Manoel Aranda Neto on 17.06.24. +// + +import Foundation + +class TimeBasedEpochGenerator { + static let shared = TimeBasedEpochGenerator() + + // Private initializer to prevent multiple instances + private init() {} + + private var lastEntropy = [UInt8](repeating: 0, count: 10) + private var lastTimestamp: UInt64 = 0 + + private let lock = NSLock() + + func v7() -> UUID { + var uuid: UUID? + + lock.withLock { + uuid = generateUUID() + } + + // or fallback to UUID v4 + return uuid ?? UUID() + } + + private func generateUUID() -> UUID? { + let timestamp = Date().timeIntervalSince1970 + let unixTimeMilliseconds = UInt64(timestamp * 1000) + + var uuidBytes = [UInt8]() + + let timeBytes = unixTimeMilliseconds.bigEndianData.suffix(6) // First 6 bytes for the timestamp + uuidBytes.append(contentsOf: timeBytes) + + if unixTimeMilliseconds == lastTimestamp { + var c = true + for i in (0 ..< 10).reversed() { + if c { + var temp = lastEntropy[i] + temp = temp &+ 0x01 + c = lastEntropy[i] == 0xFF + lastEntropy[i] = temp + } + } + } else { + lastTimestamp = unixTimeMilliseconds + + // Prepare the random part (10 bytes to complete the UUID) + let status = SecRandomCopyBytes(kSecRandomDefault, lastEntropy.count, &lastEntropy) + // If we can't generate secure random bytes, use a fallback + if status != errSecSuccess { + let randomData = (0 ..< 10).map { _ in UInt8.random(in: 0 ... 255) } + lastEntropy = randomData + } + } + uuidBytes.append(contentsOf: lastEntropy) + + // Set version (7) in the version byte + uuidBytes[6] = (uuidBytes[6] & 0x0F) | 0x70 + + // Set the UUID variant (10xx for standard UUIDs) + uuidBytes[8] = (uuidBytes[8] & 0x3F) | 0x80 + + // Ensure we have a total of 16 bytes + if uuidBytes.count == 16 { + return UUID(uuid: (uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], + uuidBytes[4], uuidBytes[5], uuidBytes[6], uuidBytes[7], + uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], + uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15])) + } + + return nil + } +} + +extension UInt64 { + // Correctly generate Data representation in big endian format + var bigEndianData: Data { + var bigEndianValue = bigEndian + return Data(bytes: &bigEndianValue, count: MemoryLayout.size) + } +} diff --git a/PostHog/Utils/UUIDUtils.swift b/PostHog/Utils/UUIDUtils.swift index c7888eb9c..437ece52f 100644 --- a/PostHog/Utils/UUIDUtils.swift +++ b/PostHog/Utils/UUIDUtils.swift @@ -12,48 +12,6 @@ import Foundation public extension UUID { static func v7() -> Self { - let timestamp = Date().timeIntervalSince1970 - let unixTimeMilliseconds = UInt64(timestamp * 1000) - let timeBytes = unixTimeMilliseconds.bigEndianData.suffix(6) // First 6 bytes for the timestamp - - // Prepare the random part (10 bytes to complete the UUID) - var randomBytes = [UInt8](repeating: 0, count: 10) - let status = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes) - // If we can't generate secure random bytes, use a fallback - if status != errSecSuccess { - let randomData = (0 ..< 10).map { _ in UInt8.random(in: 0 ... 255) } - randomBytes = randomData - } - - // Combine parts - var uuidBytes = [UInt8]() - uuidBytes.append(contentsOf: timeBytes) - uuidBytes.append(contentsOf: randomBytes) - - // Set version (7) in the version byte - uuidBytes[6] = (uuidBytes[6] & 0x0F) | 0x70 - - // Set the UUID variant (10xx for standard UUIDs) - uuidBytes[8] = (uuidBytes[8] & 0x3F) | 0x80 - - // Ensure we have a total of 16 bytes - if uuidBytes.count == 16 { - let uuid = UUID(uuid: (uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], - uuidBytes[4], uuidBytes[5], uuidBytes[6], uuidBytes[7], - uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], - uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15])) - return uuid - } else { - // or fallback to UUID v4 - return UUID() - } - } -} - -extension UInt64 { - // Correctly generate Data representation in big endian format - var bigEndianData: Data { - var bigEndianValue = bigEndian - return Data(bytes: &bigEndianValue, count: MemoryLayout.size) + TimeBasedEpochGenerator.shared.v7() } } diff --git a/PostHogTests/UUIDTest.swift b/PostHogTests/UUIDTest.swift index a811032cf..039584425 100644 --- a/PostHogTests/UUIDTest.swift +++ b/PostHogTests/UUIDTest.swift @@ -13,8 +13,37 @@ import Nimble import Quick class UUIDTest: QuickSpec { + private func compareULongs(_ l1: UInt64, _ l2: UInt64) -> Int { + let high1 = Int32(bitPattern: UInt32((l1 >> 32) & 0xFFFF_FFFF)) + let high2 = Int32(bitPattern: UInt32((l2 >> 32) & 0xFFFF_FFFF)) + var diff = compareUInts(high1, high2) + if diff == 0 { + let low1 = Int32(bitPattern: UInt32(l1 & 0xFFFF_FFFF)) + let low2 = Int32(bitPattern: UInt32(l2 & 0xFFFF_FFFF)) + diff = compareUInts(low1, low2) + } + return diff + } + + private func compareUInts(_ i1: Int32, _ i2: Int32) -> Int { + if i1 < 0 { + return Int(i2 < 0 ? (i1 - i2) : 1) + } + return Int(i2 < 0 ? -1 : (i1 - i2)) + } + override func spec() { - it("test duplicated") { + it("mostSignificantBits") { + let uuid = UUID(uuidString: "019025e6-b135-7e40-97df-ae0cebef184c")! + expect(uuid.mostSignificantBits) == 112_631_663_430_041_152 + } + + it("leastSignificantBits") { + let uuid = UUID(uuidString: "019025e6-b135-7e40-97df-ae0cebef184c")! + expect(uuid.leastSignificantBits) == -7_503_087_083_654_801_332 + } + + it("test sorted and duplicated") { let count = 10000 var created: [UUID] = [] @@ -22,9 +51,17 @@ class UUIDTest: QuickSpec { created.append(UUID.v7()) } + let sortedUUIDs = created.sorted { uuid1, uuid2 in + if uuid1.mostSignificantBits != uuid2.mostSignificantBits { + return uuid1.mostSignificantBits < uuid2.mostSignificantBits + } + return uuid1.leastSignificantBits < uuid2.leastSignificantBits + } + var unique: Set = Set(minimumCapacity: count) for i in 0 ..< created.count { + expect(sortedUUIDs[i]) == created[i] if !unique.insert(created[i]).inserted { fatalError("Duplicate at index \(i)") } @@ -32,3 +69,33 @@ class UUIDTest: QuickSpec { } } } + +extension UUID { + var mostSignificantBits: Int64 { + let uuidBytes = withUnsafePointer(to: uuid) { + $0.withMemoryRebound(to: UInt8.self, capacity: 16) { + Array(UnsafeBufferPointer(start: $0, count: 16)) + } + } + + var mostSignificantBits: UInt64 = 0 + for i in 0 ..< 8 { + mostSignificantBits = (mostSignificantBits << 8) | UInt64(uuidBytes[i]) + } + return Int64(bitPattern: mostSignificantBits) + } + + var leastSignificantBits: Int64 { + let uuidBytes = withUnsafePointer(to: uuid) { + $0.withMemoryRebound(to: UInt8.self, capacity: 16) { + Array(UnsafeBufferPointer(start: $0, count: 16)) + } + } + + var leastSignificantBits: UInt64 = 0 + for i in 8 ..< 16 { + leastSignificantBits = (leastSignificantBits << 8) | UInt64(uuidBytes[i]) + } + return Int64(bitPattern: leastSignificantBits) + } +} From 750995f5aa781836f019d5e43e15dae2ea2e2d09 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 17 Jun 2024 15:09:36 +0200 Subject: [PATCH 5/5] fix lint --- PostHog/Utils/TimeBasedEpochGenerator.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/PostHog/Utils/TimeBasedEpochGenerator.swift b/PostHog/Utils/TimeBasedEpochGenerator.swift index 2fcf03129..5c6c159a0 100644 --- a/PostHog/Utils/TimeBasedEpochGenerator.swift +++ b/PostHog/Utils/TimeBasedEpochGenerator.swift @@ -39,14 +39,12 @@ class TimeBasedEpochGenerator { uuidBytes.append(contentsOf: timeBytes) if unixTimeMilliseconds == lastTimestamp { - var c = true - for i in (0 ..< 10).reversed() { - if c { - var temp = lastEntropy[i] - temp = temp &+ 0x01 - c = lastEntropy[i] == 0xFF - lastEntropy[i] = temp - } + var check = true + for index in (0 ..< 10).reversed() where check { + var temp = lastEntropy[index] + temp = temp &+ 0x01 + check = lastEntropy[index] == 0xFF + lastEntropy[index] = temp } } else { lastTimestamp = unixTimeMilliseconds