Skip to content

Commit

Permalink
Separation of API, State, and Side Effects (#160)
Browse files Browse the repository at this point in the history
* Separation of API, State and Side Effects. Largely left tests alone except for adding a couple and adjusting setup to accommodate changes.

* Use consistent nullability pattern so we aren't converting between empty string and null with identifiers.
Added proper deserializing of the profile attributes store property

* There is indeed a better way to do that

* Renamed some of the new classes and added new tests for them

* Doc blocks, minor cleanup, naming conventions

* Fix nullability test
Don't need immutable profile to expose attributes, but would be good to expose single attribute getter

* First round of PR comments

* missed another arg label

---------

Co-authored-by: Evan Masseau <>
  • Loading branch information
evan-masseau authored May 1, 2024
1 parent 8a64598 commit 3c92609
Show file tree
Hide file tree
Showing 25 changed files with 1,128 additions and 417 deletions.
100 changes: 16 additions & 84 deletions sdk/analytics/src/main/java/com/klaviyo/analytics/Klaviyo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import android.content.Intent
import com.klaviyo.analytics.model.Event
import com.klaviyo.analytics.model.EventKey
import com.klaviyo.analytics.model.EventMetric
import com.klaviyo.analytics.model.PROFILE_IDENTIFIERS
import com.klaviyo.analytics.model.Profile
import com.klaviyo.analytics.model.ProfileKey
import com.klaviyo.analytics.networking.ApiClient
import com.klaviyo.analytics.networking.KlaviyoApiClient
import com.klaviyo.analytics.state.KlaviyoState
import com.klaviyo.analytics.state.State
import com.klaviyo.analytics.state.StateSideEffects
import com.klaviyo.core.Registry
import com.klaviyo.core.config.Config
import com.klaviyo.core.config.LifecycleException
Expand All @@ -35,10 +37,8 @@ object Klaviyo {
)
val lifecycleCallbacks: ActivityLifecycleCallbacks get() = NoOpLifecycleCallbacks

private val profileOperationQueue = ProfileOperationQueue()

init {
// Since analytics platform owns ApiClient, we must register the service on initialize
// Since analytics module owns ApiClient, we must register the service on initialize
if (!Registry.isRegistered<ApiClient>()) Registry.register<ApiClient> { KlaviyoApiClient }
}

Expand All @@ -63,7 +63,8 @@ object Klaviyo {
registerActivityLifecycleCallbacks(Registry.lifecycleCallbacks)
} ?: throw LifecycleException()

UserInfo.startObservers()
Registry.register<State>(KlaviyoState())
Registry.register<StateSideEffects>(StateSideEffects())
}

/**
Expand All @@ -79,35 +80,7 @@ object Klaviyo {
* @return Returns [Klaviyo] for call chaining
*/
fun setProfile(profile: Profile): Klaviyo = safeApply {
if (UserInfo.isIdentified) {
// If a profile with external identifiers is already in state, we must reset.
// This conditional is important to preserve merging with an anonymous profile.
resetProfile()
}

// Copy the profile object, so we aren't mutating the argument
val mutableProfile = Profile().merge(profile)

// Route identifiers to the explicit setter functions to re-use that validator logic
mutableProfile.externalId?.let {
setExternalId(it)
mutableProfile.externalId = null
}

mutableProfile.email?.let {
setEmail(it)
mutableProfile.email = null
}

mutableProfile.phoneNumber?.let {
setPhoneNumber(it)
mutableProfile.phoneNumber = null
}

// Enqueue any remaining profile attributes
if (mutableProfile.propertyCount() > 0) {
profileOperationQueue.debounceProfileUpdate(mutableProfile)
}
Registry.get<State>().setProfile(profile)
}

/**
Expand All @@ -131,7 +104,7 @@ object Klaviyo {
/**
* @return The email of the currently tracked profile, if set
*/
fun getEmail(): String? = safeCall { UserInfo.email.ifEmpty { null } }
fun getEmail(): String? = safeCall { Registry.get<State>().email }

/**
* Assigns a phone number to the currently tracked Klaviyo profile
Expand All @@ -158,9 +131,7 @@ object Klaviyo {
/**
* @return The phone number of the currently tracked profile, if set
*/
fun getPhoneNumber(): String? = safeCall {
UserInfo.phoneNumber.ifEmpty { null }
}
fun getPhoneNumber(): String? = safeCall { Registry.get<State>().phoneNumber }

/**
* Assigns a unique identifier to associate the currently tracked Klaviyo profile
Expand Down Expand Up @@ -188,9 +159,7 @@ object Klaviyo {
/**
* @return The external ID of the currently tracked profile, if set
*/
fun getExternalId(): String? = safeCall {
UserInfo.externalId.ifEmpty { null }
}
fun getExternalId(): String? = safeCall { Registry.get<State>().externalId }

/**
* Saves a push token and registers to the current profile
Expand All @@ -202,18 +171,12 @@ object Klaviyo {
*
* @param pushToken The push token provided by the device push service
*/
fun setPushToken(pushToken: String) = safeApply {
UserInfo.setPushToken(pushToken) {
Registry.get<ApiClient>().enqueuePushToken(pushToken, UserInfo.getAsProfile())
}
}
fun setPushToken(pushToken: String) = safeApply { Registry.get<State>().pushToken = pushToken }

/**
* @return The device push token, if one has been assigned to currently tracked profile
*/
fun getPushToken(): String? = safeCall {
UserInfo.pushToken.ifEmpty { null }
}
fun getPushToken(): String? = safeCall { Registry.get<State>().pushToken }

/**
* Assign an attribute to the currently tracked profile by key/value pair
Expand All @@ -229,32 +192,7 @@ object Klaviyo {
* @return Returns [Klaviyo] for call chaining
*/
fun setProfileAttribute(propertyKey: ProfileKey, value: String): Klaviyo = safeApply {
if (PROFILE_IDENTIFIERS.contains(propertyKey)) {
value.trim().ifEmpty {
Registry.log.warning(
"Empty string for $propertyKey will be ignored. To clear identifiers use resetProfile."
)
null
}?.also { validatedIdentifier ->
var property by when (propertyKey) {
ProfileKey.EXTERNAL_ID -> UserInfo::externalId
ProfileKey.EMAIL -> UserInfo::email
ProfileKey.PHONE_NUMBER -> UserInfo::phoneNumber
else -> return@safeApply
}

if (property != validatedIdentifier) {
property = validatedIdentifier
profileOperationQueue.debounceProfileUpdate(UserInfo.getAsProfile())
} else {
Registry.log.info(
"$propertyKey value was unchanged, the update will be ignored."
)
}
}
} else {
profileOperationQueue.debounceProfileUpdate(Profile(mapOf(propertyKey to value)))
}
Registry.get<State>().setAttribute(propertyKey, value)
}

/**
Expand All @@ -266,13 +204,7 @@ object Klaviyo {
* This should be called whenever an active user in your app is removed
* (e.g. after a logout)
*/
fun resetProfile() = safeApply {
// Flush any pending profile changes immediately
profileOperationQueue.flushProfile()

// Clear profile identifiers from state
UserInfo.reset()
}
fun resetProfile() = safeApply { Registry.get<State>().reset() }

/**
* Creates an [Event] associated with the currently tracked profile
Expand All @@ -281,8 +213,7 @@ object Klaviyo {
* @return Returns [Klaviyo] for call chaining
*/
fun createEvent(event: Event): Klaviyo = safeApply {
Registry.log.verbose("Enqueuing ${event.metric.name} event")
Registry.get<ApiClient>().enqueueEvent(event, UserInfo.getAsProfile())
Registry.get<ApiClient>().enqueueEvent(event, Registry.get<State>().getAsProfile())
}

/**
Expand Down Expand Up @@ -327,6 +258,7 @@ object Klaviyo {
/**
* Checks whether a notification intent originated from Klaviyo
*/
@Suppress("MemberVisibilityCanBePrivate")
val Intent.isKlaviyoIntent: Boolean
get() = this.getStringExtra("com.klaviyo._k")?.isNotEmpty() ?: false
}

This file was deleted.

149 changes: 0 additions & 149 deletions sdk/analytics/src/main/java/com/klaviyo/analytics/UserInfo.kt

This file was deleted.

Loading

0 comments on commit 3c92609

Please sign in to comment.