Skip to content

Analytics

The Deepdots Popup Native SDK includes a built-in analytics layer that collects behavioral data from your users and forwards it to a dedicated integration in your Deepdots workspace. It shares the event model and the backend channel with the Web SDK, so the dashboards read the same regardless of platform.

The helper methods (track, trackMessage, setMetric, …) have the same names on Android and iOS. The snippets below are in Kotlin; call the identical method from Swift with Swift syntax. Where the platforms differ — initialization, lifecycle, navigation — both are shown.

Pass an analytics object to InitOptions with the publicKey and integration id of the integration created in your Deepdots workspace. Without it the SDK runs in dry-run mode — every event payload is printed to the console (Logcat / Xcode) but nothing is sent.

val options = InitOptions(
popupOptions = PopupOptions(publicKey = "<your-public-key>"),
analytics = AnalyticsKeys(
publicKey = "<your-analytics-public-key>",
integration = "<your-integration-id>",
),
provideLang = { "en" },
metadata = mapOf("userId" to "customer-123"), // optional — identify the user
)
val sdk = DeepdotsPopups().apply { initialize(options) }
let options = InitOptions(
popupOptions: PopupOptions(id: nil, publicKey: "<your-public-key>", companyId: nil),
provideLang: { Locale.current.language.languageCode?.identifier ?? "en" },
analytics: AnalyticsKeys(
publicKey: "<your-analytics-public-key>",
integration: "<your-integration-id>"
),
metadata: ["userId": "customer-123"]
)
let instance = DeepdotsSDK.DeepdotsPopups()
instance.initialize(options: options)

The following data is collected with zero extra code once the SDK is initialized, tracking is enabled, and you wire the lifecycle and navigation hooks:

DataHowWhere it appears
Screen views (deepdots_page_view)setPath() on each navigationEvents
Active engagement time (deepdots_user_engagement)Foreground/background lifecycleEvents
Persistent user identity (user_id)Generated on first launch, stored in SharedPreferences (Android) / NSUserDefaults (iOS)Metadata
Device type, OS version, device model, app versionCollected from the platformContext
Language (deepdots_language)provideLang resolver, falling back to the platform localeContext

Each flush sends the accumulated events as a batch. The backend groups batches by session so you see a single timeline per visit, not one record per flush.


Unlike the browser, the native SDK cannot observe navigation or foreground/background transitions on its own. Two hooks must be wired by the host — without them there are no page_view events, no engagement time, and no session boundaries.

Call setPath(path) on every screen change. The first call begins navigation tracking; each later call closes the previous screen’s deepdots_page_view (with its duration) and opens the next:

sdk.setPath("/home")
sdk.setPath("/products/42") // closes "/home" with its duration, opens "/products/42"

The path also feeds popup route triggers and route-exit popups, so keep it current even if you only care about popups.

Connect the SDK to the app lifecycle so a session opens in the foreground and closes in the background.

// In your Activity / lifecycle observer:
override fun onStart() { super.onStart(); sdk.onForeground() }
override fun onStop() { super.onStop(); sdk.onBackground() }
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { _ in
instance.onForeground()
}
NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { _ in
instance.onBackground()
}

A session is one continuous visit. The backend owns the session id and stitches batches together by user_id. Both ends are signalled explicitly:

  • deepdots_session_start — on every session open: initialize(), returning to the foreground, granting consent with setTrackingEnabled(true), and after a user change.
  • deepdots_session_end — on close, with a reason. The closing batch is sent with completed: true, which is what tells the backend the record is finished.

The closing batch flushes everything still open, in order: the current screen’s deepdots_page_view, any pending deepdots_mini_service_exit, the accumulated deepdots_user_engagement, and finally deepdots_session_end.

reasonWhen
backgroundThe app goes to background (onBackground())
user_changesetUserId() switched the user
tracking_disabledsetTrackingEnabled(false)
manualendSession()

Closes the session explicitly — at logout or at the end of a self-contained flow. The next tracked event opens a new one.

sdk.endSession()

Reports a user change — login, logout, or account switch. It closes the previous user’s session with reason: user_change, swaps the identity, and opens a new session, so the two users never share a timeline:

sdk.setUserId("customer-123") // login
sdk.setUserId() // logout — back to the anonymous id

Use track(name, params?) to record any business event. Use lowercase snake_case names to stay consistent with the automatic events.

sdk.track("add_to_cart", mapOf("product_id" to "p-123", "value" to 49.9, "currency" to "EUR"))
sdk.track("checkout_started")

trackSearch records a query and its result count; the SDK derives has_results from the count.

sdk.trackSearch("running shoes", 0) // no results — has_results: false
sdk.trackSearch("t-shirt", 142) // has_results: true
sdk.trackFindabilityFriction("checkout_address")

Group related steps under the same funnel and taskId so the backend can compute conversion rates:

sdk.trackFunnelStep("onboarding", "account_created", "task-42")
sdk.trackFunnelStep("onboarding", "profile_completed", "task-42")

Record a meaningful interaction — a moment that signals the user got real value out of your app. interactionType is the grouping dimension, so keep a small, stable set of names (get_help, homepage, contact_support):

sdk.trackMeaningfulInteraction("get_help")
sdk.trackMeaningfulInteraction("homepage", mapOf("screen" to "/home"))

Each call emits a deepdots_meaningful_interaction event that powers the Effectiveness dashboard.


A mini-service is any bounded workflow inside your app (checkout, onboarding wizard, support chat). Signal the boundaries and the SDK tracks entry, exit, and duration:

sdk.enterMiniService("checkout", "home_banner")
// … user completes or abandons …
sdk.exitMiniService("checkout") // emits mini_service_exit with the duration

Multiple mini-services can be active at once; always close each one by name. Any survey shown while a mini-service is active receives a mini_service metadata tag, so you can filter CSAT by workflow context.


setUserAttributes attaches business-level dimensions to the user’s analytics context, included in every subsequent flush and cumulative across calls:

sdk.setUserAttributes(mapOf(
"plan" to "pro",
"registration_status" to "registered",
"sector" to "retail",
))

setContactAttributes sends the attributes to POST /sdk/popups/contact, creating or updating the user’s contact record. It is a suspend function and only fires when a userId was provided and tracking is enabled; it returns true if a POST was made, false if the attributes were unchanged (deduplication).

val sent = sdk.setContactAttributes(mapOf("language" to "en", "age" to 34, "plan" to "premium"))

You can also pass contactAttributes in InitOptions to fire the update on startup.


setMetric(key, value) records a measurable value — a quantity reported alongside the user’s context, such as cart value. Metrics land in a dedicated metrics field of the payload, separate from user attributes.

sdk.setMetric("cart_value", 49.99)
sdk.setMetric("items_in_cart", 3)
  • Persistent — re-sent on every flush until it changes.
  • Overwrites by key — the same key replaces the previous value.
  • Coerced to string on the wire.
  • Respects the kill-switch — a no-op while tracking is disabled.

Use attributes for the who (dimensions you group by) and metrics for the how much (quantities you measure).


Track the lifecycle of your app’s notifications (push and in-app) so Deepdots can measure delivery, click-through, and conversion. Call trackMessage at each stage of the funnel:

sdk.trackMessage("delivered", id = "msg-42", title = "Summer Sale", channel = "push", campaign = "summer_sale")
sdk.trackMessage("clicked", id = "msg-42", title = "Summer Sale", channel = "push")
sdk.trackMessage("converted", id = "msg-42", title = "Summer Sale", channel = "push", value = 49.9, currency = "EUR")
ArgumentTypeDescription
stage"delivered" / "clicked" / "converted"Stage of the message funnel
idStringCorrelates the stages of the same message
titleStringGrouping dimension for the Messaging metrics
channel"push" / "in_app"Delivery channel
campaignString?Campaign name (optional)
value / currencyDouble? / String?Conversion value (typical on converted)
paramsMap?Any extra key/value pairs

Each call emits one deepdots_message event; the backend groups by title to compute delivered counts, CTR, unique click-through users, conversion rate, and action users.

CTR and conversion rate are ratios over delivered, so the stages must line up:

  1. Send all three stages. delivered goes out when the message reaches the device, before the user opens it. Without it there is no denominator.
  2. Use the same id across the three stages. Unique per send, not per campaign.
  3. One id, one channel. A campaign sent both as push and in-app needs two different id values sharing the same campaign.
  4. One call per stage.

The SDK discards calls that break these rules instead of forwarding them, and warns on the console:

[DeepdotsPopups] trackMessage discarded (channel_conflict): message_id "msg-42" was already reported on channel "push"; discarding "in_app"
RuleWhat is discardedreason
channel must be push or in_appAny other valueinvalid_channel
Each (id, stage) pair is sent onceThe 2nd call to the same stage of the same messageduplicate_stage
An id keeps its channelEvents on a channel other than the first one seenchannel_conflict

The checks last for the session and are per device, tracking up to 500 message ids (oldest evicted first). If these warnings appear, they point at a real double-count — fix the call site.


Uncaught errors on the Kotlin side are captured automatically, persisted to storage, and replayed on the next launch — so the crash that ended a session still reaches Deepdots even though the process died before the next flush. They surface as deepdots_app_crash events and power the Stability metrics.

Report handled errors manually:

try {
checkout()
} catch (e: Throwable) {
sdk.reportError(e, severity = "error", context = mapOf("screen" to "Checkout", "order_id" to "o-42"))
}
ArgumentValuesDefault
severity"fatal" / "error" / "warning""error"
handledBooleantrue
contextfree-form map (prefixed ctx_ in the payload)

Crash reporting respects the same consent kill-switch as the rest of analytics.


Set trackingEnabled = false in InitOptions to start with all analytics and contact tracking disabled — useful when you need explicit consent first.

val options = InitOptions(
popupOptions = PopupOptions(publicKey = "<your-public-key>"),
trackingEnabled = false,
)
// Later, once the user gives consent:
sdk.setTrackingEnabled(true)

setTrackingEnabled(false) closes the current session with reason: tracking_disabled and suspends all outbound calls — data collected before the opt-out is still delivered, not dropped. setTrackingEnabled(true) resumes them, assigns a persistent user_id if none was stored, and opens a new session.


Inspect the current buffer without flushing, or force a flush (useful in development):

val preview = sdk.previewAnalytics() // the AnalyticsEnvelope that would be sent
sdk.flushAnalytics() // send now

Flushes also happen automatically (periodically in the foreground, on buffer size, and on onBackground()). The channel is hardened so the closing batch is not lost:

  • Retries transient failures — a network error or a 5xx / 408 / 429 puts the batch back at the front of the buffer, in chronological order, retried on the next flush.
  • Reports permanent failures — a 4xx (for example a 406 for an unknown Contact) is logged with its status and body, and the batch is discarded rather than failing silently.
  • Keeps one record per visit — until the backend returns a session id, batches are serialized instead of sent in parallel.