diff --git a/CHANGELOG.md b/CHANGELOG.md index 955f172..3205310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## 0.0.10 *(2025-03-05)* + +### THIS SDK IS NOT YET STABLE +Please feel free to try but note that this is a work in progress. Feel free to join Open Attribution for questions or contributing. + +### Changes +- Record events with trackEvent +- Record purchases with trackPurchase + + ## 0.0.9 *(2025-02-13)* ### THIS SDK IS NOT YET STABLE diff --git a/OpenAttribution/src/main/java/dev/openattribution/sdk/OAWorker.kt b/OpenAttribution/src/main/java/dev/openattribution/sdk/OAWorker.kt index 5fa69ea..3ff13ea 100644 --- a/OpenAttribution/src/main/java/dev/openattribution/sdk/OAWorker.kt +++ b/OpenAttribution/src/main/java/dev/openattribution/sdk/OAWorker.kt @@ -4,6 +4,7 @@ import android.content.Context import android.provider.Settings import android.util.Log import androidx.work.CoroutineWorker +import androidx.work.Data import androidx.work.WorkerParameters import kotlinx.coroutines.Dispatchers import com.google.android.gms.ads.identifier.AdvertisingIdClient @@ -17,16 +18,12 @@ import java.util.UUID import java.util.concurrent.TimeUnit -data class TrackingEvent( - val oaUid: String, - val ifa: String?, - val androidId: String, - val eventId: String, - val eventUid: String, - val eventTime: Long, -) -class TrackAppOpenWorker( + + + + +class TrackEventWorker( private val appContext: Context, workerParams: WorkerParameters ) : CoroutineWorker(appContext, workerParams) { @@ -34,102 +31,153 @@ class TrackAppOpenWorker( override suspend fun doWork(): Result { Log.d("MyOA", "doWork start") - val userIdManager = UserIdManager.getInstance(appContext) - val baseUrl = OpenAttribution.getBaseUrl() - - try { - val androidId = Settings.Secure.getString(appContext.contentResolver, Settings.Secure.ANDROID_ID) - val myTimestamp = System.currentTimeMillis() - val eventId = "app_open" - val myUid = UUID.randomUUID().toString() - val myOaUid = userIdManager.getUserId() - val basePackageName = appContext.packageName - val appendedPackageName = if (EmulatorDetector.isEmulator()) { - "${basePackageName}_test" - } else { - basePackageName - } + val eventId = inputData.getString(EVENT_ID_KEY) ?: return Result.failure() + val revenueAmount = if (inputData.keyValueMap.containsKey(EVENT_REVENUE_KEY)) { + inputData.getString(EVENT_REVENUE_KEY) + } else { + null + } + val currency = inputData.getString(EVENT_CURRENCY_KEY) - val gaid = withContext(Dispatchers.IO) { - try { - val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(appContext) - adInfo.id - } catch (e: Exception) { - Log.e("OpenAttribution", "Error retrieving GAID: ${e.message}") - null - } - } + return trackEvent(appContext, eventId, revenueAmount, currency) - val (url, trackingEvent) = constructTrackingRequest( - baseUrl, - appendedPackageName, - myOaUid, - gaid, - androidId ?: "unknown", - eventId, - myUid, - myTimestamp - ) + } + + companion object { + private const val EVENT_ID_KEY = "event_id" + private const val EVENT_REVENUE_KEY = "revenue" + private const val EVENT_CURRENCY_KEY = "currency" - val client = OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) + fun createWorkRequest(eventId: String): androidx.work.OneTimeWorkRequest { + val inputData = Data.Builder() + .putString(EVENT_ID_KEY, eventId) .build() - val jsonBody = JSONObject().apply { - put("oa_uid", trackingEvent.oaUid) - put("ifa", trackingEvent.ifa) - put("android_id", trackingEvent.androidId) - put("event_id", trackingEvent.eventId) - put("event_uid", trackingEvent.eventUid) - put("event_time", trackingEvent.eventTime) - } + return androidx.work.OneTimeWorkRequestBuilder() + .setInputData(inputData) + .build() + } - val request = Request.Builder() - .url(url) - .post(jsonBody.toString().toRequestBody("application/json".toMediaType())) + fun createRevenueWorkRequest(eventId: String, revenueAmount: Double, currency: String): androidx.work.OneTimeWorkRequest { + val inputData = Data.Builder() + .putString(EVENT_ID_KEY, eventId) + .putString(EVENT_REVENUE_KEY, revenueAmount.toString()) + .putString(EVENT_CURRENCY_KEY, currency) .build() - withContext(Dispatchers.IO) { - client.newCall(request).execute() - }.use { response -> - return if (response.isSuccessful) { - Log.i("OpenAttribution", "Tracking request successful: ${response.code}") - Result.success() + return androidx.work.OneTimeWorkRequestBuilder() + .setInputData(inputData) + .build() + } + + suspend fun trackEvent(context: Context, eventId: String, revenueAmount: String? = null, currency: String? = null): Result { + Log.d("TrackEventWorker", "Tracking event: $eventId") + val userIdManager = UserIdManager.getInstance(context) + val baseUrl = OpenAttribution.getBaseUrl() + + try { + val androidId = + Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) + val myTimestamp = System.currentTimeMillis() + val myUid = UUID.randomUUID().toString() + val myOaUid = userIdManager.getUserId() + val basePackageName = context.packageName + val appendedPackageName = if (EmulatorDetector.isEmulator()) { + "${basePackageName}_test" } else { - Log.w("OpenAttribution", "Tracking request failed: ${response.code}") - Result.retry() + basePackageName + } + + val gaid = withContext(Dispatchers.IO) { + try { + val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context) + adInfo.id + } catch (e: Exception) { + Log.e("OpenAttribution", "Error retrieving GAID: ${e.message}") + null + } + } + + val (url, trackingEvent) = constructTrackingRequest( + baseUrl, + appendedPackageName, + myOaUid, + gaid, + androidId ?: "unknown", + eventId, + myUid, + myTimestamp + ) + + val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build() + + val jsonBody = JSONObject().apply { + put("oa_uid", trackingEvent.oaUid) + put("ifa", trackingEvent.ifa) + put("android_id", trackingEvent.androidId) + put("event_id", trackingEvent.eventId) + put("event_uid", trackingEvent.eventUid) + put("event_time", trackingEvent.eventTime) + + + revenueAmount?.let { + put("revenue", it) + put( + "currency", + currency ?: "USD" + ) + } } + + val request = Request.Builder() + .url(url) + .post(jsonBody.toString().toRequestBody("application/json".toMediaType())) + .build() + + withContext(Dispatchers.IO) { + client.newCall(request).execute() + }.use { response -> + return if (response.isSuccessful) { + Log.i("OpenAttribution", "Tracking request successful: ${response.code}") + Result.success() + } else { + Log.w("OpenAttribution", "Tracking request failed: ${response.code}") + Result.retry() + } + } + } catch (e: Exception) { + Log.e("OpenAttribution", "Error in TrackAppOpenWorker: ${e.message}") + return Result.retry() } - } catch (e: Exception) { - Log.e("OpenAttribution", "Error in TrackAppOpenWorker: ${e.message}") - return Result.retry() } - } - private fun constructTrackingRequest( - baseUrl: String, - packageName: String, - oauid: String, - gaid: String?, - androidId: String, - eventId: String, - eventUid: String, - eventTime: Long - ): Pair { - val url = "$baseUrl/collect/events/$packageName" - - val trackingEvent = TrackingEvent( - oaUid = oauid, - ifa = gaid, - androidId = androidId, - eventId = eventId, - eventUid = eventUid, - eventTime = eventTime - ) - - Log.i("OpenAttribution", "Constructing tracking URL $url") - return Pair(url, trackingEvent) + private fun constructTrackingRequest( + baseUrl: String, + packageName: String, + oauid: String, + gaid: String?, + androidId: String, + eventId: String, + eventUid: String, + eventTime: Long + ): Pair { + val url = "$baseUrl/collect/events/$packageName" + + val trackingEvent = TrackingEvent( + oaUid = oauid, + ifa = gaid, + androidId = androidId, + eventId = eventId, + eventUid = eventUid, + eventTime = eventTime + ) + + Log.i("OpenAttribution", "Constructing tracking URL $url") + return Pair(url, trackingEvent) + } } } diff --git a/OpenAttribution/src/main/java/dev/openattribution/sdk/Track.kt b/OpenAttribution/src/main/java/dev/openattribution/sdk/Track.kt index ec4863e..88d96a3 100644 --- a/OpenAttribution/src/main/java/dev/openattribution/sdk/Track.kt +++ b/OpenAttribution/src/main/java/dev/openattribution/sdk/Track.kt @@ -3,15 +3,8 @@ package dev.openattribution.sdk import android.content.Context import android.content.SharedPreferences import android.os.Build -import android.provider.Settings import android.util.Log -import com.google.android.gms.ads.identifier.AdvertisingIdClient -import kotlinx.coroutines.* -import okhttp3.OkHttpClient -import okhttp3.Request import java.util.UUID -import java.util.concurrent.TimeUnit -import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager @@ -82,34 +75,45 @@ object EmulatorDetector { class OpenAttribution private constructor(private val context: Context) { - companion object { - private var myBaseUrl: String? = null - - // Method to initialize the SDK - fun initialize(context: Context, baseUrl: String): OpenAttribution { - - Log.d("MyOA", "InitStart") - - - val instance = OpenAttribution(context) - instance.scheduleTrackAppOpen() + companion object { + private var myBaseUrl: String? = null + private var instance: OpenAttribution? = null + + // Method to initialize the SDK + fun initialize(context: Context, baseUrl: String): OpenAttribution { + Log.d("MyOA", "InitStart") + myBaseUrl = baseUrl + instance = OpenAttribution(context.applicationContext) + instance?.scheduleTrackAppOpen() + return instance!! + } - myBaseUrl = baseUrl -// return OpenAttribution(context) - return instance + fun trackEvent(context: Context, eventName: String) { + if (instance == null) { + throw IllegalStateException("OpenAttribution not initialized. Call OpenAttribution.initialize() first.") } + val workRequest = TrackEventWorker.createWorkRequest(eventName) + WorkManager.getInstance(context).enqueue(workRequest) + } - // Getter for the base URL to ensure it's set - fun getBaseUrl(): String { - return myBaseUrl ?: throw IllegalStateException("Base URL is not yet initialized. Call OpenAttribution.initialize() first.") + fun trackPurchase(context: Context, revenueAmount: Double, currency: String, eventName: String="iap_purchase") { + if (instance == null) { + Log.w("OpenAttribution", "SDK not initialized. Auto-initializing with default settings.") } + val workRequest = TrackEventWorker.createRevenueWorkRequest(eventName, revenueAmount, currency) + WorkManager.getInstance(context).enqueue(workRequest) + } + + + fun getBaseUrl(): String { + return myBaseUrl ?: throw IllegalStateException("Base URL is not yet initialized. Call OpenAttribution.initialize() first.") } + } private fun scheduleTrackAppOpen() { - val workRequest = OneTimeWorkRequestBuilder() - .build() + val workRequest = TrackEventWorker.createWorkRequest("app_open") WorkManager.getInstance(context).enqueue(workRequest) } +} -} \ No newline at end of file diff --git a/OpenAttribution/src/main/java/dev/openattribution/sdk/TrackingEvent.kt b/OpenAttribution/src/main/java/dev/openattribution/sdk/TrackingEvent.kt new file mode 100644 index 0000000..c398fd4 --- /dev/null +++ b/OpenAttribution/src/main/java/dev/openattribution/sdk/TrackingEvent.kt @@ -0,0 +1,12 @@ +package dev.openattribution.sdk + +data class TrackingEvent( + val oaUid: String, + val ifa: String?, + val androidId: String, + val eventId: String, + val eventUid: String, + val eventTime: Long, + val customProperties: Map? = null +) + diff --git a/README.md b/README.md index c7625e6..e4ea529 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ To have a fully functional SDK which can be used to track and attribute installs - [x] Library installable via Maven - [x] user input server endpoint ie `https://demo.openattribution.dev` -- [ ] Events: +- [x] Events: - [x] app_open tracking and attributing - - [ ] Basic event tracking - - [ ] Basic revenue tracking + - [x] Basic event tracking + - [x] Basic revenue tracking - [ ] Documentation for how to use and next steps ## Install @@ -52,14 +52,48 @@ class MyApplication : Application() { super.onCreate() // Initialize the OpenAttribution SDK, replace with your domain openAttribution = OpenAttribution.initialize(this, "https://demo.openattribution.dev") - } - } ``` +## Tracking Events + +Use `trackEvent` to log custom events in your app. + +| Parameter | Type | Description | +| ----------- | --------- | --------------------------------------------------------- | +| `context` | `Context` | Android context (e.g., Activity or Application). | +| `eventName` | `String` | Name of the custom event to track (e.g., "button_click"). | + + +Example use: +```kotlin + import dev.openattribution.sdk.OpenAttribution + + ... + + OpenAttribution.trackEvent(context, "my_event_name") + +``` + + + +## Track Purchases + +Use `trackPurchase` to track revenue for In App Purchases in your app. +| Parameter | Type | Description | Default | +|-----------------|----------|--------------------------------------------------|------------------| +| `context` | `Context`| Android context (e.g., Activity or Application). | - | +| `revenueAmount` | `Double` | Purchase amount (e.g., 12.34). | - | +| `currency` | `String` | ISO 4217 currency code (e.g., "USD"). | - | +| `eventName` | `String` | Event name for tracking. | `"iap_purchase"` | + + +```kotlin + OpenAttribution.trackPurchase(context, 12.34, "USD") +``` ## Local Development: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7cda1ca..c888fcc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -oaReleaseVersion = "0.0.9" +oaReleaseVersion = "0.0.10" agp = "8.7.2" kotlin = "1.9.24" coreKtx = "1.15.0"