Skip to content

Commit

Permalink
feat: migrate 'api key' storage data to 'instance name' storage (#143)
Browse files Browse the repository at this point in the history
* feat: migrate 'api key' storage data to 'instance name' storage

* fix: extracted migration logic to ApiKeyStorageMigration class
  • Loading branch information
falconandy authored Aug 22, 2023
1 parent 1d44778 commit 67af8e3
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 27 deletions.
3 changes: 3 additions & 0 deletions android/src/main/java/com/amplitude/android/Amplitude.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.amplitude.android

import android.content.Context
import com.amplitude.android.migration.ApiKeyStorageMigration
import com.amplitude.android.migration.RemnantDataMigration
import com.amplitude.android.plugins.AnalyticsConnectorIdentityPlugin
import com.amplitude.android.plugins.AnalyticsConnectorPlugin
Expand Down Expand Up @@ -48,6 +49,8 @@ open class Amplitude(
}

override suspend fun buildInternal(identityConfiguration: IdentityConfiguration) {
ApiKeyStorageMigration(this).execute()

if ((this.configuration as Configuration).migrateLegacyData) {
RemnantDataMigration(this).execute()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.amplitude.android.migration

import com.amplitude.android.Amplitude
import com.amplitude.android.Configuration
import com.amplitude.android.utilities.AndroidStorage

class ApiKeyStorageMigration(
private val amplitude: Amplitude
) {
suspend fun execute() {
val configuration = amplitude.configuration as Configuration
val logger = amplitude.logger

val storage = amplitude.storage as? AndroidStorage
if (storage != null) {
val apiKeyStorage = AndroidStorage(configuration.context, configuration.apiKey, logger, storage.prefix)
StorageKeyMigration(apiKeyStorage, storage, logger).execute()
}

val identifyInterceptStorage = amplitude.identifyInterceptStorage as? AndroidStorage
if (identifyInterceptStorage != null) {
val apiKeyStorage = AndroidStorage(configuration.context, configuration.apiKey, logger, identifyInterceptStorage.prefix)
StorageKeyMigration(apiKeyStorage, identifyInterceptStorage, logger).execute()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.amplitude.android.migration

import com.amplitude.android.utilities.AndroidStorage
import com.amplitude.common.Logger
import com.amplitude.core.Storage
import java.io.File
import java.util.UUID

class StorageKeyMigration(
private val source: AndroidStorage,
private val destination: AndroidStorage,
private val logger: Logger
) {
suspend fun execute() {
if (source.storageKey == destination.storageKey) {
return
}
moveSourceEventFilesToDestination()
moveSimpleValues()
}

private suspend fun moveSourceEventFilesToDestination() {
try {
source.rollover()
val sourceEventFiles = source.readEventsContent() as List<String>
if (sourceEventFiles.isEmpty()) {
return
}

for (sourceEventFilePath in sourceEventFiles) {
val sourceEventFile = File(sourceEventFilePath)
var destinationEventFile =
File(sourceEventFilePath.replace(source.storageKey, destination.storageKey))
if (destinationEventFile.exists()) {
var fileExtension = destinationEventFile.extension
if (fileExtension != "") {
fileExtension = ".$fileExtension"
}
destinationEventFile = File(
destinationEventFile.parent,
"${destinationEventFile.nameWithoutExtension}-${UUID.randomUUID()}$fileExtension"
)
}
try {
sourceEventFile.renameTo(destinationEventFile)
} catch (e: Exception) {
logger.error("can't rename $sourceEventFile to $destinationEventFile: ${e.message}")
}
}
} catch (e: Exception) {
logger.error("can't move event files: ${e.message}")
}
}

private suspend fun moveSimpleValues() {
moveSimpleValue(Storage.Constants.PREVIOUS_SESSION_ID)
moveSimpleValue(Storage.Constants.LAST_EVENT_TIME)
moveSimpleValue(Storage.Constants.LAST_EVENT_ID)

moveSimpleValue(Storage.Constants.OPT_OUT)
moveSimpleValue(Storage.Constants.Events)
moveSimpleValue(Storage.Constants.APP_VERSION)
moveSimpleValue(Storage.Constants.APP_BUILD)

moveFileIndex()
}

private suspend fun moveSimpleValue(key: Storage.Constants) {
try {
val sourceValue = source.read(key) ?: return

val destinationValue = destination.read(key)
if (destinationValue == null) {
try {
destination.write(key, sourceValue)
} catch (e: Exception) {
logger.error("can't write destination $key: ${e.message}")
return
}
}

source.remove(key)
} catch (e: Exception) {
logger.error("can't move $key: ${e.message}")
}
}

private fun moveFileIndex() {
try {
val sourceFileIndexKey = "amplitude.events.file.index.${source.storageKey}"
val destinationFileIndexKey = "amplitude.events.file.index.${destination.storageKey}"
if (source.sharedPreferences.contains(sourceFileIndexKey)) {
val fileIndex = source.sharedPreferences.getLong(sourceFileIndexKey, -1)
try {
destination.sharedPreferences.edit().putLong(destinationFileIndexKey, fileIndex).commit()
} catch (e: Exception) {
logger.error("can't write file index: ${e.message}")
return
}
source.sharedPreferences.edit().remove(sourceFileIndexKey).commit()
}
} catch (e: Exception) {
logger.error("can't move file index: ${e.message}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ import java.io.File

class AndroidStorage(
context: Context,
apiKey: String,
val storageKey: String,
private val logger: Logger,
private val prefix: String?
internal val prefix: String?
) : Storage, EventsFileStorage {

companion object {
const val STORAGE_PREFIX = "amplitude-android"
}

private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("${getPrefix()}-$apiKey", Context.MODE_PRIVATE)
internal val sharedPreferences: SharedPreferences =
context.getSharedPreferences("${getPrefix()}-$storageKey", Context.MODE_PRIVATE)
private val storageDirectory: File = context.getDir(getDir(), Context.MODE_PRIVATE)
private val eventsFile =
EventsFileManager(storageDirectory, apiKey, AndroidKVS(sharedPreferences))
EventsFileManager(storageDirectory, storageKey, AndroidKVS(sharedPreferences))
private val eventCallbacksMap = mutableMapOf<String, EventCallBack>()

override suspend fun writeEvent(event: BaseEvent) {
Expand All @@ -51,6 +51,10 @@ class AndroidStorage(
sharedPreferences.edit().putString(key.rawVal, value).apply()
}

override suspend fun remove(key: Storage.Constants) {
sharedPreferences.edit().remove(key.rawVal).apply()
}

override suspend fun rollover() {
eventsFile.rollover()
}
Expand Down Expand Up @@ -120,7 +124,7 @@ class AndroidStorageProvider : StorageProvider {
val configuration = amplitude.configuration as com.amplitude.android.Configuration
return AndroidStorage(
configuration.context,
configuration.apiKey,
configuration.instanceName,
configuration.loggerProvider.getLogger(amplitude),
prefix
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package com.amplitude.android.migration

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.amplitude.android.utilities.AndroidStorage
import com.amplitude.common.jvm.ConsoleLogger
import com.amplitude.core.Storage
import com.amplitude.core.events.BaseEvent
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.File
import java.util.UUID

@RunWith(RobolectricTestRunner::class)
class StorageKeyMigrationTest {
@Test
fun `simple values should be migrated`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val logger = ConsoleLogger()

val source = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)
val destination = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)
val sourceFileIndexKey = "amplitude.events.file.index.${source.storageKey}"
val destinationFileIndexKey = "amplitude.events.file.index.${destination.storageKey}"

runBlocking {
source.write(Storage.Constants.PREVIOUS_SESSION_ID, "123")
source.write(Storage.Constants.LAST_EVENT_TIME, "456")
source.write(Storage.Constants.LAST_EVENT_ID, "789")
}
source.sharedPreferences.edit().putLong(sourceFileIndexKey, 1234567).commit()

var destinationPreviousSessionId = destination.read(Storage.Constants.PREVIOUS_SESSION_ID)
var destinationLastEventTime = destination.read(Storage.Constants.LAST_EVENT_TIME)
var destinationLastEventId = destination.read(Storage.Constants.LAST_EVENT_ID)
var destinationFileIndex = destination.sharedPreferences.getLong(destinationFileIndexKey, -1)

Assertions.assertNull(destinationPreviousSessionId)
Assertions.assertNull(destinationLastEventTime)
Assertions.assertNull(destinationLastEventId)
Assertions.assertEquals(-1, destinationFileIndex)

val migration = StorageKeyMigration(source, destination, logger)
runBlocking {
migration.execute()
}

val sourcePreviousSessionId = source.read(Storage.Constants.PREVIOUS_SESSION_ID)
val sourceLastEventTime = source.read(Storage.Constants.LAST_EVENT_TIME)
val sourceLastEventId = source.read(Storage.Constants.LAST_EVENT_ID)
val sourceFileIndex = source.sharedPreferences.getLong(sourceFileIndexKey, -1)

Assertions.assertNull(sourcePreviousSessionId)
Assertions.assertNull(sourceLastEventTime)
Assertions.assertNull(sourceLastEventId)
Assertions.assertEquals(-1, sourceFileIndex)

destinationPreviousSessionId = destination.read(Storage.Constants.PREVIOUS_SESSION_ID)
destinationLastEventTime = destination.read(Storage.Constants.LAST_EVENT_TIME)
destinationLastEventId = destination.read(Storage.Constants.LAST_EVENT_ID)
destinationFileIndex = destination.sharedPreferences.getLong(destinationFileIndexKey, -1)

Assertions.assertEquals("123", destinationPreviousSessionId)
Assertions.assertEquals("456", destinationLastEventTime)
Assertions.assertEquals("789", destinationLastEventId)
Assertions.assertEquals(1234567, destinationFileIndex)
}

@Test
fun `event files should be migrated`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val logger = ConsoleLogger()

val source = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)
val destination = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)

runBlocking {
source.writeEvent(createEvent(1))
source.writeEvent(createEvent(22))
source.rollover()
source.writeEvent(createEvent(333))
source.rollover()
source.writeEvent(createEvent(4444))
source.rollover()
}

var sourceEventFiles = source.readEventsContent() as List<String>
Assertions.assertEquals(3, sourceEventFiles.size)

val sourceFileSizes = sourceEventFiles.map { File(it).length() }

var destinationEventFiles = destination.readEventsContent() as List<String>
Assertions.assertEquals(0, destinationEventFiles.size)

val migration = StorageKeyMigration(source, destination, logger)
runBlocking {
migration.execute()
}

sourceEventFiles = source.readEventsContent() as List<String>
Assertions.assertEquals(0, sourceEventFiles.size)

destinationEventFiles = destination.readEventsContent() as List<String>
Assertions.assertEquals(3, destinationEventFiles.size)

for ((index, destinationEventFile) in destinationEventFiles.withIndex()) {
val fileSize = File(destinationEventFile).length()
Assertions.assertEquals(sourceFileSizes[index], fileSize)
}
}

@Test
fun `missing source should not fail`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val logger = ConsoleLogger()

val source = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)
val destination = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)

var destinationPreviousSessionId = destination.read(Storage.Constants.PREVIOUS_SESSION_ID)
var destinationLastEventTime = destination.read(Storage.Constants.LAST_EVENT_TIME)
var destinationLastEventId = destination.read(Storage.Constants.LAST_EVENT_ID)

Assertions.assertNull(destinationPreviousSessionId)
Assertions.assertNull(destinationLastEventTime)
Assertions.assertNull(destinationLastEventId)

val migration = StorageKeyMigration(source, destination, logger)
runBlocking {
migration.execute()
}

destinationPreviousSessionId = destination.read(Storage.Constants.PREVIOUS_SESSION_ID)
destinationLastEventTime = destination.read(Storage.Constants.LAST_EVENT_TIME)
destinationLastEventId = destination.read(Storage.Constants.LAST_EVENT_ID)

Assertions.assertNull(destinationPreviousSessionId)
Assertions.assertNull(destinationLastEventTime)
Assertions.assertNull(destinationLastEventId)

val destinationEventFiles = destination.readEventsContent() as List<String>
Assertions.assertEquals(0, destinationEventFiles.size)
}

@Test
fun `event files with duplicated names should be migrated`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val logger = ConsoleLogger()

val source = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)
val destination = AndroidStorage(context, UUID.randomUUID().toString(), logger, null)

runBlocking {
source.writeEvent(createEvent(1))
source.rollover()
source.writeEvent(createEvent(22))
source.rollover()
}

val sourceEventFiles = source.readEventsContent() as List<String>
Assertions.assertEquals(2, sourceEventFiles.size)

val sourceFileSizes = sourceEventFiles.map { File(it).length() }

runBlocking {
destination.writeEvent(createEvent(333))
destination.rollover()
}

var destinationEventFiles = destination.readEventsContent() as List<String>
Assertions.assertEquals(1, destinationEventFiles.size)

val destinationFileSizes = destinationEventFiles.map { File(it).length() }

val migration = StorageKeyMigration(source, destination, logger)
runBlocking {
migration.execute()
}

destinationEventFiles = destination.readEventsContent() as List<String>
Assertions.assertEquals("-0", destinationEventFiles[0].substring(destinationEventFiles[0].length - 2))
Assertions.assertTrue(destinationEventFiles[1].contains("-0-"))
Assertions.assertEquals("-1", destinationEventFiles[2].substring(destinationEventFiles[0].length - 2))
Assertions.assertEquals(destinationFileSizes[0], File(destinationEventFiles[0]).length())
Assertions.assertEquals(sourceFileSizes[0], File(destinationEventFiles[1]).length())
Assertions.assertEquals(sourceFileSizes[1], File(destinationEventFiles[2]).length())
}

private fun createEvent(eventIndex: Int): BaseEvent {
val event = BaseEvent()
event.eventType = "event-$eventIndex"
return event
}
}
Loading

0 comments on commit 67af8e3

Please sign in to comment.