Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,22 @@ internal class IdentifiersEncoder(

val googleAdId = googleAdIdManager.getId()
if (googleAdId != null) {
result.addIfNotNull(GAID, googleAdId, ::normalize)
result.addIfNotNull(GAID, googleAdId, ::normalizeDeviceId)
}

for (identifier in identifiers) {
when (identifier) {
is OptableIdentifier.Email -> result.addIfNotNull(EMAIL, identifier.value, ::encrypt)
is OptableIdentifier.PhoneNumber -> result.addIfNotNull(PHONE, identifier.value, ::encrypt)
is OptableIdentifier.Email -> result.addHashed(EMAIL, identifier.value)
is OptableIdentifier.PhoneNumber -> result.addHashed(PHONE, identifier.value)
is OptableIdentifier.Hem -> result.addIfValidHash(EMAIL, identifier.value)
is OptableIdentifier.HashedPhoneNumber -> result.addIfValidHash(PHONE, identifier.value)
is OptableIdentifier.PostalCode -> result.addIfNotNull(POSTAL, identifier.value, ::normalize)
is OptableIdentifier.IPv4 -> result.addIfNotNull(IPV4, identifier.value, ::removeWhitespaces)
is OptableIdentifier.IPv6 -> result.addIfNotNull(IPV6, identifier.value, ::normalize)
is OptableIdentifier.AppleIdfa -> result.addIfNotNull(IDFA, identifier.value, ::normalize)
is OptableIdentifier.RokuRida -> result.addIfNotNull(RIDA, identifier.value, ::normalize)
is OptableIdentifier.SamsungTifa -> result.addIfNotNull(TIFA, identifier.value, ::normalize)
is OptableIdentifier.AmazonFireAfai -> result.addIfNotNull(AFAI, identifier.value, ::normalize)
is OptableIdentifier.AppleIdfa -> result.addIfNotNull(IDFA, identifier.value, ::normalizeDeviceId)
is OptableIdentifier.RokuRida -> result.addIfNotNull(RIDA, identifier.value, ::normalizeDeviceId)
is OptableIdentifier.SamsungTifa -> result.addIfNotNull(TIFA, identifier.value, ::normalizeDeviceId)
is OptableIdentifier.AmazonFireAfai -> result.addIfNotNull(AFAI, identifier.value, ::normalizeDeviceId)
is OptableIdentifier.NetId -> result.addIfNotNull(NETID, identifier.value, ::removeWhitespaces)
is OptableIdentifier.ID5 -> result.addIfNotNull(ID5, identifier.value, ::removeWhitespaces)
is OptableIdentifier.Utiq -> result.addIfNotNull(UTIQ, identifier.value, ::normalize)
Expand All @@ -60,7 +60,7 @@ internal class IdentifiersEncoder(
if (normalize(googleAdId ?: "") == normalize(identifier.value)) {
continue
}
result.addIfNotNull(GAID, identifier.value, ::normalize)
result.addIfNotNull(GAID, identifier.value, ::normalizeDeviceId)
}

is OptableIdentifier.Custom -> {
Expand All @@ -79,14 +79,29 @@ internal class IdentifiersEncoder(
return result
}

/**
* Adds the encoded value, skipping it when it is empty after encoding, so "key:" is never sent.
*/
private fun MutableList<String>.addIfNotNull(key: String, value: String?, encoder: (String) -> String) {
if (value == null) return

val encodedValue = encoder(value)
if (encodedValue.isEmpty()) return

val result = "$key:$encodedValue"
this.add(result)
}

/**
* Adds the SHA-256 hash of the normalized value, skipping it when the normalized value is empty,
* so the hash of an empty string is never sent as if it were an ID.
*/
private fun MutableList<String>.addHashed(key: String, value: String?) {
if (value == null || normalize(value).isEmpty()) return

this.add("$key:${encrypt(value)}")
}

/**
* Adds an already-hashed value, skipping it entirely unless it is a SHA-256
* digest. This keeps a plaintext Email or Phone number off the wire.
Expand Down Expand Up @@ -136,6 +151,16 @@ internal class IdentifiersEncoder(
return encryptedValue
}

/**
* Normalizes a device advertising ID like [normalize], returning an empty string (so it is dropped) for an
* all-zero ID such as "00000000-0000-0000-0000-000000000000": the placeholder Android and iOS return when the
* user has opted out or tracking isn't authorized, shared by every such device.
*/
private fun normalizeDeviceId(value: String): String {
val normalized = normalize(value)
return if (normalized.all { it == '0' || it == '-' }) "" else normalized
}

/**
* Normalizes an identifier string by removing all whitespace and lowercasing the result.
* This is used for hash-based EIDs (email, phone).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,61 @@ class OptableIdentifiersTest {
assertEquals(expected, actual)
}

@Test
fun `drop identifiers that are empty after normalization`() {
val ids = listOf(
OptableIdentifier.Email(""),
OptableIdentifier.Email(" "),
OptableIdentifier.PhoneNumber(" "),
OptableIdentifier.PostalCode(" "),
OptableIdentifier.IPv4(" "),
OptableIdentifier.IPv6(" "),
OptableIdentifier.AppleIdfa(" "),
OptableIdentifier.GoogleGaid(" "),
OptableIdentifier.RokuRida(" "),
OptableIdentifier.SamsungTifa(" "),
OptableIdentifier.AmazonFireAfai(" "),
OptableIdentifier.NetId(" "),
OptableIdentifier.ID5(" "),
OptableIdentifier.Utiq(" "),
OptableIdentifier.Custom("v", " "),
OptableIdentifier.Custom("c2", ""),
OptableIdentifier.Hem(" "),
OptableIdentifier.HashedPhoneNumber(""),
)
val actual = identifiersEncoder.encode(ids)
assertEquals(emptyList<String>(), actual)
}

@Test
fun `drop all-zero device advertising IDs`() {
val zeros = "00000000-0000-0000-0000-000000000000"
val ids = listOf(
OptableIdentifier.AppleIdfa(zeros),
OptableIdentifier.GoogleGaid(zeros),
OptableIdentifier.RokuRida(zeros),
OptableIdentifier.SamsungTifa(" $zeros "),
OptableIdentifier.AmazonFireAfai(zeros.replace("-", "")),
)
val actual = identifiersEncoder.encode(ids)
assertEquals(emptyList<String>(), actual)
}

@Test
fun `drop an all-zero device GAID`() {
every { mockGoogleAdIdManager.getId() } returns "00000000-0000-0000-0000-000000000000"

val actual = identifiersEncoder.encode(emptyList())
assertEquals(emptyList<String>(), actual)
}

@Test
fun `keep device advertising IDs that only contain some zeros`() {
val id = "00000000-0000-0000-0000-000000000001"
val actual = identifiersEncoder.encode(listOf(OptableIdentifier.AppleIdfa(id)))
assertEquals(listOf("a:$id"), actual)
}

@Test
fun `encode raw`() {
val ids = listOf(
Expand Down
Loading