Skip to content

Commit

Permalink
chore: migrate UUID from v4 to v7 (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Jun 17, 2024
1 parent dd74634 commit 080bacd
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 9 deletions.
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)
}
}

0 comments on commit 080bacd

Please sign in to comment.