Giter Club home page Giter Club logo

klaviyo-android-sdk's Introduction

klaviyo-android-sdk

GitHub Latest GitHub release (latest SemVer) GitHub Workflow Status

The Klaviyo Android SDK allows developers to incorporate Klaviyo analytics and push notification functionality in their native Android applications. The SDK assists in identifying users and tracking user events via the latest Klaviyo Client APIs. To reduce performance overhead, API requests are queued and sent in batches. The queue is persisted to local storage so that data is not lost if the device is offline or the app is terminated.

Once integrated, your marketing team will be able to better understand your app users' needs and send them timely push notifications via FCM (Firebase Cloud Messaging).

⚠️ We support Android API level 23 and above ⚠️

Installation

  1. Include the JitPack repository in your project's build file

    Kotlin DSL
    // settings.gradle.kts
    dependencyResolutionManagement {
        repositories {
            maven(url = "https://jitpack.io")
        }
    }
    Groovy
    // settings.gradle
    dependencyResolutionManagement {
        repositories {
            maven { url "https://jitpack.io" }
        }
    }
  2. Add the dependencies to your app's build file

    Kotlin DSL
    // build.gradle.kts
    dependencies {
        implementation("com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.0")
        implementation("com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.0")
    }
    Groovy
     // build.gradle
     dependencies {
         implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:2.4.0"
         implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.4.0"
     }

Initialization

The SDK must be initialized with the short alphanumeric public API key for your Klaviyo account, also known as your Site ID. We require access to the applicationContext so the SDK can be responsive to changes in application state and network conditions, and access SharedPreferences to persist data. Upon initialize, the SDK registers listeners for your application's activity lifecycle callbacks, to gracefully manage background processes.

Klaviyo.initialize() must be called before any other SDK methods can be invoked. We recommend initializing from the earliest point in your application code, such as the Application.onCreate() method.

// Application subclass 
import android.app.Application
import com.klaviyo.analytics.Klaviyo

class YourApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        /* ... */
        
        // Initialize is required before invoking any other Klaviyo SDK functionality 
        Klaviyo.initialize("KLAVIYO_PUBLIC_API_KEY", applicationContext)
    }
}

Profile Identification

The SDK provides methods to identify profiles via the Create Client Profile API. A profile can be identified by any combination of the following:

  • External ID: A unique identifier used by customers to associate Klaviyo profiles with profiles in an external system, such as a point-of-sale system. Format varies based on the external system.
  • Individual's email address
  • Individual's phone number in E.164 format

Identifiers are persisted to local storage so that the SDK can keep track of the current profile.

Profile identifiers and other attributes can be set all at once using the Profile data class:

val profile = Profile(
    externalId = "USER_IDENTIFIER",
    email = "[email protected]",
    phoneNumber = "+12223334444",
    properties = mapOf(
        ProfileKey.FIRST_NAME to "Kermit",
        ProfileKey.CUSTOM("instrument") to "banjo"
    )
)

Klaviyo.setProfile(profile)

Or individually with additive fluent setters:

Klaviyo.setExternalId("USER_IDENTIFIER")
    .setEmail("[email protected]")
    .setPhoneNumber("+12223334444")
    .setProfileAttribute(ProfileKey.FIRST_NAME, "Kermit")
    .setProfileAttribute(ProfileKey.CUSTOM("instrument"), "banjo")

Either way, the SDK will group and batch API calls to improve performance.

Reset Profile

To start a new profile altogether (e.g. if a user logs out) either call Klaviyo.resetProfile() to clear the currently tracked profile identifiers (e.g. on logout), or use Klaviyo.setProfile(profile) to overwrite it with a new profile object.

// Start a profile for Kermit
Klaviyo.setEmail("[email protected]")
    .setPhoneNumber("+12223334444")
    .setProfileAttribute(ProfileKey.FIRST_NAME, "Kermit")

// Stop tracking Kermit
Klaviyo.resetProfile()

// Start a new profile for Robin
Klaviyo.setEmail("[email protected]")
    .setPhoneNumber("+5556667777")
    .setProfileAttribute(ProfileKey.FIRST_NAME, "Robin")

Note: We trim leading and trailing whitespace off of identifier values. Empty strings will be ignored with a logged warning. If you are trying to remove an identifier's value, use setProfile or resetProfile.

Anonymous Tracking

Klaviyo will track unidentified users with an autogenerated ID whenever a push token is set or an event is created. That way, you can collect push tokens and track events prior to collecting profile identifiers such as email or phone number. When an identifier is provided, Klaviyo will merge the anonymous user with an identified user.

Event Tracking

The SDK also provides tools for tracking analytics events via the Create Client Event API. A list of common Klaviyo-defined event metrics is provided in EventMetric, or you can use EventMetric.CUSTOM("name") for custom event metric names. Additional event properties can be specified as part of EventModel

val event = Event(EventMetric.VIEWED_PRODUCT)
    .setProperty(EventKey.CUSTOM("Product"), "Coffee Mug")
    .setValue(10.0)
Klaviyo.createEvent(event)

Push Notifications

Prerequisites

Setup

The Klaviyo Push SDK for Android works as a wrapper around FirebaseMessagingService, so the setup process is very similar to the Firebase client documentation linked above.
In your AndroidManifest.xml file, register KlaviyoPushService to receive MESSAGING_EVENT intents.

<!-- AndroidManifest.xml -->
<manifest>
    <!-- ... -->
    <application>
        <!-- ... -->
        <service android:name="com.klaviyo.pushFcm.KlaviyoPushService" android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>
</manifest>

To specify an icon and/or color for Klaviyo notifications, add the following optional metadata elements to the application component of AndroidManifest.xml. Absent these keys, the firebase keys com.google.firebase.messaging.default_notification_icon and com.google.firebase.messaging.default_notification_color will be used if present, else we fall back on the application's launcher icon, and omit setting a color.

<!-- AndroidManifest.xml -->
<manifest>
    <!-- ... -->
    <application>
        <!-- ... -->
        <meta-data android:name="com.klaviyo.push.default_notification_icon"
            android:resource="{YOUR_ICON_RESOURCE}" />
        <meta-data android:name="com.klaviyo.push.default_notification_color"
            android:resource="{YOUR_COLOR}" />
    </application>
</manifest>

Collecting Push Tokens

In order to send push notifications to your users, you must collect their push tokens and register them with Klaviyo. This is done via the Klaviyo.setPushToken method, which registers push token and current authorization state via the Create Client Push Token API. Once registered in your manifest, KlaviyoPushService will receive new push tokens via the onNewToken method. We also recommend retrieving the current token on app startup and registering it with Klaviyo SDK. Add the following to your Application.onCreate method.

override fun onCreate(savedInstanceState: Bundle?) {
    /* ... */

    // Fetches the current push token and registers with Push SDK
    FirebaseMessaging.getInstance().token.addOnSuccessListener { pushToken ->
        Klaviyo.setPushToken(pushToken)
    }
}

Reminder: Klaviyo.initialize is required before using any other Klaviyo SDK functionality, even if you are only using the SDK for push notifications and not analytics.

Android 13 introduced a new runtime permission for displaying notifications. The Klaviyo SDK automatically adds the POST_NOTIFICATIONS permission to the manifest, but you will need to request user permission according to Android best practices and the best user experience in the context of your application. The linked resources provide code examples for requesting permission and handling the user's response.

Push tokens and multiple profiles

Klaviyo SDK will disassociate the device push token from the current profile whenever it is reset by calling setProfile or resetProfile. You should call setPushToken again after resetting the currently tracked profile to explicitly associate the device token to the new profile.

Receiving Push Notifications

KlaviyoPushService will handle displaying all notifications via the onMessageReceived method regardless of whether the app is in the foreground or background. You can send test notifications to a specific token using the push notification preview feature in order to test your integration. If you wish to customize how notifications are displayed, see Advanced Setup.

Rich Push

Rich Push is the ability to add images to push notification messages. This feature is supported in version 1.3.1 and up of the Klaviyo Android SDK. No additional setup is needed to support rich push. Downloading the image and attaching it to the notification is handled within KlaviyoPushService. If an image fails to download (e.g. if the device has a poor network connection) the notification will be displayed without an image after the download times out.

Tracking Open Events

To track push notification opens, you must call Klaviyo.handlePush(intent) when your app is launched from an intent. This method will check if the app was opened from a notification originating from Klaviyo and if so, create an Opened Push event with required message tracking parameters. For example:

// Main Activity

override fun onCreate(savedInstanceState: Bundle?) {
    /* ... */

    onNewIntent(intent)
}

override fun onNewIntent(intent: Intent?) {
    /* ... */

    // Tracks when a system tray notification is opened
    Klaviyo.handlePush(intent)
}

Note: Intent handling may differ depending on your app's architecture. By default, the Klaviyo SDK will use your app's launch intent for a tapped notification. Adjust this example to your use-case, ensuring that Klaviyo.handlePush(intent) is called whenever your app is opened from a notification.

Deep Linking

Deep Links allow you to navigate to a particular page within your app in response to the user opening a notification. There are broadly three steps to implement deep links in your app.

  1. Add intent filters for incoming links:

    Add an intent filter to the activity element of your AndroidManifest.xml file. Replace the scheme and host to match the URI scheme that you will embed in your push notifications.

    <!-- AndroidManifest.xml -->
    <manifest>
        <!-- ... -->
        <application>
            <!-- ... -->
            <activity>
                <!-- ... -->
                <intent-filter android:label="@string/filter_view_example_gizmos">
                    <action android:name="android.intent.action.VIEW" />
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
                    <!-- Accepts URIs formatted "example://host.com” -->
                    <data android:scheme="example" android:host="host.com"/>
                </intent-filter>
            </activity>
        </application>
    </manifest>
  2. Read data from incoming intents:

    When the app is opened from a deep link, the intent that started the activity contains data for the deep link. You can parse the URI from the intent's data property and use it to navigate to the appropriate part of your app.

    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        
        onNewIntent(intent)
    }
    
    override fun onNewIntent(intent: Intent?) {
        // Tracks when a system tray notification is opened
        Klaviyo.handlePush(intent)
    
        // Read deep link data from intent
        val action: String? = intent?.action 
        val deepLink: Uri? = intent?.data
    }
  3. Test your deep links:

    Using android debug bridge (adb), run the following command to launch your app via an intent containing a deep link to test your deep link handler.

    adb shell am start
        -W -a android.intent.action.VIEW
        -d <URI> <PACKAGE>

    To perform integration testing, you can send a preview push notification containing a deep link from the Klaviyo push editor.

For additional resources on deep linking, refer to Android developer documentation.

Advanced Setup

If you'd prefer to have your own implementation of FirebaseMessagingService, follow the FCM setup docs including referencing your own service class in the manifest.

<!-- AndroidManifest.xml -->
<manifest>
    <!-- ... -->
    <application>
        <!-- ... -->
        <service android:name="your.package.name.YourPushService" android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
    </application>
</manifest>

The Application code snippets above for handling push tokens and intents are still required.

You may either subclass KlaviyoPushService or invoke the necessary Klaviyo SDK methods in your service.

  1. Subclass KlaviyoPushService:

    import com.google.firebase.messaging.RemoteMessage
    import com.klaviyo.pushFcm.KlaviyoPushService
    import com.klaviyo.pushFcm.KlaviyoRemoteMessage.isKlaviyoNotification
    
    class YourPushService : KlaviyoPushService() {
        override fun onNewToken(newToken: String) {
            // Invoking the super method will ensure Klaviyo SDK gets the new token
            super.onNewToken(newToken)
        }
    
        override fun onMessageReceived(message: RemoteMessage) {
            // Invoking the super method allows Klaviyo SDK to handle Klaviyo messages
            super.onMessageReceived(message)
        
            // This extension method allows you to distinguish Klaviyo from other sources
            if (!message.isKlaviyoNotification) {
                // Handle non-Klaviyo messages
            }
        }
    }
  2. Subclass FirebaseMessagingService and invoke Klaviyo SDK methods directly

    import com.google.firebase.messaging.FirebaseMessagingService
    import com.google.firebase.messaging.RemoteMessage
    import com.klaviyo.analytics.Klaviyo
    import com.klaviyo.pushFcm.KlaviyoNotification
    import com.klaviyo.pushFcm.KlaviyoRemoteMessage.isKlaviyoNotification
    
    class YourPushService : FirebaseMessagingService() {
    
        override fun onNewToken(newToken: String) {
            super.onNewToken(newToken)
            Klaviyo.setPushToken(newToken)
        }
    
        override fun onMessageReceived(message: RemoteMessage) {
            super.onMessageReceived(message)
    
            // This extension method allows you to distinguish Klaviyo from other sources
            if (message.isKlaviyoNotification) {
                // Handle displaying a notification from Klaviyo
                KlaviyoNotification(message).displayNotification(this)
            } else {
                // Handle non-Klaviyo messages
            }
        }
    }

Note: Klaviyo uses data messages to provide consistent notification formatting. As a result, all Klaviyo notifications are handled via onMessageReceived regardless of the app being in the background or foreground. If you are working with multiple remote sources, you can check whether a message originated from Klaviyo with the extension method RemoteMessage.isKlaviyoNotification.

Custom Notification Display

If you wish to fully customize the display of notifications, we provide a set of RemoteMessage extensions such as import com.klaviyo.pushFcm.KlaviyoRemoteMessage.body to access all the properties sent from Klaviyo. We also provide an Intent.appendKlaviyoExtras(RemoteMessage) extension method, which attaches the data to your notification intent that the Klaviyo SDK requires in order to track opens when you call Klaviyo.handlePush(intent).

Troubleshooting

The SDK contains logging at different levels from verbose to assert. By default, the SDK logs at the error level in a production environment and at the warning level in a debug environment. You can change the log level by adding the following metadata tag to your manifest file.

  • 0 = disable logging entirely
  • 1 = Verbose and above
  • 2 = Debug and above
  • 3 = Info and above
  • 4 = Warning and above
  • 5 = Error and above
  • 6 = Assert only
<!-- AndroidManifest.xml -->    
<manifest>
    <!-- ... -->
    <application>
        <!-- Enable SDK debug logging -->
        <meta-data
            android:name="com.klaviyo.core.log_level"
            android:value="2" />
    </application>
</manifest>

Contributing

See the contributing guide to learn how to contribute to the Klaviyo Android SDK. We welcome your feedback in the issues section of our public GitHub repository.

License

The Klaviyo Android SDK is available under the MIT license. See LICENSE for more info.

Code Documentation

Browse complete code documentation autogenerated with Dokka here.

klaviyo-android-sdk's People

Contributors

ajaysubra avatar evan-masseau avatar jenren-tech avatar jordan-griffin avatar kennyklaviyo avatar ndurell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

isabella232

klaviyo-android-sdk's Issues

Add a .unregisterPushPayload() API method

Description

I'm looking for a way to unsubscribe (opting out) our users from a push subscription. Looking at the available SDKs, iOS has a way to unsubscribe the users:

https://github.com/klaviyo/klaviyo-swift-sdk/blob/bb2506d192a8cdb9396c81061a90469c1f2c1486/Sources/KlaviyoSwift/KlaviyoAPI.swift#L121

        case .unregisterPushToken:
            return "client/push-token-unregister"
        }

Proposed Solution

Klaviyo Android SDK should have an equivalent API or method caller for the API **Unregister Push Payload**:
https://developers.klaviyo.com/en/reference/unregister_client_push_token

Alternatives Considered

- Calling HTTP directly, but seems like a hacky solution, considering we're using an SDK for other APIs

Additional Context

- We're building on top of Flutter, so we're building a communication interface using [platform channels](https://docs.flutter.dev/platform-integration/platform-channels)

Manually logging OPENED_PUSH not showing on klaviyo's dashboard

Description

We are trying to integrate Klaviyo's notification into project that already uses Firebase Messaging.
In FirebaseMessagingService's onMessageReceived we customize how the notification looks, so we are not able to use KlaviyoNotification to display notification.

So we extract the information from Klaviyo's notification and display.

    val isKlaviyoNotification = remoteMessage.isKlaviyoNotification
    val title = if (isKlaviyoNotification) remoteMessage.title else remoteMessage.notification?.title
    val body = if (isKlaviyoNotification) remoteMessage.body else remoteMessage.notification?.body
    val deepLinkUri = if (isKlaviyoNotification) remoteMessage.deepLink else remoteMessage.data[DEEP_LINK_KEY]?.toUri()

// ... displaying notification with custom intent

Then when notification clicked, if it is Klaviyo's notification we are trying to track it like this:

            if (isKlaviyo) {
                storage.getPushNotificationsToken()?.let { token ->
                    klaviyoManager.logOpenedPush(token, id.orEmpty())
                }
            }

Tracking method:

    fun logOpenedPush(pushToken: String, notificationId: String) {
        val event = Event(EventType.OPENED_PUSH)
            .setProperty(EventKey.PUSH_TOKEN, pushToken)
        if (notificationId.isNotEmpty()) {
            event.setProperty(EventKey.EVENT_ID, notificationId)
        }
        klaviyo.createEvent(event)
    }

I couldn't find any document related to loggin EventType.OPENED_PUSH manually except in the code of EventKey:

/**
 * All event property keys recognised by the Klaviyo APIs
 * Custom properties can be defined using the [CUSTOM] inner class
 */
sealed class EventKey(name: String) : Keyword(name) {
    object EVENT_ID : EventKey("\$event_id")
    object VALUE : EventKey("\$value")

    /**
     * For [EventType.OPENED_PUSH] events, append the device token as an event property
     */
    object PUSH_TOKEN : EventKey("push_token")

    class CUSTOM(propertyName: String) : EventKey(propertyName)
}

So I added push_token as shown above in my tracking opened push

Checklist

  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Expected behavior

I expected to see open rate on Klaviyo's dashboard in campaign report

Actual behavior

It shows only Delievered notifications number

unnamed

Steps to reproduce

Log OPENED_PUSH without using Klaviyo's handleNotification and newIntent

The Klaviyo Android SDK version information

1.1.0

Device Information

Emulator

Android Studio Version

Flamingo Patch 2 ( latest )

Android API Level

API Level 33

Notification not receiving when the app is killed. (lock screen)

Description

When the app is in the background or the foreground, notifications are received as expected. However, when the app is killed (e.g., removed from the recent apps list or on the lock screen), notifications do not appear.

Checklist

  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Expected behavior

Notifications should be received even when the app is not actively running or is in the background.

Actual behavior

Notifications are not received when the app is in the killed state (lock screen).

Steps to reproduce

Ensure the Klaviyo-integrated app is installed.
Launch the app.
Receive a notification when the app is in the background or foreground (working as expected).
Kill the app (e.g., remove it from the recent apps list).
Send a notification to the device.

The Klaviyo Android SDK version information

1.3.4

Device Information

pixel 6

Android Studio Version

Android Studio Giraffe | 2022.3.1 Patch 2

Android API Level

33

Weird behaviour with external_id

Description

I noticed, that this SDK for some reasons also push external_id to custom properties.

So, I see it twice at profile page at dashboard.

Checklist

  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Expected behavior

No response

Actual behavior

No response

Steps to reproduce

No response

The Klaviyo Android SDK version information

1.1.0

Device Information

No response

Android Studio Version

No response

Android API Level

No response

I am getting error when i try integrate klaviyo sdk with android app

Description

I am getting below error : 
> Task :app:processDebugResources FAILED

Execution failed for task ':app:processDebugResources'.
> A failure occurred while executing com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$TaskAction
   > AAPT2 aapt2-7.0.3-7396180-osx Daemon #0: Unexpected error during link, attempting to stop daemon.
     This should not happen under normal circumstances, please file an issue if it does.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

here is build.gradle  app level gradle dependencies 

dependencies {

    implementation "com.github.klaviyo.klaviyo-android-sdk:analytics:2.0.0"
    implementation "com.github.klaviyo.klaviyo-android-sdk:push-fcm:2.0.0"
}

build.gradle project level 

 dependencies {
        classpath 'com.android.tools.build:gradle:7.0.3'
        classpath 'com.google.gms:google-services:4.3.10'
        // Add the Crashlytics Gradle plugin
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
        classpath 'com.vanniktech:gradle-maven-publish-plugin:0.15.1'

    }

please help me how to fix this issue .

Checklist

  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Expected behavior

Code should build successfully

Actual behavior

I am getting above error

Steps to reproduce

try to add gradle dependencies

The Klaviyo Android SDK version information

2.0.0

Device Information

compileSdkVersion 34 // Update to API level 34
ndkVersion '25.2.9519653'
buildToolsVersion "34.0.0" // Update to the corresponding build tools version

defaultConfig {
    applicationId "com.test.android"
    minSdkVersion 23
    targetSdkVersion 34
    versionCode 69
    versionName "3.20"
    multiDexEnabled true

}

Android Studio Version

Android Studio Hedgehog | 2023.1.1 Patch 1
Build #AI-231.9392.1.2311.11255304, built on December 27, 2023
Runtime version: 17.0.7+0-17.0.7b1000.6-10550314 aarch64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
macOS 14.2.1
GC: G1 Young Generation, G1 Old Generation
Memory: 2048M
Cores: 8
Metal Rendering is ON
Registry:
external.system.auto.import.disabled=true
ide.text.editor.with.preview.show.floating.toolbar=false
ide.instant.shutdown=false

Non-Bundled Plugins:
Dart (231.9411)
PythonCore (231.9225.4)
io.flutter (77.1.2)

Android API Level

34

java.util.ConcurrentModificationException

Checklist

  • I have read the contributing guidelines
  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the master branch or latest release of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Description

Firebase report error

Expected behavior

Firebase do not report this error

Actual behavior

Fatal Exception: java.util.ConcurrentModificationException
java.util.ArrayList$Itr.next (ArrayList.java:860)
com.klaviyo.core.networking.KlaviyoNetworkMonitor.broadcastNetworkChange (KlaviyoNetworkMonitor.kt:127)
com.klaviyo.core.networking.KlaviyoNetworkMonitor.access$broadcastNetworkChange (KlaviyoNetworkMonitor.kt:16)
com.klaviyo.core.networking.KlaviyoNetworkMonitor$networkCallback$1.onAvailable (KlaviyoNetworkMonitor.kt:33)
android.net.ConnectivityManager$CallbackHandler.handleMessage (ConnectivityManager.java:2792)
android.os.Handler.dispatchMessage (Handler.java:108)
android.os.Looper.loop (Looper.java:206)
android.os.HandlerThread.run (HandlerThread.java:65)

Steps to reproduce

none

The Klaviyo Android SDK version information

2.4.0

Device Information

Moto G Stylus 5G Android 14 \Galaxy S8 Android 9......

Android Studio Version

No response

Android API Level

android COMPILE_SDK_VERSION 34
android TARGET_SDK_VERSION 33
android MIN_SDK_VERSION 24

Klaviyo.setPushToken() may cause app crash

Description

We have observed a significant number of IllegalThreadStateException exceptions for our app in the Google Play Store Console. Upon examining the stack trace, it appears that these exceptions are triggered by the invocation of Klaviyo.setPushToken().

Checklist

  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Expected behavior

The app run normally and no crash

Actual behavior

The app performs well in most cases, but on some case, it may lead to crashes (about 1% probability).

Steps to reproduce

1、Integrate the SDK within the app and initialize it in the Application class. Code snippet as below:
     
Klaviyo.initialize(“klaviyo_public_key”, context)
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
    Klaviyo.setPushToken(token)
}

2、Configure the KlaviyoPushService in the manifest。
3、Run the app, the app may throw IllegalThreadStateException. The stack trace as below.

Fatal Exception: java.lang.IllegalThreadStateException:
       at java.lang.Thread.start(Thread.java:872)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.initBatch(KlaviyoApiClient.kt:205)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.enqueueRequest(KlaviyoApiClient.kt:113)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.enqueuePushToken(KlaviyoApiClient.kt:81)
       at com.klaviyo.analytics.Klaviyo.setPushToken(Klaviyo.kt:153)
       at com.klaviyo.pushFcm.KlaviyoPushService.onNewToken(KlaviyoPushService.kt:29)
       at com.google.firebase.messaging.FirebaseMessagingService.handleIntent(FirebaseMessagingService.java:163)
       at com.google.firebase.messaging.EnhancedIntentService.lambda$processIntent$0(EnhancedIntentService.java:78)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
       at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.1.0:2)
       at java.lang.Thread.run(Thread.java:923)

or 

Fatal Exception: java.lang.IllegalThreadStateException:
       at java.lang.Thread.start(Thread.java:960)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.initBatch(KlaviyoApiClient.kt:205)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.enqueueRequest(KlaviyoApiClient.kt:113)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.enqueuePushToken(KlaviyoApiClient.kt:81)
       at com.klaviyo.analytics.Klaviyo.setPushToken(Klaviyo.kt:153)
       at com.myapp.app.startup.KlaviyoInitializer.create$lambda-0(KlaviyoInitializer.kt:17)
       at com.google.android.gms.tasks.zzm.run(com.google.android.gms:play-services-tasks@@18.0.1:1)
       at android.os.Handler.handleCallback(Handler.java:942)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loopOnce(Looper.java:226)
       at android.os.Looper.loop(Looper.java:313)
       at android.app.ActivityThread.main(ActivityThread.java:8757)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:571)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1067)


From the stack trace, we learn that this issue arises because line 205 of KlaviyoApiClient.kt is executed twice, indicating that handlerThread.start() is being called twice. This is the root cause of the IllegalThreadStateException being thrown.


From the code analysis, it's clear that Klaviyo.setPushToken() is being called in both the callback registered in the Application's onCreate() and in KlaviyoPushService's onNewToken(). If you check the thread id in the logs, you will find that Klaviyo.setPushToken() is running on the main thread within the Application's callback, while in KlaviyoPushService, it's executed on a worker thread. Since Klaviyo.setPushToken() lacks synchronization and is not thread-safe, in a multi-threaded scenario, it's possible for the handlerThread.start() to be executed twice due to a race condition. This leads to the occurrence of the error.

The Klaviyo Android SDK version information

1.3.1

Device Information

No specific

Android Studio Version

No specific

Android API Level

No specific

UninitializedPropertyAccessException on setPushToken

Description

When setting a push token the app might crash. It's a rare crash that I can't recreate, but it does happen for some people.

Checklist

  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Expected behavior

Push token should be set (and it is most of the time!).

Actual behavior

Fatal Exception: kotlin.UninitializedPropertyAccessException: lateinit property initialBody has not been initialized
       at com.klaviyo.analytics.networking.requests.PushTokenApiRequest.equals(PushTokenApiRequest.kt:96)
       at java.util.concurrent.ConcurrentLinkedDeque.contains(ConcurrentLinkedDeque.java:1081)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.enqueueRequest(KlaviyoApiClient.kt:118)
       at com.klaviyo.analytics.networking.KlaviyoApiClient.enqueuePushToken(KlaviyoApiClient.kt:81)
       at com.klaviyo.analytics.Klaviyo.setPushToken(Klaviyo.kt:153)


### Steps to reproduce

```markdown
Klaviyo.initialize(BuildConfig.KLAVIYO_API_KEY, application)
application.registerActivityLifecycleCallbacks(Klaviyo.lifecycleCallbacks)
FirebaseMessaging.getInstance().token.addOnSuccessListener { pushToken ->
    Klaviyo.setPushToken(pushToken)
}

The Klaviyo Android SDK version information

1.3.5

Device Information

Samsung Galaxy S21 FE 5G, Android 14 | Samsung Galaxy Note9, Android 10 | Samsung Galaxy S9+, Android 10 | Samsung Galaxy A42 5G, Android 12

Android Studio Version

2023.1.1

Android API Level

34

Test issue

Issue Overview

Just testing our zapier integration

Expected behavior

Actual behavior

Steps to reproduce

Collecting custom metrics

Checklist

  • I have read the contributing guidelines
  • I have determined whether this bug is also reproducible in a vanilla Android project
  • If possible, I've reproduced the issue using the master branch or latest release of this package.
  • This issue hasn't been addressed in an existing GitHub issue or pull request.

Description

I'm unable to set properties on events from objects outside of the module.

On this example it works fine

val productName = "Shirt"

Event(EventMetric.VIEWED_PRODUCT)
  .setProperty(EventKey.CUSTOM("ProductName"), productName)

However if the value of the property comes from an object of another module.

Event(EventMetric.VIEWED_PRODUCT)
  .setProperty(EventKey.CUSTOM("ProductName"), product.itemName)

It will complain with the following message:

Smart cast to 'String' is impossible, because 'product.itemName' is a public API property declared in different module

Which is weird since product.itemName is of type String. However if I typecast into String it will work, even though IDE complains as "No cast needed".

Event(EventMetric.VIEWED_PRODUCT)
  .setProperty(EventKey.CUSTOM("ProductName"), product.itemName as String)

Expected behavior

To be able to collect set event properties of objects coming from another module.

Actual behavior

Smart cast to 'String' is impossible, because 'product.itemName' is a public API property declared in different module.

Steps to reproduce

1 - Create a Kotlin library module, in this case it was a Kotlin Multiplatform Module
2 - Create an class inside the module
3 - On the app module instantiate the class
4 - Create an event and set a property of the object

The Klaviyo Android SDK version information

2.3.0

Device Information

No response

Android Studio Version

No response

Android API Level

No response

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.