-
Notifications
You must be signed in to change notification settings - Fork 726
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Analyics: stop double reporting posthog utds
- Loading branch information
1 parent
5ccc486
commit 0a284bb
Showing
4 changed files
with
234 additions
and
10 deletions.
There are no files selected for viewing
79 changes: 79 additions & 0 deletions
79
...r/src/androidTest/java/im/vector/app/features/ReportedDecryptionFailurePersistenceTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
* Copyright (c) 2024 New Vector Ltd | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package im.vector.app.features | ||
|
||
import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
import androidx.test.platform.app.InstrumentationRegistry | ||
import im.vector.app.InstrumentedTest | ||
import im.vector.app.features.analytics.ReportedDecryptionFailurePersistence | ||
import kotlinx.coroutines.test.runTest | ||
import org.amshove.kluent.shouldBeEqualTo | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
|
||
@RunWith(AndroidJUnit4::class) | ||
class ReportedDecryptionFailurePersistenceTest : InstrumentedTest { | ||
|
||
private val context = InstrumentationRegistry.getInstrumentation().targetContext | ||
|
||
@Test | ||
fun shouldPersistReportedUtds() = runTest { | ||
val persistence = ReportedDecryptionFailurePersistence(context) | ||
persistence.load() | ||
|
||
val eventIds = listOf("$0000", "$0001", "$0002", "$0003") | ||
eventIds.forEach { | ||
persistence.markAsReported(it) | ||
} | ||
|
||
eventIds.forEach { | ||
persistence.hasBeenReported(it) shouldBeEqualTo true | ||
} | ||
|
||
persistence.hasBeenReported("$0004") shouldBeEqualTo false | ||
|
||
persistence.persist() | ||
|
||
// Load a new one | ||
val persistence2 = ReportedDecryptionFailurePersistence(context) | ||
persistence2.load() | ||
|
||
eventIds.forEach { | ||
persistence2.hasBeenReported(it) shouldBeEqualTo true | ||
} | ||
} | ||
|
||
@Test | ||
fun testSaturation() = runTest { | ||
val persistence = ReportedDecryptionFailurePersistence(context) | ||
|
||
for (i in 1..6000) { | ||
persistence.markAsReported("000$i") | ||
} | ||
|
||
// This should have saturated the bloom filter, making the rate of false positives too high. | ||
// A new bloom filter should have been created to avoid that and the recent reported events should still be in the new filter. | ||
for (i in 5800..6000) { | ||
persistence.hasBeenReported("000$i") shouldBeEqualTo true | ||
} | ||
|
||
// Old ones should not be there though | ||
for (i in 1..1000) { | ||
persistence.hasBeenReported("000$i") shouldBeEqualTo false | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
...or/src/main/java/im/vector/app/features/analytics/ReportedDecryptionFailurePersistence.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* Copyright (c) 2024 New Vector Ltd | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package im.vector.app.features.analytics | ||
|
||
import android.content.Context | ||
import android.util.LruCache | ||
import com.google.common.hash.BloomFilter | ||
import com.google.common.hash.Funnels | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import timber.log.Timber | ||
import java.io.File | ||
import java.io.FileOutputStream | ||
import javax.inject.Inject | ||
|
||
private const val REPORTED_UTD_FILE_NAME = "im.vector.analytics.reported_utd" | ||
private const val EXPECTED_INSERTIONS = 5000 | ||
|
||
/** | ||
* This class is used to keep track of the reported decryption failures to avoid double reporting. | ||
* It uses a bloom filter to limit the memory/disk usage. | ||
*/ | ||
class ReportedDecryptionFailurePersistence @Inject constructor( | ||
private val context: Context, | ||
) { | ||
|
||
// Keep a cache of recent reported failures in memory. | ||
// They will be persisted to the a new bloom filter if the previous one is getting saturated. | ||
// Should be around 30KB max in memory. | ||
// Also allows to have 0% false positive rate for recent failures. | ||
private val inMemoryReportedFailures: LruCache<String, Unit> = LruCache(300) | ||
|
||
// Thread-safe and lock-free. | ||
// The expected insertions is 5000, and expected false positive probability of 3% when close to max capability. | ||
// The persisted size is expected to be around 5KB (100 times less than if it was raw strings). | ||
private var bloomFilter: BloomFilter<String> = BloomFilter.create<String>(Funnels.stringFunnel(Charsets.UTF_8), EXPECTED_INSERTIONS) | ||
|
||
/** | ||
* Mark an event as reported. | ||
* @param eventId the event id to mark as reported. | ||
*/ | ||
suspend fun markAsReported(eventId: String) { | ||
// Add to in memory cache. | ||
inMemoryReportedFailures.put(eventId, Unit) | ||
bloomFilter.put(eventId) | ||
|
||
// check if the filter is getting saturated? and then replace | ||
if (bloomFilter.approximateElementCount() > EXPECTED_INSERTIONS - 500) { | ||
// The filter is getting saturated, and the false positive rate is increasing. | ||
// It's time to replace the filter with a new one. And move the in-memory cache to the new filter. | ||
bloomFilter = BloomFilter.create<String>(Funnels.stringFunnel(Charsets.UTF_8), EXPECTED_INSERTIONS) | ||
inMemoryReportedFailures.snapshot().keys.forEach { | ||
bloomFilter.put(it) | ||
} | ||
persist() | ||
} | ||
Timber.v("## Bloom filter stats: expectedFpp: ${bloomFilter.expectedFpp()}, size: ${bloomFilter.approximateElementCount()}") | ||
} | ||
|
||
/** | ||
* Check if an event has been reported. | ||
* @param eventId the event id to check. | ||
* @return true if the event has been reported. | ||
*/ | ||
fun hasBeenReported(eventId: String): Boolean { | ||
// First check in memory cache. | ||
if (inMemoryReportedFailures.get(eventId) != null) { | ||
return true | ||
} | ||
return bloomFilter.mightContain(eventId) | ||
} | ||
|
||
/** | ||
* Load the reported failures from disk. | ||
*/ | ||
suspend fun load() { | ||
withContext(Dispatchers.IO) { | ||
try { | ||
val file = File(context.applicationContext.cacheDir, REPORTED_UTD_FILE_NAME) | ||
if (file.exists()) { | ||
file.inputStream().use { | ||
bloomFilter = BloomFilter.readFrom(it, Funnels.stringFunnel(Charsets.UTF_8)) | ||
} | ||
} | ||
} catch (e: Throwable) { | ||
Timber.e(e, "## Failed to load reported failures") | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Persist the reported failures to disk. | ||
*/ | ||
suspend fun persist() { | ||
withContext(Dispatchers.IO) { | ||
try { | ||
val file = File(context.applicationContext.cacheDir, REPORTED_UTD_FILE_NAME) | ||
if (!file.exists()) file.createNewFile() | ||
FileOutputStream(file).buffered().use { | ||
bloomFilter.writeTo(it) | ||
} | ||
Timber.v("## Successfully saved reported failures, size: ${file.length()}") | ||
} catch (e: Throwable) { | ||
Timber.e(e, "## Failed to save reported failures") | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters