Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate UUID from v4 to v7 #145

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
## Next

- chore: migrate UUID from v4 to v7 ([#145](https://github.com/PostHog/posthog-ios/pull/145))

## 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

Expand Down
12 changes: 12 additions & 0 deletions PostHog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -81,6 +82,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 */; };
Expand Down Expand Up @@ -276,6 +279,7 @@
3AE3FB48299391DF00AFFC18 /* PostHogStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStorage.swift; sourceTree = "<group>"; };
3AE3FB4A2993A68500AFFC18 /* PostHogStorageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogStorageTest.swift; sourceTree = "<group>"; };
3AE3FB4D2993D1D600AFFC18 /* PostHogSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionManager.swift; sourceTree = "<group>"; };
690B2DF22C205B5600AE3B45 /* TimeBasedEpochGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeBasedEpochGenerator.swift; sourceTree = "<group>"; };
690FF02F2AE7C5BA00A0B06B /* PostHogExampleWithPods.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleWithPods.xcodeproj; path = PostHogExampleWithPods/PostHogExampleWithPods.xcodeproj; sourceTree = "<group>"; };
690FF0532AE7DB3700A0B06B /* PostHogExampleWithSPM.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleWithSPM.xcodeproj; path = PostHogExampleWithSPM/PostHogExampleWithSPM.xcodeproj; sourceTree = "<group>"; };
690FF05E2AE7E2D400A0B06B /* Data+Gzip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Gzip.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -326,6 +330,8 @@
6999918E2AFE1B39000DCB78 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
699991902AFE1B39000DCB78 /* PostHogExampleMacOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PostHogExampleMacOS.entitlements; sourceTree = "<group>"; };
699991992AFE1BAB000DCB78 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
699C5FE52C20178E007DB818 /* UUIDUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDUtils.swift; sourceTree = "<group>"; };
699C5FEE2C20242A007DB818 /* UUIDTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDTest.swift; sourceTree = "<group>"; };
69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogReplayIntegration.swift; sourceTree = "<group>"; };
69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionReplayConfig.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -470,6 +476,8 @@
690FF0BE2AEFA97F00A0B06B /* FileUtils.swift */,
690FF0B42AEBBD3C00A0B06B /* DictUtils.swift */,
690FF0AE2AEB9C1400A0B06B /* DateUtils.swift */,
699C5FE52C20178E007DB818 /* UUIDUtils.swift */,
690B2DF22C205B5600AE3B45 /* TimeBasedEpochGenerator.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -548,6 +556,7 @@
690FF0E22AEFD12900A0B06B /* PostHogConfigTest.swift */,
690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */,
690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */,
699C5FEE2C20242A007DB818 /* UUIDTest.swift */,
);
path = PostHogTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -1081,6 +1090,8 @@
69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */,
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 */,
Expand All @@ -1094,6 +1105,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 */,
Expand Down
6 changes: 3 additions & 3 deletions PostHog/Models/PostHogEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? [:]
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion PostHog/PostHogSessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "")
}
}
Expand Down
86 changes: 86 additions & 0 deletions PostHog/Utils/TimeBasedEpochGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// 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 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

// 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<UInt64>.size)
}
}
17 changes: 17 additions & 0 deletions PostHog/Utils/UUIDUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// 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 {
TimeBasedEpochGenerator.shared.v7()
}
}
4 changes: 2 additions & 2 deletions PostHogTests/PostHogSessionManagerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
101 changes: 101 additions & 0 deletions PostHogTests/UUIDTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// UUIDTest.swift
// PostHogTests
//
// Created by Manoel Aranda Neto on 17.06.24.
//

import Foundation

import Foundation
import Nimble
@testable import PostHog
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("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] = []
for _ in 0 ..< count {
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<UUID> = 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)")
}
}
}
}
}

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)
}
}
Loading