Skip to content

Commit

Permalink
feat: migrating storage to version v3 (#226)
Browse files Browse the repository at this point in the history
* feat: Migrate storage to v3
  • Loading branch information
izaaz authored Oct 2, 2024
1 parent c494b82 commit 3be476b
Show file tree
Hide file tree
Showing 32 changed files with 1,169 additions and 269 deletions.
17 changes: 7 additions & 10 deletions android/src/main/java/com/amplitude/android/Amplitude.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package com.amplitude.android

import android.content.Context
import com.amplitude.android.migration.ApiKeyStorageMigration
import com.amplitude.android.migration.RemnantDataMigration
import com.amplitude.android.migration.MigrationManager
import com.amplitude.android.plugins.AnalyticsConnectorIdentityPlugin
import com.amplitude.android.plugins.AnalyticsConnectorPlugin
import com.amplitude.android.plugins.AndroidContextPlugin
import com.amplitude.android.plugins.AndroidLifecyclePlugin
import com.amplitude.android.plugins.AndroidNetworkConnectivityCheckerPlugin
import com.amplitude.android.storage.AndroidStorageContextV3
import com.amplitude.core.Amplitude
import com.amplitude.core.events.BaseEvent
import com.amplitude.core.platform.plugins.AmplitudeDestination
import com.amplitude.core.platform.plugins.GetAmpliExtrasPlugin
import com.amplitude.core.utilities.FileStorage
import com.amplitude.id.IdentityConfiguration
import kotlinx.coroutines.launch

Expand All @@ -36,23 +35,21 @@ open class Amplitude(

override fun createIdentityConfiguration(): IdentityConfiguration {
val configuration = configuration as Configuration
val storageDirectory = configuration.context.getDir("${FileStorage.STORAGE_PREFIX}-${configuration.instanceName}", Context.MODE_PRIVATE)

return IdentityConfiguration(
instanceName = configuration.instanceName,
apiKey = configuration.apiKey,
identityStorageProvider = configuration.identityStorageProvider,
storageDirectory = storageDirectory,
logger = configuration.loggerProvider.getLogger(this)
storageDirectory = AndroidStorageContextV3.getIdentityStorageDirectory(configuration),
logger = configuration.loggerProvider.getLogger(this),
fileName = AndroidStorageContextV3.getIdentityStorageFileName()
)
}

override suspend fun buildInternal(identityConfiguration: IdentityConfiguration) {
ApiKeyStorageMigration(this).execute()
val migrationManager = MigrationManager(this)
migrationManager.migrateOldStorage()

if ((this.configuration as Configuration).migrateLegacyData) {
RemnantDataMigration(this).execute()
}
this.createIdentityContainer(identityConfiguration)

if (this.configuration.offline != AndroidNetworkConnectivityCheckerPlugin.Disabled) {
Expand Down
27 changes: 19 additions & 8 deletions android/src/main/java/com/amplitude/android/Configuration.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.amplitude.android

import android.content.Context
import com.amplitude.android.storage.AndroidStorageContextV3
import com.amplitude.android.utilities.AndroidLoggerProvider
import com.amplitude.android.utilities.AndroidStorageProvider
import com.amplitude.core.Configuration
import com.amplitude.core.EventCallBack
import com.amplitude.core.LoggerProvider
import com.amplitude.core.ServerZone
import com.amplitude.core.StorageProvider
import com.amplitude.core.events.IngestionMetadata
import com.amplitude.core.events.Plan
import com.amplitude.id.FileIdentityStorageProvider
import com.amplitude.id.IdentityStorageProvider
import java.io.File

open class Configuration(
apiKey: String,
Expand All @@ -20,7 +20,7 @@ open class Configuration(
override var flushIntervalMillis: Int = FLUSH_INTERVAL_MILLIS,
override var instanceName: String = DEFAULT_INSTANCE,
override var optOut: Boolean = false,
override var storageProvider: StorageProvider = AndroidStorageProvider(),
override var storageProvider: StorageProvider = AndroidStorageContextV3.eventsStorageProvider,
override var loggerProvider: LoggerProvider = AndroidLoggerProvider(),
override var minIdLength: Int? = null,
override var partnerId: String? = null,
Expand All @@ -41,8 +41,8 @@ open class Configuration(
var minTimeBetweenSessionsMillis: Long = MIN_TIME_BETWEEN_SESSIONS_MILLIS,
autocapture: Set<AutocaptureOption> = setOf(AutocaptureOption.SESSIONS),
override var identifyBatchIntervalMillis: Long = IDENTIFY_BATCH_INTERVAL_MILLIS,
override var identifyInterceptStorageProvider: StorageProvider = AndroidStorageProvider(),
override var identityStorageProvider: IdentityStorageProvider = FileIdentityStorageProvider(),
override var identifyInterceptStorageProvider: StorageProvider = AndroidStorageContextV3.identifyInterceptStorageProvider,
override var identityStorageProvider: IdentityStorageProvider = AndroidStorageContextV3.identityStorageProvider,
var migrateLegacyData: Boolean = true,
override var offline: Boolean? = false,
override var deviceId: String? = null,
Expand Down Expand Up @@ -84,7 +84,7 @@ open class Configuration(
flushIntervalMillis: Int = FLUSH_INTERVAL_MILLIS,
instanceName: String = DEFAULT_INSTANCE,
optOut: Boolean = false,
storageProvider: StorageProvider = AndroidStorageProvider(),
storageProvider: StorageProvider = AndroidStorageContextV3.eventsStorageProvider,
loggerProvider: LoggerProvider = AndroidLoggerProvider(),
minIdLength: Int? = null,
partnerId: String? = null,
Expand All @@ -106,8 +106,8 @@ open class Configuration(
trackingSessionEvents: Boolean = true,
@Suppress("DEPRECATION") defaultTracking: DefaultTrackingOptions = DefaultTrackingOptions(),
identifyBatchIntervalMillis: Long = IDENTIFY_BATCH_INTERVAL_MILLIS,
identifyInterceptStorageProvider: StorageProvider = AndroidStorageProvider(),
identityStorageProvider: IdentityStorageProvider = FileIdentityStorageProvider(),
identifyInterceptStorageProvider: StorageProvider = AndroidStorageContextV3.identifyInterceptStorageProvider,
identityStorageProvider: IdentityStorageProvider = AndroidStorageContextV3.identityStorageProvider,
migrateLegacyData: Boolean = true,
offline: Boolean? = false,
deviceId: String? = null,
Expand Down Expand Up @@ -154,6 +154,8 @@ open class Configuration(
this.defaultTracking = defaultTracking
}

private var storageDirectory: File? = null

// A backing property to store the autocapture options. Any changes to `trackingSessionEvents`
// or the `defaultTracking` options will be reflected in this property.
private var _autocapture: MutableSet<AutocaptureOption> = autocapture.toMutableSet()
Expand Down Expand Up @@ -181,4 +183,13 @@ open class Configuration(
private fun DefaultTrackingOptions.updateAutocaptureOnPropertyChange() {
_autocapture = autocaptureOptions
}

internal fun getStorageDirectory(): File {
if (storageDirectory == null) {
val dir = context.getDir("amplitude", Context.MODE_PRIVATE)
storageDirectory = File(dir, "${context.packageName}/$instanceName/analytics/")
storageDirectory?.mkdirs()
}
return storageDirectory!!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.amplitude.android.migration

import com.amplitude.android.storage.AndroidStorageV2
import com.amplitude.common.Logger
import com.amplitude.core.Storage
import com.amplitude.core.utilities.toEvents
import org.json.JSONArray

class AndroidStorageMigration(
private val source: AndroidStorageV2,
private val destination: AndroidStorageV2,
private val logger: Logger
) {
suspend fun execute() {
moveEventsToDestination()
moveSimpleValues()
}

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

for (sourceEventFilePath in sourceEventFiles) {
val events = source.getEventsString(sourceEventFilePath)
var count = 0
val baseEvents = JSONArray(events).toEvents()
for (event in baseEvents) {
try {
count++
destination.writeEvent(event)
} catch (e: Exception) {
logger.error("can't move event ($event) from file $sourceEventFilePath: ${e.message}")
}
}
logger.debug("Migrated $count/${baseEvents.size} events from $sourceEventFilePath")
source.removeFile(sourceEventFilePath)
}
source.cleanupMetadata()
destination.rollover()
} 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)
}

private suspend fun moveSimpleValue(key: Storage.Constants) {
try {
val sourceValue = source.read(key) ?: return
val destinationValue = destination.read(key)
if (destinationValue == null) {
try {
logger.debug("Migrating $key with value $sourceValue")
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}")
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.amplitude.android.migration

import com.amplitude.common.Logger
import com.amplitude.id.IdentityStorage

class IdentityStorageMigration(
private val source: IdentityStorage,
private val destination: IdentityStorage,
private val logger: Logger
) {
fun execute() {
try {
val identity = source.load()
logger.debug("Loaded old identity: $identity")
if (identity.userId != null) {
destination.saveUserId(identity.userId)
}
if (identity.deviceId != null) {
destination.saveDeviceId(identity.deviceId)
}
source.delete()
} catch (e: Exception) {
logger.error("Unable to migrate file identity storage: ${e.message}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.amplitude.android.migration

import android.content.Context
import android.content.SharedPreferences
import com.amplitude.android.Amplitude
import com.amplitude.android.Configuration
import com.amplitude.android.storage.AndroidStorageContextV1
import com.amplitude.android.storage.AndroidStorageContextV2
import com.amplitude.android.storage.LegacySdkStorageContext
import com.amplitude.android.storage.StorageVersion
import com.amplitude.common.Logger

internal class MigrationManager(private val amplitude: Amplitude) {
private val sharedPreferences: SharedPreferences
private val config: Configuration = amplitude.configuration as Configuration
private val logger: Logger = amplitude.logger
private val currentStorageVersion: Int

init {
sharedPreferences = config.context.getSharedPreferences(
"amplitude-android-${config.instanceName}",
Context.MODE_PRIVATE
)
currentStorageVersion = sharedPreferences.getInt("storage_version", 0)
}

suspend fun migrateOldStorage() {
if (currentStorageVersion < StorageVersion.V3.rawValue) {
logger.debug("Migrating storage to version ${StorageVersion.V3.rawValue}")
safePerformMigration()
} else {
amplitude.logger.debug("Storage already at version ${StorageVersion.V3.rawValue}")
}
}

internal suspend fun safePerformMigration() {
try {
val config = amplitude.configuration as Configuration
if (config.migrateLegacyData) {
val legacySdkStorageContext = LegacySdkStorageContext(amplitude)
legacySdkStorageContext.migrateToLatestVersion()
}

val storageContextV1 = AndroidStorageContextV1(amplitude, config)
storageContextV1.migrateToLatestVersion()

val storageContextV2 = AndroidStorageContextV2(amplitude, config)
storageContextV2.migrateToLatestVersion()

sharedPreferences.edit().putInt("storage_version", StorageVersion.V3.rawValue).apply()
} catch (ex: Throwable) {
logger.error("Failed to migrate storage: ${ex.message}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import org.json.JSONObject
* 4. deletes data from sqlite table
*/

class RemnantDataMigration(
val amplitude: Amplitude,
) {
class RemnantDataMigration(val amplitude: Amplitude, private val databaseStorage: DatabaseStorage) {
companion object {
const val DEVICE_ID_KEY = "device_id"
const val USER_ID_KEY = "user_id"
Expand All @@ -26,11 +24,7 @@ class RemnantDataMigration(
const val PREVIOUS_SESSION_ID_KEY = "previous_session_id"
}

lateinit var databaseStorage: DatabaseStorage

suspend fun execute() {
databaseStorage = DatabaseStorageProvider.getStorage(amplitude)

val firstRunSinceUpgrade = amplitude.storage.read(Storage.Constants.LAST_EVENT_TIME)?.toLongOrNull() == null

moveDeviceAndUserId()
Expand All @@ -41,6 +35,8 @@ class RemnantDataMigration(
moveIdentifies()
}
moveEvents()
amplitude.storage.rollover()
amplitude.identifyInterceptStorage.rollover()
}

private fun moveDeviceAndUserId() {
Expand Down
Loading

0 comments on commit 3be476b

Please sign in to comment.