Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrating storage to version v3 #226

Merged
merged 13 commits into from
Oct 2, 2024
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