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

Merged
merged 7 commits into from
Jun 14, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

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

## 3.3.1 - 2024-06-11

- chore: change host to new address ([#137](https://github.com/PostHog/posthog-android/pull/137))
Expand Down
4 changes: 2 additions & 2 deletions posthog-android/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<issue
id="GradleDependency"
message="A newer version of androidx.lifecycle:lifecycle-process than 2.6.2 is available: 2.8.1"
message="A newer version of androidx.lifecycle:lifecycle-process than 2.6.2 is available: 2.8.2"
errorLine1=" implementation(&quot;androidx.lifecycle:lifecycle-process:${PosthogBuildConfig.Dependencies.LIFECYCLE}&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
Expand All @@ -14,7 +14,7 @@

<issue
id="GradleDependency"
message="A newer version of androidx.lifecycle:lifecycle-common-java8 than 2.6.2 is available: 2.8.1"
message="A newer version of androidx.lifecycle:lifecycle-common-java8 than 2.6.2 is available: 2.8.2"
errorLine1=" implementation(&quot;androidx.lifecycle:lifecycle-common-java8:${PosthogBuildConfig.Dependencies.LIFECYCLE}&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
Expand Down
5 changes: 3 additions & 2 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.posthog.internal.PostHogQueue
import com.posthog.internal.PostHogSendCachedEventsIntegration
import com.posthog.internal.PostHogSerializer
import com.posthog.internal.PostHogThreadFactory
import com.posthog.vendor.uuid.TimeBasedEpochGenerator
import java.util.UUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
Expand Down Expand Up @@ -202,7 +203,7 @@ public class PostHog private constructor(
synchronized(anonymousLock) {
anonymousId = getPreferences().getValue(ANONYMOUS_ID) as? String
if (anonymousId.isNullOrBlank()) {
var uuid = UUID.randomUUID()
var uuid = TimeBasedEpochGenerator.generate()
// when getAnonymousId method is available, pass-through the value for modification
config?.getAnonymousId?.let { uuid = it(uuid) }
anonymousId = uuid.toString()
Expand Down Expand Up @@ -698,7 +699,7 @@ public class PostHog private constructor(
override fun startSession() {
synchronized(sessionLock) {
if (sessionId == sessionIdNone) {
sessionId = UUID.randomUUID()
sessionId = TimeBasedEpochGenerator.generate()
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion posthog/src/main/java/com/posthog/PostHogEvent.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.posthog

import com.google.gson.annotations.SerializedName
import com.posthog.vendor.uuid.TimeBasedEpochGenerator
import java.util.Date
import java.util.UUID

Expand All @@ -22,7 +23,7 @@ public data class PostHogEvent(
val properties: Map<String, Any>? = null,
// refactor to use PostHogDateProvider
val timestamp: Date = Date(),
val uuid: UUID? = UUID.randomUUID(),
val uuid: UUID? = TimeBasedEpochGenerator.generate(),
@Deprecated("Do not use")
val type: String? = null,
@Deprecated("Do not use it, prefer [uuid]")
Expand Down
5 changes: 3 additions & 2 deletions posthog/src/main/java/com/posthog/internal/PostHogQueue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package com.posthog.internal
import com.posthog.PostHogConfig
import com.posthog.PostHogEvent
import com.posthog.PostHogVisibleForTesting
import com.posthog.vendor.uuid.TimeBasedEpochGenerator
import java.io.File
import java.io.IOException
import java.util.Date
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.schedule
Expand Down Expand Up @@ -75,7 +75,8 @@ internal class PostHogQueue(
dirCreated = true
}

val file = File(dir, "${UUID.randomUUID()}.event")
val uuid = TimeBasedEpochGenerator.generate()
val file = File(dir, "$uuid.event")
synchronized(dequeLock) {
deque.add(file)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// borrowed, adapted and converted to Kotlin from https://github.com/cowtowncoder/java-uuid-generator/blob/master/src/main/java/com/fasterxml/uuid/impl/TimeBasedEpochGenerator.java

package com.posthog.vendor.uuid

import java.security.SecureRandom
import java.util.Random
import java.util.UUID
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock

internal object TimeBasedEpochGenerator {
private const val ENTROPY_BYTE_LENGTH = 10

private const val TIME_BASED_EPOCH_RAW = 7

/*
/ **********************************************************************
/ * Configuration
/ **********************************************************************
*/
private var lastTimestamp: Long = -1
private val lastEntropy = ByteArray(ENTROPY_BYTE_LENGTH)

private val numberGenerator: Random = SecureRandom()
private val lock: Lock = ReentrantLock()

/*
/ **********************************************************************
/ * UUID generation
/ **********************************************************************
*/

/**
* @return unix epoch time based UUID
*/
fun generate(): UUID {
return generate(System.currentTimeMillis())
}

/**
* @param rawTimestamp unix epoch millis
* @return unix epoch time based UUID
*/
@Throws(IllegalStateException::class)
fun generate(rawTimestamp: Long): UUID {
return construct(rawTimestamp)
}

private fun toLong(
buffer: ByteArray,
offset: Int,
): Long {
val l1 = toInt(buffer, offset)
val l2 = toInt(buffer, offset + 4)
val l = (l1 shl 32) + ((l2 shl 32) ushr 32)
return l
}

private fun toInt(
buffer: ByteArray,
offset: Int,
): Long {
var theOffset = offset
return (
(buffer[theOffset].toInt() shl 24) +
((buffer[++theOffset].toInt() and 0xFF) shl 16) +
((buffer[++theOffset].toInt() and 0xFF) shl 8) +
(buffer[++theOffset].toInt() and 0xFF)
).toLong()
}

private fun toShort(
buffer: ByteArray,
offset: Int,
): Long {
var theOffset = offset
return (
((buffer[theOffset].toInt() and 0xFF) shl 8) +
(buffer[++theOffset].toInt() and 0xFF)
).toLong()
}

private fun constructUUID(
l1: Long,
l2: Long,
): UUID {
// first, ensure type is ok
var theL1 = l1
var theL2 = l2
theL1 = theL1 and 0xF000L.inv() // remove high nibble of 6th byte
theL1 = theL1 or (TIME_BASED_EPOCH_RAW shl 12).toLong()
// second, ensure variant is properly set too (8th byte; most-sig byte of second long)
theL2 = ((theL2 shl 2) ushr 2) // remove 2 MSB
theL2 = theL2 or (2L shl 62) // set 2 MSB to '10'
return UUID(theL1, theL2)
}

/*
/ ********************************************************************************
/ * Package helper methods
/ ********************************************************************************
*/

/**
* Method that will construct actual [UUID] instance for given
* unix epoch timestamp: called by [.generate] but may alternatively be
* called directly to construct an instance with known timestamp.
* NOTE: calling this method directly produces somewhat distinct UUIDs as
* "entropy" value is still generated as necessary to avoid producing same
* [UUID] even if same timestamp is being passed.
*
* @param rawTimestamp unix epoch millis
*
* @return unix epoch time based UUID
*
* @since 4.3
*/
@Throws(IllegalStateException::class)
private fun construct(rawTimestamp: Long): UUID {
lock.lock()
try {
if (rawTimestamp == lastTimestamp) {
var c = true
for (i in ENTROPY_BYTE_LENGTH - 1 downTo 0) {
if (c) {
var temp = lastEntropy[i]
temp = (temp + 0x01).toByte()
c = lastEntropy[i] == 0xff.toByte()
lastEntropy[i] = temp
}
}
check(!c) { "overflow on same millisecond" }
} else {
lastTimestamp = rawTimestamp
numberGenerator.nextBytes(lastEntropy)
}
return constructUUID(
(rawTimestamp shl 16) or toShort(lastEntropy, 0),
toLong(lastEntropy, 2),
)
} finally {
lock.unlock()
}
}
}
4 changes: 2 additions & 2 deletions posthog/src/test/java/com/posthog/PostHogTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import com.posthog.internal.PostHogPrintLogger
import com.posthog.internal.PostHogSendCachedEventsIntegration
import com.posthog.internal.PostHogSerializer
import com.posthog.internal.PostHogThreadFactory
import com.posthog.vendor.uuid.TimeBasedEpochGenerator
import okhttp3.mockwebserver.MockResponse
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
import java.util.UUID
import java.util.concurrent.Executors
import kotlin.test.AfterTest
import kotlin.test.Test
Expand Down Expand Up @@ -857,7 +857,7 @@ internal class PostHogTest {

@Test
fun `allows for modification of the uuid generation mechanism`() {
val expected = UUID.randomUUID()
val expected = TimeBasedEpochGenerator.generate()
val config =
PostHogConfig(API_KEY, getAnonymousId = {
assertNotEquals(it, expected, "Expect two unique UUIDs")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.posthog.API_KEY
import com.posthog.PostHogConfig
import com.posthog.mockHttp
import com.posthog.shutdownAndAwaitTermination
import com.posthog.vendor.uuid.TimeBasedEpochGenerator
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.SocketPolicy
import org.junit.Rule
Expand All @@ -13,7 +14,6 @@ import java.io.File
import java.text.ParsePosition
import java.util.Calendar
import java.util.Date
import java.util.UUID
import java.util.concurrent.Executors
import kotlin.test.AfterTest
import kotlin.test.Test
Expand Down Expand Up @@ -61,7 +61,8 @@ internal class PostHogSendCachedEventsIntegrationTest {
fullFile.mkdirs()

content.forEach {
val file = File(fullFile.absoluteFile, "${UUID.randomUUID()}.event")
val uuid = TimeBasedEpochGenerator.generate()
val file = File(fullFile.absoluteFile, "$uuid.event")
file.writeText(it)
date?.let { theDate ->
val cal = Calendar.getInstance()
Expand Down Expand Up @@ -213,7 +214,8 @@ internal class PostHogSendCachedEventsIntegrationTest {

// write a new file
val folder = File(storagePrefix, API_KEY)
val file = File(folder, "${UUID.randomUUID()}.event")
val uuid = TimeBasedEpochGenerator.generate()
val file = File(folder, "$uuid.event")

val tempEvent = File("src/test/resources/json/other-event.json").readText()
file.writeText(tempEvent)
Expand Down
86 changes: 86 additions & 0 deletions posthog/src/test/java/com/posthog/vendor/uuid/UUIDComparator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.posthog.vendor.uuid

import java.util.UUID

/**
* Default [java.util.UUID] comparator is not very useful, since
* it just does blind byte-by-byte comparison which does not work well
* for time+location - based UUIDs. Additionally, it also uses signed
* comparisons for longs which can lead to unexpected behavior
* This comparator does implement proper lexical ordering: starting with
* type (different types are collated
* separately), followed by time and location (for time/location based),
* and simple lexical (byte-by-byte) ordering for name/hash and random
* versions.
*
* @author tatu
*/
public class UUIDComparator : Comparator<UUID> {
public override fun compare(
u1: UUID,
u2: UUID,
): Int {
return staticCompare(u1, u2)
}

private fun staticCompare(
u1: UUID,
u2: UUID,
): Int {
// First: major sorting by types
val type = u1.version()
var diff = type - u2.version()
if (diff != 0) {
return diff
}
// Second: for time-based version, order by time stamp:
// 1 = TIME_BASED
if (type == 1) {
diff = compareULongs(u1.timestamp(), u2.timestamp())
if (diff == 0) {
// or if that won't work, by other bits lexically
diff = compareULongs(u1.leastSignificantBits, u2.leastSignificantBits)
}
} else {
// note: java.util.UUIDs compares with sign extension, IMO that's wrong, so:
diff =
compareULongs(
u1.mostSignificantBits,
u2.mostSignificantBits,
)
if (diff == 0) {
diff =
compareULongs(
u1.leastSignificantBits,
u2.leastSignificantBits,
)
}
}
return diff
}

private fun compareULongs(
l1: Long,
l2: Long,
): Int {
var diff = compareUInts((l1 shr 32).toInt(), (l2 shr 32).toInt())
if (diff == 0) {
diff = compareUInts(l1.toInt(), l2.toInt())
}
return diff
}

private fun compareUInts(
i1: Int,
i2: Int,
): Int {
/* bit messier due to java's insistence on signed values: if both
* have same sign, normal comparison (by subtraction) works fine;
* but if signs don't agree need to resolve differently
*/
if (i1 < 0) {
return if ((i2 < 0)) (i1 - i2) else 1
}
return if ((i2 < 0)) -1 else (i1 - i2)
}
}
Loading
Loading