Skip to main content

Tracking

The Tracking module sends analytics events from your app and from the SDK itself. You choose one of two backends when you initialize it: Snowflake, where events land in a table owned by your data team, or Mixpanel. The rest of the API is the same for both.

info

Snowflake is the backend for new integrations. The Mixpanel backend stays available for apps that already use it; see Mixpanel at the end of this page.

Getting the Module​

let tracking = try manager.tracking()

All methods throw TrackingException. In KapeClientSDK and client-sdk the methods are blocking and carry the plain names, such as initializeSnowflake. In KapeClientSDKAsync and client-sdk-async the same methods are async and end in Async, such as initializeSnowflakeAsync. The examples on this page use the async names.

Initializing with Snowflake​

Call initialize_snowflake once, after the SdkManager exists and before the first event. Whichever initializer is called first, initialize_snowflake or initialize, decides the backend for the life of the process; later calls do nothing and succeed.

try await manager.tracking().initializeSnowflakeAsync(
config: SnowflakeConfiguration(
optOutTrackingByDefault: nil,
categories: nil,
bufferBytes: nil,
flushIntervalSeconds: nil,
maxQueuedBytes: nil
)
)

SnowflakeConfiguration​

Every field is optional and falls back to its default.

NameTypeDefaultDescription
optOutTrackingByDefaultBool?falseStart with tracking switched off. Events are then dropped until opt_in_tracking is called
categories[EventCategory]?allThe event categories the user allows. Events of other categories are dropped
bufferBytesUInt32?4 MiBSize of the queued events, before compression, that triggers an upload. Between 64 KiB and 4 MiB
flushIntervalSecondsUInt32?30Seconds after the last upload at which queued events are uploaded even if the buffer is not full. Not zero
maxQueuedBytesUInt32?20 MiBSize of the events that may wait on disk for upload, at least bufferBytes. Beyond it the oldest events are deleted to make room for new ones

initialize_snowflake throws SdkException.InitializeTracking (code 805) for a value out of range. Nothing is initialized then, so you can call it again with a corrected configuration.

What the SDK needs from your app​

Nothing beyond the SDK setup. The ClientDetails you set there are attached to every event, and the events wait for upload in a file next to the cache directory, so they survive a restart. No login is needed: events are sent for guests, signed-in users and users with no session alike.

Sending Events​

track_event​

suspend fun trackEventAsync(
eventName: String,
propertiesJson: String?,
category: EventCategory?
)
ParameterDescription
eventName1 to 128 characters after trimming
propertiesJsonA JSON object, or null for none. Stored as it is, nested values included
categoryAn event category, or null. An event without a category is never filtered out
try await manager.tracking().trackEventAsync(
eventName: "connection_established",
propertiesJson: "{\"protocol\":\"lightway\",\"attempt\":1}",
category: .usageMetrics
)

The call returns once the event is queued on disk; it never waits for the network. A successful call does not guarantee a row in Snowflake: the event is dropped, and the call still succeeds, while tracking is opted out, when its category is not permitted, or when collection has been switched off remotely for this SDK version, OS or app version. An event larger than 256 KiB when serialized is rejected with SdkException.TrackEventError (code 800), as is a name out of range or a propertiesJson that is not a JSON object.

warning

Property names end up as keys of a Snowflake VARIANT column, where an unquoted identifier must start with a letter or an underscore. Use lowercase snake_case names without a leading $, so that queries can read properties:protocol without quoting. Agree on event and property names with the data team before you ship them; renaming later means rewriting their queries.

Timed events​

time_event starts a timer for an event name. The next track_event with the same name carries a duration in seconds, measured on the SDK's clock, and stops the timer.

suspend fun timeEventAsync(eventName: String, category: EventCategory?)
suspend fun clearTimedEventAsync(eventName: String)
suspend fun clearTimedEventsAsync()
try await manager.tracking().timeEventAsync(eventName: "fetch_token", category: nil)
let token = try await manager.tokens().fetchTokenByTypeAsync(tokenType: .connectionAuthorizationToken, advancedSearchOptions: nil)
try await manager.tracking().trackEventAsync(
eventName: "fetch_token",
propertiesJson: "{\"token_type\":\"CAT\"}",
category: nil
)

Timers are not kept across a restart on the Snowflake backend. A timer that is never tracked stays until clear_timed_event, clear_timed_events or reset removes it.

Super properties​

Super properties are attached to every event until they are cleared. Register them as a JSON object; a later call adds to or overwrites the earlier ones.

fun registerSuperProperties(propertiesJson: String)
fun clearSuperProperties()
try manager.tracking().registerSuperProperties(propertiesJson: "{\"os\":\"iOS\",\"app_build\":\"1234\"}")

On the Snowflake backend super properties never mix with the event's own properties: they are stored beside them, so neither can overwrite the other. clear_super_properties removes what you registered and leaves what the SDK adds.

Event categories​

Categories let the user consent to some kinds of tracking and not others. The set given at initialization can be changed at any time with set_event_categories; an event whose category is not in the set is dropped before it is stored.

CategoryIntended for
ClusterMetricsServer-side aggregates; carries its own random id
QualityMetricsConnection quality
UsageMetricsFeature usage
ErrorMetricsErrors the user ran into
DebugMetricsDiagnostics
PerformanceMetricsPerformance and marketing
info

ClusterMetrics events are not attributed to the user. They carry a separate random id as their distinct_id, which reset replaces together with the user's id.

Opting in and out​

suspend fun optInTrackingAsync()
suspend fun optOutTrackingAsync()

While opted out, events are dropped instead of queued. opt_out_tracking uploads what is already queued, waits for that at most 10 seconds, then deletes whatever is left together with the timers. Super properties and the ids are kept, so opting in again continues under the same identity.

reset​

Replaces the user's distinct_id and the cluster-metrics id with fresh ones and discards the timers. Call it on logout. Events already queued keep the id they were tracked with and are still uploaded; the call never waits for the network.

flush​

Uploads everything queued and waits until it is delivered, or fails with SdkException.FlushTrackingEvents (code 804) when it cannot be. You do not need it in normal operation: the SDK uploads on its own every flushIntervalSeconds, or sooner when the buffer fills. It is meant for the moments before the process may be killed, such as an iOS extension being suspended.

get_distinct_id​

The current distinct_id, or null before the module is initialized. On the Snowflake backend it is a random UUID per install, or the distinct_id you registered as a super property.

Errors​

All methods throw TrackingException with an SdkException as its source:

CodeVariantThrown by
800TrackEventErrortrack_event: invalid name, properties that are not a JSON object, an event over 256 KiB, or the queue file cannot be written
801RegisterPropertiesFailedregister_super_properties: propertiesJson is not a JSON object
802TimeEventErrortime_event: the timer cannot be stored
803ClearTimedEventsclear_timed_event, clear_timed_events
804FlushTrackingEventsflush: the events could not be delivered
805InitializeTrackinginitialize_snowflake: a configuration value out of range, or the platform has no Snowflake backend (WASM)

Before the module is initialized every other method is a no-op that succeeds, so you can wire tracking calls through your app without guarding each one.

Mixpanel​

initialize starts the Mixpanel backend instead. Everything on this page applies, with these differences:

  • The SDK's own properties are merged into the event's properties, winning over yours, and country_code is sent as mp_country_code.
  • reset uploads the queued events before switching the id, so it may wait on the network.
  • initialize does not throw; a failure is only logged.
suspend fun initializeAsync(config: MixpanelConfiguration)

MixpanelConfiguration​

NameTypeDefaultDescription
projectTokenStringThe token of the Mixpanel project
optOutTrackingByDefaultBool?falseStart with tracking switched off
categories[EventCategory]?allThe event categories the user allows