From 46b244f21f794b165d6ee8caf44318643a357aa6 Mon Sep 17 00:00:00 2001 From: Dave Mead Date: Fri, 12 Jul 2024 04:16:18 -0700 Subject: [PATCH 1/5] Add S2S guide --- .../docs/api/s2s-api/s2s-developer-guide.mdx | 1286 +++++++++++++++++ 1 file changed, 1286 insertions(+) create mode 100644 src/content/docs/api/s2s-api/s2s-developer-guide.mdx diff --git a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx new file mode 100644 index 000000000..9387ff69b --- /dev/null +++ b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx @@ -0,0 +1,1286 @@ +--- +title: "S2S developer guide" +description: "Implement Adjust 100% S2S" +slug: en/api/s2s-api/s2s-developer-guide +sidebar-position: 7 +--- + +Adjust offers a server-to-server (S2S) interface as an alternative to integrating an SDK into your app. If you choose to implement Adjust entirely through S2S, you'll need to modify your app to replicate the Adjust SDK's functions. This guide provides step-by-step instructions on two key aspects: + +- How to make the necessary updates to your app +- How to send S2S requests to Adjust +
+ This comprehensive walkthrough will help you implement a full S2S solution + with Adjust. + + + +For mixed audience apps as defined in COPPA, Adjust strongly recommends using a 100% SDK implementation and avoiding any S2S implementation. + +Children's apps as defined in COPPA are not supported for S2S, and have to use a 100% SDK implementation. + + + +## Before you begin + +### Enable S2S session measurement + +Adjust has to enable S2S session measurement for your app. Please contact your Adjust representative or support@adjust.com to proceed. + +### Set up S2S Security + +[Implement S2S Security](https://dev.adjust.com/en/api/s2s-api/security) to safeguard your S2S activities and prevent spoofed requests. Generate a token in your Adjust dashboard and include it with each incoming request. Adjust's servers will reject any requests lacking the correct token, ensuring robust protection for your data. + +### Queue and persist events locally + +Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, it's crucial to capture and store these events locally until they can be successfully transmitted to Adjust's servers. + +Implement a local event queue with persistence: + +1. Create a queue to store activities when they occur. +2. For each activity, include a `created_at_unix` timestamp in seconds (e.g., 1484085154) representing when the event occurred on the device. +3. Save this queue to local storage (e.g., SQLite database or files) to persist across app restarts. +4. Attempt to send activities to Adjust's servers when the queue is non-empty and the device is online. +5. Remove activities from the queue only after successful transmission. +
+ This approach helps mitigate data loss during the following: + +- Brief network interruptions (e.g., 5G to WiFi handovers) +- Extended periods without connectivity +- App crashes or force closes before transmission +
+ Without local queuing, you risk losing 10–20% of install data, which can + significantly impact attribution accuracy. By implementing this queuing + system, you ensure that Adjust receives a complete and accurate picture of + user activity, enabling precise attribution even for events that occur + offline. + +### Add iOS frameworks + +1. Link required frameworks in Xcode. +
+ +- Open your project in Xcode. +- Select your target in the project navigator. +- Go to the "General" tab. +- Scroll to the "Frameworks, Libraries, and Embedded Content" section. +- Click the "+" button. +- Search for and add the frameworks that your app requires from the list below. +
+ + + | Framework | Description | | :--- | :--- | |{" "} +
`AdSupport.framework`
|{" "} +
+ Required to collect IDFA. Also required to collect Limit Ad Tracking + status for pre-ATT iOS versions. +
{" "} + | |
`AppTrackingTransparency.framework`
|{" "} +
+ Required to show the AppTrackingTransparency prompt and collect IDFA on + devices running iOS 14.5 and later. +
{" "} + | |
`AdServices.framework`
|{" "} +
+ Required for Adjust to perform attribution for Apple Search Ads campaigns. +
{" "} + | |
`StoreKit.framework`
|{" "} +
Required to run SKAdNetwork campaigns.
| +
+ +## Required parameters + +### Base parameters + +These parameters have to be included in every S2S request. + + + + +```swift +// Create dictionary for params to include on all S2S requests to Adjust +var params: [String: String] = [:] + +// Hard-coded +params["s2s"] = "1" + +// "ios" or "android" +params["os_name"] = "ios" + +// Replace with your Adjust app token +params["app_token"] = "4w565xzmb54d" +``` + + + +```kotlin +// Create map for params to include on all S2S requests to Adjust +val params = mutableMapOf() + +// Hard-coded +params["s2s"] = "1" + +// "ios" or "android" +params["os_name"] = "ios" + +// Replace with your Adjust app token +params["app_token"] = "4w565xzmb54d" + +```` + + + +### Device IDs and tracking statuses + +Every S2S request has to include at least one device identifier. Due to privacy measures implemented by mobile operating systems, the advertising ID may not always be available. Therefore it's crucial to include the advertising ID when available, and to always include backup identifiers. + +Tracking statuses are also required to be included on all S2S requests, as Adjust's system has some dependencies on them. + +#### iOS + +##### IDFA + +IDFA is generally only available for iOS devices where users have opted to share it with your app via the AppTrackingTransparency (ATT) prompt. Follow these steps if you choose to show the ATT prompt and collect IDFA in your app: + +1. Add ATT description in Xcode. +
+ - Open your project's Info.plist file. + - In the editor, right-click on "Information Property List" and choose "Add Row" to add a key to the root. + - Set the key to `NSUserTrackingUsageDescription`. + - Set the value to a string explaining why you're requesting tracking permission (e.g., "This identifier will be used to deliver personalized ads to you."). Be sure to review [Apple's guidelines](https://developer.apple.com/documentation/bundleresources/information_property_list/nsusertrackingusagedescription) for this text. +
+2. Implement ATT prompt and IDFA retrieval. +
+
+ ATT has the following requirements, which the code example below addresses: +
+
+ - While ATT support begins with iOS 14, user consent for IDFA retrieval is only required from iOS 14.5 onwards. Therefore, Adjust recommends targeting the ATT prompt specifically to users on iOS 14.5 and later versions. + - The ATT prompt requires an active app state to display. Showing it immediately after other system prompts may fail unless you first confirm the app state is again active. + - The earliest places to show the prompt are in `applicationDidBecomeActive` (App Delegate) or `sceneDidBecomeActive` (Scene Delegate). It's not possible to show the ATT prompt in `didFinishLaunchingWithOptions` (App Delegate). +
+ + + + +```swift +import AppTrackingTransparency +import AdSupport +import UIKit + +struct IDFAInfo { + let idfa: UUID? + let attStatus: ATTrackingManager.AuthorizationStatus? + let trackingEnabled: Bool? +} + +func getIDFAInfo(completion: @escaping (IDFAInfo) -> Void) { + // Show ATT prompt to get IDFA and updated ATT status + if #available(iOS 14.5, *) { + ATTrackingManager.requestTrackingAuthorization { status in + DispatchQueue.main.async { + let idfa = (status == .authorized) ? + ASIdentifierManager.shared().advertisingIdentifier : nil + completion(IDFAInfo(idfa: idfa, attStatus: status, trackingEnabled: + nil)) + } + } + // Do not show ATT prompt. Just get IDFA and tracking status. + } else { + let manager = ASIdentifierManager.shared() + let trackingEnabled = manager.isAdvertisingTrackingEnabled + let idfa = trackingEnabled ? manager.advertisingIdentifier : nil + completion(IDFAInfo(idfa: idfa, attStatus: nil, trackingEnabled: + trackingEnabled)) + } +} + +private func requestTrackingAuthorization( + completion: @escaping (ATTrackingManager.AuthorizationStatus) -> Void) { + let trackingStatus = ATTrackingManager.trackingAuthorizationStatus + switch trackingStatus { + + // Only show ATT prompt if status is not determined + case .notDetermined: + // If app state is active, show ATT prompt + if UIApplication.shared.applicationState == .active { + ATTrackingManager.requestTrackingAuthorization { status in + completion(status) + } + // Wait until app state is active, then show ATT prompt + } else { + NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main + ) { [weak self] _ in + guard let self = self else { return } + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + ATTrackingManager.requestTrackingAuthorization { status in + completion(status) + } + } + } + + // For all other statuses, return existing status + default: + completion(trackingStatus) + } +} + +// Usage example +getIDFAInfo { info in + // Include IDFA if available + if let idfa = info.idfa?.uuidString { + params["idfa"] = idfa + } + + // Include either ATT status or tracking status, never both + if let attStatus = info.attStatus { + params["att_status"] = String(attStatus.rawValue) + } else if let trackingEnabled = info.trackingEnabled { + params["tracking_enabled"] = trackingEnabled ? "1" : "0" + } +} + +```` + + + + +##### IDFV + +IDFV is a backup identifier available on all modern iOS devices. + + + + +```swift +let idfv: UUID? = UIDevice.current.identifierForVendor + +if let idfvString = idfv?.uuidString { +params["idfv"] = idfvString +} + +```` + + + +##### Primary dedupe token + +To consistently measure app activities across uninstalls and reinstalls, generate a random version 4 UUID (the "primary dedupe token") and save it in the iOS keychain. The primary dedupe token is a backup identifier that you should generate for all devices. + + + + +```swift +import Foundation +import Security + +// App's bundle ID +let bundleId = "com.example.app" + +// Collect the primary dedupe token from the keychain +func getPrimaryDedupeToken(bundleId: String) -> UUID? { + // Define the query to search for the token in the keychain + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: "primary_dedupe_token", + kSecAttrService as String: bundleId, + kSecReturnData as String: true + ] + + var item: CFTypeRef? + // Attempt to fetch the token from the keychain + let status = SecItemCopyMatching(query as CFDictionary, &item) + + // If the fetch was successful, convert the result to a UUID + guard status == errSecSuccess, + let existingItem = item as? Data, + let uuidString = String(data: existingItem, encoding: .utf8), + let token = UUID(uuidString: uuidString) else { + // Return nil if the token doesn't exist or couldn't be collected + return nil + } + + return token +} + +// Save the primary dedupe token to the keychain +func setPrimaryDedupeToken(_ token: UUID, bundleId: String) -> Bool { + let tokenData = token.uuidString.data(using: .utf8)! + // Define the attributes for storing the token in the keychain + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: "primary_dedupe_token", + kSecAttrService as String: bundleId, + kSecValueData as String: tokenData, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock + ] + + // Attempt to add the token to the keychain + let status = SecItemAdd(query as CFDictionary, nil) + + // Return true if the token was successfully added, false otherwise + return status == errSecSuccess +} + +// Collect the existing primary dedupe token or create a new one if it doesn't exist +func getOrCreatePrimaryDedupeToken() -> UUID { + // Try to collect an existing token + if let existingToken = getPrimaryDedupeToken(bundleId: bundleId) { + return existingToken + } else { + // If no token exists, generate a new one + let newToken = UUID() + // Attempt to save the new token + if setPrimaryDedupeToken(newToken, bundleId: bundleId) { + return newToken + } else { + // If saving fails, throw a fatal error + fatalError("Failed to save primary dedupe token") + } + } +} + +// Usage example +let primaryDedupeToken = getOrCreatePrimaryDedupeToken() + +// Convert to lowercase string +params["primary_dedupe_token"] = primaryDedupeToken.uuidString.lowercased() +```` + + + + +#### Android - Google Play + +##### Google Advertising ID + +Google Advertising ID is available on Android devices with Google Play Services, provided the user has not opted to delete their advertising ID. + +1. Add the necessary dependency to your app's `build.gradle` file: + + + + +```kotlin +// build.gradle.kts + +dependencies { +implementation("com.google.android.gms:play-services-ads-identifier:18.1.0") +} + +```` + + + +```groovy +// build.gradle + +dependencies { + implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0' +} +```` + + + + +You can check for the most recent version on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-ads-identifier). + +2. Add the following permission to your `AndroidManifest.xml` file: + +```xml + +``` + +3. If your app uses R8 or ProGuard, add these rules to your `proguard-rules.pro` file (create the file in your app module's directory if it doesn't exist): + +``` +-keep class com.google.android.gms.common.ConnectionResult { + int SUCCESS; +} +-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { + com.google.android.gms.ads.identifier. + AdvertisingIdClient$Info getAdvertisingIdInfo( + android.content.Context); +} +-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { + java.lang.String getId(); + boolean isLimitAdTrackingEnabled(); +} +``` + +These rules preserve classes and methods needed for Google Advertising ID retrieval during code optimization. Skip this step if you're not using R8 or ProGuard. + +4. Implement the code to collect Google Advertising ID and tracking status: + + + + +```kotlin +import com.google.android.gms.ads.identifier.AdvertisingIdClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +data class GPSAdInfo(val gpsAdid: String?, val isTrackingEnabled: Boolean) + +suspend fun getGoogleAdvertisingIdInfo(context: Context): +GPSAdInfo? { +return withContext(Dispatchers.IO) { +try { +val adInfo = +AdvertisingIdClient.getAdvertisingIdInfo(context) +val gpsAdid = +if (adInfo.isLimitAdTrackingEnabled) null else adInfo.id +GPSAdInfo(gpsAdid, !adInfo.isLimitAdTrackingEnabled) +} catch (e: Exception) { +// Handle exceptions (e.g., Google Play Services not available) +null +} +} +} + +// Usage example +// As getGoogleAdvertisingIdInfo is a suspending function, +// it should be called from within a coroutine scope. +lifecycleScope.launch { +val adInfo = getGoogleAdvertisingIdInfo(context) +adInfo?.let { info -> +// Include Google Advertising ID if available +info.gpsAdid?.let { params["gps_adid"] = it } + + // Include tracking status if available + info.isTrackingEnabled?.let { params["tracking_enabled"] + = if (it) "1" else "0" } + } + +} + +```` + + + +##### App Set ID + +App Set ID is a backup identifier available on all Android devices with Google Play Services installed and running API Level 30 (Android 11) or later. + +1. Add the necessary dependency to your app's `build.gradle` file: + + + + +```kotlin +// build.gradle.kts + +dependencies { + implementation("com.google.android.gms:play-services-appset:16.1.0") +} +```` + + + + +```groovy +// build.gradle + +dependencies { +implementation 'com.google.android.gms:play-services-appset:16.1.0' +} + +```` + + + +You can check for the most recent version on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-appset). + +2. Implement the code to collect App Set ID: + + + + +```kotlin +import com.google.android.gms.appset.AppSet +import com.google.android.gms.appset.AppSetIdClient +import com.google.android.gms.appset.AppSetIdInfo +import com.google.android.gms.tasks.Tasks +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun getAppSetId(context: Context): String? { + return withContext(Dispatchers.IO) { + try { + val client: AppSetIdClient = AppSet.getClient(context) + val taskResult = Tasks.await(client.appSetIdInfo) + taskResult.id + } catch (e: Exception) { + // Handle exceptions (e.g., Google Play Services not available) + null + } + } +} + +// Usage example +// As getAppSetId is a suspending function, +// it should be called from within a coroutine scope. +lifecycleScope.launch { + val appSetId = getAppSetId(context) + appSetId?.let { id -> + val params = mutableMapOf() + params["google_app_set_id"] = id + } +} +```` + + + + +## Additional parameters + +These parameters are not required, however if you use them, you should include them in all S2S requests. + +### Unix timestamp + +Adjust strongly recommends including timestamps on S2S requests, as this provides the time when the activities occurred on the device, and therefore improves attribution accuracy. + + + + +```swift +// Unix timestamp of when activity occurred on device +// Code example shows how to retrieve current time in seconds +// Example value: "1484085154" +params["created_at_unix"] = String(Int(Date().timeIntervalSince1970)) +``` + + + + +```kotlin +// Unix timestamp of when activity occurred on device +// Code example shows how to retrieve current time in seconds +// Example value: "1484085154" +params["created_at_unix"] = (System.currentTimeMillis() / 1000).toString() +``` + + + + +### Probabilistic modeling data points + +In order to use probabilistic modeling as an attribution method, include the below parameters on all S2S requests. Adjust strongly recommends implementing this, as it enables more comprehensive attribution, particularly for iOS. + + + + +```swift +import UIKit + +// Device name +// Example value: "iPhone10,5" +// Device name +// Example value: "iPhone10,5" +var systemInfo = utsname() +uname(&systemInfo) +let machineMirror = Mirror(reflecting: systemInfo.machine) +let deviceName = machineMirror.children.reduce("") { + identifier, element in + guard let value = element.value as? Int8, value != 0 + else { return identifier } + return identifier + String(UnicodeScalar(UInt8(value))) +} +params["device_name"] = deviceName + +// Device type +// Example value: "iPhone" +params["device_type"] = UIDevice.current.model + +// OS version +// Example value: "17.5.1" +params["os_version"] = UIDevice.current.systemVersion + +// IP address +// Retrieve the device's IP address from requests to your server +params["ip_address"] = "192.0.0.1" // Example value +``` + + + + +```kotlin +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import android.content.pm.PackageManager + +// Usage example +val context: Context = // ... get your context here ... + +val isGooglePlayGamesForPC = context.packageManager + .hasSystemFeature("com.google.android.play.feature + .HPE_EXPERIENCE") + +// Device name +params["device_name"] = if (isGooglePlayGamesForPC) null + else Build.MODEL + +// OS version +params["os_version"] = if (isGooglePlayGamesForPC) null + else Build.VERSION.RELEASE + +// Device type +params["device_type"] = when { + isGooglePlayGamesForPC -> "pc" + (context.resources.configuration.uiMode and + Configuration.UI_MODE_TYPE_MASK) == Configuration + .UI_MODE_TYPE_TELEVISION -> "tv" + else -> when (context.resources.configuration.screenLayout + and Configuration.SCREENLAYOUT_SIZE_MASK) { + Configuration.SCREENLAYOUT_SIZE_SMALL, + Configuration.SCREENLAYOUT_SIZE_NORMAL -> "phone" + Configuration.SCREENLAYOUT_SIZE_LARGE, + Configuration.SCREENLAYOUT_SIZE_XLARGE -> "tablet" + else -> null + } +} + +// IP address +// Retrieve the device's IP address from requests to +// your server +params["ip_address"] = "192.0.0.1" // Example value +``` + + + + +### Environment + +When testing, you can send S2S requests in sandbox mode. Adjust records these separately from your production data. If you don't pass this parameter, the default value is "production". + + + + +```swift kotlin +// For testing (sandbox environment) +params["environment"] = "sandbox" + +// For production use +params["environment"] = "production" +``` + + + + +### Global callback parameters + +When using [raw data exports](https://help.adjust.com/en/article/raw-data-exports), you can include custom "global callback parameters" in all your S2S requests to add custom parameters to the raw data. This is commonly used to include your internal user ID in your exported raw data. + +Global callback parameters are represented as a JSON object containing string key-value pairs. + + + +```swift +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value"}' +``` + + + +## Requests + + + +Ensure all parameter values are URL encoded before sending requests. + + + +### Global partner parameters + +When integrating with certain partners, you may need to include custom "global partner parameters" in all your S2S requests. The Adjust server will then pass these parameters on all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. + +Global partner parameters are represented as a JSON object containing string key-value pairs. + + + +```swift kotlin +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM"}' +``` + + + +### Session + +Sessions form the foundation of Adjust implementation and are the only technically required activity. A session generally represents an app open. The Adjust server logs successful session requests as follows: + +- It records the first session for a device as an "install" activity. +- It records subsequent sessions as "session" activities. +- It records a "reattribution" or "reattribution reinstall" activity if [reattribution criteria](https://help.adjust.com/en/article/reattribution) are satisfied. + +
+ For reference, the Adjust SDK sends a session request to Adjust server when + either of the following occurs: + +- The user opens the app for the first time after install or reinstall. +- The user reopens the app after it has been closed or in the background for at least 30 minutes. +
+ When sending S2S session requests with the `created_at_unix` parameter, the + Adjust server requires this value to be at least 20 minutes later than the + `created_at_unix` time of the last successfully logged session. +
+ +Send a session request. + + + + ```bash curl -X POST "https://app.adjust.com/session" \ -H "Authorization: + Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ -H "Content-Type: + application/x-www-form-urlencoded" \ -d "s2s=1\ &os_name=ios\ + &app_token=i9dukg8o5slc\ &idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ + &att_status=3\ &idfv=59E27F41-A86B-4560-B585-63161F871C4B\ + &primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ + &created_at_unix=1484085154\ &device_name=iPhone16%2C2\ + &device_type=iPhone\ &os_version=17.5.1\ &ip_address=192.0.0.1\ + &environment=sandbox\ + &callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ + &partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D" + \ -w "\n\nHTTP Status Code: %{http_code}\n" \ -s ``` + + + +This is the response format when Adjust successfully logs the first session for the device. You can use the Adjust [testing console](https://help.adjust.com/en/article/testing-console) to forget your device and test this multiple times, if needed. + + + + +```json +{ + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-09T01:31:14.373Z+0000", + "message": "Install tracked", + "ask_in": 2000 +} + +HTTP Status Code: 200 +``` + + + + +This is the response format when Adjust successfully logs subsequent sessions for the device. + + + + +```json +{ + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-09T02:31:14.373Z+0000", + "message": "Session tracked", + "ask_in": 5000 +} + +HTTP Status Code: 200 +``` + + + + +### Attribution + +After sending a session request, you may need to send an attribution request to the Adjust server. The session response includes an ask_in parameter, indicating how many milliseconds to wait before making the attribution request. +Attribution requests serve three main purposes for developers: + +- Obtain the attribution source for installs or reinstalls, allowing you to personalize the user experience. +- Retrieve deferred deep links for users who clicked an Adjust deep link before installation. +- Get updated attribution information in case of reattribution. +
+ +There are two options for sending attribution requests: + +- Send after every session (covers all use cases) +- Send only after install or reinstall (covers the first two use cases, which are most common) +
+ To implement the second option, follow these steps: + +1. Create a flag in your app. +2. Use this flag to determine if the first session has been recorded. +3. Send the attribution request only when this flag indicates it's the first session. + + + +```swift +import Foundation + +/// Helps to manage app-wide persistent settings and states +class UserDefaultsManager { +static let shared = UserDefaultsManager() +private let userDefaults = UserDefaults.standard + + // ... + + private let FIRST_SESSION_RECORDED_KEY = "first_session_recorded" + + var firstSessionRecorded: Bool { + get { + return userDefaults.bool(forKey: FIRST_SESSION_RECORDED_KEY) + } + set { + userDefaults.set(newValue, forKey: FIRST_SESSION_RECORDED_KEY) + } + } + + // ... + +} + +// Usage example +let userDefaultsManager = UserDefaultsManager.shared + +if !userDefaultsManager.firstSessionRecorded { +// Send first session request +// If the request is successful: +userDefaultsManager.firstSessionRecorded = true +// Wait according to "ask_in" time +// Send attribution request for first session +} else { +// Send subsequent session requests +} + +```` + + +```kotlin +import android.content.Context +import android.content.SharedPreferences + +/** Helps to manage app-wide persistent settings and states */ +class SharedPreferencesManager private constructor(context: Context) { + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + companion object { + private const val PREFS_NAME = "AppPrefs" + private const val FIRST_SESSION_RECORDED_KEY = "first_session_recorded" + + // ... + + @Volatile + private var instance: SharedPreferencesManager? = null + + fun getInstance(context: Context): SharedPreferencesManager { + return instance ?: synchronized(this) { + instance ?: SharedPreferencesManager(context.applicationContext).also { + instance = it + } + } + } + } + + var firstSessionRecorded: Boolean + get() = sharedPreferences.getBoolean(FIRST_SESSION_RECORDED_KEY, false) + set(value) = sharedPreferences.edit().putBoolean( + FIRST_SESSION_RECORDED_KEY, value).apply() + + // ... +} + +// Usage example + +val userDefaultsManager = UserDefaultsManager.getInstance(context) + +if (!userDefaultsManager.firstSessionRecorded) { + // Send first session request + // If the request is successful: + userDefaultsManager.firstSessionRecorded = true + // Wait according to "ask_in" time + // Send attribution request for first session +} else { + // Send subsequent session requests +} +```` + + + + +Send an attribution request. + + + + ```bash curl -X POST "https://app.adjust.com/attribution" \ -H + "Authorization: Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ -H "Content-Type: + application/x-www-form-urlencoded" \ -d "s2s=1\ &os_name=ios\ + &app_token=4w565xzmb54d\ &idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ + &att_status=3\ &idfv=59E27F41-A86B-4560-B585-63161F871C4B\ + &primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ + &created_at_unix=1484085154\ &device_name=iPhone16%2C2\ + &device_type=iPhone\ &os_version=17.5.1\ &ip_address=192.0.0.1\ + &environment=sandbox\ + &callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ + &partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D" + \ -w "\n\nHTTP Status Code: %{http_code}\n" \ -s ``` + + + +Below is an example attribution response that contains a deferred deep link. + + + + +```json +{ + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-10T23:48:27.244Z+0000", + "message": "Attribution found", + "attribution": { + "tracker_token": "18msd3tn", + "tracker_name": "Test", + "network": "Test", + "deeplink": "example://summer-clothes?promo=beach\u0026adj_t=18msd3tn\u0026adjust_no_sdkclick=1" + } +} + +HTTP Status Code: 200 +``` + + + + +Please note that sometimes an attribution response may also contain an ask_in parameter. This means that the Adjust server has not yet completed the attribution process, and you should send another attribution request after the ask_in time elapses. + +If the attribution response contains a deferred deep link, here is a suggested approach to handle it: + +1. Store the deep link persistently in the app (e.g., in UserDefaults for iOS or SharedPreferences for Android) for later use. This is often necessary because many apps have onboarding screens and login processes that need to be completed before handling the deferred deep link. + +2. When ready to process the deep link, do the following: + - Retrieve and parse the stored link. + - Implement domain-agnostic handling to treat all links equivalently: + - Adjust universal links (e.g., example.go.link) + - Other universal links (e.g., example.com) + - App scheme deep links (e.g., example://) + - Extract relevant information (path, query parameters, fragment) regardless of link type. + - Navigate to the deep link screen based on the extracted information. + +Example equivalence: + +- `https://example.go.link/summer-clothes?promo=beach` +- `https://example.com/summer-clothes?promo=beach` +- `example://summer-clothes?promo=beach` +
+ These should all lead to the same destination in your app. + +Here's a reference implementation demonstrating these concepts: + + + +```swift +import Foundation +import UIKit + +/// Handles the storage and retrieval of the deferred deep link and onboarding status +class UserDefaultsManager { +static let shared = UserDefaultsManager() +private let userDefaults = UserDefaults.standard + + // ... + + private let DEFERRED_LINK_KEY = "deferredLink" + private let HAS_COMPLETED_ONBOARDING_KEY = "hasCompletedOnboarding" + + // ... + + func getDeferredLink() -> String? { + return userDefaults.string(forKey: DEFERRED_LINK_KEY) + } + + func setDeferredLink(_ value: String) { + userDefaults.set(value, forKey: DEFERRED_LINK_KEY) + } + + func removeDeferredLink() { + userDefaults.removeObject(forKey: DEFERRED_LINK_KEY) + } + + func getHasCompletedOnboarding() -> Bool { + return userDefaults.bool(forKey: HAS_COMPLETED_ONBOARDING_KEY) + } + + func setHasCompletedOnboarding(_ value: Bool) { + userDefaults.set(value, forKey: HAS_COMPLETED_ONBOARDING_KEY) + } + +} + +/// Demonstrates how to handle onboarding and deferred deep links +class ViewController: UIViewController { +let userDefaultsManager = UserDefaultsManager.shared + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !userDefaultsManager.getHasCompletedOnboarding() { + // Show onboarding screens and login + // On completion, set hasCompletedOnboarding to true + userDefaultsManager.setHasCompletedOnboarding(true) + } + + // Check if there's a stored deferred deep link + if let deferredLinkString = userDefaultsManager.getDeferredLink(), + let deferredLink = URL(string: deferredLinkString) { + // Remove the stored URL to avoid handling it again later + userDefaultsManager.removeDeferredLink() + // Handle deferred deep link + DeeplinkHandler.handleDeeplink(deferredLink, navigationController: self.navigationController) + } else { + // Show main content + } + } + +} + +/// Handles the logic for processing and navigating to deep links +class DeeplinkHandler { +static func handleDeeplink(\_ incomingLink: URL, navigationController: UINavigationController?) { +// Extract path, query items, and fragment from the link +let components = URLComponents(url: incomingLink, resolvingAgainstBaseURL: true) +let path = components?.path ?? "" +let queryItems = components?.queryItems +let fragment = components?.fragment + + // Implement navigation or other app-specific + // logic based on the deep link components. + DispatchQueue.main.async { + if path == "/summer-clothes" { + let promoCode = queryItems?.first(where: { $0.name == "promo" })?.value + let summerClothesVC = SummerClothesViewController(promoCode: promoCode) + navigationController?.pushViewController(summerClothesVC, animated: true) + } + } + } + +} + +// Usage example when receiving the attribution response: + +if let deeplink = attributionResponse["attribution"]["deeplink"] as? String { +UserDefaultsManager.shared.setDeferredLink(deeplink) +} + +```` + + +```kotlin +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity + +/** Helps to manage app-wide persistent settings and states */ +class SharedPreferencesManager private constructor(context: Context) { + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + companion object { + private const val PREFS_NAME = "AppPrefs" + + // ... + + private const val DEFERRED_LINK_KEY = "deferredLink" + private const val HAS_COMPLETED_ONBOARDING_KEY = "hasCompletedOnboarding" + + @Volatile + private var instance: SharedPreferencesManager? = null + + fun getInstance(context: Context): SharedPreferencesManager { + return instance ?: synchronized(this) { + instance ?: SharedPreferencesManager(context.applicationContext).also { + instance = it + } + } + } + } + + // ... + + fun getDeferredLink(): String? { + return sharedPreferences.getString(DEFERRED_LINK_KEY, null) + } + + fun setDeferredLink(value: String) { + sharedPreferences.edit().putString(DEFERRED_LINK_KEY, value).apply() + } + + fun removeDeferredLink() { + sharedPreferences.edit().remove(DEFERRED_LINK_KEY).apply() + } + + fun getHasCompletedOnboarding(): Boolean { + return sharedPreferences.getBoolean(HAS_COMPLETED_ONBOARDING_KEY, false) + } + + fun setHasCompletedOnboarding(value: Boolean) { + sharedPreferences.edit().putBoolean(HAS_COMPLETED_ONBOARDING_KEY, value).apply() + } +} + +/** Demonstrates how to handle onboarding and deferred deep links */ +class MainActivity : AppCompatActivity() { + private lateinit var sharedPreferencesManager: SharedPreferencesManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + sharedPreferencesManager = SharedPreferencesManager.getInstance(this) + } + + override fun onResume() { + super.onResume() + + if (!sharedPreferencesManager.getHasCompletedOnboarding()) { + // Show onboarding screens and login + // On completion, set hasCompletedOnboarding to true + sharedPreferencesManager.setHasCompletedOnboarding(true) + } + + // Check if there's a stored deferred deep link + sharedPreferencesManager.getDeferredLink()?.let { deferredLinkString -> + // Remove the stored URL to avoid handling it again later + sharedPreferencesManager.removeDeferredLink() + // Handle deferred deep link + DeeplinkHandler.handleDeeplink(deferredLinkString, supportFragmentManager) + } ?: run { + // Show main content + } + } +} + +/** Handles the logic for processing and navigating to deep links */ +object DeeplinkHandler { + fun handleDeeplink(incomingLink: String, fragmentManager: androidx.fragment.app.FragmentManager) { + // Extract path, query items, and fragment from the link + val uri = android.net.Uri.parse(incomingLink) + val path = uri.path ?: "" + val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } + val fragment = uri.fragment + + // Implement navigation or other app-specific + // logic based on the deep link components. + if (path == "/summer-clothes") { + val promoCode = queryParams["promo"] + val summerClothesFragment = SummerClothesFragment.newInstance(promoCode) + fragmentManager.beginTransaction() + .replace(R.id.fragment_container, summerClothesFragment) + .addToBackStack(null) + .commit() + } + } +} + +// Usage example +attributionResponse["attribution"]?.get("deeplink")?.asString?.let { deeplink -> + SharedPreferencesManager.getInstance(context).setDeferredLink(deeplink) + } +```` + + + + +### Post-install event + +After you send at least one successful session request for a device, you can send [post-install events](https://dev.adjust.com/en/api/s2s-api/events). These are typically events that represent marketing goals, and that networks can use to optimize campaigns. + + + + +```swift kotlin +// Add event token to existing params +params["event_token"] = "2y7e81" + +// Add revenue and currency, if applicable +// These parameters are equivalent to $19.99 +params["revenue"] = "19.99" +params["currency"] = "USD" +``` + + + + +#### Callback parameters + +When using [raw data exports](https://help.adjust.com/en/article/raw-data-exports), you can include custom "callback parameters" in specific event requests to add event-level custom data. For instance, on a purchase event, you might want to include your internal transaction ID in the raw data for that event. + +Callback parameters are represented as a JSON object containing string key-value pairs. + + + + +```swift kotlin +// If callback_params exists, add the event callback parameters to it (e.g., txn_id) +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value", "txn_id": "8837853376"}' + +// If callback_params does not exist, create it +params["callback_params"] = '{"txn_id": "8837853376"}' +``` + + + + +#### Partner parameters + +When integrating with certain partners, you may need to include custom "partner parameters" in your event requests. The Adjust server will then include these parameters on the callbacks it makes to partners for relevant events. This is most commonly used to enable dynamic remarketing campaigns, typically for events like view_item, add_to_cart, and purchase. + +Partner parameters are represented as a JSON object containing string key-value pairs. + + + + +```swift kotlin +// If partner_params exists, add the event partner parameters to it (e.g., item_id) +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM", "item_id": "[\"76524\",\"62599\"]"}' + +// If partner_params does not exist, create it +params["partner_params"] = '{"item_id": "[\"76524\",\"62599\"]"}' +``` + + + + +Send an event request. + + + + ```bash curl -X POST "https://app.adjust.com/event" \ -H "Authorization: + Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ -H "Content-Type: + application/x-www-form-urlencoded" \ -d "s2s=1\ &os_name=ios\ + &app_token=4w565xzmb54d\ &idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ + &att_status=3\ &idfv=59E27F41-A86B-4560-B585-63161F871C4B\ + &primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ + &created_at_unix=1484085154\ &device_name=iPhone16%2C2\ + &device_type=iPhone\ &os_version=17.5.1\ &ip_address=192.0.0.1\ + &environment=sandbox\ + &callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%2C%20%22txn_id%22%3A%20%228837853376%22%7D\ + &partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%2C%20%22item_id%22%3A%20%22%5B%5C%2276524%5C%22%2C%5C%2262599%5C%22%5D%22%7D" + \ &event_token=2y7e81\ &revenue=19.99\ ¤cy=USD\ -w "\n\nHTTP Status + Code: %{http_code}\n" \ -s ``` + + + + + + +```json +{ + "status": "OK" +} + +HTTP Status Code: 200 +``` + + + From b99f8fe99184e0e07e666f2540e8f52565b8a768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Mon, 15 Jul 2024 14:14:40 +0200 Subject: [PATCH 2/5] Clean up article formatting --- .../docs/api/s2s-api/s2s-developer-guide.mdx | 955 ++++++++++-------- 1 file changed, 524 insertions(+), 431 deletions(-) diff --git a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx index 9387ff69b..35ef03698 100644 --- a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx +++ b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx @@ -5,97 +5,97 @@ slug: en/api/s2s-api/s2s-developer-guide sidebar-position: 7 --- -Adjust offers a server-to-server (S2S) interface as an alternative to integrating an SDK into your app. If you choose to implement Adjust entirely through S2S, you'll need to modify your app to replicate the Adjust SDK's functions. This guide provides step-by-step instructions on two key aspects: +Adjust offers a server-to-server (S2S) interface as an alternative to integrating the Adjust SDK into your app. If you choose to implement Adjust through S2S, you need to modify your app to replicate the Adjust SDK's functions. This guide provides step-by-step instructions for the following: -- How to make the necessary updates to your app -- How to send S2S requests to Adjust -
- This comprehensive walkthrough will help you implement a full S2S solution - with Adjust. +- How to make the necessary updates to your app. +- How to send S2S requests to Adjust. - + -For mixed audience apps as defined in COPPA, Adjust strongly recommends using a 100% SDK implementation and avoiding any S2S implementation. +For mixed audience apps [as defined by COPPA](https://help.adjust.com/en/article/coppa-compliance#how-can-i-identify-my-app-audience), Adjust recommends using a 100% SDK implementation and avoiding any S2S implementation. -Children's apps as defined in COPPA are not supported for S2S, and have to use a 100% SDK implementation. +Children's apps as defined by COPPA aren't supported for S2S, and have to use a 100% SDK implementation. -## Before you begin +## [Before you begin](before-you-begin) + +Here's what you need to do before you get started. + +### [Enable S2S session measurement](enable-s2s-session-measurement) + +Adjust needs to enable S2S session measurement for your app. Contact your Adjust representative or support@adjust.com to proceed. -### Enable S2S session measurement +### [Set up S2S Security](set-up-s2s-security) -Adjust has to enable S2S session measurement for your app. Please contact your Adjust representative or support@adjust.com to proceed. +[Implement S2S Security](/en/api/s2s-api/security) to safeguard your S2S activities and prevent spoofed requests. Generate a token in your Adjust dashboard and include it in each incoming request. -### Set up S2S Security + + +Adjust's servers reject any requests sent without the correct token. This ensures robust protection for your data. -[Implement S2S Security](https://dev.adjust.com/en/api/s2s-api/security) to safeguard your S2S activities and prevent spoofed requests. Generate a token in your Adjust dashboard and include it with each incoming request. Adjust's servers will reject any requests lacking the correct token, ensuring robust protection for your data. + -### Queue and persist events locally +### [Queue and persist events locally](queue-and-persist-events-locally) -Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, it's crucial to capture and store these events locally until they can be successfully transmitted to Adjust's servers. +Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, you must capture and store these events locally until they can be successfully transmitted to Adjust's servers. -Implement a local event queue with persistence: +To implement a local event queue with persistence: 1. Create a queue to store activities when they occur. -2. For each activity, include a `created_at_unix` timestamp in seconds (e.g., 1484085154) representing when the event occurred on the device. -3. Save this queue to local storage (e.g., SQLite database or files) to persist across app restarts. +2. For each activity, include a `created_at_unix` timestamp in seconds (for example: `1484085154`) representing when the event occurred on the device. +3. Save this queue to local storage (for example: SQLite database or files) to persist across app restarts. 4. Attempt to send activities to Adjust's servers when the queue is non-empty and the device is online. 5. Remove activities from the queue only after successful transmission. -
- This approach helps mitigate data loss during the following: - -- Brief network interruptions (e.g., 5G to WiFi handovers) -- Extended periods without connectivity -- App crashes or force closes before transmission -
- Without local queuing, you risk losing 10–20% of install data, which can - significantly impact attribution accuracy. By implementing this queuing - system, you ensure that Adjust receives a complete and accurate picture of - user activity, enabling precise attribution even for events that occur - offline. - -### Add iOS frameworks - -1. Link required frameworks in Xcode. -
- -- Open your project in Xcode. -- Select your target in the project navigator. -- Go to the "General" tab. -- Scroll to the "Frameworks, Libraries, and Embedded Content" section. -- Click the "+" button. -- Search for and add the frameworks that your app requires from the list below. -
+ +This approach helps mitigate data loss in the following scenarios: + +- Brief network interruptions (for example: 5G to WiFi handovers). +- Extended periods without connectivity. +- App crashes or force closes before transmission. + +Without local queuing, you risk losing 10–20% of install data, which can significantly impact attribution accuracy. By implementing this queuing system, you ensure that Adjust receives a complete and accurate picture of user activity, enabling precise attribution even for events that occur offline. + +### [Add iOS frameworks](add-ios-frameworks) + +You must link frameworks to your project to support certain iOS features. To add frameworks to your project: + +1. Open your project in Xcode. +2. Select your target in the project navigator. +3. Go to the **General** tab. +4. Scroll to the **Frameworks, Libraries, and Embedded Content** section. +5. Click the **+** button. +6. Search for and add the frameworks that your app requires from the list below. - | Framework | Description | | :--- | :--- | |{" "} -
`AdSupport.framework`
|{" "} -
- Required to collect IDFA. Also required to collect Limit Ad Tracking - status for pre-ATT iOS versions. -
{" "} - | |
`AppTrackingTransparency.framework`
|{" "} -
- Required to show the AppTrackingTransparency prompt and collect IDFA on - devices running iOS 14.5 and later. -
{" "} - | |
`AdServices.framework`
|{" "} -
- Required for Adjust to perform attribution for Apple Search Ads campaigns. -
{" "} - | |
`StoreKit.framework`
|{" "} -
Required to run SKAdNetwork campaigns.
| + +| Framework | Description | +| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `AdSupport.framework` | Required to collect IDFA. Also required to collect Limit Ad Tracking status for pre-ATT iOS versions. | +| `AppTrackingTransparency.framework` | Required to show the AppTrackingTransparency prompt and collect IDFA on devices running iOS 14.5 and later. | +| `AdServices.framework` | Required for Adjust to perform attribution for Apple Search Ads campaigns. | +| `StoreKit.framework` | Required to run SKAdNetwork campaigns. | +
-## Required parameters +## [Required parameters](required-parameters) -### Base parameters +The following parameters are required in each S2S request. -These parameters have to be included in every S2S request. + + +| Parameter | Description | +| ----------- | ----------------------------------------------------------------------- | +| `s2s` | Indicates that the request is an S2S request. Must be hardcoded to `1`. | +| `os_name` | The name of the mobile operating system. Must be `ios` or `android`. | +| `app_token` | Your Adjust app token. | + +
+ +You can store these parameters as a variable in your app to re-use them whenever a request is sent. - + ```swift // Create dictionary for params to include on all S2S requests to Adjust @@ -111,8 +111,9 @@ params["os_name"] = "ios" params["app_token"] = "4w565xzmb54d" ``` - - + + + ```kotlin // Create map for params to include on all S2S requests to Adjust val params = mutableMapOf() @@ -126,42 +127,37 @@ params["os_name"] = "ios" // Replace with your Adjust app token params["app_token"] = "4w565xzmb54d" -```` - +``` + + -### Device IDs and tracking statuses +### [Device IDs and tracking statuses](device-ids-and-tracking-statuses) -Every S2S request has to include at least one device identifier. Due to privacy measures implemented by mobile operating systems, the advertising ID may not always be available. Therefore it's crucial to include the advertising ID when available, and to always include backup identifiers. +Every S2S request must include **at least one** device identifier. Due to privacy measures implemented by mobile operating systems, the advertising ID may not always be available. Therefore, you should include the advertising ID when available and must always include backup identifiers. -Tracking statuses are also required to be included on all S2S requests, as Adjust's system has some dependencies on them. +Adjust also requires tracking statuses in each request. -#### iOS +#### [iOS device IDs](ios-device-ids) ##### IDFA -IDFA is generally only available for iOS devices where users have opted to share it with your app via the AppTrackingTransparency (ATT) prompt. Follow these steps if you choose to show the ATT prompt and collect IDFA in your app: +The ID for Advertisers (IDFA) is available only for iOS devices where users have opted to share it with your app via the App Tracking Transparency (ATT) prompt. Follow these steps if you choose to show the ATT prompt and collect IDFA in your app: 1. Add ATT description in Xcode. -
- - Open your project's Info.plist file. - - In the editor, right-click on "Information Property List" and choose "Add Row" to add a key to the root. - - Set the key to `NSUserTrackingUsageDescription`. - - Set the value to a string explaining why you're requesting tracking permission (e.g., "This identifier will be used to deliver personalized ads to you."). Be sure to review [Apple's guidelines](https://developer.apple.com/documentation/bundleresources/information_property_list/nsusertrackingusagedescription) for this text. -
+ 1. Open your project's `Info.plist` file. + 2. In the editor, right-click on **Information Property List** and choose **Add Row** to add a key to the root. + 3. Set the key to `NSUserTrackingUsageDescription`. + 4. Set the value to a string explaining why you're requesting tracking permission (for example: "This identifier will be used to deliver personalized ads to you."). Be sure to review [Apple's guidelines](https://developer.apple.com/documentation/bundleresources/information_property_list/nsusertrackingusagedescription) for this text. 2. Implement ATT prompt and IDFA retrieval. -
-
- ATT has the following requirements, which the code example below addresses: -
-
- - While ATT support begins with iOS 14, user consent for IDFA retrieval is only required from iOS 14.5 onwards. Therefore, Adjust recommends targeting the ATT prompt specifically to users on iOS 14.5 and later versions. - - The ATT prompt requires an active app state to display. Showing it immediately after other system prompts may fail unless you first confirm the app state is again active. - - The earliest places to show the prompt are in `applicationDidBecomeActive` (App Delegate) or `sceneDidBecomeActive` (Scene Delegate). It's not possible to show the ATT prompt in `didFinishLaunchingWithOptions` (App Delegate). -
- - +ATT has the following requirements: + +- While ATT support begins with iOS 14, user consent for IDFA retrieval is only required from iOS 14.5 onwards. Therefore, Adjust recommends targeting the ATT prompt specifically to users on iOS 14.5 and later versions. +- The ATT prompt requires an active app state to display. Showing it immediately after other system prompts may fail unless you first confirm the app state is again active. +- The earliest places to show the prompt are in `applicationDidBecomeActive` (App Delegate) or `sceneDidBecomeActive` (Scene Delegate). It's not possible to show the ATT prompt in `didFinishLaunchingWithOptions` (App Delegate). + +The following code example addresses all of these requirements: ```swift import AppTrackingTransparency @@ -185,7 +181,7 @@ func getIDFAInfo(completion: @escaping (IDFAInfo) -> Void) { nil)) } } - // Do not show ATT prompt. Just get IDFA and tracking status. + // Don't show ATT prompt. Just get IDFA and tracking status. } else { let manager = ASIdentifierManager.shared() let trackingEnabled = manager.isAdvertisingTrackingEnabled @@ -200,7 +196,7 @@ private func requestTrackingAuthorization( let trackingStatus = ATTrackingManager.trackingAuthorizationStatus switch trackingStatus { - // Only show ATT prompt if status is not determined + // Only show ATT prompt if status isn't determined case .notDetermined: // If app state is active, show ATT prompt if UIApplication.shared.applicationState == .active { @@ -246,36 +242,23 @@ getIDFAInfo { info in params["tracking_enabled"] = trackingEnabled ? "1" : "0" } } - -```` - - - +``` ##### IDFV -IDFV is a backup identifier available on all modern iOS devices. +The ID for Vendors (IDFV) is a backup identifier available on all modern iOS devices. - - - ```swift let idfv: UUID? = UIDevice.current.identifierForVendor if let idfvString = idfv?.uuidString { -params["idfv"] = idfvString + params["idfv"] = idfvString } +``` -```` - - - -##### Primary dedupe token - -To consistently measure app activities across uninstalls and reinstalls, generate a random version 4 UUID (the "primary dedupe token") and save it in the iOS keychain. The primary dedupe token is a backup identifier that you should generate for all devices. +##### Primary deduplication token - - +To consistently measure app activities across uninstalls and reinstalls, generate a random version 4 UUID (the "primary deduplication token") and save it in the iOS keychain. The primary deduplication token is a backup identifier that you should generate for all devices. ```swift import Foundation @@ -352,203 +335,202 @@ let primaryDedupeToken = getOrCreatePrimaryDedupeToken() // Convert to lowercase string params["primary_dedupe_token"] = primaryDedupeToken.uuidString.lowercased() -```` - - - +``` -#### Android - Google Play +#### [Google Play device IDs (Android)](google-play-device-ids-android) ##### Google Advertising ID -Google Advertising ID is available on Android devices with Google Play Services, provided the user has not opted to delete their advertising ID. +The Google Play Services Advertising ID (GPS ADID) is available on Android devices with Google Play Services, provided the user hasn't opted to delete their advertising ID. -1. Add the necessary dependency to your app's `build.gradle` file: + + +You can check for the most recent version of the Play Services Ads Identifier library on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-ads-identifier). + + + +1. Add the `play-services-ads-identifier` dependency to your app's `build.gradle` file: - + -```kotlin -// build.gradle.kts + +```kotlin dependencies { -implementation("com.google.android.gms:play-services-ads-identifier:18.1.0") + implementation("com.google.android.gms:play-services-ads-identifier:18.1.0") } +``` -```` - - + -```groovy -// build.gradle + + + + +```groovy dependencies { implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0' } -```` +``` - - + -You can check for the most recent version on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-ads-identifier). + + 2. Add the following permission to your `AndroidManifest.xml` file: -```xml - -``` + -3. If your app uses R8 or ProGuard, add these rules to your `proguard-rules.pro` file (create the file in your app module's directory if it doesn't exist): + ```xml + + ``` -``` --keep class com.google.android.gms.common.ConnectionResult { - int SUCCESS; -} --keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { - com.google.android.gms.ads.identifier. - AdvertisingIdClient$Info getAdvertisingIdInfo( - android.content.Context); -} --keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { - java.lang.String getId(); - boolean isLimitAdTrackingEnabled(); -} -``` + -These rules preserve classes and methods needed for Google Advertising ID retrieval during code optimization. Skip this step if you're not using R8 or ProGuard. +3. If your app uses R8 or ProGuard, add these rules to your `proguard-rules.pro` file to preserve classes and methods needed for Google Advertising ID retrieval during code optimization (create the file in your app module's directory if it doesn't exist): -4. Implement the code to collect Google Advertising ID and tracking status: + - - - -```kotlin -import com.google.android.gms.ads.identifier.AdvertisingIdClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -data class GPSAdInfo(val gpsAdid: String?, val isTrackingEnabled: Boolean) - -suspend fun getGoogleAdvertisingIdInfo(context: Context): -GPSAdInfo? { -return withContext(Dispatchers.IO) { -try { -val adInfo = -AdvertisingIdClient.getAdvertisingIdInfo(context) -val gpsAdid = -if (adInfo.isLimitAdTrackingEnabled) null else adInfo.id -GPSAdInfo(gpsAdid, !adInfo.isLimitAdTrackingEnabled) -} catch (e: Exception) { -// Handle exceptions (e.g., Google Play Services not available) -null -} -} -} + ```java + -keep class com.google.android.gms.common.ConnectionResult { + int SUCCESS; + } + -keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { + com.google.android.gms.ads.identifier. + AdvertisingIdClient$Info getAdvertisingIdInfo( + android.content.Context); + } + -keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { + java.lang.String getId(); + boolean isLimitAdTrackingEnabled(); + } + ``` -// Usage example -// As getGoogleAdvertisingIdInfo is a suspending function, -// it should be called from within a coroutine scope. -lifecycleScope.launch { -val adInfo = getGoogleAdvertisingIdInfo(context) -adInfo?.let { info -> -// Include Google Advertising ID if available -info.gpsAdid?.let { params["gps_adid"] = it } - - // Include tracking status if available - info.isTrackingEnabled?.let { params["tracking_enabled"] - = if (it) "1" else "0" } - } + -} +4. Implement the code to collect Google Advertising ID and tracking status: -```` - - + ```kotlin + import com.google.android.gms.ads.identifier.AdvertisingIdClient + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.withContext + + data class GPSAdInfo(val gpsAdid: String?, val isTrackingEnabled: Boolean) + + suspend fun getGoogleAdvertisingIdInfo(context: Context):GPSAdInfo? { + return withContext(Dispatchers.IO) { + try { + val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context) + val gpsAdid = if (adInfo.isLimitAdTrackingEnabled) null else adInfo.idGPSAdInfo(gpsAdid, !adInfo.isLimitAdTrackingEnabled) + } catch (e: Exception) { + // Handle exceptions (for example: Google Play Services not available) + null + } + } + } + + // Usage example + // As getGoogleAdvertisingIdInfo is a suspending function, + // it should be called from within a coroutine scope. + lifecycleScope.launch { + val adInfo = getGoogleAdvertisingIdInfo(context) adInfo?.let { info -> + // Include Google Advertising ID if available + info.gpsAdid?.let { params["gps_adid"] = it } + // Include tracking status if available + info.isTrackingEnabled?.let { params["tracking_enabled"] = if (it) "1" else "0" } + } + } + ``` ##### App Set ID App Set ID is a backup identifier available on all Android devices with Google Play Services installed and running API Level 30 (Android 11) or later. + + +You can check for the most recent version of the Play Services App Set library on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-appset). + + + 1. Add the necessary dependency to your app's `build.gradle` file: - + -```kotlin -// build.gradle.kts + +```kotlin dependencies { implementation("com.google.android.gms:play-services-appset:16.1.0") } -```` +``` - - - -```groovy -// build.gradle + + + + + + +```groovy dependencies { -implementation 'com.google.android.gms:play-services-appset:16.1.0' + implementation 'com.google.android.gms:play-services-appset:16.1.0' } +``` -```` - - + -You can check for the most recent version on the [Google Maven Repository](https://maven.google.com/web/index.html#com.google.android.gms:play-services-appset). + + 2. Implement the code to collect App Set ID: - - - -```kotlin -import com.google.android.gms.appset.AppSet -import com.google.android.gms.appset.AppSetIdClient -import com.google.android.gms.appset.AppSetIdInfo -import com.google.android.gms.tasks.Tasks -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -suspend fun getAppSetId(context: Context): String? { - return withContext(Dispatchers.IO) { - try { - val client: AppSetIdClient = AppSet.getClient(context) - val taskResult = Tasks.await(client.appSetIdInfo) - taskResult.id - } catch (e: Exception) { - // Handle exceptions (e.g., Google Play Services not available) - null - } - } -} - -// Usage example -// As getAppSetId is a suspending function, -// it should be called from within a coroutine scope. -lifecycleScope.launch { - val appSetId = getAppSetId(context) - appSetId?.let { id -> - val params = mutableMapOf() - params["google_app_set_id"] = id - } -} -```` - - - + ```kotlin + import com.google.android.gms.appset.AppSet + import com.google.android.gms.appset.AppSetIdClient + import com.google.android.gms.appset.AppSetIdInfo + import com.google.android.gms.tasks.Tasks + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.withContext + + suspend fun getAppSetId(context: Context): String? { + return withContext(Dispatchers.IO) { + try { + val client: AppSetIdClient = AppSet.getClient(context) + val taskResult = Tasks.await(client.appSetIdInfo) + taskResult.id + } catch (e: Exception) { + // Handle exceptions (for example: Google Play Services not available) + null + } + } + } + + // Usage example + // As getAppSetId is a suspending function, + // it should be called from within a coroutine scope. + lifecycleScope.launch { + val appSetId = getAppSetId(context) + appSetId?.let { id -> + val params = mutableMapOf() + params["google_app_set_id"] = id + } + } + ``` ## Additional parameters -These parameters are not required, however if you use them, you should include them in all S2S requests. +These parameters aren't required. If you use any of these parameters, you should include them in all requests. ### Unix timestamp -Adjust strongly recommends including timestamps on S2S requests, as this provides the time when the activities occurred on the device, and therefore improves attribution accuracy. +Including a Unix timestamp with each S2S request improves attribution accuracy by providing the time at which the activity occured on the device. - + ```swift // Unix timestamp of when activity occurred on device @@ -558,7 +540,7 @@ params["created_at_unix"] = String(Int(Date().timeIntervalSince1970)) ``` - + ```kotlin // Unix timestamp of when activity occurred on device @@ -567,15 +549,28 @@ params["created_at_unix"] = String(Int(Date().timeIntervalSince1970)) params["created_at_unix"] = (System.currentTimeMillis() / 1000).toString() ``` - + ### Probabilistic modeling data points -In order to use probabilistic modeling as an attribution method, include the below parameters on all S2S requests. Adjust strongly recommends implementing this, as it enables more comprehensive attribution, particularly for iOS. +To use probabilistic matching as an attribution method, you need to include the following parameters on all S2S requests: + + + +| Parameter | Description | +| ------------- | ---------------------------------------------------------- | +| `device_name` | The name of the device. | +| `device_type` | The device type or model. | +| `os_version` | The version of the operating system running on the device. | +| `ip_address` | The IP address of the device | + +
+ +Adjust strongly recommends adding these parameters as it enables more comprehensive attribution, particularly for iOS. - + ```swift import UIKit @@ -608,8 +603,8 @@ params["os_version"] = UIDevice.current.systemVersion params["ip_address"] = "192.0.0.1" // Example value ``` - - + + ```kotlin import android.content.Context @@ -654,17 +649,33 @@ params["device_type"] = when { params["ip_address"] = "192.0.0.1" // Example value ``` - + ### Environment -When testing, you can send S2S requests in sandbox mode. Adjust records these separately from your production data. If you don't pass this parameter, the default value is "production". +You can specify the environment that requests are being sent in by passing an `environment` parameter. Requests from different environments are kept separate in Adjust to enable testing. The following values are available: + +- `sandbox`: use this while testing to keep your requests separate from your production data. +- `production`: use this when you release your app. + +If you don't pass this parameter, the default value is `production`. - + + +```swift +// For testing (sandbox environment) +params["environment"] = "sandbox" + +// For production use +params["environment"] = "production" +``` + + + -```swift kotlin +```kotlin // For testing (sandbox environment) params["environment"] = "sandbox" @@ -672,21 +683,30 @@ params["environment"] = "sandbox" params["environment"] = "production" ``` - + ### Global callback parameters -When using [raw data exports](https://help.adjust.com/en/article/raw-data-exports), you can include custom "global callback parameters" in all your S2S requests to add custom parameters to the raw data. This is commonly used to include your internal user ID in your exported raw data. +When using [raw data exports](https://help.adjust.com/en/article/raw-data-exports), you can include "global callback parameters" in all your S2S requests to add custom parameters to the raw data. This is commonly used to include your internal user ID in your exported raw data. Global callback parameters are represented as a JSON object containing string key-value pairs. - + + ```swift params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value"}' ``` - + + + + +```kotlin +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value"}' +``` + + ## Requests @@ -699,124 +719,136 @@ Ensure all parameter values are URL encoded before sending requests. ### Global partner parameters -When integrating with certain partners, you may need to include custom "global partner parameters" in all your S2S requests. The Adjust server will then pass these parameters on all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. +When integrating with certain partners, you may need to include "global partner parameters" in all your S2S requests. Adjust's servers pass these parameters in all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. Global partner parameters are represented as a JSON object containing string key-value pairs. - -```swift kotlin + + +```swift params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM"}' ``` - + + + + +```kotlin +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM"}' +``` + + ### Session -Sessions form the foundation of Adjust implementation and are the only technically required activity. A session generally represents an app open. The Adjust server logs successful session requests as follows: +Sessions form the foundation of Adjust implementation and are the only required activity. A session typically represents an app open. The Adjust server logs successful session requests as follows: - It records the first session for a device as an "install" activity. - It records subsequent sessions as "session" activities. - It records a "reattribution" or "reattribution reinstall" activity if [reattribution criteria](https://help.adjust.com/en/article/reattribution) are satisfied. -
- For reference, the Adjust SDK sends a session request to Adjust server when - either of the following occurs: - -- The user opens the app for the first time after install or reinstall. -- The user reopens the app after it has been closed or in the background for at least 30 minutes. -
- When sending S2S session requests with the `created_at_unix` parameter, the - Adjust server requires this value to be at least 20 minutes later than the - `created_at_unix` time of the last successfully logged session. -
- -Send a session request. + + + For reference, the Adjust SDK sends a session request to Adjust server when either of the following occurs: + + - The user opens the app for the first time after install or reinstall. + - The user reopens the app after it has been closed or in the background for at least 30 minutes. + + + + When sending S2S session requests with the `created_at_unix` parameter, the Adjust server requires this value to be at least 20 minutes later than the `created_at_unix` time of the last successfully logged session. + + + +```sh +curl -X POST "https://app.adjust.com/session" \ -H "Authorization: Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ +-H "Content-Type: application/x-www-form-urlencoded" \ +-d "s2s=1\ +&os_name=ios\ +&app_token=i9dukg8o5slc\ +&idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ +&att_status=3\ +&idfv=59E27F41-A86B-4560-B585-63161F871C4B\ +&primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ +&created_at_unix=1484085154\ +&device_name=iPhone16%2C2\ +&device_type=iPhone\ +&os_version=17.5.1\ +&ip_address=192.0.0.1\ +&environment=sandbox\ +&callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ +&partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D"\ +-w "\n\nHTTP Status Code: %{http_code}\n"\ +-s +``` - - - ```bash curl -X POST "https://app.adjust.com/session" \ -H "Authorization: - Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ -H "Content-Type: - application/x-www-form-urlencoded" \ -d "s2s=1\ &os_name=ios\ - &app_token=i9dukg8o5slc\ &idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ - &att_status=3\ &idfv=59E27F41-A86B-4560-B585-63161F871C4B\ - &primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ - &created_at_unix=1484085154\ &device_name=iPhone16%2C2\ - &device_type=iPhone\ &os_version=17.5.1\ &ip_address=192.0.0.1\ - &environment=sandbox\ - &callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ - &partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D" - \ -w "\n\nHTTP Status Code: %{http_code}\n" \ -s ``` - - + This is the response format when Adjust successfully logs the first session for the device. You can use the Adjust [testing console](https://help.adjust.com/en/article/testing-console) to forget your device and test this multiple times, if needed. - - + -```json +```console { - "app_token": "4w565xzmb54d", - "adid": "df6c5653080670612cd2f9766ddc0814", - "timestamp": "2024-07-09T01:31:14.373Z+0000", - "message": "Install tracked", - "ask_in": 2000 + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-09T01:31:14.373Z+0000", + "message": "Install tracked", + "ask_in": 2000 } HTTP Status Code: 200 ``` - - + This is the response format when Adjust successfully logs subsequent sessions for the device. - - + -```json +```console { - "app_token": "4w565xzmb54d", - "adid": "df6c5653080670612cd2f9766ddc0814", - "timestamp": "2024-07-09T02:31:14.373Z+0000", - "message": "Session tracked", - "ask_in": 5000 + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-09T02:31:14.373Z+0000", + "message": "Session tracked", + "ask_in": 5000 } HTTP Status Code: 200 ``` - - + ### Attribution -After sending a session request, you may need to send an attribution request to the Adjust server. The session response includes an ask_in parameter, indicating how many milliseconds to wait before making the attribution request. +After sending a session request, you may need to send an attribution request to the Adjust server. The session response includes an `ask_in` parameter, indicating how many milliseconds to wait before making the attribution request. + Attribution requests serve three main purposes for developers: - Obtain the attribution source for installs or reinstalls, allowing you to personalize the user experience. - Retrieve deferred deep links for users who clicked an Adjust deep link before installation. - Get updated attribution information in case of reattribution. -
There are two options for sending attribution requests: - Send after every session (covers all use cases) - Send only after install or reinstall (covers the first two use cases, which are most common) -
- To implement the second option, follow these steps: + +To implement the second option, follow these steps: 1. Create a flag in your app. 2. Use this flag to determine if the first session has been recorded. 3. Send the attribution request only when this flag indicates it's the first session. - + + ```swift import Foundation -/// Helps to manage app-wide persistent settings and states +// Helps to manage app-wide persistent settings and states class UserDefaultsManager { static let shared = UserDefaultsManager() private let userDefaults = UserDefaults.standard @@ -851,17 +883,19 @@ userDefaultsManager.firstSessionRecorded = true // Send subsequent session requests } -```` - - +``` + + + + ```kotlin import android.content.Context import android.content.SharedPreferences -/** Helps to manage app-wide persistent settings and states */ +/*_ Helps to manage app-wide persistent settings and states */ class SharedPreferencesManager private constructor(context: Context) { - private val sharedPreferences: SharedPreferences = - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) +private val sharedPreferences: SharedPreferences = +context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) companion object { private const val PREFS_NAME = "AppPrefs" @@ -887,6 +921,7 @@ class SharedPreferencesManager private constructor(context: Context) { FIRST_SESSION_RECORDED_KEY, value).apply() // ... + } // Usage example @@ -894,75 +929,82 @@ class SharedPreferencesManager private constructor(context: Context) { val userDefaultsManager = UserDefaultsManager.getInstance(context) if (!userDefaultsManager.firstSessionRecorded) { - // Send first session request - // If the request is successful: - userDefaultsManager.firstSessionRecorded = true - // Wait according to "ask_in" time - // Send attribution request for first session +// Send first session request +// If the request is successful: +userDefaultsManager.firstSessionRecorded = true +// Wait according to "ask_in" time +// Send attribution request for first session } else { - // Send subsequent session requests +// Send subsequent session requests } -```` - +``` + + Send an attribution request. - - - ```bash curl -X POST "https://app.adjust.com/attribution" \ -H - "Authorization: Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ -H "Content-Type: - application/x-www-form-urlencoded" \ -d "s2s=1\ &os_name=ios\ - &app_token=4w565xzmb54d\ &idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ - &att_status=3\ &idfv=59E27F41-A86B-4560-B585-63161F871C4B\ - &primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ - &created_at_unix=1484085154\ &device_name=iPhone16%2C2\ - &device_type=iPhone\ &os_version=17.5.1\ &ip_address=192.0.0.1\ - &environment=sandbox\ - &callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ - &partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D" - \ -w "\n\nHTTP Status Code: %{http_code}\n" \ -s ``` - - + + +```sh +curl -X POST "https://app.adjust.com/attribution" \ +-H "Authorization: Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ +-H "Content-Type:application/x-www-form-urlencoded" \ +-d "s2s=1\ +&os_name=ios\ +&app_token=4w565xzmb54d\ +&idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ +&att_status=3\ +&idfv=59E27F41-A86B-4560-B585-63161F871C4B\ +&primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ +&created_at_unix=1484085154\ +&device_name=iPhone16%2C2\ +&device_type=iPhone\ +&os_version=17.5.1\ +&ip_address=192.0.0.1\ +&environment=sandbox\ +&callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%7D\ +&partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%7D"\ +-w "\n\nHTTP Status Code: %{http_code}\n" \ +-s +``` + + Below is an example attribution response that contains a deferred deep link. - - + ```json { - "app_token": "4w565xzmb54d", - "adid": "df6c5653080670612cd2f9766ddc0814", - "timestamp": "2024-07-10T23:48:27.244Z+0000", - "message": "Attribution found", - "attribution": { - "tracker_token": "18msd3tn", - "tracker_name": "Test", - "network": "Test", - "deeplink": "example://summer-clothes?promo=beach\u0026adj_t=18msd3tn\u0026adjust_no_sdkclick=1" - } + "app_token": "4w565xzmb54d", + "adid": "df6c5653080670612cd2f9766ddc0814", + "timestamp": "2024-07-10T23:48:27.244Z+0000", + "message": "Attribution found", + "attribution": { + "tracker_token": "18msd3tn", + "tracker_name": "Test", + "network": "Test", + "deeplink": "example://summer-clothes?promo=beach\u0026adj_t=18msd3tn\u0026adjust_no_sdkclick=1" + } } - -HTTP Status Code: 200 ``` - - + -Please note that sometimes an attribution response may also contain an ask_in parameter. This means that the Adjust server has not yet completed the attribution process, and you should send another attribution request after the ask_in time elapses. +Please note that sometimes an attribution response may also contain an `ask_in` parameter. This means that the Adjust server hasn't yet completed the attribution process, and you should send another attribution request after the `ask_in` time elapses. If the attribution response contains a deferred deep link, here is a suggested approach to handle it: -1. Store the deep link persistently in the app (e.g., in UserDefaults for iOS or SharedPreferences for Android) for later use. This is often necessary because many apps have onboarding screens and login processes that need to be completed before handling the deferred deep link. +1. Store the deep link persistently in the app (for example: in `UserDefaults` for iOS or `SharedPreferences` for Android) for later use. This is often necessary because many apps have onboarding screens and login processes that need to be completed before handling the deferred deep link. 2. When ready to process the deep link, do the following: - Retrieve and parse the stored link. - Implement domain-agnostic handling to treat all links equivalently: - - Adjust universal links (e.g., example.go.link) - - Other universal links (e.g., example.com) - - App scheme deep links (e.g., example://) + - Adjust universal links (for example: `example.go.link`) + - Other universal links (for example: `example.com`) + - App scheme deep links (for example: `example://`) - Extract relevant information (path, query parameters, fragment) regardless of link type. - Navigate to the deep link screen based on the extracted information. @@ -971,13 +1013,14 @@ Example equivalence: - `https://example.go.link/summer-clothes?promo=beach` - `https://example.com/summer-clothes?promo=beach` - `example://summer-clothes?promo=beach` -
- These should all lead to the same destination in your app. + +These should all lead to the same destination in your app. Here's a reference implementation demonstrating these concepts: - + + ```swift import Foundation import UIKit @@ -1071,9 +1114,11 @@ if let deeplink = attributionResponse["attribution"]["deeplink"] as? String { UserDefaultsManager.shared.setDeferredLink(deeplink) } -```` - - +``` + + + + ```kotlin import android.content.Context import android.content.SharedPreferences @@ -1081,10 +1126,10 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity -/** Helps to manage app-wide persistent settings and states */ +/\*_ Helps to manage app-wide persistent settings and states _/ class SharedPreferencesManager private constructor(context: Context) { - private val sharedPreferences: SharedPreferences = - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) +private val sharedPreferences: SharedPreferences = +context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) companion object { private const val PREFS_NAME = "AppPrefs" @@ -1127,11 +1172,12 @@ class SharedPreferencesManager private constructor(context: Context) { fun setHasCompletedOnboarding(value: Boolean) { sharedPreferences.edit().putBoolean(HAS_COMPLETED_ONBOARDING_KEY, value).apply() } + } -/** Demonstrates how to handle onboarding and deferred deep links */ +/*_ Demonstrates how to handle onboarding and deferred deep links */ class MainActivity : AppCompatActivity() { - private lateinit var sharedPreferencesManager: SharedPreferencesManager +private lateinit var sharedPreferencesManager: SharedPreferencesManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -1158,16 +1204,17 @@ class MainActivity : AppCompatActivity() { // Show main content } } + } -/** Handles the logic for processing and navigating to deep links */ +/* Handles the logic for processing and navigating to deep links */ object DeeplinkHandler { - fun handleDeeplink(incomingLink: String, fragmentManager: androidx.fragment.app.FragmentManager) { - // Extract path, query items, and fragment from the link - val uri = android.net.Uri.parse(incomingLink) - val path = uri.path ?: "" - val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } - val fragment = uri.fragment +fun handleDeeplink(incomingLink: String, fragmentManager: androidx.fragment.app.FragmentManager) { +// Extract path, query items, and fragment from the link +val uri = android.net.Uri.parse(incomingLink) +val path = uri.path ?: "" +val queryParams = uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } +val fragment = uri.fragment // Implement navigation or other app-specific // logic based on the deep link components. @@ -1180,25 +1227,39 @@ object DeeplinkHandler { .commit() } } + } // Usage example attributionResponse["attribution"]?.get("deeplink")?.asString?.let { deeplink -> - SharedPreferencesManager.getInstance(context).setDeferredLink(deeplink) - } -```` + SharedPreferencesManager.getInstance(context).setDeferredLink(deeplink) +} +``` - + ### Post-install event -After you send at least one successful session request for a device, you can send [post-install events](https://dev.adjust.com/en/api/s2s-api/events). These are typically events that represent marketing goals, and that networks can use to optimize campaigns. +After you send at least one successful session request for a device, you can send [post-install events](/en/api/s2s-api/events). These are typically events that represent marketing goals, and that networks can use to optimize campaigns. - + + +```swift +// Add event token to existing params +params["event_token"] = "2y7e81" -```swift kotlin +// Add revenue and currency, if applicable +// These parameters are equivalent to $19.99 +params["revenue"] = "19.99" +params["currency"] = "USD" +``` + + + + +```kotlin // Add event token to existing params params["event_token"] = "2y7e81" @@ -1208,7 +1269,7 @@ params["revenue"] = "19.99" params["currency"] = "USD" ``` - + #### Callback parameters @@ -1218,17 +1279,28 @@ When using [raw data exports](https://help.adjust.com/en/article/raw-data-export Callback parameters are represented as a JSON object containing string key-value pairs. - + -```swift kotlin -// If callback_params exists, add the event callback parameters to it (e.g., txn_id) +```swift +// If callback_params exists, add the event callback parameters to it (for example: txn_id) params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value", "txn_id": "8837853376"}' // If callback_params does not exist, create it params["callback_params"] = '{"txn_id": "8837853376"}' ``` - + + + +```kotlin +// If callback_params exists, add the event callback parameters to it (for example: txn_id) +params["callback_params"] = '{"user_id": "2696775149", "user_category": "high value", "txn_id": "8837853376"}' + +// If callback_params does not exist, create it +params["callback_params"] = '{"txn_id": "8837853376"}' +``` + + #### Partner parameters @@ -1238,49 +1310,70 @@ When integrating with certain partners, you may need to include custom "partner Partner parameters are represented as a JSON object containing string key-value pairs. - + + +```swift +// If partner_params exists, add the event partner parameters to it (for example: item_id) +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM", "item_id": "[\"76524\",\"62599\"]"}' -```swift kotlin -// If partner_params exists, add the event partner parameters to it (e.g., item_id) +// If partner_params does not exist, create it +params["partner_params"] = '{"item_id": "[\"76524\",\"62599\"]"}' +``` + + + + +```kotlin +// If partner_params exists, add the event partner parameters to it (for example: item_id) params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM", "item_id": "[\"76524\",\"62599\"]"}' // If partner_params does not exist, create it params["partner_params"] = '{"item_id": "[\"76524\",\"62599\"]"}' ``` - + Send an event request. - - - ```bash curl -X POST "https://app.adjust.com/event" \ -H "Authorization: - Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ -H "Content-Type: - application/x-www-form-urlencoded" \ -d "s2s=1\ &os_name=ios\ - &app_token=4w565xzmb54d\ &idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ - &att_status=3\ &idfv=59E27F41-A86B-4560-B585-63161F871C4B\ - &primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ - &created_at_unix=1484085154\ &device_name=iPhone16%2C2\ - &device_type=iPhone\ &os_version=17.5.1\ &ip_address=192.0.0.1\ - &environment=sandbox\ - &callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%2C%20%22txn_id%22%3A%20%228837853376%22%7D\ - &partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%2C%20%22item_id%22%3A%20%22%5B%5C%2276524%5C%22%2C%5C%2262599%5C%22%5D%22%7D" - \ &event_token=2y7e81\ &revenue=19.99\ ¤cy=USD\ -w "\n\nHTTP Status - Code: %{http_code}\n" \ -s ``` - - + + +```sh +curl -X POST "https://app.adjust.com/event" \ +-H "Authorization:Bearer ADD_YOUR_AUTH_TOKEN_HERE" \ +-H "Content-Type:application/x-www-form-urlencoded" \ +-d "s2s=1\ +&os_name=ios\ +&app_token=4w565xzmb54d\ +&idfa=29DDE430-CE81-4F00-A50C-689595AAD142\ +&att_status=3\ +&idfv=59E27F41-A86B-4560-B585-63161F871C4B\ +&primary_dedupe_token=3b35fcfb-6115-4cff-830f-e32a248c487d\ +&created_at_unix=1484085154\ +&device_name=iPhone16%2C2\ +&device_type=iPhone\ +&os_version=17.5.1\ +&ip_address=192.0.0.1\ +&environment=sandbox\ +&callback_params=%7B%22user_id%22%3A%20%222696775149%22%2C%20%22user_category%22%3A%20%22high%20value%22%2C%20%22txn_id%22%3A%20%228837853376%22%7D\ +&partner_params=%7B%22analytics_user_id%22%3A%20%223913132433%22%2C%20%22analytics_session_id%22%3A%20%22nzFC9LKSqM%22%2C%20%22item_id%22%3A%20%22%5B%5C%2276524%5C%22%2C%5C%2262599%5C%22%5D%22%7D"\ +&event_token=2y7e81\ +&revenue=19.99\ +¤cy=USD\ +-w "\n\nHTTP Status Code: %{http_code}\n"\ +-s +``` - - + -```json + + +```console { - "status": "OK" + "status": "OK" } HTTP Status Code: 200 ``` - - + From fc58d7b31b300bf6610e1de4e87e0d8e54ccb6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Wed, 17 Jul 2024 14:29:41 +0200 Subject: [PATCH 3/5] Addressed some feedback --- .github/styles/Microsoft/HeadingAcronyms.yml | 2 + .../docs/api/s2s-api/s2s-developer-guide.mdx | 57 +++++++++++++++---- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/.github/styles/Microsoft/HeadingAcronyms.yml b/.github/styles/Microsoft/HeadingAcronyms.yml index 6674d3e80..cebf603f7 100644 --- a/.github/styles/Microsoft/HeadingAcronyms.yml +++ b/.github/styles/Microsoft/HeadingAcronyms.yml @@ -24,3 +24,5 @@ exceptions: - AAR - CPU - IDE + - IDFA + - IDFV diff --git a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx index 35ef03698..83818fb50 100644 --- a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx +++ b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx @@ -5,16 +5,14 @@ slug: en/api/s2s-api/s2s-developer-guide sidebar-position: 7 --- -Adjust offers a server-to-server (S2S) interface as an alternative to integrating the Adjust SDK into your app. If you choose to implement Adjust through S2S, you need to modify your app to replicate the Adjust SDK's functions. This guide provides step-by-step instructions for the following: +Adjust offers a server-to-server (S2S) interface for mobile, console, and Connected TV (CTV) apps. If you choose to implement Adjust through S2S, you need to modify your app to replicate the Adjust SDK's functions. This guide provides step-by-step instructions for the following: - How to make the necessary updates to your app. - How to send S2S requests to Adjust. -For mixed audience apps [as defined by COPPA](https://help.adjust.com/en/article/coppa-compliance#how-can-i-identify-my-app-audience), Adjust recommends using a 100% SDK implementation and avoiding any S2S implementation. - -Children's apps as defined by COPPA aren't supported for S2S, and have to use a 100% SDK implementation. +If your app needs to comply with COPPA, use the Adjust SDK and leverage its built-in COPPA features. Don't send non-compliant data using the S2S interface. @@ -24,7 +22,9 @@ Here's what you need to do before you get started. ### [Enable S2S session measurement](enable-s2s-session-measurement) -Adjust needs to enable S2S session measurement for your app. Contact your Adjust representative or support@adjust.com to proceed. +If you're using the S2S interface for a mobile app, Adjust needs to enable S2S session measurement for your app. Contact your Adjust representative or support@adjust.com to proceed. + +S2S session measurement is enabled automatically for [CTV measurement](https://help.adjust.com/en/article/ctv-advision) and [PC and Console measurement](https://help.adjust.com/en/article/measurement-for-pc-and-console-games) when you select a platform. ### [Set up S2S Security](set-up-s2s-security) @@ -64,7 +64,7 @@ You must link frameworks to your project to support certain iOS features. To add 2. Select your target in the project navigator. 3. Go to the **General** tab. 4. Scroll to the **Frameworks, Libraries, and Embedded Content** section. -5. Click the **+** button. +5. Select the **\+** button. 6. Search for and add the frameworks that your app requires from the list below. @@ -87,11 +87,41 @@ The following parameters are required in each S2S request. | Parameter | Description | | ----------- | ----------------------------------------------------------------------- | | `s2s` | Indicates that the request is an S2S request. Must be hardcoded to `1`. | -| `os_name` | The name of the mobile operating system. Must be `ios` or `android`. | +| `os_name` | The name of the mobile operating system. See the list of options below. | | `app_token` | Your Adjust app token. |
+ + +- `android` +- `android-tv` +- `apple-tv` +- `bada` +- `blackberry` +- `fire-tv` +- `ios` +- `linux` +- `macos` +- `nintendo` +- `playstation` + +--- + +- `roku-os` +- `server` +- `smart-cast` +- `steamos` +- `symbian` +- `tizen` +- `unknown` +- `webos` +- `windows` +- `windows-phone` +- `xbox` + + + You can store these parameters as a variable in your app to re-use them whenever a request is sent. @@ -104,7 +134,7 @@ var params: [String: String] = [:] // Hard-coded params["s2s"] = "1" -// "ios" or "android" +// The name of the operating system running on your device params["os_name"] = "ios" // Replace with your Adjust app token @@ -126,7 +156,6 @@ params["os_name"] = "ios" // Replace with your Adjust app token params["app_token"] = "4w565xzmb54d" - ``` @@ -138,6 +167,10 @@ Every S2S request must include **at least one** device identifier. Due to privac Adjust also requires tracking statuses in each request. +#### [PC and Console/CTV device IDs](pc-console-ctv-device-ids) + +For PC and Console and CTV measurement, you can pass a unique `external_device_id` parameter with each call to use as a device identifier. This value can be any unique string that identifies the device. + #### [iOS device IDs](ios-device-ids) ##### IDFA @@ -353,7 +386,7 @@ You can check for the most recent version of the Play Services Ads Identifier li - + ```kotlin @@ -748,14 +781,14 @@ Sessions form the foundation of Adjust implementation and are the only required - It records subsequent sessions as "session" activities. - It records a "reattribution" or "reattribution reinstall" activity if [reattribution criteria](https://help.adjust.com/en/article/reattribution) are satisfied. - + For reference, the Adjust SDK sends a session request to Adjust server when either of the following occurs: - The user opens the app for the first time after install or reinstall. - The user reopens the app after it has been closed or in the background for at least 30 minutes. - + When sending S2S session requests with the `created_at_unix` parameter, the Adjust server requires this value to be at least 20 minutes later than the `created_at_unix` time of the last successfully logged session. From d2e01cb5ca2344f51a822fa3e62a559cb2d3cc28 Mon Sep 17 00:00:00 2001 From: Dave Mead Date: Mon, 29 Jul 2024 00:16:43 -0700 Subject: [PATCH 4/5] Update guide, remove outdated checklist --- .../api/s2s-api/attribution-checklist.mdx | 114 ----- .../docs/api/s2s-api/s2s-developer-guide.mdx | 396 +++++++++++++++--- .../docs/api/s2s-api/sdk-to-s2s-events.mdx | 2 +- 3 files changed, 340 insertions(+), 172 deletions(-) delete mode 100644 src/content/docs/api/s2s-api/attribution-checklist.mdx diff --git a/src/content/docs/api/s2s-api/attribution-checklist.mdx b/src/content/docs/api/s2s-api/attribution-checklist.mdx deleted file mode 100644 index 04fe4f611..000000000 --- a/src/content/docs/api/s2s-api/attribution-checklist.mdx +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: "Server-to-server (S2S) attribution checklist" -description: "Adjust's S2S attribution checklist provides a rundown of the requirements for your in-app solution." -slug: en/api/s2s-api/attribution-checklist -sidebar-position: 5 -sidebar-label: S2S attribution checklist ---- - -Server-to-server (S2S) attribution and session measurement requires a custom in-app solution that replicates the Adjust SDK’s basic functionality. This S2S attribution checklist provides a rundown of the requirements for your in-app solution. Meeting these requirements guarantees the security of the information Adjust receives and the accuracy of reports. - -## [Before you begin](before-you-begin) - -Explore the Adjust SDK's minimal extent of code required for attribution and session measurement. Integrating this code into your app is Adjust’s preferred method of attribution and session measurement. The code is easy to integrate and provides the functionality that your custom solution will have to replicate. - -Follow the guides linked below to see the basic integration steps. - -📖 [Android](/en/sdk/android) / [iOS](/en/sdk/ios) / [Windows](/en/sdk/windows) / [Adobe Air](https://github.com/adjust/adobe_air_sdk#basic-integration) / [Unity](/en/sdk/unity) / [Cordova](https://github.com/adjust/cordova_sdk#basic-integration) / [Marmalade](https://github.com/adjust/marmalade_sdk#basic-integration) / [Xamarin](https://github.com/adjust/xamarin_sdk#basic-integration) / [Cocos2d-x](https://github.com/adjust/cocos2dx_sdk#basic-integration) / [React Native](/en/sdk/react-native) / [Titanium](https://github.com/adjust/titanium_sdk#basic-integration) / [Corona](https://github.com/adjust/corona_sdk#basic-integration) - -## [Checklist overview](checklist-overview) - -Adjust’s server-to-server attribution checklist covers 5 requirements under 3 key areas (data integrity and security, advertising and device IDs, and third-party integrations). Every item is fundamental to accurate attribution and session measurement. Your in-app solution will have to reproduce the Adjust SDK’s basic functionality, which meets these requirements by default. - -## [1. Maintain security and integrity](1-maintain-security-and-integrity) - -You must guarantee the security and integrity of the information your app creates, collects, and sends to your server by securing your requests and buffering information locally. - -### [Secure app-to-server requests](secure-app-to-server-requests) - -Mobile app install fraud is prevalent within the mobile industry and has cost marketers billions of dollars. The first essential step to defending against mobile app install fraud is to secure your app-to-server requests. If you can't guarantee your data’s security, Adjust can't know whether the information it receives from your server is legitimate or not. This leaves you vulnerable to fraudulent data within your reporting. - -If you don't meet this requirement, you are susceptible to spoofed installs in your reporting and expenditure, which can negatively impact your ad budget. If you can't guarantee the security of your requests, an s2s integration is discouraged. - -#### [How Adjust secures installs](how-adjust-secures-installs) - -Adjust secures every install through the [Adjust SDK Signature](https://help.adjust.com/en/article/sdk-signature). This signature is a cryptographic hash secured with an [App Secret](https://help.adjust.com/en/article/sdk-signature#what-is-an-app-secret), which is implemented into the Adjust SDK and sent with every reported install. Adjust verifies this hash on every install and denies attribution to any traffic that can't be verified. - -### [Buffer information locally](buffer-information-locally) - -Users might open your app for the first time (counted as an **install**) or trigger sessions while their device is offline. Accurate attribution is impossible if offline activity never reaches Adjust's servers. - -If you don’t meet this requirement any short outages such as 4G handovers, or longer periods without network or WiFi coverage will result in data loss. In total, 10–20% of installs don't reach Adjust upon first attempt. If Adjust doesn't receive this data, attribution is performed based on the current data, rather than what actually occurred. - -#### [How Adjust buffers information](how-adjust-buffers-information) - -The Adjust SDK places all in-app activity in a queue, so it can send the data to Adjust's servers when a connection is available. - -## [2. Collect and create advertising and device IDs](2-collect-and-create-advertising-and-device-ids) - -Android’s Google Play Store advertising ID (GPS_ADID) and iOS’s ID for advertisers (IDFA) are both [advertising IDs](https://help.adjust.com/en/article/device-identifiers#advertising-ids). The device user can reset - or disable access to - both of these IDs. Therefore, Adjust also relies on device IDs and Universally unique identifiers (UUIDs) (iOS only) for attribution and session measurement. Both of these IDs can't be reset by the end user without resetting their device. - -### [Gather every possible advertising and device ID](gather-every-possible-advertising-and-device-id) - -Advertising IDs are resettable. Deliberate, repeated resetting of advertising IDs is common (for example: to cheat in-app reward systems). Also, around 15% of users on iOS have Limit Ad Tracking (LAT) enabled, thereby disabling access to their IDFA. For these reasons, Adjust relies on additional IDs to accurately attribute and continuously record in-app sessions. - -If you don’t meet this requirement every session recorded without a previously recorded advertising ID or without an advertising ID (all LAT-enabled users on iOS) will be attributed as a new install. - -#### [How Adjust collects IDs](how-adjust-collects-ids) - -The Adjust SDK collects every legally available advertising and device ID by default. Adjust maps these IDs, so, if one is reset, the new ID can be mapped to other IDs already held the system for that user. - -### [Generate a universally unique identifier and persist it to the device keychain (iOS)](generate-a-universally-unique-identifier-and-persist-it-to-the-device-keychain-ios) - -When users reset their advertising ID, uninstall and reinstall your app, or enable LAT, Adjust won't be able to retrieve their IDFA and/or IDFV. To continuously record users’ in-app sessions, Adjust relies on a permanent, locally generated UUID persisted to the device keychain. Adjust maps the UUID to other device information. This allows Adjust to seamlessly measure the user’s in-app activity when: - -- A user enables LAT -- A user resets their advertising ID -- Adjust doesn't receive the original advertising ID and/or ID for vendors (IDFV) on iOS - -If you don’t meet this requirement any reporting is likely to include installs originating from device farms, where advertising IDs are continuously reset to simulate fresh installs. Any user who enables LAT will be attributed as a new install upon each session. - -#### [How Adjust manages UUIDs](how-adjust-manages-uuids) - -Adjust generates a UUID upon install. This is mapped to other device information in Adjust's systems. - -## [3. Third-party integrations and additional data](3-third-party-integrations-and-additional-data) - -Critical information required for attribution to Apple Search Ads, the Google Play Store and third-party app stores (for example: Amazon Appstore) can only be collected within your app through third-party integrations. - -You must support the following: - -### [Android](android) - -- [Google Play Store referrer API](https://developer.android.com/google/play/installreferrer/igetinstallreferrerservice.html). -- Collection of the [instruction set for Dalvik VM detected through inflection](https://android.googlesource.com/platform/libcore/+/master/libart/src/main/java/dalvik/system/VMRuntime.java#109). -- [Deep link reattribution](https://developer.android.com/training/app-links/deep-linking.html). - -### [iOS](ios) - -- [Apple Search Ads attribution API](https://searchads.apple.com/help/reporting/0028-apple-ads-attribution-api). -- [Deep link reattribution](https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html). - -This information, collected within your app, must also be forwarded to Adjust immediately upon receipt to be considered for attribution. - -### [Collect the necessary data for attribution across all sources](collect-the-necessary-data-for-attribution-across-all-sources) - -Adjust relies on the information sent through these integrations for accurate and comprehensive attribution and deeplink reattribution. - -Without the Google Play Store referrer, Adjust will be unable to attribute: - -- Over 50% of Android installs -- All Google organic search installs -- Any third-party app store installs - -Additionally, [click injection filtering](https://help.adjust.com/en/article/click-injection-filtering) won't work. This will leave you vulnerable to a major source of mobile ad fraud on Android. Furthermore, without the instruction set for Dalvik VM, Adjust won't know if installs originate from virtual devices. - -Without the Apple Search Ads attribution API, Adjust will be unable to attribute: - -- Apple Search Ads installs - -#### [How Adjust collects the necessary attribution data](how-adjust-collects-the-necessary-attribution-data) - -The code required for these integrations is available in the Adjust SDK guides for [Android](/en/sdk/android) and [iOS](/en/sdk/ios). You can also enable deep link reattribution with the Adjust SDK by following the steps in the deep linking guides for [Android](/en/sdk/android/features/deep-links) and [iOS](/en/sdk/ios/features/deep-links). - -Collection of the instruction set for Dalvik VM occurs automatically when the Adjust Android SDK is added to your app. diff --git a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx index 83818fb50..0d068f1ef 100644 --- a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx +++ b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx @@ -2,7 +2,7 @@ title: "S2S developer guide" description: "Implement Adjust 100% S2S" slug: en/api/s2s-api/s2s-developer-guide -sidebar-position: 7 +sidebar-position: 6 --- Adjust offers a server-to-server (S2S) interface for mobile, console, and Connected TV (CTV) apps. If you choose to implement Adjust through S2S, you need to modify your app to replicate the Adjust SDK's functions. This guide provides step-by-step instructions for the following: @@ -32,20 +32,20 @@ S2S session measurement is enabled automatically for [CTV measurement](https://h -Adjust's servers reject any requests sent without the correct token. This ensures robust protection for your data. +The Adjust backend rejects any requests without the correct token. This ensures robust protection for your data. ### [Queue and persist events locally](queue-and-persist-events-locally) -Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, you must capture and store these events locally until they can be successfully transmitted to Adjust's servers. +Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, you must capture and store these events locally until they can be successfully transmitted to the Adjust backend. To implement a local event queue with persistence: 1. Create a queue to store activities when they occur. 2. For each activity, include a `created_at_unix` timestamp in seconds (for example: `1484085154`) representing when the event occurred on the device. 3. Save this queue to local storage (for example: SQLite database or files) to persist across app restarts. -4. Attempt to send activities to Adjust's servers when the queue is non-empty and the device is online. +4. Attempt to send activities to the Adjust backend when the queue is non-empty and the device is online. 5. Remove activities from the queue only after successful transmission. This approach helps mitigate data loss in the following scenarios: @@ -87,8 +87,8 @@ The following parameters are required in each S2S request. | Parameter | Description | | ----------- | ----------------------------------------------------------------------- | | `s2s` | Indicates that the request is an S2S request. Must be hardcoded to `1`. | -| `os_name` | The name of the mobile operating system. See the list of options below. | | `app_token` | Your Adjust app token. | +| `os_name` | The name of the mobile operating system. See the list of options below. | @@ -122,7 +122,7 @@ The following parameters are required in each S2S request. -You can store these parameters as a variable in your app to re-use them whenever a request is sent. +Create these parameters on your server. They will be used for all S2S requests to Adjust, regardless of the device. Additional device-specific parameters will be added as needed. For simplicity, this guide demonstrates all parameter handling on the client side, though in practice, much of this will occur server-side. @@ -134,13 +134,30 @@ var params: [String: String] = [:] // Hard-coded params["s2s"] = "1" -// The name of the operating system running on your device +// The name of the operating system running on the device params["os_name"] = "ios" // Replace with your Adjust app token params["app_token"] = "4w565xzmb54d" ``` + + + +```objc +// Create dictionary for params to include on all S2S requests to Adjust +NSMutableDictionary *params = [NSMutableDictionary dictionary]; + +// Hard-coded +params[@"s2s"] = @"1"; + +// The name of the operating system running on the device +params[@"os_name"] = @"ios"; + +// Replace with your Adjust app token +params[@"app_token"] = @"4w565xzmb54d"; +``` + @@ -151,13 +168,30 @@ val params = mutableMapOf() // Hard-coded params["s2s"] = "1" -// "ios" or "android" -params["os_name"] = "ios" +// The name of the operating system running on the device +params["os_name"] = "android" // Replace with your Adjust app token params["app_token"] = "4w565xzmb54d" ``` + + + +```java +// Create map for params to include on all S2S requests to Adjust +Map params = new HashMap<>(); + +// Hard-coded +params.put("s2s", "1"); + +// The name of the operating system running on the device +params.put("os_name", "android"); + +// Replace with your Adjust app token +params.put("app_token", "4w565xzmb54d"); +``` + @@ -192,6 +226,9 @@ ATT has the following requirements: The following code example addresses all of these requirements: + + + ```swift import AppTrackingTransparency import AdSupport @@ -277,10 +314,119 @@ getIDFAInfo { info in } ``` + + + +```objc +#import +#import +#import + +@interface IDFAInfo : NSObject +@property (nonatomic, strong) NSUUID *idfa; +@property (nonatomic, assign) ATTrackingManagerAuthorizationStatus attStatus; +@property (nonatomic, strong) NSNumber *trackingEnabled; +@end + +@implementation IDFAInfo +@end + +void getIDFAInfo(void (^completion)(IDFAInfo *)) { + // Show ATT prompt to get IDFA and updated ATT status + if (@available(iOS 14.5, *)) { + [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler: + ^(ATTrackingManagerAuthorizationStatus status) { + dispatch_async(dispatch_get_main_queue(), ^{ + IDFAInfo *info = [[IDFAInfo alloc] init]; + info.attStatus = status; + if (status == ATTrackingManagerAuthorizationStatusAuthorized) { + info.idfa = [[ASIdentifierManager sharedManager] + advertisingIdentifier]; + } + completion(info); + }); + }]; + // Don't show ATT prompt. Just get IDFA and tracking status. + } else { + ASIdentifierManager *manager = [ASIdentifierManager sharedManager]; + IDFAInfo *info = [[IDFAInfo alloc] init]; + info.trackingEnabled = @(manager.isAdvertisingTrackingEnabled); + if (manager.isAdvertisingTrackingEnabled) { + info.idfa = manager.advertisingIdentifier; + } + completion(info); + } +} + +void requestTrackingAuthorization(void (^completion) + (ATTrackingManagerAuthorizationStatus)) { + ATTrackingManagerAuthorizationStatus trackingStatus = + ATTrackingManager.trackingAuthorizationStatus; + + switch (trackingStatus) { + // Only show ATT prompt if status isn't determined + case ATTrackingManagerAuthorizationStatusNotDetermined: + // If app state is active, show ATT prompt + if ([UIApplication sharedApplication].applicationState == + UIApplicationStateActive) { + [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler: + ^(ATTrackingManagerAuthorizationStatus status) { + completion(status); + }]; + // Wait until app state is active, then show ATT prompt + } else { + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *note) { + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:UIApplicationDidBecomeActiveNotification + object:nil]; + [ATTrackingManager + requestTrackingAuthorizationWithCompletionHandler: + ^(ATTrackingManagerAuthorizationStatus status) { + completion(status); + }]; + }]; + } + break; + // For all other statuses, return existing status + default: + completion(trackingStatus); + break; + } +} + +// Usage example +getIDFAInfo(^(IDFAInfo *info) { + // Include IDFA if available + if (info.idfa) { + params[@"idfa"] = [info.idfa UUIDString]; + } + + // Include either ATT status or tracking status, never both + if (info.attStatus != nil) { + params[@"att_status"] = [NSString stringWithFormat:@"%ld", + (long)info.attStatus]; + } else if (info.trackingEnabled != nil) { + params[@"tracking_enabled"] = [info.trackingEnabled boolValue] ? + @"1" : @"0"; + } +}); +``` + + + + ##### IDFV The ID for Vendors (IDFV) is a backup identifier available on all modern iOS devices. + + + ```swift let idfv: UUID? = UIDevice.current.identifierForVendor @@ -289,10 +435,27 @@ if let idfvString = idfv?.uuidString { } ``` + + + +```objc +NSUUID *idfv = [[UIDevice currentDevice] identifierForVendor]; + +if (idfv) { + params[@"idfv"] = [idfv UUIDString]; +} +``` + + + + ##### Primary deduplication token To consistently measure app activities across uninstalls and reinstalls, generate a random version 4 UUID (the "primary deduplication token") and save it in the iOS keychain. The primary deduplication token is a backup identifier that you should generate for all devices. + + + ```swift import Foundation import Security @@ -370,6 +533,95 @@ let primaryDedupeToken = getOrCreatePrimaryDedupeToken() params["primary_dedupe_token"] = primaryDedupeToken.uuidString.lowercased() ``` + + + +```objc +#import +#import + +// App's bundle ID +NSString *const bundleId = @"com.example.app"; + +// Collect the primary dedupe token from the keychain +NSUUID *getPrimaryDedupeToken(NSString *bundleId) { + // Define the query to search for the token in the keychain + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrAccount: @"primary_dedupe_token", + (__bridge id)kSecAttrService: bundleId, + (__bridge id)kSecReturnData: @YES + }; + + CFTypeRef item = NULL; + // Attempt to fetch the token from the keychain + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, + &item); + + // If the fetch was successful, convert the result to a UUID + if (status == errSecSuccess && item != NULL) { + NSData *existingItem = (__bridge_transfer NSData *)item; + NSString *uuidString = [[NSString alloc] + initWithData:existingItem encoding:NSUTF8StringEncoding]; + return [[NSUUID alloc] initWithUUIDString:uuidString]; + } + + // Return nil if the token doesn't exist or couldn't be collected + return nil; +} + +// Save the primary dedupe token to the keychain +BOOL setPrimaryDedupeToken(NSUUID *token, NSString *bundleId) { + NSData *tokenData = [[token UUIDString] + dataUsingEncoding:NSUTF8StringEncoding]; + // Define the attributes for storing the token in the keychain + NSDictionary *query = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrAccount: @"primary_dedupe_token", + (__bridge id)kSecAttrService: bundleId, + (__bridge id)kSecValueData: tokenData, + (__bridge id)kSecAttrAccessible: + (__bridge id)kSecAttrAccessibleAfterFirstUnlock + }; + + // Attempt to add the token to the keychain + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); + // Return YES if the token was successfully added, NO otherwise + return status == errSecSuccess; +} + +// Collect the existing primary dedupe token or create a new one if it doesn't exist +NSUUID *getOrCreatePrimaryDedupeToken(void) { + // Try to collect an existing token + NSUUID *existingToken = getPrimaryDedupeToken(bundleId); + if (existingToken) { + return existingToken; + } else { + // If no token exists, generate a new one + NSUUID *newToken = [NSUUID UUID]; + // Attempt to save the new token + if (setPrimaryDedupeToken(newToken, bundleId)) { + return newToken; + } else { + // If saving fails, throw an exception + @throw [NSException exceptionWithName:@"TokenSaveError" + reason:@"Failed to save primary dedupe token" + userInfo:nil]; + } + } +} + +// Usage example +NSUUID *primaryDedupeToken = getOrCreatePrimaryDedupeToken(); + +// Convert to lowercase string +params[@"primary_dedupe_token"] = [[primaryDedupeToken UUIDString] + lowercaseString]; +``` + + + + #### [Google Play device IDs (Android)](google-play-device-ids-android) ##### Google Advertising ID @@ -446,21 +698,23 @@ dependencies { 4. Implement the code to collect Google Advertising ID and tracking status: + + + ```kotlin import com.google.android.gms.ads.identifier.AdvertisingIdClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext - data class GPSAdInfo(val gpsAdid: String?, val isTrackingEnabled: Boolean) - - suspend fun getGoogleAdvertisingIdInfo(context: Context):GPSAdInfo? { + suspend fun getGoogleAdvertisingIdInfo( + context: Context): AdvertisingIdClient.Info? { return withContext(Dispatchers.IO) { try { - val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context) - val gpsAdid = if (adInfo.isLimitAdTrackingEnabled) null else adInfo.idGPSAdInfo(gpsAdid, !adInfo.isLimitAdTrackingEnabled) + AdvertisingIdClient.getAdvertisingIdInfo(context) } catch (e: Exception) { - // Handle exceptions (for example: Google Play Services not available) - null + // Handle exceptions + // (e.g., Google Play Services not available) + null } } } @@ -469,15 +723,22 @@ dependencies { // As getGoogleAdvertisingIdInfo is a suspending function, // it should be called from within a coroutine scope. lifecycleScope.launch { - val adInfo = getGoogleAdvertisingIdInfo(context) adInfo?.let { info -> - // Include Google Advertising ID if available - info.gpsAdid?.let { params["gps_adid"] = it } - // Include tracking status if available - info.isTrackingEnabled?.let { params["tracking_enabled"] = if (it) "1" else "0" } + val adInfo = getGoogleAdvertisingIdInfo(context) + adInfo?.let { info -> + // Include Google Advertising ID if tracking is not limited + if (!info.isLimitAdTrackingEnabled) { + params["gps_adid"] = info.id + } + // Set tracking status + params["tracking_enabled"] = if (info.isLimitAdTrackingEnabled) + "0" else "1" } } ``` + + + ##### App Set ID App Set ID is a backup identifier available on all Android devices with Google Play Services installed and running API Level 30 (Android 11) or later. @@ -521,6 +782,9 @@ dependencies { 2. Implement the code to collect App Set ID: + + + ```kotlin import com.google.android.gms.appset.AppSet import com.google.android.gms.appset.AppSetIdClient @@ -536,7 +800,7 @@ dependencies { val taskResult = Tasks.await(client.appSetIdInfo) taskResult.id } catch (e: Exception) { - // Handle exceptions (for example: Google Play Services not available) + // Handle exceptions (e.g., Google Play Services not available) null } } @@ -554,6 +818,9 @@ dependencies { } ``` + + + ## Additional parameters These parameters aren't required. If you use any of these parameters, you should include them in all requests. @@ -742,17 +1009,9 @@ params["callback_params"] = '{"user_id": "2696775149", "user_category": "high va -## Requests - - - -Ensure all parameter values are URL encoded before sending requests. - - - ### Global partner parameters -When integrating with certain partners, you may need to include "global partner parameters" in all your S2S requests. Adjust's servers pass these parameters in all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. +When integrating with certain partners, you may need to include "global partner parameters" in all your S2S requests. The Adjust backend passes these parameters in all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. Global partner parameters are represented as a JSON object containing string key-value pairs. @@ -760,22 +1019,30 @@ Global partner parameters are represented as a JSON object containing string key ```swift -params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM"}' +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_visitor_id": "nzFC9LKSqM"}' ``` ```kotlin -params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_session_id": "nzFC9LKSqM"}' +params["partner_params"] = '{"analytics_user_id": "3913132433", "analytics_visitor_id": "nzFC9LKSqM"}' ``` +## Requests + + + +Ensure all parameter values are URL encoded before sending requests. + + + ### Session -Sessions form the foundation of Adjust implementation and are the only required activity. A session typically represents an app open. The Adjust server logs successful session requests as follows: +Sessions form the foundation of Adjust implementation and are the only required activity. A session typically represents an app open. The Adjust backend logs successful session requests as follows: - It records the first session for a device as an "install" activity. - It records subsequent sessions as "session" activities. @@ -783,14 +1050,14 @@ Sessions form the foundation of Adjust implementation and are the only required - For reference, the Adjust SDK sends a session request to Adjust server when either of the following occurs: + For reference, the Adjust SDK sends a session request to the Adjust backend when either of the following occurs: - The user opens the app for the first time after install or reinstall. - The user reopens the app after it has been closed or in the background for at least 30 minutes. - When sending S2S session requests with the `created_at_unix` parameter, the Adjust server requires this value to be at least 20 minutes later than the `created_at_unix` time of the last successfully logged session. + When sending S2S session requests with the `created_at_unix` parameter, the Adjust backend requires this value to be at least 20 minutes later than the `created_at_unix` time of the last successfully logged session. @@ -856,7 +1123,7 @@ HTTP Status Code: 200 ### Attribution -After sending a session request, you may need to send an attribution request to the Adjust server. The session response includes an `ask_in` parameter, indicating how many milliseconds to wait before making the attribution request. +After sending a session request, you may need to send an attribution request to the Adjust backend. The session response includes an `ask_in` parameter, indicating how many milliseconds to wait before making the attribution request. Attribution requests serve three main purposes for developers: @@ -925,7 +1192,7 @@ userDefaultsManager.firstSessionRecorded = true import android.content.Context import android.content.SharedPreferences -/*_ Helps to manage app-wide persistent settings and states */ +/* Helps to manage app-wide persistent settings and states */ class SharedPreferencesManager private constructor(context: Context) { private val sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -1026,7 +1293,7 @@ Below is an example attribution response that contains a deferred deep link. -Please note that sometimes an attribution response may also contain an `ask_in` parameter. This means that the Adjust server hasn't yet completed the attribution process, and you should send another attribution request after the `ask_in` time elapses. +Please note that sometimes an attribution response may also contain an `ask_in` parameter. This means that the Adjust backend hasn't yet completed the attribution process, and you should send another attribution request after the `ask_in` time elapses. If the attribution response contains a deferred deep link, here is a suggested approach to handle it: @@ -1035,9 +1302,9 @@ If the attribution response contains a deferred deep link, here is a suggested a 2. When ready to process the deep link, do the following: - Retrieve and parse the stored link. - Implement domain-agnostic handling to treat all links equivalently: - - Adjust universal links (for example: `example.go.link`) - - Other universal links (for example: `example.com`) - - App scheme deep links (for example: `example://`) + - Adjust universal links (e.g., `example.go.link`) + - Other universal links (e.g., `example.com`) + - App scheme deep links (e.g., `example://`) - Extract relevant information (path, query parameters, fragment) regardless of link type. - Navigate to the deep link screen based on the extracted information. @@ -1049,6 +1316,12 @@ Example equivalence: These should all lead to the same destination in your app. + + +The Adjust backend returns all deferred deep links in app scheme format (e.g., `example://`), regardless of their original format. + + + Here's a reference implementation demonstrating these concepts: @@ -1121,30 +1394,37 @@ let userDefaultsManager = UserDefaultsManager.shared /// Handles the logic for processing and navigating to deep links class DeeplinkHandler { -static func handleDeeplink(\_ incomingLink: URL, navigationController: UINavigationController?) { -// Extract path, query items, and fragment from the link -let components = URLComponents(url: incomingLink, resolvingAgainstBaseURL: true) -let path = components?.path ?? "" -let queryItems = components?.queryItems -let fragment = components?.fragment + static func handleDeeplink(_ incomingLink: URL, + navigationController: UINavigationController?) { + // Extract path, query items, and fragment from the link + let components = URLComponents(url: incomingLink, + resolvingAgainstBaseURL: true) + let path = components?.path ?? "" + let queryItems = components?.queryItems + let fragment = components?.fragment // Implement navigation or other app-specific // logic based on the deep link components. DispatchQueue.main.async { if path == "/summer-clothes" { - let promoCode = queryItems?.first(where: { $0.name == "promo" })?.value - let summerClothesVC = SummerClothesViewController(promoCode: promoCode) - navigationController?.pushViewController(summerClothesVC, animated: true) + let promoCode = queryItems?.first(where: { + $0.name == "promo" + })?.value + let summerClothesVC = SummerClothesViewController( + promoCode: promoCode) + navigationController?.pushViewController( + summerClothesVC, animated: true) } } } } -// Usage example when receiving the attribution response: +// Usage example when receiving the attribution response -if let deeplink = attributionResponse["attribution"]["deeplink"] as? String { -UserDefaultsManager.shared.setDeferredLink(deeplink) +if let deeplink = attributionResponse["attribution"]["deeplink"] + as? String { + UserDefaultsManager.shared.setDeferredLink(deeplink) } ``` @@ -1159,7 +1439,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity -/\*_ Helps to manage app-wide persistent settings and states _/ +/* Helps to manage app-wide persistent settings and states / class SharedPreferencesManager private constructor(context: Context) { private val sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) @@ -1208,7 +1488,7 @@ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) } -/*_ Demonstrates how to handle onboarding and deferred deep links */ +/* Demonstrates how to handle onboarding and deferred deep links */ class MainActivity : AppCompatActivity() { private lateinit var sharedPreferencesManager: SharedPreferencesManager @@ -1338,7 +1618,7 @@ params["callback_params"] = '{"txn_id": "8837853376"}' #### Partner parameters -When integrating with certain partners, you may need to include custom "partner parameters" in your event requests. The Adjust server will then include these parameters on the callbacks it makes to partners for relevant events. This is most commonly used to enable dynamic remarketing campaigns, typically for events like view_item, add_to_cart, and purchase. +When integrating with certain partners, you may need to include custom "partner parameters" in your event requests. The Adjust backend will then include these parameters on the callbacks it makes to partners for relevant events. This is most commonly used to enable dynamic remarketing campaigns, typically for events like view_item, add_to_cart, and purchase. Partner parameters are represented as a JSON object containing string key-value pairs. @@ -1410,3 +1690,5 @@ HTTP Status Code: 200 ``` +import { Code } from "astro-expressive-code/components" +import { link } from "fs" diff --git a/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx b/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx index 96de834c4..ac28756ea 100644 --- a/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx +++ b/src/content/docs/api/s2s-api/sdk-to-s2s-events.mdx @@ -2,7 +2,7 @@ title: "Transition SDK to S2S events" description: "You can change your setup to stop sending Adjust SDK events and instead send server-to-server (S2S) events." slug: en/api/s2s-api/sdk-to-s2s-events -sidebar-position: 6 +sidebar-position: 5 sidebar-label: Transition SDK to S2S events --- From 103ce0ca79cfba6ec6d69e4185a117e9f3a4ad26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Mon, 29 Jul 2024 11:21:13 +0200 Subject: [PATCH 5/5] Fix build issues and style guide issues --- .../docs/api/s2s-api/s2s-developer-guide.mdx | 152 +++++++++--------- 1 file changed, 75 insertions(+), 77 deletions(-) diff --git a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx index 0d068f1ef..f9c6d8301 100644 --- a/src/content/docs/api/s2s-api/s2s-developer-guide.mdx +++ b/src/content/docs/api/s2s-api/s2s-developer-guide.mdx @@ -32,20 +32,20 @@ S2S session measurement is enabled automatically for [CTV measurement](https://h -The Adjust backend rejects any requests without the correct token. This ensures robust protection for your data. +Adjust's servers reject any requests without the correct token. This ensures robust protection for your data. ### [Queue and persist events locally](queue-and-persist-events-locally) -Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, you must capture and store these events locally until they can be successfully transmitted to the Adjust backend. +Users may trigger important events, such as app installs or sessions, while their device is offline. To ensure accurate attribution, you must capture and store these events locally until they can be successfully transmitted to Adjust's servers. To implement a local event queue with persistence: 1. Create a queue to store activities when they occur. 2. For each activity, include a `created_at_unix` timestamp in seconds (for example: `1484085154`) representing when the event occurred on the device. 3. Save this queue to local storage (for example: SQLite database or files) to persist across app restarts. -4. Attempt to send activities to the Adjust backend when the queue is non-empty and the device is online. +4. Attempt to send activities to Adjust's servers when the queue is non-empty and the device is online. 5. Remove activities from the queue only after successful transmission. This approach helps mitigate data loss in the following scenarios: @@ -698,45 +698,45 @@ dependencies { 4. Implement the code to collect Google Advertising ID and tracking status: - + - ```kotlin - import com.google.android.gms.ads.identifier.AdvertisingIdClient - import kotlinx.coroutines.Dispatchers - import kotlinx.coroutines.withContext - - suspend fun getGoogleAdvertisingIdInfo( - context: Context): AdvertisingIdClient.Info? { - return withContext(Dispatchers.IO) { - try { - AdvertisingIdClient.getAdvertisingIdInfo(context) - } catch (e: Exception) { - // Handle exceptions - // (e.g., Google Play Services not available) - null - } +```kotlin +import com.google.android.gms.ads.identifier.AdvertisingIdClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun getGoogleAdvertisingIdInfo( + context: Context): AdvertisingIdClient.Info? { + return withContext(Dispatchers.IO) { + try { + AdvertisingIdClient.getAdvertisingIdInfo(context) + } catch (e: Exception) { + // Handle exceptions + // (for example: Google Play Services not available) + null } } +} - // Usage example - // As getGoogleAdvertisingIdInfo is a suspending function, - // it should be called from within a coroutine scope. - lifecycleScope.launch { - val adInfo = getGoogleAdvertisingIdInfo(context) - adInfo?.let { info -> - // Include Google Advertising ID if tracking is not limited - if (!info.isLimitAdTrackingEnabled) { - params["gps_adid"] = info.id - } - // Set tracking status - params["tracking_enabled"] = if (info.isLimitAdTrackingEnabled) - "0" else "1" +// Usage example +// As getGoogleAdvertisingIdInfo is a suspending function, +// it should be called from within a coroutine scope. +lifecycleScope.launch { + val adInfo = getGoogleAdvertisingIdInfo(context) + adInfo?.let { info -> + // Include Google Advertising ID if tracking is not limited + if (!info.isLimitAdTrackingEnabled) { + params["gps_adid"] = info.id } + // Set tracking status + params["tracking_enabled"] = if (info.isLimitAdTrackingEnabled) + "0" else "1" } - ``` +} +``` - + ##### App Set ID @@ -782,43 +782,43 @@ dependencies { 2. Implement the code to collect App Set ID: - + - ```kotlin - import com.google.android.gms.appset.AppSet - import com.google.android.gms.appset.AppSetIdClient - import com.google.android.gms.appset.AppSetIdInfo - import com.google.android.gms.tasks.Tasks - import kotlinx.coroutines.Dispatchers - import kotlinx.coroutines.withContext - - suspend fun getAppSetId(context: Context): String? { - return withContext(Dispatchers.IO) { - try { - val client: AppSetIdClient = AppSet.getClient(context) - val taskResult = Tasks.await(client.appSetIdInfo) - taskResult.id - } catch (e: Exception) { - // Handle exceptions (e.g., Google Play Services not available) - null - } +```kotlin +import com.google.android.gms.appset.AppSet +import com.google.android.gms.appset.AppSetIdClient +import com.google.android.gms.appset.AppSetIdInfo +import com.google.android.gms.tasks.Tasks +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun getAppSetId(context: Context): String? { + return withContext(Dispatchers.IO) { + try { + val client: AppSetIdClient = AppSet.getClient(context) + val taskResult = Tasks.await(client.appSetIdInfo) + taskResult.id + } catch (e: Exception) { + // Handle exceptions (for example: Google Play Services not available) + null } } +} - // Usage example - // As getAppSetId is a suspending function, - // it should be called from within a coroutine scope. - lifecycleScope.launch { - val appSetId = getAppSetId(context) - appSetId?.let { id -> - val params = mutableMapOf() - params["google_app_set_id"] = id - } +// Usage example +// As getAppSetId is a suspending function, +// it should be called from within a coroutine scope. +lifecycleScope.launch { + val appSetId = getAppSetId(context) + appSetId?.let { id -> + val params = mutableMapOf() + params["google_app_set_id"] = id } - ``` +} +``` - + ## Additional parameters @@ -1011,7 +1011,7 @@ params["callback_params"] = '{"user_id": "2696775149", "user_category": "high va ### Global partner parameters -When integrating with certain partners, you may need to include "global partner parameters" in all your S2S requests. The Adjust backend passes these parameters in all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. +When integrating with certain partners, you may need to include "global partner parameters" in all your S2S requests. Adjust's servers passes these parameters in all callbacks it makes to partners. This is commonly used for analytics partners that require their own proprietary user ID in the callbacks they receive. Global partner parameters are represented as a JSON object containing string key-value pairs. @@ -1042,7 +1042,7 @@ Ensure all parameter values are URL encoded before sending requests. ### Session -Sessions form the foundation of Adjust implementation and are the only required activity. A session typically represents an app open. The Adjust backend logs successful session requests as follows: +Sessions form the foundation of Adjust implementation and are the only required activity. A session typically represents an app open. Adjust's servers log successful session requests as follows: - It records the first session for a device as an "install" activity. - It records subsequent sessions as "session" activities. @@ -1050,14 +1050,14 @@ Sessions form the foundation of Adjust implementation and are the only required - For reference, the Adjust SDK sends a session request to the Adjust backend when either of the following occurs: + For reference, the Adjust SDK sends a session request to Adjust's servers when either of the following occurs: - The user opens the app for the first time after install or reinstall. - The user reopens the app after it has been closed or in the background for at least 30 minutes. - When sending S2S session requests with the `created_at_unix` parameter, the Adjust backend requires this value to be at least 20 minutes later than the `created_at_unix` time of the last successfully logged session. + When sending S2S session requests with the `created_at_unix` parameter, Adjust's servers require this value to be at least 20 minutes later than the `created_at_unix` time of the last successfully logged session. @@ -1123,7 +1123,7 @@ HTTP Status Code: 200 ### Attribution -After sending a session request, you may need to send an attribution request to the Adjust backend. The session response includes an `ask_in` parameter, indicating how many milliseconds to wait before making the attribution request. +After sending a session request, you may need to send an attribution request to Adjust's servers. The session response includes an `ask_in` parameter, indicating how many milliseconds to wait before making the attribution request. Attribution requests serve three main purposes for developers: @@ -1293,7 +1293,7 @@ Below is an example attribution response that contains a deferred deep link. -Please note that sometimes an attribution response may also contain an `ask_in` parameter. This means that the Adjust backend hasn't yet completed the attribution process, and you should send another attribution request after the `ask_in` time elapses. +Please note that sometimes an attribution response may also contain an `ask_in` parameter. This means that Adjust's servers haven't yet completed the attribution process, and you should send another attribution request after the `ask_in` time elapses. If the attribution response contains a deferred deep link, here is a suggested approach to handle it: @@ -1302,9 +1302,9 @@ If the attribution response contains a deferred deep link, here is a suggested a 2. When ready to process the deep link, do the following: - Retrieve and parse the stored link. - Implement domain-agnostic handling to treat all links equivalently: - - Adjust universal links (e.g., `example.go.link`) - - Other universal links (e.g., `example.com`) - - App scheme deep links (e.g., `example://`) + - Adjust universal links (for example: `example.go.link`) + - Other universal links (for example: `example.com`) + - App scheme deep links (for example: `example://`) - Extract relevant information (path, query parameters, fragment) regardless of link type. - Navigate to the deep link screen based on the extracted information. @@ -1318,7 +1318,7 @@ These should all lead to the same destination in your app. -The Adjust backend returns all deferred deep links in app scheme format (e.g., `example://`), regardless of their original format. +Adjust's servers returns all deferred deep links in app scheme format (for example: `example://`), regardless of their original format. @@ -1618,7 +1618,7 @@ params["callback_params"] = '{"txn_id": "8837853376"}' #### Partner parameters -When integrating with certain partners, you may need to include custom "partner parameters" in your event requests. The Adjust backend will then include these parameters on the callbacks it makes to partners for relevant events. This is most commonly used to enable dynamic remarketing campaigns, typically for events like view_item, add_to_cart, and purchase. +When integrating with certain partners, you may need to include custom "partner parameters" in your event requests. Adjust's servers will then include these parameters on the callbacks it makes to partners for relevant events. This is most commonly used to enable dynamic remarketing campaigns, typically for events like `view_item`, `add_to_cart`, and purchase. Partner parameters are represented as a JSON object containing string key-value pairs. @@ -1690,5 +1690,3 @@ HTTP Status Code: 200 ``` -import { Code } from "astro-expressive-code/components" -import { link } from "fs"