-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: migrate 'api key' storage data to 'instance name' storage (#143)
* feat: migrate 'api key' storage data to 'instance name' storage * fix: extracted migration logic to ApiKeyStorageMigration class
- Loading branch information
1 parent
1d44778
commit 67af8e3
Showing
9 changed files
with
374 additions
and
27 deletions.
There are no files selected for viewing
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
26 changes: 26 additions & 0 deletions
26
android/src/main/java/com/amplitude/android/migration/ApiKeyStorageMigration.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,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() | ||
} | ||
} | ||
} |
106 changes: 106 additions & 0 deletions
106
android/src/main/java/com/amplitude/android/migration/StorageKeyMigration.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,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}") | ||
} | ||
} | ||
} |
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
197 changes: 197 additions & 0 deletions
197
android/src/test/java/com/amplitude/android/migration/StorageKeyMigrationTest.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,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 | ||
} | ||
} |
Oops, something went wrong.