Would you like to develop a new SDK for Countly? Then this guide is for you. Before starting, bear in mind that there are a lot of SDKs that Countly has already developed. Please check whether the SDK you are going to develop is not already available.
Integration
Initialization
To start making requests to the server, SDK needs 3 things: the URL of the server where you will be making requests, the app_key of the app for which you will be reporting, and your current device_id to uniquely identify this device.
Server URL - The SDK needs to provide the ability for the user to specify the URL for the server where their Countly instance is installed. This be used for all requests. The SDK must validate the server URL:
- URL must be non-null and non-empty
- URL must be a valid URL format (throw/warn on invalid)
- Trailing slashes must be stripped automatically
- If certificate/public key pinning is enabled, the URL must use HTTPS
App Key - The App key should be provided by the SDK user. This value identifies to which dashboard applications this request is going to be tied to. Your app should be created on the Countly server. After app creation, the server will provide an app key for the user. The same app key is used for the same app on different platforms.
Device ID - A device ID is required to uniquely identify a device or user. If you have some unique user ID which you can retrieve, you may use it. If not, you may provide platform-specific device identification (as Advertising identifier in Google Play Services on Android) or use existing implementations (as OpenUDID). More info about device ID is described here.
Init Configuration
The SDK should take a config (short for configuration) object during initialization. In that config object should all the init specic fields added. All initial configuration should be doable through that object and if possible, no configuration should be done with function calls before init.
That config object should also have the option to change all the available SDK tweakables like queue sizes and thresholds.
Depending on what features are implemented in the SDK, tweakables for them would also be provided through the config object, for example: debug parameters, id generation methods, user location, consent etc.
Depending on the SDK platform, there might be some other mandatory fields. If during init those are not provided, an exception can be thrown to indicate that.
For more information of what values can be sent to the server, look api reference below:
Networking Configuration
The following config options control how the SDK communicates with the server:
CountlyConfig.setHttpPostForced(isForced: boolean) // Logic Forces the SDK to use HTTP POST for all requests instead of GET. Default is true (POST is used).
CountlyConfig.addCustomNetworkRequestHeaders(headers: Map<String, String>) // Logic Adds custom HTTP header key/value pairs to every request sent to the server. Useful for authentication or routing through proxies.
CountlyConfig.enablePublicKeyPinning(certificates: String[]) // Logic Enables SSL public key pinning. The SDK will only accept server connections if the public key of the server's SSL certificate matches one of the provided certificates. Certificates should be base64-encoded strings.
CountlyConfig.enableCertificatePinning(certificates: String[]) // Logic Enables SSL certificate pinning. The SDK will only accept server connections if the full certificate matches one provided. Certificates should be base64-encoded strings.
CountlyConfig.setRequestDropAgeHours(dropAgeHours: int) // Logic Sets a time frame in hours after which old requests are dropped from the queue. For example, setting this to 10 means any request created more than 10 hours ago will be dropped when the SDK attempts to send it. Default is 0 (disabled).
CountlyConfig.setRequestTimeoutDuration(timeout: int) // Logic Sets the network request timeout in seconds. Default is 30 seconds. Minimum value is 1 second.
CountlyConfig.setParameterTamperingProtectionSalt(salt: String) // Logic Enables parameter tampering protection. When set, the SDK will compute a SHA256 checksum of the request data combined with the salt and append it as "checksum256" to every request. The server verifies this checksum to ensure the request data has not been tampered with in transit.
SDK Tweakables
The following config options control SDK behavior thresholds:
CountlyConfig.setEventQueueSizeToSend(threshold: int) // Logic Sets the number of events that will trigger a request to the server. When the event queue reaches this threshold, events are combined into a request. Default is typically 100.
CountlyConfig.setUpdateSessionTimerDelay(delay: int) // Logic Sets the interval in seconds for automatic session update requests. Minimum value is 1. Default is 60 seconds.
CountlyConfig.setMaxRequestQueueSize(maxSize: int) // Logic Sets the maximum number of requests that can be stored in the request queue. When the queue exceeds this size, oldest requests are removed. Default is 1000.
CountlyConfig.enableManualSessionControl() // Logic Enables manual session handling. When enabled, the SDK will not automatically manage sessions. The developer must call beginSession, updateSession, and endSession manually.
CountlyConfig.enableManualSessionControlHybridMode() // Logic Enables hybrid manual session control. Sessions are managed manually but certain automatic behaviors (like session extension on activity) are preserved.
CountlyConfig.disableBackoffMechanism() // Logic Disables the exponential backoff mechanism for failed network requests. When disabled, the SDK will retry failed requests without increasing delays.
CountlyConfig.enableExplicitStorageMode() // Logic Prevents the SDK from writing request/event queues to disk automatically. The developer must explicitly signal when to persist data. Platform-specific.
Consent Configuration
CountlyConfig.setRequiresConsent(requiresConsent: boolean) // Logic When set to true, no features will function until consent is explicitly given. All features are disabled by default until consent is granted for each feature group.
CountlyConfig.setConsentEnabled(featureNames: String[]) // Logic Gives consent to specific feature groups during init. Only has effect if requiresConsent is set to true. See User Consent section for all feature names.
CountlyConfig.giveAllConsents() // Logic Gives consent for all available features during init. Shorthand for enabling every consent feature individually.
Location Configuration
CountlyConfig.setLocation(countryCode: String, city: String, gpsCoordinates: String, ipAddress: String) // Logic Sets initial location data during init. All parameters are optional (nullable). - countryCode: ISO 3166-1 alpha-2 country code (e.g., "US") - city: city name (e.g., "New York") - gpsCoordinates: latitude,longitude string (e.g., "40.7128,-74.0060") - ipAddress: IP address for geolocation
CountlyConfig.disableLocation() // Logic Disables location tracking. When set, an empty location request is sent to the server to prevent it from inferring location from the IP address.
Custom Metrics
CountlyConfig.setMetricOverride(metricOverride: Map<String, String>) // Logic Allows overriding default device metrics or adding additional custom metrics that will be sent with session requests. This can be used to override values like device name, OS version, app version, or to add entirely new metrics.
User Properties During Init
CountlyConfig.setUserProperties(userProperties: Map<String, Object>) // Logic Allows providing user properties during initialization. These properties are sent to the server after init completes. This is useful for setting user data without needing to call setProperties + save after init.
Logging / Debug Mode
The SDK should have a logging mode flag. If that is enabled, the SDK should print logs to the console.
If this flag is not enabled, no logs should be printed to the console.
This flag functions independently of the log listener (described below).
| Log printed to console | Log printed to listener | |
|---|---|---|
| Nothing set | No | No |
| Only flag set | Yes | No |
| Only listener set | No | Yes |
| Flag and listener set | Yes | Yes |
Log Messages
No two places should have the same log message. This means that all messages should be unique. Knowing the log message and the sdk version it should be unambiguous to know which line printed that message.
To help with being unambiguous, the log messages could include the internal "module" or "section" name from where it was called. If the function called is part of the public API of the SDK, that log should include the function name.
[ModuleName] functionName, Some message
If there is a message tag/group mechanism on the platform then the tag name "Countly" should be used. If there is no tag/group mechanism available then the name "Countly" should be added to each message.
[Countly] [ModuleName] functionName, Some message
All calls, that can be called by developers, should produce a log message to indicate what is being called.
The goal is to have a good enough log coverage so that it's easy to understand what was happening with the SDK and how it was used when a SDK integrator provides his logs.
Log Levels
When implementing logs, the SDK should follow these levels:
-
Error - this is an issue that needs attention right now. E.g.:
- try-catch block errors
- missing `url` or `app_key`
-
Warning - this is something that is potentially an issue. E.g.:
- deprecated methods
- wrong parameters provided
- SDK is not initialized yet
-
Info - informs about the usual working of the SDK. E.g.:
- public method calls
- provided init config options
- initializations of modules
-
Debug - this should contain logs from the internal workings of the SDK. E.g.:
- request success response
- failed request
- unique inner function checks
-
Verbose - this should give an even deeper look into the SDK's inner working (noisy). E.g.:
- No consent given
By using log levels "info" and above it should be clear what the dev was doing with the SDK and what was the call order. "Debug" and above should give us a good enough look into the SDK's internal state and a good overview of it's configuration.
Platforms that don't have explicit log levels should print them manually at the start of the log in square brackets. Either of these variants currently seem fine (as long as it's consistent for the SDK)
- [ERROR], [WARNING], [INFO], [DEBUG], [VERBOSE]
- [Error], [Warning], [Info], [Debug], [Verbose]
- [error], [warning], [info], [debug], [verbose]
If there are the same amount of log levels available but with different names then the closest ones available for the platform should be picked.
If there are less than 5 log levels available then multiple ones should be printed in the same log level and the appropriate tag in square brackets should be printed at the start.
For example, if there are only 3 levels "error", "info", "debug". "Error" and "Warning" would be printed in the error "channel" and "debug" and "verbose" would be printed in the "debug" channel".
E: [Error] [Countly] [ModuleName] functionName, Countly issue bla bla E: [Warning] [Countly] [ModuleName] functionName, You are trying to use this feature in a deprecated way
Depending on the function call, the function parameters should also be printed, and they should be enclosed in closed brackets. For example event keys or for simple numerical values.
I: [Info] [Countly] [ModuleEvent] recordEvent, event is being recorded key: [Login], count: [2], segmentation: [{"isLoggedIn":true, "name":"Something"}]For functions which receive a callback, a bool is enough to indicate if a callback was or wasn't provided.
Log Listener
In some cases there might be situations where an integrated SDK is not behaving as expected in a release build which is used by clients. In those cases it would be valuable to see the inner workings of the SDK but it's not possible to access SDK logs. It wouldn't also be recommendable to print those logs is a published setting as they might leak private info to other apps on the same device.
For this reason exists the log listener feature. During init the developer can setup a log listener. For example, it could be a callback. If such a listener is set, logs should be printed to it. For every log that is provided, there should be 2 values. First the string of the message that would be printed and then the log level of that message so that it can be used as an indication of the message importance.
Countly Code Generator
If you would like to understand how SDKs work by generating mobile or web code for events, user profiles, crash reporting, and all other features that come with Countly in general, we suggest you use the below tool which is a point and click service that builds the necessary code for you.
SDK Storage and Requests
Making Requests
The Countly server is a simple HTTP-based REST API server, and all SDK requests should be made to /i endpoint with two required parameters: app_key and device_id.
Other optional parameters need to be provided based on what this request should do. You may checklist all the parameters that the Countly Server can accept in api reference /i endpoint.
There are some parameters that should be added to all requests, even though they are not mandatory. Together with the required parameters, they form the base request. Every request sent to the server should be formed from this base request. The parameters in this base request are:
- "app_key" - the application key for this countly app (retrievable on the dashboard)
- "device_id" - the current user's device ID
- "timestamp" - the timestamp in ms of when this request is created
- "hour" - the hour of the timestamp
- "dow" - the day of the week for this timestamp. 0 - sunday, ... , 6 - saturday.
- "tz" - this device's timezone offset
- "sdk_version" - the SDK's version
- "sdk_name" - the SDK's name
- "av" - the application's version string
- "rr" - the remaining request count in the queue minus 1 (appended at send time, not at creation)
- "t" - the device ID type (platform-specific, sent by iOS and Web SDKs)
In cases where some devices may be offline, etc., and requests should be queued, it is highly recommended you add a timestamp to each request, displaying when it was created.
Encoding URI Components
Due to the possible use of the ‘&’ and ‘?’ symbols in encoded JSON strings, SDKs should encode uri components before adding them to the request and sending it to the server.
Using GET or POST
By default, the preferred method is to make GET requests for the Countly servers. However, there may be some length limitations for GET requests based on specific platform or server settings. Thus, the best practice is to make a POST request when the data reaches over 2,000 characters for a single request.
Before making each request, you will need to check if the data you are going to send is less than 2,000 characters. If so, use GET. If you have more characters, use POST.
Additionally, the SDK should be able to switch to post completely if a user should so specify in the SDK configuration/settings.
When making POST requests, the used content type should be "application/x-www-form-urlencoded".
SDK Metadata
The SDK should send the following metadata with every request.
- SDK name:
Query String Key: sdk_name Query String Value: [language]-[origin]-[platform] Example: &sdk_name=objc-native-ios
- SDK version:
Query String Key: sdk_version Query String Value: SDK version as string Example: &sdk_version=20.10.0
The SDK versions decode to [year].[month].[minor release number]. The SDK major versions (year, month) should follow the server versions. Those are usually incremented twice a year during our major releases. Minor release numbers should start at "0". Major version numbers (year, month) should stay the same even if the SDK version is released a couple of months after the indicated date. If the servers version is released in October 2020 the major versions would be "20.10". The first version released by the SDK, that would be in sync with this server version, would have the version "20.10.0". If the SDK needs to release an update in January of 2021 and no major server release has happened, the next version released by the SDK will be "20.10.1".
No zeroes should be added to the second number (indicating the month) in case the release happens before October.
Storage
Some things in the SDK are stored persistently, for example, request queue, event queue etc. Those should be stored in the device storage.
If possible, those persistent values should be segmented by the appKey. That means that for every appKey there should be different storage for their queues.
Request Queue
In some cases, users might be offline, thus they are not able to make requests to the server. In other cases, the server may be down or in maintenance, thus unable to accept requests. In both cases, the SDK should handle queuing and persistently storing requests made to the Countly server and should wait for a successful response from the server before removing a request from the queue.
Note that requests should be made in historical order, meaning you must also preserve the order of your queue.
Simple flow on how requests should appear as follows:
- Initiating request - either a new event reported or session call, etc.
- Creating a payload - take all the parameters (including the current timestamp) and the values needed for a request and generate a payload which will be included in the HTTP request
- This payload is inserted into the queue (First In, First Out)
- All updates to the queue should be persistently stored. Based on the environment, you may directly use storage for the queue
- On some other thread there should be a request processor which takes the first request in the queue, applies the checksum if needed, determines the request type (GET or POST) based on the length, and makes the HTTP request
- if the request is successful (defined below), then it should be removed from the queue and the next request will be processed upon the next iteration
- if the request failed, the request processor should have a cool-down period, lasting a minute or so (configurable value), and it will then try the same request again until it is completed

There are multiple scenarios why a request might fail, so to ensure that the request is successfully delivered to the server SDK, you will need to assure the following has taken place:
- The HTTP response code was successful (which is any 2xx code or code between 200 <= x < 300)
- The returned request is a JSON object
- That JSON object contains the field "result" (there can be other fields)
When sending request to a Countly server, it would respond with a JSON object, which should have a property named "result". Usually that value will be "Success". There may be scenarios where a different "result" value is returned or where additional fields may be added.
If the previously described things are "true", then it means the request was successfully delivered to the server and can be removed from the queue.
Queue Size Limit
We need to limit the queue size so that it doesn’t overflow, and so that syncing up won’t take too long if some specific server is down for too long. This limit would be in the number of stored queries, and this limit should be available for the end-user to change as the SDK settings.
In case this limit is reached, the SDK should remove older queries and insert new ones. The default limit may change from what the SDK needs, but the suggested limit is 1,000 queries.
Request Drop by Age
SDKs should support dropping requests that exceed a configurable age (in hours). When enabled via setRequestDropAgeHours, requests created more than the specified hours ago are discarded before sending. The request's timestamp is compared against the current time to determine age.
Request Queue Storage Format
The storage format is platform-specific but should meet these requirements:
- Requests must be stored as serialized query strings
- Android: uses SharedPreferences with request strings joined by ":::" delimiter
- iOS: uses NSKeyedArchiver to serialize an NSMutableArray of query strings to file
- Web: uses localStorage with a JSON array of request objects
- Storage should be segmented by app_key to support multi-instancing
GET vs POST Request Threshold
Requests are sent as POST by default. When the request data length exceeds 2,048 characters (Android/iOS) or 2,000 characters (Web), the SDK should switch to POST. Additionally, crash requests (&crash=) should always use POST regardless of size. If setHttpPostForced is enabled, all requests use POST.
Parameter Tampering (checksum256) Algorithm
// Algorithm: SHA-256(request_data_string + salt)
// The salt is a shared secret configured by the developer
1. Take the full request query string (e.g., "app_key=xxx&device_id=yyy&...")
2. Append the salt directly: requestData + salt
3. Compute SHA-256 hash of the combined string
4. Convert to hex string
5. Append as &checksum256={hex_hash} to the request
// Platform differences:
- Android/iOS: use natural parameter insertion order, lowercase hex output
- Web: sorts parameter keys alphabetically before building query string, UPPERCASE hex output
- For file/picture uploads: URL-decode the request data before hashing
// If salt is not configured, no checksum should be appended.Bot/Crawler Detection
SDKs should implement bot/crawler detection to avoid polluting analytics data. When a request is identified as coming from a bot or crawler, it should be silently dropped. Android checks the device name against a configurable list (default: "Calypso AppCrawler"). Web uses a comprehensive regex of ~40 bot user-agent strings (Googlebot, Baiduspider, HeadlessChrome, etc.) and sets an ignore_visitor flag that prevents all requests from being queued.
Custom Endpoint Override
Some requests need to be sent to endpoints other than /i (e.g., /o/sdk for remote config, feedback widget data). SDKs use an internal &new_end_point= tag embedded in the request data. The request processor strips this tag before sending and uses its value to route to the correct endpoint.
Event Queue
Similary to the request queue, each SDK should also have the event queue. It is used for the purposes of combining multiple events together and decreasing the total request amount.
This queue is also FIFO, and has a maximum size.
Other Storage
Some other features might also require storage to store some things persistently (remote config, etc).
Those would have their own storage format.
Recording Time of Data
To properly report and process data (especially queued data), you should also provide the time when the data was recorded. You will need to provide 3 parameters with each request:
- timestamp: 13-digit UTC millisecond unique timestamp of the moment of action
- hour: Current user local hour (0 - 23)
- dow: Current user day of the week (0-Sunday, 1 - Monday, ... 6 - Saturday)
- tz: Current user time zone in minutes (120 for UTC+02, -300 for UTC-05)
As multiple events may be combined in a single request, you should also provide these parameters automatically in every event object.
The suggested millisecond timestamp should be unique, meaning if events were reported in the same timestamp, the SDK should update the millisecond timestamp in the order in which the events were reported. The pseudo-code to the unique millisecond timestamp could appear as follows:
//variable to hold last used timestamp
lastMsTs = 0;
function getUniqueMsTimestamp(){
//get current timestamp in miliseconds
ts = getMsTimestamp();
//if last used timestamp is equal or greater
if(lastMsTs >= ts){
//increase last used
lastMsTs++;
}
else{
//store current timestamp as last used
lastMsTs = ts;
}
//return timestamp
return lastMsTs;
}If it’s impossible to use a millisecond timestamp on a specific platform, you may also use a 10-digit UTC seconds timestamp.
General SDK Structure Overview
Depending on the SDK’s environment/language, a different set of features could be supported. Some of these features may be supported on any platform, whereas others are quite platform-specific. For example, a desktop app may not provide telecom operator information.
Note that function and argument namings are only examples of what it could be. Try to follow your platform/environment/language best practices when creating and naming functions and variables.
Core features are the minimal set of features that the SDK should support, and these features are platform-independent.
Initialization
In its official SDKs, Countly is used as a singleton object or basically an object with a shared instance. Still, there are some parameters that need to be provided before the SDK can work. Usually, there is an "init" method that accepts the URL, app key, and device_id (or the SDK generates it itself if it’s not provided):
Countly.init(string url="https://try.count.ly", string app_key, string device_id, ...)
Multi-Instance Support
The SDK should support running multiple independent instances simultaneously. Each instance operates with its own configuration (server URL, app key, device ID), its own request and event queues, and its own persistent storage. Instances must not share state or interfere with each other.
A typical multi-instance pattern:
CountlyConfig config1 = new CountlyConfig(url1, appKey1);
CountlyConfig config2 = new CountlyConfig(url2, appKey2);
Countly instance1 = Countly.createInstance(config1);
Countly instance2 = Countly.createInstance(config2);
// Each instance tracks independently
instance1.events().recordEvent("event_a");
instance2.events().recordEvent("event_b");Multi-instancing requirements:
- Each instance must have isolated persistent storage, segmented by app key
- Each instance must maintain its own device ID, session state, and consent state
- Lifecycle callbacks (foreground/background) must be forwarded to all active instances
- The singleton/shared instance pattern should still be available as a convenience for single-instance usage
- Instances should be independently initializable and destroyable without affecting other instances
Modular Architecture
The SDK should be structured as a collection of independent modules, each responsible for a single feature (sessions, events, views, crashes, etc.). Modules should communicate through a well-defined internal interface rather than directly referencing each other.
Each module should:
- Have a clear lifecycle (init, start, stop, consent change, device ID change)
- Be independently testable
- Only activate when its required consent is granted
- React to cross-cutting events (device ID change, consent change) through a shared notification mechanism
Thread Safety
All public API methods must be thread-safe. The SDK should handle being called from any thread without causing race conditions or crashes. Internal state (queues, maps, counters) should be protected using the platform’s appropriate concurrency primitives (synchronized blocks, locks, serial queues, etc.).
Network operations and disk I/O should be performed off the main/UI thread. Callbacks to the developer should be delivered on the main/UI thread where the platform convention expects it.
Testing Requirements
SDKs must maintain a minimum of 90% code coverage across unit and integration tests. Test suites should cover:
- Unit tests for each module in isolation (mocked dependencies)
- Integration tests for cross-module interactions (e.g., consent change affecting sessions and views)
- Request validation tests verifying the correct parameters are sent for each feature
- Edge case tests for invalid inputs, out-of-order calls, and boundary conditions
- Concurrency tests to verify thread safety of public APIs
- Storage tests to verify persistence and recovery across SDK restarts
Tests should be automated and run as part of CI/CD. Coverage reports should be generated and tracked over time. Any new feature or bug fix must include corresponding test cases.
Crash Reporting
Exposed Methods
Config Methods
CountlyConfig.crashes.enableCrashReporting()
CountlyConfig.crashes.setCustomCrashSegmentation(crashSegment: Map<String, Object>)
CountlyConfig.crashes.setGlobalCrashFilterCallback(callback: GlobalCrashFilterCallback)
CountlyConfig.crashes.enableRecordAllThreadsWithCrash()
Instance Methods
CountlyInstance.addCrashBreadcrumb(record: String)
CountlyInstance.recordHandledException(exception: Error/Exception)
CountlyInstance.recordHandledException(exception: Error/Exception, customSegmentation: Map<String, Object>)
CountlyInstance.recordUnhandledException(exception: Error/Exception)
CountlyInstance.recordUnhandledException(exception: Error/Exception, customSegmentation: Map<String, Object>)
Implementation Details
For config method enableCrashReporting
CountlyConfig.crashes.enableCrashReporting() // Logic Enables automatic unhandled crash reporting. When enabled, the SDK will set up an uncaught exception handler that will capture unhandled crashes and report them to the server. - Crash recording should be gated by server-side configuration (e.g., getCrashReportingEnabled()) If the server config disables crash tracking, crashes should not be recorded.
For config method setCustomCrashSegmentation
CountlyConfig.crashes.setCustomCrashSegmentation(crashSegment: Map<String, Object>) // Valid values Accepted value types are: Integer, String, Double, Boolean, Arrays Non valid types are warned and ignored // Logic Sets custom crash segmentation which will be added to ALL recorded crashes (both handled and unhandled). This is global crash segmentation.
For config method setGlobalCrashFilterCallback
CountlyConfig.crashes.setGlobalCrashFilterCallback(callback: GlobalCrashFilterCallback) // Valid values Value is not nullable // Logic Registers a callback that will be called for each crash before it is sent to the server. The callback receives a CrashData object that can be modified or used to filter (omit) the crash entirely.
The callback's signature is GlobalCrashFilterCallback:
GlobalCrashFilterCallback {
boolean filterCrash(CrashData crash)
}
// Parameters
- crash, CrashData object containing all crash-related data
// Return value
- true: the crash should NOT be sent to the server (omit it)
- false: the crash should be sent to the serverFor config method enableRecordAllThreadsWithCrash
CountlyConfig.crashes.enableRecordAllThreadsWithCrash() // Logic Enables recording of all threads with crash reports. When enabled, the SDK will capture stack traces of all running threads, not just the crashing thread. // Internal Limits Applied - maxStackTraceThreadCount (default: 50) caps the number of threads recorded - maxStackTraceLinesPerThread (default: 30) caps lines per thread - maxStackTraceLineLength caps individual line length
For instance method addCrashBreadcrumb
CountlyInstance.addCrashBreadcrumb(record: String) // Valid values Only non null and not empty strings are accepted Non valid values are warned and ignored // Logic Adds a breadcrumb log entry that will be sent together with crash reports. Breadcrumbs are stored in memory in an array. When a crash occurs, the array is concatenated with newline symbols and submitted under the "_logs" property. Breadcrumbs are limited by the "maxBreadcrumbCount" internal limit. Breadcrumb text is limited by the "maxValueSize" internal limit.
For instance method recordHandledException
CountlyInstance.recordHandledException(exception: Error/Exception) CountlyInstance.recordHandledException(exception: Error/Exception, customSegmentation: Map<String, Object>) // Valid values Exception/error object is not nullable customSegmentation is nullable, accepted value types: Integer, String, Double, Boolean, Arrays // Logic Records a handled (non-fatal) exception/error. The "_nonfatal" property is set to true. - Stack trace is extracted and truncated to max 20,000 characters - If recordAllThreadsWithCrash is enabled, all thread stack traces are appended - Stack trace lines are limited by maxStackTraceLineLength and maxStackTraceLinesPerThread - Custom segmentation, if provided, is merged with global crash segmentation (custom overrides global) - Internal limits are applied to the combined segmentation - A CrashData object is created with: stackTrace, combinedSegmentation, breadcrumbs, crashMetrics, fatal flag - The crash is sent through the crash filter callback if registered (if it returns true, crash is dropped) - After filtering, the "_ob" metric is calculated to indicate which fields were modified - Internal limits are re-applied after filtering - The crash report is serialized to JSON and added to the request queue - Crash recording should be gated by server-side configuration (e.g., getCrashReportingEnabled()) If the server config disables crash tracking, crashes should not be recorded.
For instance method recordUnhandledException
CountlyInstance.recordUnhandledException(exception: Error/Exception) CountlyInstance.recordUnhandledException(exception: Error/Exception, customSegmentation: Map<String, Object>) // Valid values Exception/error object is not nullable customSegmentation is nullable, accepted value types: Integer, String, Double, Boolean, Arrays // Logic Records an unhandled (fatal) exception/error. The "_nonfatal" property is set to false. - Same crash preparation flow as recordHandledException applies - For automatic unhandled crash reporting (enableCrashReporting): The SDK sets an UncaughtExceptionHandler that captures unhandled crashes After recording, the previous exception handler (if any) is also notified - Native crash dumps (NDK on Android, PLCrash on iOS) are read from the crash dump folder at init, base64-encoded, and sent to the server. Dump files are deleted after recording. - Crash recording should be gated by server-side configuration (e.g., getCrashReportingEnabled()) If the server config disables crash tracking, crashes should not be recorded.
General Information
On some platforms, the automatic detection of errors and crashes is possible. In this case, your SDK may report them to the Countly server, and this is also optional as with other similar functions. If a crash report is not sent, it won't be displayed on the dashboard under the Crashes section. Below are more information on crash reporting parameters that you may use in your SDK.
In regard to crashes, all information, except the app version and OS, is optional, but you should collect as much information about the device as possible to assure each crash may be more identifiable with additional data. You should also provide a way for users to log errors manually (for example, logging handled exceptions which are not fatal).
Complete Crash Metrics Fields - The following device/environment metrics should be collected and sent alongside the crash report where available:
// Core crash fields _error - stack trace string _nonfatal - boolean (true = handled, false = unhandled/fatal) _logs - breadcrumb logs concatenated with newlines _custom - custom crash segmentation (key/value pairs) _run - app run duration in seconds _background - whether the app was in background when crash occurred // Device metrics (collected automatically) _device - device model name _os - operating system name _os_version - OS version _resolution - screen resolution _app_version - application version _cpu - CPU info _opengl - OpenGL/WebGL version _root - whether device is rooted/jailbroken _ram_total - total RAM _ram_current - available RAM at crash time _disk_total - total disk space _disk_current - available disk space at crash time _bat - battery level _orientation - device orientation at crash time _online - network connectivity status _muted - whether device is muted (platform-specific) _name - crash name/title _type - crash type/category // Platform-specific fields _native_cpp - boolean, true for NDK/native crash dumps (Android) _binary_images - binary image addresses/UUIDs for symbolication (iOS) _build_uuid - app build UUID (iOS) _executable_name - executable binary name (iOS) _architecture - CPU architecture (iOS) _app_build - app build number, separate from _app_version (iOS) _plcrash - flag for PLCrashReporter-originated reports (iOS) _javascript - always true (Web) _not_os_specific - always true (Web) _view - current view name at time of crash (Web)
Basically, for automatically captured errors, you should set the _nonfatal property to false, whereas on user logged errors the _nonfatal property should be true. You should also provide a way to set custom key/values to be reported as segments with crash reports, either by providing global default segments or setting separately for automatically tracked errors and user logged errors.
Additionally, there should be a way for the SDK user to leave breadcrumbs that would be submitted together with the crash reports. In order to collect breadcrumbs as logs, create an empty array upon initialization and provide a method to add breadcrumbs as strings into that array as elements for log. Also, in the event of a crash, concatenate the array with new line symbols and submit under the _logs property. There is no need to persistently save those logs on a device, as we would like to have a clean log on every app start. Exception: when using native crash reporters (e.g., PLCrashReporter on iOS), breadcrumbs must be persisted to file since in-memory data is lost during a native crash.
The end API could look like this (but it should be totally based on the specific platform error handling):
- Countly.enable_auto_error_reporting(map segments)
- Countly.log_handled_error(string title, string stack, map segments)
- Countly.log_unhandled_error(string title, string stack, map segments)
- Countly.add_breadcrumb(string log)
Networking and Params
Crash object that can be sent to the server is:
crash : all parameters (core crash fields, device metrics, platform specific fields) formes a flattened json object
Crash requests are sent to the "/i" endpoint.
Crash requests should be sent through the request queue.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'crash={"_os":"Android","_os_version":"4.1","_manufacture":"Samsung","_device":"Galaxy S4","_resolution":"1900x1080","_app_version":"2.1","_cpu":"armv7","_opengl":"2.1","_ram_current":1024,"_ram_total":4096,"_disk_current":3000,"_disk_total":10240,"_bat":99,"_orientation":"portrait","_root":false,"_online":true,"_muted":false,"_background":false,"_name":"Null Pointer exception","_error":"Some error stack here","_nonfatal":true,"_logs":"logs provided here","_run":180,"_custom":{"facebook_sdk":"3.5","admob":"6.5"}}' \
--data ...remaining common paramsStorage
Crash information is not stored persistently (in a way that would survive SDK shutdowns and further inits). A request is formed and it is stored as request in the request queue.
Consent
The feature depends on "crashes" consent.
Crash Filtering
There should be a way to filter and edit the crash report before sending it to the server. To do this, a callback should be registered while initializing the SDK.
config.crashes.setGlobalCrashFilterCallback(callback); Countly.init(config);
This callback will take a "CrashData" object containing all crash-related data and modify or omit it. If "true" is returned from the callback, the crash must be omitted.
interface CrashFilterCallback {
boolean filterCrash(CrashData crash);
}The crash data object must contain:
- Stack trace string that is concatenated with new lines.
- List of breadcrumbs
- Crash metrics
- Crash segmentation
- Crash Name (iOS only)
- Crash Description (iOS only)
- Whether or not a crash is unhandled (fatal)
While preparing "CrashData", SDK internal limits should be applied to the stack trace, crash segmentation, and breadcrumbs.
All of those attributes can be modified. "CrashData" must provide setters and getters to achieve that.
Setters mustn't accept invalid values like null, undefined, etc.
Also, there must be a way to report which fields have been modified for the server.
A metric for crashes will be calculated as an integer of "_ob" bits. If some fields changed while filtering, bits must be set as:
- 01000000 for description -> "_ob" metric will be 64 (iOS only)
- 00100000 for name -> "_ob" metric will be 32 (iOS only)
- 00010000 for stack trace -> "_ob" metric will be 16
- 00001000 for crash segmentation -> "_ob" metric will be 8
- 00000100 for breadcrumbs -> "_ob" metric will be 4
- 00000010 for crash metrics -> "_ob" metric will be 2
- 00000001 for unhandled -> "_ob" metric will be 1
For example, if crash segmentation, unhandled, and stack trace is changed, the "_ob" metric must be 25.
Platform Note: Android uses 5 checksum slots (stack trace, crash segmentation, breadcrumbs, crash metrics, fatal) while iOS uses all 7 (adding name and description). The "_ob" calculation uses SHA256 checksums computed before and after filtering to detect changes. Web SDKs do not implement "_ob" calculation.
After the crash filtering is completed, SDK internal limits must be applied to the stack trace, crash segmentation, and breadcrumbs. Unsupported data types must also be removed from crash metrics.
The "_ob" metric calculation must not be affected by SDK internal limit changes. It must only indicate whether or not the developer modified that field.
The "_ob" metric must be appended to the crash metrics and sent to the server.
Events
Exposed Methods
Instance Methods
CountlyInstance.recordEvent(key: String)
CountlyInstance.recordEvent(key: String, count: int)
CountlyInstance.recordEvent(key: String, count: int, sum: double)
CountlyInstance.recordEvent(key: String, segmentation: Map<String, Object>)
CountlyInstance.recordEvent(key: String, segmentation: Map<String, Object>, count: int)
CountlyInstance.recordEvent(key: String, segmentation: Map<String, Object>, count: int, sum: double)
CountlyInstance.recordEvent(key: String, segmentation: Map<String, Object>, count: int, sum: double, dur: double)
CountlyInstance.startEvent(key: String)
CountlyInstance.cancelEvent(key: String)
CountlyInstance.endEvent(key: String)
CountlyInstance.endEvent(key: String, segmentation: Map<String, Object>, count: int, sum: double)
Implementation Details
For instance method recordEvent
CountlyInstance.recordEvent(key: String, segmentation: Map<String, Object>, count: int, sum: double, dur: double) // Valid values key is not nullable and not empty, non valid values are warned and call is omitted segmentation is nullable, accepted value types: Integer, String, Double, Boolean, Arrays count must be = 1, defaults to 1 sum defaults to 0 dur defaults to 0 // Logic Records an event with the given parameters. - A unique event ID is generated for each event - Event key is truncated by the "maxKeyLength" internal limit - Segmentation keys are truncated by "maxKeyLength" - Segmentation string values are truncated by "maxValueSize" - Segmentation entry count is limited by "maxSegmentationValues" - Unsupported data types are removed from segmentation (only Integer, String, Double, Boolean, Arrays allowed) - Before recording, any pending user profile data is saved - If visibility tracking is enabled, a "cly_v" segmentation key is added (1=foreground, 0=background) - If previous name recording is enabled, "cly_pen" (previous event name) and "cly_cvn" (current view name) are added (only for custom events, not internal events) - A "peid" (previous event ID) is tracked and assigned to each custom event (not internal events). The peid is initialized to empty string and updated to the last custom event's ID after recording. - "pvid" (previous view ID) is attached to view internal event. - "cvid" (current view ID) is attached to ALL events (both custom and internal) except view internal event. - Event key truncation and segmentation limits are only applied to custom events, not internal events - Negative count values are corrected to 1 with a warning (not rejected) - Event filtering via SDK Behavior Settings (blacklist/whitelist) is applied for custom events - Custom event tracking can be disabled entirely via server config (getCustomEventTrackingEnabled) - The event is added to the event queue with: key, segmentation, count, sum, dur, timestamp, hour, dow, eventID, pvid (previous view ID), cvid (current view ID) - When event queue reaches the threshold, events are flushed to a request - Internal events (views, feedback, push actions) check their own specific consent before recording - Feedback widget events (NPS, Survey) are sent immediately (force flush after recording)
For instance method startEvent
CountlyInstance.startEvent(key: String) // Valid values key is not nullable and not empty // Logic Starts a timed event. The event key and start timestamp are stored. - If an event with the same key is already running, the call is ignored and a warning is logged. - The timed event runs until endEvent or cancelEvent is called for the same key.
For instance method endEvent
CountlyInstance.endEvent(key: String, segmentation: Map<String, Object>, count: int, sum: double) // Valid values key is not nullable and not empty segmentation is nullable count defaults to 1 sum defaults to 0 // Logic Ends a previously started timed event. - Consent for "events" is checked; if not given, the call silently returns without recording - The duration is calculated from the difference between start time and current time - The event is recorded with the ORIGINAL start timestamp, hour, and day-of-week (not the end time) - If no timed event with the given key exists, the call is ignored and a warning is logged - The event is recorded with the calculated duration - All timed events are cleared on device ID change (without merge) and SDK halt/reset
For instance method cancelEvent
CountlyInstance.cancelEvent(key: String) // Valid values key is not nullable and not empty // Logic Cancels a previously started timed event without recording it. - If no timed event with the given key exists, the call is ignored and a warning is logged - The timed event entry is removed from the timed events map
General Information
Events are the basic Countly data reporting tool that conveys something has happened in the app. Common examples are: someone clicked a button, performed a specific action, etc.
Events can be grouped under two categories:
- Internal Events
- Developer provided Events
Internal Events has a [CLY]_ prefix for its key and used for providing a specific kind of information (a feature) to the server. For example Views feature is reported as an internal Event and will be displayed at a unique section on the server (Analytics > Page Views).
Developer provided Events can be about any kind of information (non specific). So all developer provided Events will have an aggregate display section (Events > All Events).
An Event would have the following properties:
- key - Mandatory, non empty string
- count - Mandatory, integer, minimum 1
- sum - Optional value, double, can be negative
- dur - Optional value, double, minimum 0
- segmentation - Optional, object with key-value pairs
- id - Mandatory, generated internally, event id
- pvid - Mandatory for view internal event, previous view id, tracked internally
- cvid - Mandatory for all events except view internal event, current view id, tracked internally
- peid - Mandatory for all event except view and widget internal events, previous event id, tracked internally
- cly_v - Mandatory if enableVisibilityTracking called, will be added to segmentation, indicate app is in foreground or background, not added to the view internal end events,tracked internally
- cly_pen - Mandatory if enablePreviousNameRecording called, will be added to segmentation, previous event name, only added to the custom events, tracked internally
- cly_pvn - Mandatory if enablePreviousNameRecording called, will be added to segmentation, previous view name, only added to the view internal events, tracked internally
- cly_cvn - Mandatory if enablePreviousNameRecording called, will be added to segmentation, current view name, only added to the custom events, tracked internally
More on Event formatting and information about feature could be found below documents:
Here are some examples:
User logged into the game:
{ key: 'login', count: 1 }User completed a level in the game with a score of 500:
{ key: 'level_completed', count: 1, segmentation: { level: 2, score: 500 } }User purchased something in the app worth 2.99 on the main screen:
{ key: 'purchase', count: 1, sum: 2.99, dur: 30, segmentation: { screen: 'main' } }As you can imagine, your SDK should provide methods to cover these combinations, either by default values or by function parameter overloading, etc.:
- Countly.events.recordEvent(string key)
- Countly.events.recordEvent(string key, int count)
- Countly.events.recordEvent(string key, double sum)
- Countly.events.recordEvent(string key, double duration)
- Countly.events.recordEvent(string key, int count, double sum)
- Countly.events.recordEvent(string key, map segmentation)
- Countly.events.recordEvent(string key, map segmentation, int count)
- Countly.events.recordEvent(string key, map segmentation, int count, double sum)
- Countly.events.recordEvent(string key, map segmentation, int count, double sum, double duration)
Note: count value defaults to 1 internally if not provided as it is mandatory.
Event Queue Threshold: The default event queue size threshold is 100 events across all SDKs. When the queue reaches this threshold, events are batched into a JSON array and combined into a request. This threshold can be overridden by the server via SDK Behavior Settings (eqs key). If the queue exceeds the batch size, only the threshold number of events are spliced into each request, leaving the rest for the next batch.
Event ID Generation: Each event gets a unique ID. The format varies by platform but typically consists of a random component plus a timestamp (e.g., iOS uses 6 random bytes base64-encoded + timestamp in ms; Web uses 8 hex chars + timestamp). The ID must be unique per event instance.
Immediate Flush Events: Certain internal events bypass the queue threshold and trigger an immediate flush: NPS events ([CLY]_nps), Survey events ([CLY]_survey), and Push action events ([CLY]_push_action). Star rating and other internal events do NOT trigger immediate flush.
Networking and Params
Event request structure is:
events : json array contains event objects
event object: {
"key":"EVENT_NAME",
"count":EVENT_COUNT,
"sum":EVENT_SUM,
"dur":EVENT_DUR,
"segmentation":EVENT_SEGMENTATION,
"timestamp": EVENT_TIMESTAMP,
"hour": HOUR,
"dow": DoW,
"id": EVENT_ID,
... other event related params
}Event requests are sent to the "/i" endpoint.
Event requests should be sent through the request queue.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'events=[{"key":"EVENT_NAME","count":EVENT_COUNT,"sum":EVENT_SUM,"dur":EVENT_DUR,"segmentation":EVENT_SEGMENTATION}]' \
--data ...remaining common paramsStorage
Events are not persisted directly. They have their dedicated queue, see. They are only stored in memory and packed to the request queue. The default event queue size threshold is 100 events across all SDKs. When the queue reaches this threshold, events are batched into a JSON array and combined into a request then added to the request queue
Consent
The feature depends on "events" consent.
Internal Events
The call for recording events should support recording Countly internal events. If consent is required then the ability to record them should be governed only by their respective feature consents. The ability to record internal events is in no way influenced by the " event" consent. If consent for some feature is given and "recordEvent" is used to record that features internal event, it should be recorded even if no "event" consent is given. If for some feature consent is not given and "recordEvent" is used to record it's internal event, it should not be recorded even if "event" consent is given. If no consent is required, they should be recorded as well.
At the current moment there are the following internal events and their respective required consent:
- [CLY]_nps - "feedback" consent
- [CLY]_survey - "feedback" consent
- [CLY]_star_rating - "star_rating" consent
- [CLY]_view - "view" consent
- [CLY]_orientation - "users" consent
- [CLY]_push_action - "push" consent
- [CLY]_action - "clicks" or "scroll" consent
Example 1:
- event consent is given, but 'view' consent is not given
- dev calls "recordEvent('[CLY]_view')
- event is not recorded
Example 2:
- event consent is given, and 'view' consent is also given
- dev calls "recordEvent('[CLY]_view')
- event is recorded
Example 3:
- event consent is not given, but 'view' consent is given
- dev calls "recordEvent('[CLY]_view')
- event is recorded
Timed Events
In short, you may report time with the dur property in an event. It is good practice to allow the user to measure some periods internally using the SDK API. For that purpose, the SDK needs to provide the methods below:
- startEvent(string key) - which will internally save the event key and current timestamp in the associative array/map.
- endEvent(string key, map segmentation, int count, double sum) - which will take the event starting timestamp by the event key from the map, get the current timestamp and calculate the duration of the event. It will then fill it up as a dur property and report an event, just as you would report any regular event.
- endEvent(string key) - which will simply end the event with a 1 as the count, 0 as the sum, and nil as the segmentation values.
If possible, the SDK may provide a way to start multiple timed events with the same key, such as returning an event instance in the method and then calling the end method on that instance.
If not, the following calls should be ignored: 1. events which have already started 2. events which have attempted to start again 3. events which have already ended 4. events which have attempted to end. Otherwise, they will provide an informative error.
Device Metrics
Exposed Methods
Config Methods
CountlyConfig.setMetricOverride(metricOverride: Map<String, String>)
Instance Methods
CountlyInstance.recordMetrics(metricOverride: Map<String, String>)
Implementation Details
For config method setMetricOverride
CountlyConfig.setMetricOverride(metricOverride: Map<String, String>) // Valid values Function should not accept empty or null value // Logic It should accept values and add to an empty metric override map that SDK internally has. And in every metric retrieval, SDK should append metric overrides after forming metrics.
For instance method recordMetrics
CountlyInstance.recordMetrics(metricOverride: Map<String, String>) // Valid values Function should not accept empty or null value // Logic It should accept values and gather the SDK collected metrics and should apply metric overrides provided from configuration if any then metric overrides provided through function. After forming metrics, it should generate a request with common parameters and a "metrics" parameter where SDK will attach formed metrics to there. Afterwards, SDK will add created request to the request queue. - Requires "metrics" consent
General Information
Metrics can be reported together with the begin_session=1 parameter on every session start. Or it can have its dedicated call. Collect as many metrics as possible or allow some values to be provided by the user upon initialization. Possible metrics are listed in below documentations:
Complete Device Metrics List:
_device - device model name _os - operating system name _os_version - OS version _resolution - screen resolution (e.g., "1080x1920") _density - screen density (e.g., "XHDPI" on Android, "@2x" on iOS) _locale - device locale (e.g., "en_US") _app_version - application version _carrier - mobile carrier name (deprecated on iOS 16.4+) _store - app store source (Android-specific) _manufacturer - device manufacturer (e.g., "Samsung") _device_type - device form factor: "mobile", "tablet", "smarttv", "desktop" _has_hinge - whether device is foldable (Android-specific) _browser - browser name (Web-specific, via User-Agent Client Hints) _browser_version - browser version (Web-specific) _ua - user agent string (Web-specific, sent with every request)
One thing that we should agree on is identifying platforms with the same string overall SDKs, so here is the list of how we would suggest identifying platforms for the server through the _os metric.
- Android - for Android
- BeOS - for BeOS
- BlackBerry - for BlackBerry
- iOS - for iOS
- Linux - for Linux
- Open BSD - for Open BSD
- os/2 - for OS/2
- macOS - for Mac OS X
- QNX - for QNX
- Roku - for Roku
- SearchBot - for SearchBots
- Sun OS - for Sun OS
- Symbian - for Symbian
- Tizen - for Tizen
- tvOS - for Apple TV
- Unix - for Unix
- Unknown - if the operating system is unknown
- watchOS - for Apple Watch
- Windows - for Windows
- Windows Phone for Windows Phone
Networking and Params
Manual metrics request is formed like:
metrics : metrics formed like json object
Metrics requests are sent to the "/i" endpoint.
Manual metrics requests should be sent through the request queue.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'metrics={"_device":"name of the device","_os":"device OS","_os_version":"device OS version","_resolution":"resolution of the device/application","_app_version":"application version","_manufacturer":"device manufacturer","_carrier":"device carrier","_orientation":"device orientation","_has_hinge":"device has hinge sensor"}' \
--data ...remaining common params
# and if you are sending with begin session add following parameter
# --data 'begin_session=1'Storage
Metrics are not stored. They are fetched from device on demand. Only metric overrides provided from configuration are stored in memory.
Consent
Only manual recording metrics require "metrics" consent.
Sessions
Exposed Methods
Config Methods
CountlyConfig.enableManualSessionControl()
CountlyConfig.enableManualSessionControlHybridMode()
CountlyConfig.setUpdateSessionTimerDelay(delay: int)
Instance Methods
CountlyInstance.beginSession()
CountlyInstance.updateSession()
CountlyInstance.endSession()
Implementation Details
For config method enableManualSessionControl
CountlyConfig.enableManualSessionControl() // Logic Enables manual session control. When enabled, the SDK does not automatically handle beginning, updating, and ending sessions. The developer must call beginSession, updateSession, and endSession manually.
For config method enableManualSessionControlHybridMode
CountlyConfig.enableManualSessionControlHybridMode() // Logic Enables hybrid manual session control mode. In this mode: - The developer must manually call beginSession and endSession - Session updates (updateSession) are handled automatically by the SDK - This only works when manual session control is also enabled - Calls to updateSession are ignored in hybrid mode
For instance methods beginSession / updateSession / endSession
CountlyInstance.beginSession() CountlyInstance.updateSession() CountlyInstance.endSession() // Logic These methods are only functional when manual session control is enabled. If manual session control is not enabled, calls are ignored with a warning. beginSession: - Requires "sessions" consent and session tracking must be enabled via SDK behavior settings if disabled - If a session is already running, the call is ignored and a health check metric is logged - Prepares device metrics (with metric overrides if set) - Saves any pending user profile data before sending - Sends a begin_session request that includes: metrics, location data (if set), and session flag - If orientation tracking is enabled, records the initial orientation - Marks the session start timestamp for duration calculation - Resets the "first view" flag for view tracking updateSession: - Requires "sessions" consent and session tracking enabled - If no session is running, the call is ignored and a health check metric is logged - Sends a session duration update with seconds elapsed since last update - Saves any pending user profile data before sending - Ignored in hybrid mode (SDK handles updates automatically) endSession: - Requires "sessions" consent - If no session is running, the call is ignored and a health check metric is logged - Calculates final session duration - Sends an end_session request with the remaining duration
General Information
For SDK's to track use sessions, there are 3 kinds of calls:
- begin_session - indicates that a session was just started or the app came to the foreground
- end_session - indicates the the session ended or the app went to the background
- session_duration (update) - indicates that the session is still continuing and records the elapsed time since the last update or begin_session call
SDK sessions can be managed in 2 ways:
- automatically by the SDK (with sufficient integration) - session requests would be sent according to the app lifecycle together with automatic session update requests after periodic timeframes (60 seconds by default)
- manually by the developer - session requests would be triggered by the developer. This includes also the periodic update requests
Networking and Params
Here is the API documentation for sessions:
Starting a Session
The SDK should then send the begin_session=1 param. This same request should also contain metrics parameters.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'begin_session=1' \
--data 'metrics={...}' \
--data ...remaining common paramsSession Update
Each minute of the session should be extended by sending the session_duration param with the number of seconds that passed since the previous session request (begin_session or session_duration, whichever was last). It might look something like:
curl --request POST \ --url 'https://YOUR_SERVER/i' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'session_duration=60' \ --data ...remaining common params
Ending a Session
The SDK should send the end_session=1 param, including the session_duration parameter with how many seconds passed since the last session request (begin_session or session_duration, whichever was last). It might look something like this:
curl --request POST \ --url 'https://YOUR_SERVER/i' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'end_session=1' \ --data 'session_duration=60' \ --data ...remaining common params
Here are a few example requests generated by different session lengths:
begin_session=1&metrics={...} // first begin session request
session_duration=60 // first update session request
session_duration=60 // second update session request
end_session=1&session_duration=30 // end session requestbegin_session=1&metrics={...} // first begin session request
end_sesson=1&session_duration=30 // end session requestStorage
Session are not stored directly. They are packed as request and added to the request queue.
Consent
The feature depends on "sessions" consent.
Automatic Session Tracking
Automatic session tracking is the default behavior. The SDK manages the full session lifecycle, begin, periodic updates, and end, without any explicit calls from the developer. Sessions are tied to the platform's app lifecycle.
How It Works
For apps that have a visual component (not command line or server-side tracking), the SDK should track sessions when the app is visible and in the foreground:
- Session begins when the app enters the foreground (first visible screen)
- Session updates are sent periodically (every 60 seconds by default) while the app remains in the foreground
- Session ends when the app goes to the background (no visible screens remain)
Returning to the foreground after being in the background starts a new session (subject to session cooldown behavior).
Lifecycle Integration
The SDK must integrate with the platform's app lifecycle to trigger session events correctly. Regardless of platform, the core principles are:
- Hook into the platform's lifecycle observers to detect foreground/background transitions.
- On platforms where multiple visual components have independent lifecycles, maintain an internal counter to track active components. A
begin_sessionis sent only on 0 > 1 transition, and anend_sessiononly on 1 > 0. Intermediate transitions must NOT trigger session events. - Where possible, register lifecycle callbacks automatically during initialization. Otherwise, the developer must forward lifecycle events to the SDK manually.
- While a session is active, send periodic
session_durationupdates every 60 seconds (configurable, minimum 1 second). Updates report elapsed seconds since the last session request. The SDK may provide a configuration to disable update requests entirely, sending only begin and end.
Precautions
- Component recreation events (e.g., screen rotation) should NOT trigger session begin/end, the internal counter handles this correctly.
- If the SDK is initialized while the app is already in the foreground, it should detect this and send
begin_sessionimmediately. - Unbalanced lifecycle calls (counter going negative) should be ignored with an error log.
- Consent withdrawal mid-session should end the session immediately. Consent granted while in foreground should begin one automatically.
- Changing the device ID without merge should end the current session and start a new one.
Manual Session Tracking
Most official SDKs implement automatic session handling, meaning SDK users don't need to separately manage session calls. However, SDKs should provide a way to disable automatic session handling and allow developers to make session calls themselves.
Enabling Manual Mode
Manual session control is enabled through a configuration flag enableManualSessionControl during SDK initialization.
When enabled, the SDK will not automatically send any session requests. The developer is fully responsible for calling begin, update, and end session methods.
All manual session methods require "sessions" consent if consent is enabled.
Hybrid Mode
Some SDKs also support a hybrid mode that combines manual session control with SDK-managed periodic updates, enabled via configuration method enableManualSessionControlHybridMode.
In hybrid mode:
- The developer manually calls
beginSession()andendSession() - The SDK automatically sends periodic
session_durationupdates (every 60 seconds by default) - Manual calls to
updateSession()are disabled and will be ignored
This is useful for apps that need explicit control over when sessions start and end but don't want to manage the periodic heartbeat themselves.
Precautions
-
Out-of-order calls: The SDK must handle invalid call sequences gracefully. Calling
updateSession()orendSession()without a priorbeginSession()should be silently ignored with a warning log. CallingbeginSession()while a session is already running should also be ignored. These invalid sequences should be tracked via internal health monitoring. - No automatic lifecycle tracking: When manual mode is enabled, the SDK must not react to app lifecycle events (foreground/background) for session management. Activity lifecycle callbacks (Android) or app state observers should not trigger any session requests.
-
Developer responsibility for updates: In pure manual mode (non-hybrid), the developer is responsible for sending periodic
updateSession()calls. If they forget, the server may record unusually long gaps between session activity. The recommended interval is 60 seconds, matching the automatic mode default. - First view tracking reset: When a session ends (manually or automatically), the SDK should reset the "first view" state so that the next session correctly identifies the first viewed screen.
- Web platform considerations: On web, manual session mode is particularly useful for single-page applications (SPAs) where the developer wants fine-grained control over what constitutes a "session" beyond simple page visibility.
Session Cooldown
In some cases, it is difficult to know for sure if a session has ended, such as with web analytics when a user is leaving the page, and whether they will visit another page or not. This is why there is a small cooldown time of 15 seconds. If the end_session request is sent and then the begin_session request is sent within 15 seconds, it will be counted as the same session, and the session duration will extend this session instead of applying it to the new one.
This makes it easier to call the end_session on each page unload without worrying about starting a new session if the user visits another page.
If you don't need this behavior, simply pass the ignore_cooldown=true parameter to all the session requests and the server will not extend the session. Rather, it will always count it as a new session.
The 15-second cooldown is a default value and may be configured on the server, so don't rely on it being 15 seconds.
Content Zone
Content Builder is a feature that allows displaying server-driven content (HTML-based) to the user. The SDK periodically fetches content from the server and displays it as an overlay on top of the application. This feature is primarily available on mobile platforms (iOS, Android, Flutter, React Native).
Exposed Methods
Config Methods
CountlyConfig.content.setContentCallback(callback: ContentCallback)
CountlyConfig.content.setZoneTimerInterval(zoneTimerIntervalSeconds: int)
Instance Methods
CountlyInstance.enterContentZone()
CountlyInstance.exitContentZone()
CountlyInstance.refreshContentZone()
CountlyInstance.previewContent(contentId: String)
Implementation Details
For config method setContentCallback
CountlyConfig.content.setContentCallback(callback: ContentCallback) // Valid values Value is not nullable, when null value is given it warns // Logic Registers a global callback to be notified about content lifecycle events. The callback is called when a content is completed or closed by the user.
The callback's signature is ContentCallback:
ContentCallback {
void onContentCallback(ContentStatus contentStatus, Map<String, Object> contentData)
}
// Parameters
- contentStatus, one of the values: COMPLETED, CLOSED
- COMPLETED: the user completed the content interaction
- CLOSED: the user dismissed/closed the content
- contentData, associated data for the content eventFor config method setZoneTimerInterval
CountlyConfig.content.setZoneTimerInterval(zoneTimerIntervalSeconds: int) // Valid values Must be greater than 15 seconds, values < 15 are ignored Default value is 30 seconds // Logic Sets the interval for automatic content fetch requests while in a content zone. The SDK will periodically check the server for new content at this interval.
For instance method enterContentZone
CountlyInstance.enterContentZone() // Logic Enables content fetching and updates for the user. - If consent is not granted for "content", the call is omitted - If temporary device ID is enabled, the call is omitted - If already in a content zone (content is currently displayed), the call is omitted - Starts a periodic timer that fetches content from the server at the configured interval - On first call after SDK start, there is a 4 second initial delay if the SDK was just initialized (to allow the app to fully load) - When content is received, it is displayed as an overlay on the current activity/view that should be always at the top - After content is closed, there is a wait period of 2x zone timer interval before the next fetch
For instance method exitContentZone
CountlyInstance.exitContentZone() // Logic Disables content fetching and updates for the user. - Stops the periodic content fetch timer - Removes and destroys any currently displayed content overlay - Resets the content zone state - If consent is not granted for "content", the call is omitted
For instance method refreshContentZone
CountlyInstance.refreshContentZone() // Logic Triggers a manual refresh of the content zone. - If the "rcz" (refresh content zone) SDK behavior setting is disabled, the call is omitted - If content is already being displayed, the call is omitted - Flushes the request queue first, then re-enters the content zone - If consent is not granted for "content", the call is omitted
For instance method previewContent
CountlyInstance.previewContent(contentId: String) // Valid values contentId is not nullable and not empty // Logic Previews a specific content by its ID without starting periodic updates. - Performs a one-time fetch for the given content ID - The content_id and preview=true parameters are added to the request - If consent is not granted for "content", the call is omitted - If temporary device ID is enabled, the call is omitted - If content is already being displayed, the call is omitted
Networking and Params
Content fetch requests consist of the following parameters:
method: this is always "queue"
resolution: JSON object with portrait and landscape screen dimensions
{
"p": {"w": portraitWidth, "h": portraitHeight},
"l": {"w": landscapeWidth, "h": landscapeHeight}
}
Dimensions are in dp (density-independent pixels)
category: array of content categories (can be empty)
la: device language code (e.g., "en", "de")
dt: device type (e.g., "phone", "tablet")
content_id: (optional) specific content ID for preview
preview: (optional) "true" when previewing specific contentContent fetch requests are sent to the "/o/sdk/content" endpoint.
Content fetch requests are directly sent to the server, they will not be added to the request queue.
# Common way to fetch content
curl --request POST \
--url 'https://YOUR_SERVER/o/sdk/content' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'method=queue' \
--data 'resolution={"p":{"w":360,"h":640},"l":{"w":640,"h":360}}' \
--data 'category=[]' \
--data 'la=en' \
--data 'dt=phone' \
--data ...remaining common params
# Preview specific content
curl --request POST \
--url 'https://YOUR_SERVER/o/sdk/content' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'method=queue' \
--data 'resolution={"p":{"w":360,"h":640},"l":{"w":640,"h":360}}' \
--data 'category=[]' \
--data 'la=en' \
--data 'dt=phone' \
--data 'content_id=someContentId' \
--data 'preview=true' \
--data ...remaining common paramsResponse would look like this:
{
"html": "<URL of content>",
"geo": {
"p": {
"x": 0,
"y": 0,
"w": 360,
"h": 640
},
"l": {
"x": 0,
"y": 0,
"w": 640,
"h": 360
}
}
}The response contains "html" (the content to display) and "geo" (placement coordinates for portrait "p" and landscape "l" orientations). Coordinates x, y, w, h are in dp and must be converted to pixels using screen density. The response is validated by checking that both "geo" and "html" fields are present.
Consent
The feature depends on "content" consent.
If consent is revoked, the content zone is exited and any displayed content is removed.
Display Behavior
Content is displayed as a WebView overlay on top of the current activity/screen. The overlay supports two display modes:
- IMMERSIVE (default) - Full screen content using total device dimensions
- SAFE_AREA - Content respects device safe area insets (notch, status bar, etc.)
The overlay handles orientation changes by maintaining separate portrait and landscape configurations. It should be always at the top of all views. If pages changes, overlay should be affected by it.
Overlay should have a mechanism for WebView/iframe to be fully loaded. It should have a mechanism to await all content's resources (mainly js and css). If one of them fails to be loaded or 60 seconds passes while waiting to load content url (or its resources), content must be closed and fetching contents should continue to work. While waiting for content to be fully loaded, SDK should not block UI, underlying app must continue to work (buttons work, UI responsible).
Content overlay is not shown if a feedback widget is currently being displayed. After content is closed, there is a wait period (default ~1 minute based on timer interval) before the next content fetch occurs.
Content Communication Protocol
Content displayed in the overlay (WebView or iframe) communicates with the SDK bidirectionally using a message-passing protocol. This allows server-driven content to trigger SDK actions and receive layout updates from the host environment.
Messages FROM Content TO SDK
The content sends messages to the SDK to trigger actions. Each message is an object with one or more of the following fields:
// Close the content overlay
{
"close": 1
}
// Open a URL in the device browser or a new tab
{
"link": "https://example.com"
}
// Record one or more analytics events
{
"event": [
{ "key": "eventName1", ... },
{ "key": "eventName2", ... }
]
}
// Resize the content overlay to new dimensions (overrides server geo)
{
"resize_me": {
"p": { "x": 0, "y": 0, "w": 360, "h": 400 },
"l": { "x": 0, "y": 0, "w": 640, "h": 300 }
}
}Fields may be combined in a single message. The SDK processes all present fields. Coordinates in resize_me follow the same dp-based format as the server geo response. If close is gotten, other retrieved actions should be done first. Close should take effect as final action.
Messages FROM SDK TO Content
The SDK notifies content of environment changes so it can adapt its layout:
// Sent when the viewport or device orientation changes
{
"type": "resize",
"width": currentViewportWidth,
"height": currentViewportHeight
}Resize messages are debounced (typically ~200ms) to avoid flooding the content on rapid orientation changes.
Origin Validation
The SDK validates the origin of incoming messages against a configurable whitelist. The base SDK server URL is always included in the whitelist. Additional origins can be added at initialization time. Messages from unlisted origins are silently ignored.
Platform-Specific Request Parameters
In addition to the common parameters listed under Networking and Params, some platforms include additional fields in content fetch requests:
// Web Platform only cly_ws: 1 // Identifies request as a web SDK content request cly_origin: window.location.origin // The origin of the page loading content
Device ID Change
On a device ID change without merge, the content zone is exited and any displayed content is removed.
View Tracking
Exposed Methods
Config Methods
CountlyConfig.views.enableAutomaticViewTracking()
CountlyConfig.views.enableAutomaticViewShortNames()
CountlyConfig.views.setAutomaticViewTrackingExclusions(exclusions: List)
CountlyConfig.views.setGlobalViewSegmentation(segmentation: Map<String, Object>)
CountlyConfig.views.setTrackOrientationChanges(enabled: boolean)
CountlyConfig.views.disableViewRestartForManualRecording()
Instance Methods
String CountlyInstance.startAutoStoppedView(viewName: String)
String CountlyInstance.startAutoStoppedView(viewName: String, viewSegmentation: Map<String, Object>)
String CountlyInstance.startView(viewName: String)
String CountlyInstance.startView(viewName: String, viewSegmentation: Map<String, Object>)
CountlyInstance.stopViewWithName(viewName: String)
CountlyInstance.stopViewWithName(viewName: String, viewSegmentation: Map<String, Object>)
CountlyInstance.stopViewWithID(viewID: String)
CountlyInstance.stopViewWithID(viewID: String, viewSegmentation: Map<String, Object>)
CountlyInstance.pauseViewWithID(viewID: String)
CountlyInstance.resumeViewWithID(viewID: String)
CountlyInstance.stopAllViews(viewSegmentation: Map<String, Object>)
CountlyInstance.addSegmentationToViewWithID(viewID: String, viewSegmentation: Map<String, Object>)
CountlyInstance.addSegmentationToViewWithName(viewName: String, viewSegmentation: Map<String, Object>)
CountlyInstance.setGlobalViewSegmentation(segmentation: Map<String, Object>)
CountlyInstance.updateGlobalViewSegmentation(segmentation: Map<String, Object>)
Implementation Details
For config method enableAutomaticViewTracking
CountlyConfig.views.enableAutomaticViewTracking() // Logic Enables automatic view tracking during SDK initialization. - When enabled, the SDK hooks into the platform's lifecycle to detect screen/view changes - Each screen transition automatically starts an auto-stopped view using the screen's identifier (e.g., Activity class name on Android, ViewController name on iOS) - The previous auto-stopped view is automatically stopped when a new screen appears - Automatic views use global view segmentation if set
For config method enableAutomaticViewShortNames
CountlyConfig.views.enableAutomaticViewShortNames() // Logic When automatic view tracking is enabled, this option controls the view name format. - When enabled: uses the short/simple class name (e.g., "MainActivity") - When disabled (default): uses the full qualified name (e.g., "com.example.MainActivity") - Only affects automatically tracked views, not manually started views
For config method setAutomaticViewTrackingExclusions
CountlyConfig.views.setAutomaticViewTrackingExclusions(exclusions: List) // Logic Excludes specific screens/views from automatic view tracking. - Excluded screens will not trigger automatic view start/stop events - Only applies when automatic view tracking is enabled - Useful for splash screens, loading screens, or internal framework screens
For config method setTrackOrientationChanges
CountlyConfig.views.setTrackOrientationChanges(enabled: boolean) // Logic Enables or disables orientation change tracking. Default is true. - When enabled, orientation changes are tracked as "[CLY]_orientation" events - Requires "users" consent (not "views" consent) - The initial orientation is reported on session begin
For config method disableViewRestartForManualRecording
CountlyConfig.views.disableViewRestartForManualRecording() // Logic Prevents manually recorded views from restarting on foreground transitions. - By default, all views (including manual) restart when the app returns to foreground - When this is enabled, only automatically tracked views restart on foreground - Manual views that were stopped on background will NOT be restarted
For instance method startAutoStoppedView
String CountlyInstance.startAutoStoppedView(viewName: String, viewSegmentation: Map<String, Object>)
// Valid values
viewName is not nullable and not empty, non valid values are warned and call returns null
viewSegmentation is nullable
// Logic
Starts a view that will be automatically stopped when a new view starts (auto-stopped view).
- Only one auto-stopped view can be active at a time
- When a new auto-stopped view starts, the previously active auto-stopped view is stopped
- Returns a unique view ID string
- A "[CLY]_view" internal event is recorded with segmentation:
- "name": the truncated view name (truncated by maxKeyLength)
- "visit": "1" (always set on view start)
- "start": "1" (only on the first view in a session)
- "segment": platform name (e.g., "Android", "iOS")
- Plus global view segmentation and custom view segmentation merged
- Reserved segmentation keys ("name", "visit", "start", "segment") cannot be overridden by custom segmentation
- The view ID is used as the event ID for the view event
- Previous view ID and current view ID are tracked and sent with all events
- View segmentation is subject to internal limits (key length, value size, segmentation count)For instance method startView
String CountlyInstance.startView(viewName: String, viewSegmentation: Map<String, Object>) // Valid values viewName is not nullable and not empty, non valid values are warned and call returns null viewSegmentation is nullable // Logic Starts a manually managed view. Multiple manual views can be active simultaneously. - Manual views are NOT auto-stopped when a new view starts - They must be explicitly stopped using stopViewWithID or stopViewWithName - Returns a unique view ID string
For instance method stopViewWithName
CountlyInstance.stopViewWithName(viewName: String, viewSegmentation: Map<String, Object>) // Valid values viewName is not nullable and not empty viewSegmentation is nullable // Logic Stops a view by its name. If multiple views share the same name, the last one started is stopped. - Duration is calculated from the time the view was started - If no view with that name exists, the call is ignored and a warning is logged
For instance method stopViewWithID
CountlyInstance.stopViewWithID(viewID: String, viewSegmentation: Map<String, Object>) // Valid values viewID is not nullable and not empty viewSegmentation is nullable // Logic Stops a view by its view ID (returned from startView/startAutoStoppedView). - Duration is calculated in seconds: currentTimestamp - viewStartTimestamp - If the view was paused (viewStartTimeSeconds == 0), duration is 0 - A "[CLY]_view" event is recorded with the calculated duration and no "visit" key (end event) - Custom segmentation is merged with global view segmentation for the end event - The view entry is removed from the active views map - If no view with that ID exists, the call is ignored and a warning is logged - Requires "views" consent and view tracking must be enabled via SDK behavior settings
For instance method pauseViewWithID
CountlyInstance.pauseViewWithID(viewID: String) // Valid values viewID is not nullable and not empty // Logic Pauses a running view. While paused, the view duration timer stops accumulating. - The view remains in the active views map but does not count time - If the view is already paused or does not exist, the call is ignored
For instance method resumeViewWithID
CountlyInstance.resumeViewWithID(viewID: String) // Valid values viewID is not nullable and not empty // Logic Resumes a previously paused view. The duration timer starts accumulating again. - If the view is not paused or does not exist, the call is ignored
For instance method stopAllViews
CountlyInstance.stopAllViews(viewSegmentation: Map<String, Object>) // Valid values viewSegmentation is nullable // Logic Stops all currently running views, both auto-stopped and manually managed. - Each view's duration is calculated individually - The provided segmentation is added to each stopped view
For instance method addSegmentationToViewWithID
CountlyInstance.addSegmentationToViewWithID(viewID: String, viewSegmentation: Map<String, Object>) // Valid values viewID is not nullable and not empty viewSegmentation is not nullable and not empty // Logic Adds or updates segmentation to an active view identified by its view ID. - The segmentation is merged with the view's existing segmentation in memory - It will be included when the view is stopped
For instance method addSegmentationToViewWithName
CountlyInstance.addSegmentationToViewWithName(viewName: String, viewSegmentation: Map<String, Object>) // Valid values viewName is not nullable and not empty viewSegmentation is not nullable and not empty // Logic Adds or updates segmentation to an active view identified by its name. - If multiple views share the same name, it updates the last one started - The segmentation is merged with the view's existing segmentation in memory
For instance method setGlobalViewSegmentation
CountlyInstance.setGlobalViewSegmentation(segmentation: Map<String, Object>) // Valid values segmentation is not nullable // Logic Replaces the global view segmentation entirely. - Global segmentation is automatically added to all view events - Overwrites any previously set global view segmentation
For instance method updateGlobalViewSegmentation
CountlyInstance.updateGlobalViewSegmentation(segmentation: Map<String, Object>) // Valid values segmentation is not nullable // Logic Merges the provided segmentation with the existing global view segmentation. - New keys are added, existing keys are updated with new values - Keys not in the provided map remain unchanged
General Information
Reporting views allows you to analyze which views/screens/pages were visited by the app user and how long they spent on each. If it is possible to automatically determine when a user visits a specific view on your platform, the SDK should provide an option to automatically track views. Manual view tracking must also be available.
View ID Tracking (pvid/cvid): The SDK maintains a currentViewID and previousViewID internally. When a new view starts, the current view ID becomes the previous view ID. View events include a pvid (previous view ID) field. Non-view events (crashes, custom events, etc.) include a cvid (current view ID) field to track which view the event occurred in.
First View Flag: The "start": "1" segmentation key is only set on the first view recorded in a session, and only if a session is currently running. This flag is reset when a new session begins.
Networking and Params
View data is sent as events through the standard event pipeline. Views use the internal event key [CLY]_view and are included in regular event requests to the "/i" endpoint. No separate view-specific requests are made.
Starting a View
When a view is entered, the SDK sends a view event with visit and segmentation data. The start key is set to 1 only for the first view in a session.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'events=[{"key":"[CLY]_view","count":1,"segmentation":{"name":"VIEW_NAME","visit":1,"view":"END_POINT","domain":"DOMAIN"},"timestamp":TIMESTAMP,"hour":HOUR,"dow":DAYOFTHEWEEK}]' \
--data ...remaining common paramsEnding a View (Duration)
When a view is exited, the SDK sends a view event with the calculated dur (duration in seconds) since the view was started.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'events=[{"key":"[CLY]_view","count":1,"dur":DURATION,"segmentation":{"name":"VIEW_NAME"},"timestamp":TIMESTAMP,"hour":HOUR,"dow":DAYOFTHEWEEK}]' \
--data ...remaining common paramsStorage
Active views are held in memory (not persisted to disk). If the app is terminated unexpectedly, any in-progress view durations are lost. Global view segmentation provided via configuration is stored in memory for the lifetime of the SDK instance.
Consent
The feature depends on "views" consent. Orientation tracking requires "users" consent, not "views" consent. If "views" consent is removed while views are running, all active views are stopped immediately.
Background/Foreground View Management
When the app goes to background, all running views should be stopped (with their accumulated duration) using a willStartAgain flag. When the app returns to foreground, views that were stopped with this flag are automatically restarted. The original start segmentation is preserved and re-applied on restart.
A config option disableViewRestartForManualRecording can be used to prevent automatic restart of manually recorded views on foreground transitions.
When automatic view tracking is enabled, manual startView/stopView calls may be rejected with a warning (platform-specific behavior).
Orientation Tracking
Orientation changes are tracked as a SEPARATE internal event with key [CLY]_orientation, NOT as part of view events. The event has a segmentation of "mode": "portrait" or "mode": "landscape". Orientation tracking requires "users" consent, not "views" consent. The initial orientation is sent on session begin.
View Structure
View information is packaged into events. There are 2 kinds of view events:
- A start event to indicate a view was entered
- An end event to report the duration and indicate the view was exited
All view events use the event key [CLY]_view, are sent with a "count" of 1, and require "views" consent.
Start event segmentation keys:
-
"name"- view name (e.g., "HomeScreen"). Truncated by maxKeyLength. -
"segment"- SDK platform name (e.g., "Android", "iOS", "Web") -
"visit"- always set to"1"on view start -
"start"- set to"1"only for the first view in a session (requires a running session; flag resets when a new session begins)
End event fields:
-
"dur"- view duration in seconds (event-level field, not segmentation) -
"name"- same view name as the start event - No
"visit"or"start"keys on end events
View ID: Each view is assigned a unique ID set as the event-level "id" field (not a segmentation key). The same ID links the start and end events for a given view. The ID format is platform-dependent (typically base64-encoded random bytes + timestamp).
View ID context (pvid/cvid): View events carry a "pvid" (previous view ID) field. Non-view events carry a "cvid" (current view ID) field to associate them with the active view.
Reserved segmentation keys (cannot be overridden by custom segmentation): "name", "visit", "start", "segment", "bounce", "exit", "view", "domain", "dur".
A sample event for reporting the first view:
events=[
{
"key": "[CLY]_view",
"id": "AbCdEf1234567890",
"count": 1,
"pvid": "",
"segmentation": {
"name": "HomeScreen",
"segment": "Android",
"visit": "1",
"start": "1"
}
}
]Sample event for reporting this view's duration:
events=[
{
"key": "[CLY]_view",
"id": "AbCdEf1234567890",
"count": 1,
"dur": 30,
"pvid": "",
"segmentation": {
"name": "HomeScreen",
"segment": "Android"
}
}
]View Manual Reporting
The following section will describe a sample implementation of manual views.
First, you will need to have 2 internal private properties as string lastView and int lastViewStartTime. Then, create an internal private method reportViewDuration, which checks if lastView is null, and if not, it should report the duration for lastView by calculating it based off the current timestamp and lastViewStartTime.
After those steps, provide a reportView method to set the view name as a string parameter inside this method call reportViewDuration to report the duration of the previous view (if there is one). Then set the provided view name as lastView and the current timestamp as lastViewStartTime. Report the view as an event with the visit property and segment as your platform name. Additionally, if this is the first view a user visits in this app session, then also report the start property as true. You will also need to call reportViewDuration with the app exit event.
After manual view tracking has been implemented, you may also implement automatic view tracking (if it is available on your platform). To implement automatic view tracking, you will need to catch your platform's specific event when the view is changed and call your implemented reportView method with the view name.
Additionally, you will need to implement enabling and disabling automatic view tracking, as well as status checking, despite whether automatic view tracking is currently enabled or not.
The pseudo-code to implement view tracking could appear as follows:
class Countly {
String lastView = null;
int lastViewStartTime = 0;
boolean autoViewTracking = false;
private void reportViewDuration(){
if(lastView != null){
//create event with parameters and
//calculating dur as getCurrentTimestamp()-lastViewStartTime
}
}
void onAppExit(){
reportViewDuration();
}
void onViewChanged(String view){
if(autoViewTracking)
reportView(view);
}
public void reportView(String name){
//report previous view duration
reportViewDuration();
lastView = name;
lastViewStartTime = getCurrentTimestamp();
//create event with parameters without duration
// duration will be calculated on next view start or app exit
}
public void setAutoViewTracking(boolean enable){
autoViewTracking = enable;
}
public boolean getAutoViewTracking(){
return autoViewTracking;
}
}Additionally, if your platform supports actions on view, such as clicks, you may report them as well. Here is more information on reporting actions for views.
Device ID Management
Exposed Methods
Config Methods
CountlyConfig.setDeviceId(deviceId: String)
CountlyConfig.enableTemporaryDeviceIdMode() // rename suggestion: enableOfflineMode()
CountlyConfig.enableClearStoredDeviceId()
Instance Methods
CountlyInstance.setID(deviceId: String)
String CountlyInstance.getID()
DeviceIdType CountlyInstance.getType()
CountlyInstance.enableTemporaryIdMode() // rename suggestion: enableOfflineMode()
Advanced Instance Methods
CountlyInstance.changeWithoutMerge(deviceId: String)
CountlyInstance.changeWithMerge(deviceId: String)
Implementation Details
For config method setDeviceId
CountlyConfig.setDeviceId(deviceId: String) // Valid values deviceId is nullable. Null means the SDK will fall back to generating a UUID. // Logic Sets the device ID to use during SDK initialization. - If a valid string is provided, the SDK uses it as a DEVELOPER_SUPPLIED device ID - If null is provided, the SDK generates an SDK_GENERATED UUID - This only takes effect during init. If a stored device ID already exists (from a previous session), the stored value takes precedence unless the "clear stored device ID" flag is set - Has no effect if called after SDK initialization
For config method enableTemporaryDeviceIdMode (rename suggestion: enableOfflineMode)
This mode is functionally an offline mode, the SDK records all data locally but sends nothing to the server until a real device ID is provided. The name "temporary device ID mode" describes the implementation detail; "offline mode" better communicates the intent to integrators.
CountlyConfig.enableTemporaryDeviceIdMode() // rename suggestion: enableOfflineMode() // Logic Enables offline / temporary device ID mode at initialization time. - When enabled (and no custom device ID is provided via config), the SDK starts in offline mode (temporary device ID mode) - All requests created during this mode use a placeholder ID (e.g., "CLYTemporaryDeviceID") and are queued but NOT sent to the server - Once a real device ID is provided at runtime (via setID, changeWithoutMerge, or changeWithMerge), the placeholder is replaced in all queued requests and they are sent - If a custom device ID is also provided via setDeviceId, the custom ID takes precedence and offline mode is not entered
For config method enableClearStoredDeviceId
CountlyConfig.enableClearStoredDeviceId() // Logic Instructs the SDK to clear the persisted device ID during initialization. - When enabled, the SDK performs the following during init: 1. Retrieves the previously stored device ID (if any) 2. Temporarily uses it to flush any pending events from the previous session 3. Clears the stored device ID, device ID type, and session data from storage 4. Proceeds with normal device ID acquisition (either from config or SDK-generated) - If offline mode (temporary ID) was active in the previous session, the clearing logic is skipped to avoid sending orphaned offline data - This effectively forces a fresh start: the next init will either use a config-provided device ID or generate a new one - Useful for testing, privacy-sensitive flows, or forced user resets
For instance method setID
This is the recommended method for changing the device ID at runtime. It automatically determines whether to merge or not based on the current device ID type, so most developers do not need to think about merge semantics.
CountlyInstance.setID(deviceId: String) // Valid values deviceId is not nullable, null values are warned and ignored // Logic Sets the device ID, automatically selecting the correct merge behavior: - If the current device ID type is DEVELOPER_SUPPLIED: changes without merge (treats it as a user switch, ends session, clears consent, starts fresh) - If the current device ID type is SDK_GENERATED: changes with merge (associates the anonymous profile with the known user) - If the provided ID is the same as the current ID, the call is silently ignored
For advanced instance method changeWithoutMerge
Advanced, most integrations should use setID() instead. Use this only when you explicitly need to force a non-merge device ID change regardless of the current device ID type.
CountlyInstance.changeWithoutMerge(deviceId: String) // Valid values deviceId is not nullable, null values are warned and ignored // Logic Changes the current device ID to the provided one without server-side merge. - If the provided ID is the same as the current ID, the call is silently ignored - If already in temporary ID mode and temp ID is provided, the call is silently ignored - Flushes any pending user profile data before proceeding - Ends the current session (if running and manual session control is NOT enabled) - REMOVES ALL CONSENT without sending consent-removal requests to the server. This is critical: the consent-change source is marked as DeviceIDChangedNotMerged so that no consent requests are sent to server. - Changes the stored device ID - Clears remote config values (or marks as cached if caching enabled) - Clears content zone state - Clears all started timed events - Begins a new session with the new device ID - Triggers automatic remote config download if enabled - Triggers SDK behavior settings fetch - Notifies all modules of the device ID change (without merge) - Does NOT send an old_device_id parameter (no merge on server)
For advanced instance method changeWithMerge
Advanced, most integrations should use setID() instead. Use this only when you explicitly need to force a merge regardless of the current device ID type.
CountlyInstance.changeWithMerge(deviceId: String) // Valid values deviceId is not nullable, null values are warned and ignored // Logic Changes the current device ID to the provided one with server-side merge. - Changes the stored device ID - If the provided ID is the same as the current ID, the call is silently ignored - Sends a special request with "device_id=<new>" and "old_device_id=<old>" This tells the server to merge the old and new user profiles - Does NOT end/restart the session (session continues with new ID) - Does NOT remove consent (consent state is preserved) - Triggers automatic remote config download if enabled - Triggers SDK behavior settings fetch - Notifies all modules of the device ID change (with merge)
For instance method getID
String CountlyInstance.getID() // Logic Returns the current device ID used by the SDK.
For instance method getType
DeviceIdType CountlyInstance.getType() // Logic Returns the type of the current device ID. Possible values: DEVELOPER_SUPPLIED, SDK_GENERATED, TEMPORARY_ID
For instance method enableTemporaryIdMode (rename suggestion: enableOfflineMode)
Enters offline mode at runtime. Same behavior as the config-time equivalent, but can be triggered after initialization (e.g., on user logout).
CountlyInstance.enableTemporaryIdMode() // rename suggestion: enableOfflineMode() // Logic Puts the SDK into offline mode (temporary device ID mode). - All requests are created with a special temporary device ID placeholder - Requests are stored in the queue but NOT sent to the server - Feedback widget presentation and remote config downloads are blocked - Content zone enters/refreshes are blocked - SDK behavior settings fetches are omitted - Health check sending is omitted - When a real device ID is later provided (via setID, changeWithoutMerge, or changeWithMerge): all queued requests have their temporary ID replaced with the real ID, then sent - If the SDK initializes with a custom device ID while offline mode was active from a previous session, it exits offline mode automatically at init - Replacement mechanism: iterate stored requests, find "&device_id=<tempPlaceholder>", replace with "&device_id=<realId>". - On init, orphaned temp ID requests are cleaned up even if offline mode is not active
General Information
During the first SDK initialization, the SDK should acquire a device ID. It is a String value that will identify the user.
Device ID is either generated by the SDK or provided by the integrator as part of init configuration. The standard generation algorithm is UUID v4. If on a specific platform there are multiple ways a device ID could be generated, it should be possible to specify which method should be used.
To understand which configuration options take precedence during init, have a look at the below table.
Whatever value is acquired, it should be stored persistently. If another value was already acquired before, that one should be used unless the enableClearStoredDeviceId config flag is set. When enabled, the SDK clears the persisted device ID during init (after flushing any pending events) and proceeds with fresh device ID acquisition.
When acquiring a device ID, the SDK should take note of the source of the ID. It should know if it is SDK generated or provided by the developer.
Device ID State Management During Init
There are different state combinations possible during init. This table covers all possible combinations and should be looked as a "truth table" of how the SDK should function.
| Stored Device ID | Provided ID During Init | Action Taken by SDK | |||||
| Non-Temp ID | Temp ID | Custom ID | Temp ID | URL Provided | 'clear Stored Device ID' flag is not set | 'clear Stored Device ID' flag is set | |
| First Init | - | - | - | SDK generates ID | SDK generates ID | ||
| First Init | ⬤ | - | - | Sets custom ID | Sets custom ID | ||
| First Init | - | ⬤ | - | Enters Temp mode | Enters Temp mode | ||
| First Init | - | - | ⬤ | Sets URL Provided ID | Sets URL Provided ID | ||
| First Init | ⬤ | ⬤ | - | Sets custom ID | Sets custom ID | ||
| First Init | ⬤ | - | ⬤ | Sets URL Provided ID | Sets URL Provided ID | ||
| First Init | - | ⬤ | ⬤ | Sets URL Provided ID | Sets URL Provided ID | ||
| First Init | ⬤ | ⬤ | ⬤ | Sets URL Provided ID | Sets URL Provided ID | ||
| ⬤ | - | - | - | - | Uses Stored ID | SDK generates ID | |
| ⬤ | - | ⬤ | - | - | Uses Stored ID | Sets custom ID | |
| ⬤ | - | - | ⬤ | - | Uses Stored ID | Enters Temp mode | |
| ⬤ | - | - | - | ⬤ | Uses Stored ID | Sets URL Provided ID | |
| ⬤ | - | ⬤ | ⬤ | - | Uses Stored ID | Sets custom ID | |
|
⬤ |
- | ⬤ | - | ⬤ | Uses Stored ID | Sets URL Provided ID | |
|
⬤ |
- | - | ⬤ | ⬤ | Uses Stored ID | Sets URL Provided ID | |
|
⬤ |
- | ⬤ | ⬤ | ⬤ | Uses Stored ID | Sets URL Provided ID | |
| - | ⬤ | - | - | - | Stays in Temp mode | SDK generates ID | |
| - | ⬤ | ⬤ | - | - |
Sets custom ID, exits Temp mode |
Sets custom ID | |
| - | ⬤ | - | ⬤ | - | Stays in Temp mode | Enters Temp mode | |
| - | ⬤ | - | - | ⬤ |
Sets URL provided ID, exits Temp mode |
Sets URL Provided ID | |
| - | ⬤ | ⬤ | ⬤ | - |
Sets custom ID, exits Temp mode |
Sets custom ID | |
| - | ⬤ | ⬤ | - | ⬤ | Sets URL provided ID, exits Temp mode | Sets URL Provided ID | |
| - | ⬤ | - | ⬤ | ⬤ | Sets URL provided ID, exits Temp mode | Sets URL Provided ID | |
| - | ⬤ | ⬤ | ⬤ | ⬤ | Sets URL provided ID, exits Temp mode | Sets URL Provided ID | |
Changing Device ID
In addition to initialization, developers may need to change the device ID while the app is running. For example, when an end-user signs out and another end-user signs in. The SDK must provide a way to change the device ID at any point while the app is running.
This change can be done with or without a server-side merge. If the new and current device ID are exactly the same, the SDK must ignore the call. If an invalid value (empty or null) is provided, the request is ignored with a warning.
Without Merging
Replaces the internally used device ID with the new one and uses it for all new requests, persistently storing it for further sessions. Steps:
- Add currently recorded, but not queued, events to the request queue
- End the current session
- Clear all started timed-events
- Change the device ID and store it persistently
- Begin a new session with the new device ID
With Merging
Allows changing a device ID to an internal user ID while merging server-side data from the unauthenticated session. Steps:
- Temporarily keep the current device ID
- Change the device ID and store it persistently
- Use the old_device_id API with the old device ID to merge the data on the server
- No need to end/restart the current session or clear started timed-events
Retrieving the Current Device ID and Type
There should be a call that returns the currently used device ID (e.g., "GetDeviceId()") and another that returns the current device ID type (e.g., "GetDeviceIdType()").
There are 3 device ID types:
* SDK_GENERATED - generated by the SDK.
* DEVELOPER_SUPPLIED - provided by the developer, either during init or by changing the device ID after init.
* TEMPORARY_ID - the SDK is in offline mode (temporary ID mode).
Offline Mode (Temporary ID Mode)
Also known as "temporary device ID mode". The name "offline mode" is preferred as it better describes the behavior from the integrator's perspective: the SDK records all data locally but sends nothing to the server until a real device ID is provided.
When the SDK enters offline mode, all requests are queued under a temporary ID placeholder. Once a real device ID is acquired (via setID() or the advanced changeWithoutMerge/changeWithMerge methods), the placeholder is replaced in all queued requests and they are sent.
Storage
The device ID and its type must be stored persistently using the platform's preferred storage mechanism (e.g., SharedPreferences, UserDefaults, localStorage). On each subsequent initialization, the stored device ID takes precedence over any config-provided value (unless the "clear stored device ID" flag is set).
Consent
Device ID management itself does not require a specific consent feature group. However, changing the device ID without merge removes all granted consent without sending consent-removal requests to the server. This ensures the new user profile starts with a clean consent state. Changing the device ID with merge preserves the current consent state.
Push Notifications
Exposed Methods
Config Methods
CountlyConfig.push.setMessagingProvider(provider: MessagingProvider)
CountlyConfig.push.setTestMode(mode: PushTestMode)
CountlyConfig.push.setSendPushTokenAlways(enabled: boolean)
CountlyConfig.push.setNotificationActionHandler(handler: PushActionHandler)
CountlyConfig.push.setAllowedIntentTargets(allowList: List<String>)
Instance Methods
CountlyInstance.askForNotificationPermission()
CountlyInstance.askForNotificationPermission(options, completionHandler)
CountlyInstance.onTokenRefresh(token: String, provider: MessagingProvider)
CountlyInstance.recordPushNotificationToken()
CountlyInstance.clearPushNotificationToken()
Implementation Details
For config method setMessagingProvider
CountlyConfig.push.setMessagingProvider(provider: MessagingProvider) // Logic Sets the push messaging provider for token acquisition. - On platforms with multiple push providers, specifies which service to use - The provider value is sent to the server as "token_provider" in the token session request
For config method setTestMode
CountlyConfig.push.setTestMode(mode: PushTestMode) // Valid values mode: DEVELOPMENT (1), TEST_DISTRIBUTION (2), or PRODUCTION (0, default) // Logic Controls the push gateway environment. - DEVELOPMENT (1): uses sandbox/development push gateway - TEST_DISTRIBUTION (2): uses production gateway but marks as test - PRODUCTION (0): default, uses production push gateway - Sent as "test_mode" parameter in the token session request
For config method setSendPushTokenAlways
CountlyConfig.push.setSendPushTokenAlways(enabled: boolean) // Logic When enabled, the SDK sends the push token to the server even if the user has not granted notification permission. This is useful for silent/data notifications that do not require display permission. Default is false.
For config method setNotificationActionHandler
CountlyConfig.push.setNotificationActionHandler(handler: PushActionHandler) // Logic Sets a custom handler for notification action button URLs. - The handler is called when a user taps a notification body or action button - If the handler returns true, the SDK skips its default URL opening behavior - If the handler returns false or is not set, the SDK opens the URL via the platform's default URL opening mechanism
For config method setAllowedIntentTargets
CountlyConfig.push.setAllowedIntentTargets(allowList: List<String>) // Logic Configures an allow list for push notification action targets. - When set, the SDK enforces additional security checks before launching any component or URL from a push notification - Only targets matching entries in the allow list are processed - This mitigates exploits where a malicious notification payload could launch arbitrary components or redirect to untrusted destinations
For instance method askForNotificationPermission
CountlyInstance.askForNotificationPermission() CountlyInstance.askForNotificationPermission(options, completionHandler) // Logic Requests notification permission from the user via the platform's permission system. - On success, automatically registers for remote notifications and obtains a push token - The overload with options allows specifying permission types (alert, badge, sound, etc.) - Requires "push" consent if consent is enabled - Once permission is granted, the token is automatically sent to the server
For instance method onTokenRefresh
CountlyInstance.onTokenRefresh(token: String, provider: MessagingProvider) // Logic Called when the push messaging service provides a new or refreshed token. - The developer must call this from the platform's token refresh callback - The token is sent to the server via a token session request - Debounced: if the same token/provider is sent within 60 seconds, the call is ignored - Requires "push" consent
For instance method recordPushNotificationToken / clearPushNotificationToken
CountlyInstance.recordPushNotificationToken() CountlyInstance.clearPushNotificationToken() // Logic recordPushNotificationToken: - Manually triggers sending the current push token to the server - Useful after device ID changes or when token needs to be re-registered clearPushNotificationToken: - Sends an empty token string to the server, effectively unregistering the device - The server will no longer send push notifications to this device
General Information
Push notifications are platform-specific. Not all platforms support them, but for those that do, the SDK must handle token acquisition, registration with the server, notification display, and action tracking.
The general push notification flow is:
- Request notification permission from the user (where required by the platform)
- Register with the platform's push service and obtain a device token
- Send the token to the Countly server via a token session request
- When a notification arrives, display it (if applicable) and handle user interactions
- Record a
[CLY]_push_actionevent when the user taps the notification or an action button
Push Notification Payload Structure
Push notification payloads from the Countly server include a container key "c" with Countly-specific data nested inside the platform's native push payload structure.
Countly payload keys (nested under "c"):
"i" - Notification ID (required, used for action tracking)
"l" - Default URL / deep link (opened on notification body tap)
"m" - Media URL (image for rich notifications)
"a" - Attachment URL (rich media attachment)
"b" - Buttons array, each element containing:
"t" - button title
"l" - button link/URLExample payload:
{
"platform_envelope": {
"alert": "Your message",
"badge": 1,
"sound": "default"
},
"c": {
"i": "598e7e0f4d8c000001000001",
"l": "https://example.com",
"m": "https://example.com/image.jpg",
"b": [
{ "t": "Accept", "l": "https://example.com/accept" },
{ "t": "Decline", "l": "https://example.com/decline" }
]
}
}Actioned Events
When a user interacts with a push notification (taps the body or an action button), the SDK records an internal event with the key [CLY]_push_action.
Event segmentation:
-
"i"- the notification ID (fromc.iin the payload) -
"b"- button index (0for notification body tap,1+for action buttons) -
"p"- platform identifier:-
"a"- Android -
"i"- iOS -
"m"- macOS
-
After recording the action event, the SDK opens the associated URL (if any) via the platform's default URL handler. A small delay may be applied before opening the URL to ensure the action event is recorded first.
Action and URL Handling
When a user taps the notification body, the SDK extracts the default URL from c.l. When a user taps an action button, the SDK extracts the URL from the corresponding button entry in c.b.
If a custom action handler is configured via setNotificationActionHandler, it is invoked first. If the handler returns true, the SDK does not open the URL. Otherwise, the SDK opens it via the platform's default mechanism.
Rich Push / Media Attachments
When a media URL is present in the payload (c.m or c.a), the SDK should download the media and attach it to the notification before display. On platforms that support notification service extensions, the media download and attachment happens in the extension process.
Media download should have a reasonable timeout (e.g., 15 seconds) and retry logic (e.g., up to 3 attempts). If the download fails, the notification should still be displayed without the attachment.
Networking and Params
The push token is sent to the server via a dedicated token session request to the "/i" endpoint. This request is separate from begin_session.
Token Session Request
curl --request POST \ --url 'https://YOUR_SERVER/i' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'token_session=1' \ --data 'PLATFORM_token=PUSH_TOKEN' \ --data 'token_provider=PROVIDER' \ --data 'locale=DEVICE_LOCALE' \ --data 'test_mode=TEST_MODE' \ --data ...remaining common params
Token session parameters:
-
token_session=1- indicates this is a token registration request -
PLATFORM_token- the platform-specific token key (e.g.,android_token,ios_token) -
token_provider- push provider (e.g.,FCM,HMS) -
locale- device locale -
test_mode- push gateway mode:0=Production,1=Development,2=Test distribution
Timing: The token session request should be sent with a short delay after begin_session (e.g., 10 seconds) to ensure the session request is processed first. The same token/provider combination should be debounced (e.g., 60-second minimum interval between identical token sends).
Clearing a token: To unregister a device, send a token session request with an empty token string.
Storage
The push token and provider type should be stored persistently so the SDK can resend the token on subsequent initializations if needed. Push action data that arrives before SDK initialization should be cached and processed once the SDK is ready.
Consent
The feature depends on "push" consent. All push operations (permission requests, token sending, action recording, notification display) require this consent. When push consent is granted at runtime, the SDK should attempt to acquire and send the push token immediately.
Recording Location
Exposed Methods
Config Methods
CountlyConfig.setLocation(countryCode, city, gpsCoordinates, ipAddress)
CountlyConfig.disableLocation()
Instance Methods
CountlyInstance.setLocation(countryCode, city, gpsCoordinates, ipAddress)
CountlyInstance.disableLocation(countryCode, city, gpsCoordinates, ipAddress)
Implementation Details
For config method setLocation:
CountlyConfig.setLocation(countryCode, city, gpsCoordinates, ipAddress) // Valid values All non empty String values are valid. All nullable. Only non null, non empty String values are used. Non valid values are warned and ignored. // Logic Overrides the internal location cache with given values. Warns if city is not coupled with country.
For config method setDisableLocation:
CountlyConfig.disableLocation() // Logic Sets location tracking off (in a variable) regardless of setLocation.
For instance method setLocation:
CountlyInstance.setLocation(countryCode, city, gpsCoordinates, ipAddress)
// Valid values
All non empty String values are valid.
All nullable. Only non null, non empty String values are used. At least one non-null value should be provided.
Non valid values are warned and ignored.
countryCode: ISO country code (e.g., "US", "DE")
city: city name (e.g., "New York")
gpsCoordinates: comma-separated latitude and longitude (e.g., "56.42345,123.45325")
ipAddress: IP address string (e.g., "192.168.88.33")
// Logic
Sets location parameters.
- If both city and country code are not provided together, a warning is logged
- If any non-null value is provided, location disabled flag is reset to false
- If begin_session was already sent OR session consent is not given OR session tracking is disabled:
location is sent as a separate request immediately
- Otherwise: location values are stored and sent as part of the next begin_session request
- If the "lt" (location tracking) SDK behavior setting is disabled, the call is ignored
- Requires "location" consent
Can be called before and after init.
Before init:
- overrides the internal location cache with given values.
After init:
- Re-enables location tracking (if disabled)
- overrides the internal location cache with given values.
- records a location request to request queue (omitting IP address) if:
- (a session has started) and (auto sessions are on)
- (no session consent given) or (auto sessions are off)
- re-enabled location tracking
Warns if city is not coupled with country.For instance method disableLocation:
CountlyInstance.disableLocation() // Logic Disables sending of location data. - Sets the location disabled flag to true - Clears all stored location values (country code, city, GPS, IP) - Records an empty location (location="") request to request queue - Requires "location" consent; if consent is not given, the erasure request is still sent but the disable call itself is ignored - During init, if location consent is not given, location erasure is performed automatically
General Information
SDKs should be able to send location information to the server. This information can include:
- Country code
- City name
- Latitude and longitude values
- IP address
Automatic Begin Session requests should behave like this:
1. Should add cached location information to itself if: - (location consent given) and (location tracking is on) 2. Should add location="" to itself if: - (no location consent) or (location tracking off)
SDK records an empty location (location="") request to request queue:
1. At the end of init if: - (no location consent or location tracking off) and (no session consent)
Consent removal/addition will have this implications:
Location consent removal: - records an empty location (location="") request to request queue Location consent given: - no action
IP Address is only processed in the Server when recorded via CountlyConfig.setLocation and sent with begin session request. So any other record location calls will not trigger IP Address geolocation.
Networking and Params
Four location parameter that can be sent to server are:
country_code : Country code in the two-letter, ISO standard city : City name location : Latitude and longitude values separated by a comma, e.g. "56.42345,123.45325" ip_address : IP address
Location requests are sent to the "/i" endpoint.
Location requests should be sent through the request queue.
Empty country code, city and IP address can not be sent (simply omitted).
# Common way to send all curl --request POST \ --url 'https://YOUR_SERVER/i' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'country_code=us' \ --data 'city=panama' \ --data 'location=56.42345,123.45325' \ --data 'ip_address=0.0.0.0' \ --data ...remaining common params # Incase of empty location requests: curl --request POST \ --url 'https://YOUR_SERVER/i' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'location=' \ --data ...remaining common params
Storage
Location information is not stored persistently (in a way that would survive SDK shutdowns and further inits). Location information is stored only in memory as variables. The SDK should cache only the latest location information meaning the provided values overwrite the previously cached values totally (meaning non valid/non provided values clears the cache for those values too.).
Consent
Feature depends on `Location` consent but `session` consent also affects its behavior.
Test scenarios
Some sample situations for handling location:
1) Dev Sets Location Sometime After Init
init without location
begin_session (without location)
setLocation(gps) (location request with gps)
end_session
begin_session (with location - gps)
end_session
begin_session (with location - gps)
end_session
2) Dev Sets Location During Init and a Separate Call
init with location (city, country)
begin_session (with location - city, country)
setLocation(gps) (location request with gps)
end_session
begin_session (with location - gps)
end_session
begin_session (with location - gps)
end_session
3) Dev Sets Location During Init and After begin_session Calls
init with location (city, country)
begin_session (with location - city, country)
setLocation(gps) (location request with gps)
end_session
begin_session (with location - gps)
setLocation(ipAddress) (location request with ipAddress)
end_session
begin_session (with location - ipAddress)
end_session
4) Dev Sets Location Before First begin_session
init with location (city, country)
setLocation(gps, ipAddress) (location request with gps, ipAddress)
begin_session (with location - gps, ipAddress)
setLocation(city, country, gps2) (location request with city, country, gps2)
end_session
begin_session (with location - city, country, gps2)
end_session
Heatmaps
Heatmaps is a Web SDK-only feature that is no longer receiving new updates. Existing functionality will continue to work, but no new enhancements or features will be added. The documentation below reflects the current stable implementation.
Exposed Methods
Config Methods
CountlyConfig.heatmaps.enableClickTracking()
CountlyConfig.heatmaps.enableScrollTracking()
CountlyConfig.heatmaps.setHeatmapWhitelist(domains: List<String>)
Instance Methods
CountlyInstance.trackClicks()
CountlyInstance.trackScrolls()
Implementation Details
For config method enableClickTracking
CountlyConfig.heatmaps.enableClickTracking() // Logic Enables automatic click tracking for heatmap data. - Attaches a click event listener to the document - Records each click as a "[CLY]_action" internal event with type "click" - A 1-second cooldown is applied between tracked clicks to reduce traffic - Captures: x, y coordinates, viewport width/height, and current view URL
For config method enableScrollTracking
CountlyConfig.heatmaps.enableScrollTracking() // Logic Enables automatic scroll tracking for heatmap data. - Tracks the maximum scroll depth (y position) within each view - Records a "[CLY]_action" internal event with type "scroll" when: - A new view is navigated to, or - The page/app is closed - Only the maximum scroll depth is recorded, not every scroll event - Captures: max y position, viewport width/height, and current view URL
For config method setHeatmapWhitelist
CountlyConfig.heatmaps.setHeatmapWhitelist(domains: List<String>) // Valid values List of domain URL strings, not nullable // Logic Sets a whitelist of trusted domains for loading heatmap overlay scripts. - The Countly server URL is always included by default - When the heatmap overlay is triggered, the SDK only loads scripts from whitelisted domains - This prevents XSS attacks through malicious heatmap overlay payloads
General Information
Heatmaps is a plugin, exclusive to Web SDK, that can display the amalgamation of click and the scroll behavior of the users by attaching an overlay on the website that the Countly has integrated. Click and scroll behavior information is provided by the SDK and recorded as an event, internally, then sent into the event queue, if click or scroll tracking is enabled.
Automatic click and scroll tracking can be enabled during init or after init:
Countly.q.push(['track_scrolls']); Countly.q.push(['track_clicks']);
Countly.track_scrolls(); Countly.track_clicks();
Heatmap Overlay: A user should be able to go to the Heatmaps section in their server and click on a heatmap from the available list of views. When a user clicks any view from that list the server generates a token and directs the user to that view. The server provides the token and the necessary scripts to load as the heatmaps overlay, by setting the name property of the browser's window interface or the URL hash property to an SDK recognizable message which then should be parsed and used by the SDK to load the necessary scripts for the heatmaps overlay. The message starts with 'cly:' and includes 'app_key', 'token', 'purpose', and 'url' properties as a JSON object.
XSS Prevention: To prevent XSS attempts, the SDK should provide an option to the developer to give a list of trustable domains (a whitelist) for which the SDK would load the provided scripts from and would reject to load scripts from domains outside this list. By default the Countly server URL is included in this list. The list should be provided by the user during init, under the 'heatmap_whitelist' flag as an array of strings.
Countly.app_key = "YOUR_APP_KEY"; Countly.url = "https://try.count.ly"; Countly.heatmap_whitelist = ["https://you.domain1.com", "https://you.domain2.com"];
Countly.init({
app_key:"YOUR_APP_KEY",
url: "https://try.count.ly",
heatmap_whitelist: ["https://you.domain1.com", "https://you.domain2.com"]
});Tracking Clicks
A sample click event:
{
"key":"[CLY]_action",
"count":1,
"segmentation":{
"type":"click",
"x":120,
"y":200,
"width":1920,
"height":1200,
"view":"https://sth.com"
}
}For click tracking it is better to set a cool-down period of 1 second after a click has been recorded to reduce the traffic before another click can be tracked.
Tracking Scrolls
A sample scroll event:
{
"key":"[CLY]_action",
"count":1,
"segmentation":{
"type":"scroll",
"y":500,
"width":1920,
"height":1200,
"view":"https://sth.com"
}
}Scroll height must be stored internally in memory and with each new scroll within the same view this value must be referred to again to find the max scroll height. When a new view happens or the site is closed this max value must be recorded as the 'y' value like shown. You would not record the 'x' value.
Networking and Params
Heatmap data is sent as events through the standard event pipeline. Both click and scroll events use the internal event key [CLY]_action and are included in regular event requests to the "/i" endpoint. No separate heatmap-specific requests are made.
Click event structure:
event: {
"key": "[CLY]_action",
"count": 1,
"segmentation": {
"type": "click",
"x": X_COORDINATE,
"y": Y_COORDINATE,
"width": VIEWPORT_WIDTH,
"height": VIEWPORT_HEIGHT,
"view": "CURRENT_VIEW_URL"
}
}Scroll event structure:
event: {
"key": "[CLY]_action",
"count": 1,
"segmentation": {
"type": "scroll",
"y": MAX_SCROLL_Y,
"width": VIEWPORT_WIDTH,
"height": VIEWPORT_HEIGHT,
"view": "CURRENT_VIEW_URL"
}
}Storage
Heatmap data is not stored persistently. Click events are recorded and added to the event queue immediately. Scroll depth (maximum y position) is held in memory per view and flushed to the event queue on view change or page close.
Consent
Click tracking requires "clicks" consent. Scroll tracking requires "scrolls" consent. If consents are enabled, user actions should only be collected if the respective consent is provided, otherwise they should be ignored.
Remote Config
A/B testing and Remote Config are different features, but their SDK-related integration is interlinked.
The SDK's core utility for Remote Config and A/B testing is fetching some values from the server. Only the server decides which values to serve.
Remote Config
Downloaded remote config values have to be stored persistently.
Exposed Methods
Config Methods
CountlyConfig.enableRemoteConfigAutomaticTriggers()
CountlyConfig.enableRemoteConfigValueCaching()
CountlyConfig.remoteConfigRegisterGlobalCallback(callback: RCDownloadCallback)
Instance Methods
CountlyInstance.downloadOmittingKeys(keysToOmit: Array<String>, callback: RCDownloadCallback)
CountlyInstance.downloadSpecificKeys(keysToInclude: Array<String>, callback: RCDownloadCallback)
CountlyInstance.downloadAllKeys(callback: RCDownloadCallback)
Map<String, RCData> CountlyInstance.getValues()
RCData CountlyInstance.getValue(key: String)
CountlyInstance.registerDownloadCallback(callback: RCDownloadCallback)
CountlyInstance.removeDownloadCallback(callback: RCDownloadCallback)
CountlyInstance.clearAll()
Implementation Details
For config method enableRemoteConfigAutomaticTriggers
CountlyConfig.enableRemoteConfigAutomaticTriggers() // Logic Enables automatic download of the remote config values (in a variable) This will trigger remote config values to be downloaded in those situations: - After a device id change, values first cache cleared then downloaded again - After exiting from temporary device id mode, values are downloaded - After enrolling into a variant, values are cache cleared and downloaded again - After remote config consent is given after init, values are downloaded - After init is finished if we are not in the temporary id mode, values are downloaded
For config method enableRemoteConfigValueCaching
CountlyConfig.enableRemoteConfigValueCaching() // Logic Enables caching of remote config values. When this is enabled all remote-config values are not deleted.
For config method remoteConfigRegisterGlobalCallback
CountlyConfig.remoteConfigRegisterGlobalCallback(callback: RCDownloadCallback) // Valid values Value is not nullable, when null value is given it warns // Logic Notifies the developer about remote config values update process. - If any error is encountered while downloading, the error is returned - On a successful download, downloaded values are returned
The callback's signature is RCDownloadCallback. And its callback method is
RCDownloadCallback {
void callback(RequestResult requestResult, String error, boolean fullValueUpdate, Map<String, RCData> downloadedValues)
}
// Parameters
- requestResult, one of the values Success, NetworkIssue, Error indicates result of the download
- error, error message. If it is null, it means there is no error
- fullValueUpdate, "true" - all values updated, "false" - a subset of values updated
- downloadedValues, the whole downloaded RC set, the delta
RCData will be mentioned in the Storage section.For instance method downloadAllKeys
CountlyInstance.downloadAllKeys(callback: RCDownloadCallback) // Valid values All nullable, only non null values are accepted Non valid values are warned and ignored // Logic Before preparing the request, the function will do some checks - If no device id exists, the call is omitted. Given and registered callbacks are notified. - If temporary device id enabled or request queue is containing temporary id requests, Given and registered callbacks are notified. After those checks, SDK will prepare the fetch request using the device metrics. Then it will send the request immediately without adding it to the request queue. - If any error encountered while request sent or retrieved, given and registered callbacks are notified. The function will parse the response and notify the given and registered callbacks. And the remote config store is updated. We clear all values. If remote config value caching is enabled we cache them rather than clearing them.
For instance method downloadOmittingKeys
CountlyInstance.downloadOmittingKeys(keysToInclude: Array<String>, callback: RCDownloadCallback) // Valid values All nullable, only non null and not empty values are accepted Non valid values are warned and ignored // Logic - If keysToOmit is not empty, a parameter will be added to omit some keys to the remote config fetch request. The server will return all values except omitted ones. - If the keysToOmit is empty, this function will behave like downloading all values. We only update the keys in the storage that are changed.
For instance method downloadSpecificKeys
CountlyInstance.downloadSpecificKeys(keysToInclude: Array<String>, callback: RCDownloadCallback) // Valid values All nullable, only non null and not empty values are accepted Non valid values are warned and ignored // Logic - If keysToInclude is not empty, a parameter will be added to include some keys to the remote config fetch request. Server will return only specified values. - If the keysToInclude is empty, this function will behave like downloading all values. We only update the keys in the storage that are changed.
For instance method getValues
Map<String, RCData> CountlyInstance.getValues() // Logic This will get all saved remote config values from the storage
For instance method getValue
RCData CountlyInstance.getValue(key: String) // Valid values Only non null and not empty key is accepted If non valid key is given, the call will be omitted. // Logic This will get the remote config value of the given key if it exists
For instance method registerDownloadCallback
CountlyInstance.registerDownloadCallback(callback: RCDownloadCallback) // Valid values Only non null callback is accepted // Logic This will add the given callback to the internal callback list
For instance method removeDownloadCallback
CountlyInstance.removeDownloadCallback(callback: RCDownloadCallback) // Valid values Only non null callback is accepted // Logic This will remove given callback from the internal callback list
For instance method clearAll
CountlyInstance.clearAll() // Logic This will clear all remote config storage; caching is not important here
General Information
Remote Config allows app developers to change the behavior and appearance of their applications at any time by creating or updating custom key-value pairs on the Countly Server. The SDK fetches these values and stores them locally for the developer to use.
Downloaded remote config values must be stored persistently. When values are fetched, the behavior depends on the fetch type:
- Full download (downloadAllKeys): clears all existing values (or caches them if caching is enabled) and replaces with the new set
- Partial download (downloadSpecificKeys / downloadOmittingKeys): only updates the keys present in the response, leaving other stored keys unchanged
Remote config values are fetched via direct requests to the server (not through the request queue). This is because the developer typically needs the response immediately to configure the app's behavior.
Automatic triggers (when enabled) cause the SDK to re-download remote config values after events such as: device ID changes, exiting temporary ID mode, enrolling into a variant, consent being granted, and SDK initialization.
Value caching (when enabled) preserves old values when a full download occurs. Cached values are marked with isCurrentUsersData = false to indicate they may belong to a previous user. Fresh values are marked with isCurrentUsersData = true.
Networking and Params
Remote config fetch request might consist of 4 parameters:
keys: json array of specific keys omit_keys: json array of omitted keys method: this is always "rc" metrics: this is session metrics and they are added if session consent is given
method parameters is always sent with remote config fetch request.
keys and omit_keys are optional.
Remote config fetch requests are sent to the "/o/sdk" endpoint (NOT /i).
Remote config fetch requests are directly sent to the server, they will not be added to the request queue.
# Common way to send all
curl --request POST \
--url 'https://YOUR_SERVER/o/sdk' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'method=rc' \
--data 'keys=["url", "limit"]' \
--data 'omit_keys=["money"]' \
--data 'metrics={...}' \ # if session consent is given
--data ...remaining common params
# No key parameters only required ones
curl --request POST \
--url 'https://YOUR_SERVER/o/sdk' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'method=rc' \
--data 'metrics={...}' \ # if session consent is given
--data ...remaining common paramsResponse would look like this:
{
"key": "value",
"key1": "value2"
}Storage
Remote config values are stored persistently. And they should have a structure.
There might be a remote config key-value store structure that is keeping track of the remote config values.
Any key-value storage is accepted. For reference JSON could be used.
{
"key": {
"v": "value",
"c": 0
}
}c parameter indicates cache. When a remote config value is cached, c value must be 0. 1 means it is fresh, new value.
RCData object contains two fields:
value: any type isCurrentUsersData: boolean
isCurrentUsersData parameter is calculated like this; if the value is cached it is old user's data, false. If the value is fresh, just downloaded, then it is true.
Consent
The feature depends on "remote-config" consent, and "session" consent affects the remote config fetch request.
If consent is given, it will trigger remote config values to be downloaded if consents are enabled.
If consent is revoked and remote config storage is not cleared, they will stay as they are.
A/B Testing
Users can participate in AB tests. For that to happen, they must enroll in those tests (keys) with specific requests.
Exposed Methods
Config Methods
CountlyConfig.enrollABonRCDownload()
Instance Methods
Map<String, RCData> CountlyInstance.getAllValuesAndEnroll()
RCData CountlyInstance.getValueAndEnroll(key: String)
CountlyInstance.enrollIntoABTestsForKeys(keys: Array<String>)
CountlyInstance.exitABTestsForKeys(keys: Array<String>)
Map<String, String[]> CountlyInstance.testingGetAllVariants()
Map<String, ExperimentInformation> CountlyInstance.testingGetAllExperimentInfo()
String[] CountlyInstance.testingGetVariantsForKey(key: String)
CountlyInstance.testingDownloadVariantInformation(completionCallback: RCVariantCallback)
CountlyInstance.testingDownloadExperimentInformation(completionCallback: RCVariantCallback)
CountlyInstance.testingEnrollIntoVariant(keyName: String, variantName: String, completionCallback: RCVariantCallback)
Implementation Details
For config method enrollABonRCDownload
CountlyConfig.enrollABonRCDownload()
// Logic Enables automatic enrolling to the remote config keys while fetching. This will trigger adding a parameter to the remote config fetch request
For instance method getAllValuesAndEnroll
Map<String, RCData> CountlyInstance.getAllValuesAndEnroll() // Logic This will get all saved remote config values from the storage and will call the enrollIntoABTestsForKeys method.
For instance method getValueAndEnroll
RCData CountlyInstance.getValueAndEnroll(key: String) // Valid values Only non null and not empty key is accepted If non valid key is given, the call will be omitted. // Logic This will get the remote config value of the given key if it exists and will call the enrollIntoABTestsForKeys method for the specified key if the key exist.
For instance method enrollIntoABTestsForKeys
CountlyInstance.enrollIntoABTestsForKeys(keys: Array<String>) // Valid values Only non null and not empty keys are accepted Non valid keys are warned and ignored // Logic This call will enroll user to the AB tests for the specified keys. - If keys are null or empty call will be omitted. - If temporary device id enabled or request queue is containing temporary id requests or device id is not reachable at the moment function is called, call will be omitted.
For instance method exitABTestsForKeys
CountlyInstance.exitABTestsForKeys(keys: Array<String>) // Valid values Keys could be nullable // Logic This call will exit user from the AB tests for the specified keys. - If keys are null or keys are empty, it means exiting from all of AB tests. - If temporary device id enabled or request queue is containing temporary id requests or device id is not reachable at the moment function is called, call will be omitted.
For instance method testingGetAllVariants
Map<String, String[]> CountlyInstance.testingGetAllVariants() // Logic Before using this function, variants must be downloaded. This function will return all variants.
For instance method testingGetAllExperimentInfo
Map<String, ExperimentInfo> CountlyInstance.testingGetAllExperimentInfo() // Logic Before using this function, experiment informations must be downloaded. This function will return all experiment informations. ExperimentInfo will be mentioned in the Storage section.
For instance method testingGetVariantsForKey
String[] CountlyInstance.testingGetVariantsForKey(key: String) // Valid values Only non null and not empty key is accepted If non valid key is given, the call will be omitted. // Logic Before using this function, variants must be downloaded. This function will return the variants for the given key. - If no variants found for the specified key, null is returned.
For instance method testingDownloadVariantInformation
CountlyInstance.testingDownloadVariantInformation(completionCallback: RCVariantCallback) // Valid values Callback is nullable // Logic This function will download variants from the server. - If temporary device id enabled or request queue is containing temporary id requests or device id is not reachable at the moment function is called, call will be omitted and callback is notified. After those checks, SDK will prepare the variant fetch request. Then it will send the request immediately without adding it to the request queue. - If any error encountered while request sent or retrieved, given callback is notified. The function will parse the response and replace all variant with the fresh downloaded values.
The completionCallback's signature is RCVariantCallback. And its callback method is
RCVariantCallback {
void callback(RequestResult requestResult, String error)
}
// Parameters
- requestResult, one of the values Success, NetworkIssue, Error indicates result of the download
- error, error message. If it is null, it means there is no errorFor instance method testingDownloadExperimentInformation
CountlyInstance.testingDownloadExperimentInformation(completionCallback: RCVariantCallback) // Valid values Callback is nullable // Logic This function will download experiments from the server. - If temporary device id enabled or request queue is containing temporary id requests or device id is not reachable at the moment function is called, call will be omitted and callback is notified. After those checks, SDK will prepare the experiment fetch request. Then it will send the request immediately without adding it to the request queue. - If any error encountered while request sent or retrieved, given callback is notified. The function will parse the response and replace all experiments with the fresh downloaded values.
For instance method testingEnrollIntoVariant
CountlyInstance.testingEnrollIntoVariant(keyName: String, variantName: String, completionCallback: RCVariantCallback) // Valid values keyName and variantName are not nullable and not empty. completionCallback is nullable. If non valid values are given they are warned and call will be omitted. // Logic This function do some checks before enrolling into specified variant - If temporary device id enabled or request queue is containing temporary id requests or device id is not reachable at the moment function is called, call will be omitted and callback is notified. After those checks, SDK will prepare the enrolling into variant request. Then it will send the request immediately without adding it to the request queue. - If any error encountered while request sent or retrieved, given callback is notified. If the request is successful, the function will trigger downloading remote config values with clearing all remote config values from the storage. And the callback is notified with a Success.
Networking and Params
If the configuration enrollABonRCDownload is called the SDK will add a parameter to the remote config values fetch request: oi=1
# a remote config fetch request curl --request POST \ --url 'https://YOUR_SERVER/o/sdk' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'method=rc' \ --data ...other remote config fetch request params if any \ --data 'oi=1' \ --data ...remaining common params
Here is a casual fetching all variants request, it does not have any other parameter than method:
# a variants fetch request curl --request POST \ --url 'https://YOUR_SERVER/o/sdk' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'method=ab_fetch_variants' \ --data ...remaining common params
And the response would look like this:
{
"key1": [
{
"name": "variant1",
"value": "value1"
},
{
"name": "variant2",
"value": "value2"
}
],
"key2": [
{
"name": "variant1",
"value": "value1"
}
]
}Here is a casual fetching all experiment informations request, it does not have any other parameter than method:
# a experiment information fetch request curl --request POST \ --url 'https://YOUR_SERVER/i' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'method=ab_fetch_experiments' \ --data ...remaining common params
And the response would look like this, this will return a json array:
[
{
"id": "experiment_id_1",
"name": "Experiment Name 1",
"description": "Description for Experiment 1",
"currentVariant": "variant_A",
"variants": {
"variant_A": {
"property_key_1": "property_value_1",
"property_key_2": "property_value_2"
},
"variant_B": {
"property_key_1": "property_value_3",
"property_key_2": "property_value_4"
}
}
},
{
"id": "experiment_id_2",
"name": "Experiment Name 2",
"description": "Description for Experiment 2",
"currentVariant": "variant_B",
"variants": {
"variant_A": {
"property_key_1": "property_value_5",
"property_key_2": "property_value_6"
},
"variant_B": {
"property_key_1": "property_value_7",
"property_key_2": "property_value_8"
}
}
}
]Here is a casual enrolling into a variant request:
# an enrolling into a variant request curl --request POST \ --url 'https://YOUR_SERVER/i' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'method=ab_enroll_variant' \ --data 'key=keyName' \ --data 'variant=variantName' \ --data ...remaining common params
Storage
AB Testing is not stored persistently. Variants and experiment informations are stored in memory in a key-value pair structure like Map.
Variants are stored in a string and string array structured map where key is remote config key name and value is variants name that is specified for that remote config key.
// Key-value structure key1: [variant1, variant2] key2: [variant3, variant4]
Experiment informations are stored in a string and ExperimentInfo structured map where key is experiment id and value is ExperimentInfo structure:
ExperimentInfo:
experimentID: id of the experiment, string
experimentName: name of the experiment, string
experimentDescription: description of the experiment, string
currentVariant: active variant for that experiment, string
variants: variants for that experiment which is a string and string array key-value
structure like key is remote config key name and value is variants name that is
specified for that remote config keyConsent
The feature depends on "remote-config" consent.
If consent is revoked, in memory values are not cleared.
User Feedback
The User Feedback feature provides three feedback widget types, NPS (Net Promoter Score), Survey, and Rating. Widgets are created on the Countly Dashboard and presented to users via WebView or custom UI. Legacy star rating and rating widget APIs are documented under the Star Rating section in Legacy Features.
Exposed Methods
Instance Methods
CountlyInstance.getAvailableFeedbackWidgets(callback: RetrieveFeedbackWidgets)
CountlyInstance.presentFeedbackWidget(widgetInfo: CountlyFeedbackWidget, callback: FeedbackCallback)
CountlyInstance.getFeedbackWidgetData(widgetInfo: CountlyFeedbackWidget, callback: RetrieveFeedbackWidgetData)
CountlyInstance.reportFeedbackWidgetManually(widgetInfo: CountlyFeedbackWidget, widgetData: JSONObject, widgetResult: Map<String, Object>)
CountlyInstance.presentNPS()
CountlyInstance.presentNPS(nameIDorTag: String)
CountlyInstance.presentNPS(nameIDorTag: String, callback: FeedbackCallback)
CountlyInstance.presentSurvey()
CountlyInstance.presentSurvey(nameIDorTag: String)
CountlyInstance.presentSurvey(nameIDorTag: String, callback: FeedbackCallback)
CountlyInstance.presentRating()
CountlyInstance.presentRating(nameIDorTag: String)
CountlyInstance.presentRating(nameIDorTag: String, callback: FeedbackCallback)
Implementation Details
For instance method getAvailableFeedbackWidgets
CountlyInstance.getAvailableFeedbackWidgets(callback: RetrieveFeedbackWidgets) // Consent Requires "feedback" consent. If not given, returns empty list with error. // Preconditions Blocked in temporary device ID mode — returns empty list immediately. // Logic Sends an immediate (non-queued) request to /o/sdk?method=feedback with common params. Parses the JSON response "result" array into a list of CountlyFeedbackWidget objects. Each object maps: _id > widgetId, type > type, name > name, tg > tags[], wv > widgetVersion. Returns the list and null error on success, or empty list and error string on failure.
For instance method presentFeedbackWidget
CountlyInstance.presentFeedbackWidget(widgetInfo: CountlyFeedbackWidget, callback: FeedbackCallback)
// Consent
Requires "feedback" consent.
// Preconditions
widgetInfo must not be null. Blocked in temporary device ID mode.
// Logic
Constructs a WebView URL using the widget type, ID, and SDK params (see Constructing WebView URL).
For widgets WITHOUT wv (widget version):
- Display in a legacy dialog with a native close button provided by the SDK.
- custom JSON param: {"tc":1}
For widgets WITH wv:
- Display in a fullscreen/transparent overlay where the webview handles its own close button.
- custom JSON param: {"tc":1, "xb":1, "rw":1}
The WebView starts invisible; once the page loads, it becomes visible and interactive.
On close: dismiss the overlay and call onClosed().
On error: call onFinished(error).For instance method getFeedbackWidgetData
CountlyInstance.getFeedbackWidgetData(widgetInfo: CountlyFeedbackWidget, callback: RetrieveFeedbackWidgetData)
// Consent
Requires "feedback" consent.
// Preconditions
Blocked in temporary device ID mode.
// Logic
Sends an immediate (non-queued) GET request to:
/o/surveys/{type}/widget?widget_id=[widgetID]&shown=1&sdk_version=V&sdk_name=N&app_version=A&platform=P
The "shown=1" parameter causes the server to count this as a widget impression.
Parses the response and returns it as a JSON object via the callback.
Returns null data and error string on failure.For instance method reportFeedbackWidgetManually
CountlyInstance.reportFeedbackWidgetManually(widgetInfo: CountlyFeedbackWidget, widgetData: JSONObject, widgetResult: Map<String, Object>) // Consent Requires "feedback" consent. // Logic If widgetResult is null: - Record a "closed" event: add "closed":"1" to the segmentation. If widgetResult is not null: - Merge the contents of widgetResult into the event segmentation. Event key is determined by widget type: - NPS: "[CLY]_nps" - Survey: "[CLY]_survey" - Rating: "[CLY]_star_rating" Base segmentation always includes: platform, app_version, widget_id. // Validation NPS rating values must be in range 0-10. Rating values must be in range 1-5. String values are truncated to the maxValueSize limit. // Post-record After adding the event to the event queue, force-flush the queue immediately (combine into a request even if the event count is under the threshold). This ensures the widget result reaches the server promptly.
For more information about manual feedback reporting, you can wander documentation below:
For instance method presentNPS / presentSurvey / presentRating
CountlyInstance.presentNPS() CountlyInstance.presentNPS(nameIDorTag: String) CountlyInstance.presentNPS(nameIDorTag: String, callback: FeedbackCallback) // presentSurvey and presentRating follow the same overload pattern. // Consent Requires "feedback" consent. // Logic 1. Calls getAvailableFeedbackWidgets to fetch the current widget list. 2. Filters the list by the target type (nps, survey, or rating). 3. If nameIDorTag is provided and non-empty: - Matches against widgetId, then name, then tags (first match wins). 4. If nameIDorTag is empty or not provided: - Selects the first widget of the matching type. 5. Calls presentFeedbackWidget with the matched widget and the provided callback. 6. If no matching widget is found, calls onFinished with an error message.
General Information
Three feedback widget types are supported:
- NPS (Net Promoter Score): A 0-to-10 scale question with optional follow-up.
- Survey: A multi-question form supporting various answer formats (multiple-choice, textbox, rating scales).
- Rating: A 1-to-5 star rating with optional comment and email fields.
Each widget retrieved from the server is represented as a CountlyFeedbackWidget object containing: widgetId, type (nps/survey/rating), name, tags[], and widgetVersion (nullable, parsed from the "wv" field in the server response). When widgetVersion is present, the widget supports the newer fullscreen display mode; when absent, it uses the legacy dialog-based display.
Callback interfaces used throughout this feature:
-
RetrieveFeedbackWidgets— returns a list ofCountlyFeedbackWidgetobjects and an error string. -
RetrieveFeedbackWidgetData— returns a JSON object containing widget data and an error string. -
FeedbackCallback— providesonFinished(String error)(called on error or successful completion; error is null if no issues) andonClosed()(called when the user dismisses the widget).
Feedback widgets can be used through three methods:
- Automatic (server-rendered WebView): The SDK constructs a URL and displays the widget in a WebView. The server renders the widget UI.
-
Manual (custom UI + reportManually): The developer builds a custom UI using widget data and reports results via
reportFeedbackWidgetManually. - Custom WebView URL: The SDK constructs the widget URL, and the developer displays it in their own WebView.
The convenience methods (presentNPS, presentSurvey, presentRating) fetch the widget list, filter by type, and optionally match by widget ID, name, or tag. If nameIDorTag is empty or not provided, the first matching widget of that type is presented.
All feedback widget operations are blocked when a temporary device ID is active — an empty list is returned and present/report calls are no-ops.
WebView Communication Protocol
When displaying feedback widgets via a WebView, the SDK and the widget page communicate through a URL interception mechanism. The widget page triggers navigation to special URLs that the SDK intercepts via the WebView client’s URL loading callback.
The base communication URL is:
https://countly_action_event
All communication URLs from the widget start with this base. The SDK should intercept any URL starting with this prefix, URL-decode it, parse the query parameters, and handle the appropriate action. There are two main command types:
1. Widget Command (close)
Used when the widget’s own close button is pressed (for versioned widgets with xb=1).
https://countly_action_event/?cly_widget_command=1&close=1
When this URL is intercepted, the SDK should:
- Close/dismiss the WebView
- Record a cancel event for the widget (event key
[CLY]_nps,[CLY]_survey, or[CLY]_star_ratingdepending on widget type, with"closed":"1"in segmentation) - Notify the developer callback (
onClosed)
2. Action Event Commands
Used for link navigation.
https://countly_action_event/?cly_x_action_event=1&action=[ACTION_TYPE]&...&close=1
The action parameter determines the action type. The close=1 parameter, when present, indicates the WebView should be dismissed after handling the action.
action=link - Open external URL
https://countly_action_event/?cly_x_action_event=1&action=link&link=[URL]
The SDK should open the provided URL in the system browser / default handler.
3. External Link Interception
In addition to the action=link command above, there is a separate mechanism for external links. Any URL that the WebView tries to navigate to that ends with the query parameter cly_x_int=1 should be intercepted and opened in the system browser application instead of loading within the WebView. This applies to any URL regardless of whether it starts with the communication base URL. For example:
https://example.com/some-page?cly_x_int=1
When the SDK detects a URL ending with cly_x_int=1 in the WebView’s URL loading callback, it should open the full URL in the device’s default browser and prevent the WebView from navigating to it.
4. Page Load Handling
The WebView should start as invisible and non-interactive (not touchable, not focusable). After the page is fully loaded (check document.readyState === ‘complete’ or listen for window load event), the SDK should:
- Make the WebView visible and interactive
- For widgets without
wv(legacy): show in a dialog - For widgets with
wv(versioned): enter immersive/fullscreen mode if applicable
If page loading takes 60 seconds or more, it should be treated as a failure and the WebView should be closed. Critical resource loading errors (js, css, png, jpg, jpeg, webp) or SSL errors should also trigger a failure close.
Constructing WebView URL
Constructing a WebView URL requires a CountlyFeedbackWidget object. Using the widget type and ID, the URL is constructed as follows:
// for NPS /feedback/nps?widget_id=[widgetID]&device_id=[deviceID]&app_key=[appKey]&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]&custom=[customJSON] // for Survey /feedback/survey?widget_id=[widgetID]&device_id=[deviceID]&app_key=[appKey]&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]&custom=[customJSON] // for Rating /feedback/rating?widget_id=[widgetID]&device_id=[deviceID]&app_key=[appKey]&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]&custom=[customJSON]
The custom parameter is a JSON object that controls widget behavior. It always contains {"tc":1} to indicate support for terms and conditions. For widgets that have a wv (widget version) value, the following additional flags are added:
-
xb— set to 1. Indicates the webview handles its own close button, communicated back via the WebView Communication Protocol. -
rw— set to 1. Indicates the webview will utilize fullscreen mode.
For a widget without a version: {"tc":1}
For a widget with a version: {"tc":1, "xb":1, "rw": 1}
Even if parameter tamper protection is enabled, this URL does not use the checksum param.
For widgets without a wv field, the URL can be shown in a native view with a native close button. For widgets with a wv field, it should be displayed in a fullscreen or transparent overlay where the webview handles its own close button via the WebView Communication Protocol.
Consent
Feedback widget operations require "feedback" consent.
Legacy star rating and rating widget operations require "star-rating" consent (see the Star Rating section under Legacy Features).
Networking and Params
1. Fetch widget list
Immediate request (not queued). Returns the list of available feedback widgets for the current device ID.
curl --request POST \ --url ‘https://YOUR_SERVER/o/sdk’ \ --header ‘Content-Type: application/x-www-form-urlencoded’ \ --data ‘method=feedback’ \ --data ...remaining common params
2. Fetch widget data
Immediate GET request (not queued). The shown=1 parameter causes the server to count this as a widget impression.
# type is one of: nps, survey, rating
curl --request GET \
‘https://YOUR_SERVER/o/surveys/{type}/widget?widget_id=WIDGET_ID&shown=1&sdk_version=SDK_VERSION&sdk_name=SDK_NAME&app_version=APP_VERSION&platform=PLATFORM’3. Widget WebView URL
Not a network request per se — this URL is loaded in a WebView to display the widget.
https://YOUR_SERVER/feedback/{nps|survey|rating}?widget_id=WIDGET_ID&device_id=DEVICE_ID&app_key=APP_KEY&sdk_version=SDK_VERSION&sdk_name=SDK_NAME&app_version=APP_VERSION&platform=PLATFORM&custom={"tc":1}4. Report widget result
Reported as an event via the standard /i endpoint. The event key depends on the widget type: [CLY]_nps, [CLY]_survey, or [CLY]_star_rating. The event is force-flushed from the queue immediately.
curl --request POST \
--url ‘https://YOUR_SERVER/i’ \
--header ‘Content-Type: application/x-www-form-urlencoded’ \
--data-urlencode ‘events=[{"key":"[CLY]_nps","count":1,"segmentation":{"widget_id":"WID","platform":"PLATFORM","app_version":"VERSION","rating":8,"comment":"Great"}}]’ \
--data ...remaining common paramsStorage
Feedback widget lists and data are NOT cached. They are fetched fresh from the server every time getAvailableFeedbackWidgets or getFeedbackWidgetData is called. There is no persistent storage for feedback widget state.
Legacy star rating preferences are persisted separately (see the Star Rating section under Legacy Features).
User Profiles
Exposed Methods
Instance Methods
CountlyInstance.setProperty(key: String, value: Object)
CountlyInstance.setProperties(data: Map<String, Object>)
CountlyInstance.increment(key: String)
CountlyInstance.incrementBy(key: String, value: Number)
CountlyInstance.multiply(key: String, value: Number)
CountlyInstance.saveMax(key: String, value: Number)
CountlyInstance.saveMin(key: String, value: Number)
CountlyInstance.setOnce(key: String, value: String)
CountlyInstance.push(key: String, value: String)
CountlyInstance.pushUnique(key: String, value: String)
CountlyInstance.pull(key: String, value: String)
CountlyInstance.save()
CountlyInstance.clear()
General Information
User Profiles allows attaching identity and behavioral data to a device ID. Both predefined standard fields and arbitrary custom properties are supported. All changes are batched in memory and flushed to the server explicitly via save(), or automatically before session and event requests.
All fields are optional. If a null value is provided for any property, the SDK ignores it and logs a warning. Setting a property to an empty string signals deletion — the SDK sends a JSON null for that key so the server removes it.
Custom properties support MongoDB-style modifier operations (increment, multiply, array push/pull, etc.) that are applied server-side. Multiple modifier operations on the same key are accumulated locally and sent together on save().
Implementation Details
For instance method setProperty
CountlyInstance.setProperty(key: String, value: Object) // Valid values key is not nullable or empty value: String, Number, or null (null is ignored with a warning; empty string signals deletion) // Logic Queues a single property change. Delegates to setProperties() internally. Predefined keys are stored at the top level; all other keys are stored under "custom".
For instance method setProperties
CountlyInstance.setProperties(data: Map<String, Object>) // Valid values data is not nullable Keys: predefined field names or custom property keys Values: String, Number, boolean, or null/empty string (see logic) // Predefined keys "name" - full name (String) "username" - username/handle (String) "email" - email address (String) "organization" - company or org name (String) "phone" - phone number (String) "picture" - URL to profile picture (String, limit: 4096 chars) "gender" - gender (String, e.g. "M", "F") "byear" - birth year (Integer) // Logic Queues multiple property changes in a single call. - Predefined string values are truncated to the maxValueSize limit (256 chars by default), except "picture" which allows up to 4096 chars - Null values are ignored with a warning - Empty string value sends JSON null for that key, deleting the property on the server - Custom properties are stored under a nested "custom" JSON object - Changes are NOT sent until save() is called (or triggered automatically) - User property keys are validated against upb/upw filter lists before queuing
For instance method increment
CountlyInstance.increment(key: String) // Logic Increments the numeric value of a custom property by 1. Wire modifier: $inc with value 1
For instance method incrementBy
CountlyInstance.incrementBy(key: String, value: Number) // Valid values value is numeric (integer or double) // Logic Increments the numeric value of a custom property by the given amount. Wire modifier: $inc with the given value
For instance method multiply
CountlyInstance.multiply(key: String, value: Number) // Valid values value is numeric (integer or double) // Logic Multiplies the numeric value of a custom property by the given amount. Wire modifier: $mul
For instance methods saveMax / saveMin
CountlyInstance.saveMax(key: String, value: Number) CountlyInstance.saveMin(key: String, value: Number) // Logic Saves the value only if it is greater (saveMax) or less (saveMin) than the current server-side value. Useful for tracking high scores or minimums. Wire modifiers: $max / $min
For instance method setOnce
CountlyInstance.setOnce(key: String, value: String) // Logic Sets the value only if the property does not already exist on the server. Has no effect if the property is already set. Wire modifier: $setOnce
For instance method push
CountlyInstance.push(key: String, value: String) // Logic Appends a value to an array property, allowing duplicates. Multiple push calls on the same key before save() accumulate into an array. Wire modifier: $push
For instance method pushUnique
CountlyInstance.pushUnique(key: String, value: String) // Logic Appends a value to an array property only if it is not already present (set semantics). Multiple pushUnique calls on the same key before save() accumulate into an array. Wire modifier: $addToSet
For instance method pull
CountlyInstance.pull(key: String, value: String) // Logic Removes all occurrences of a value from an array property. Multiple pull calls on the same key before save() accumulate into an array. Wire modifier: $pull
For instance method save
CountlyInstance.save() // Logic Flushes all queued user property changes to the server. - Serializes predefined and custom properties into a JSON object sent as "user_details" - Custom modifiers ($inc, $mul, $max, $min, $setOnce, $push, $addToSet, $pull) are nested under the "custom" key within user_details - After sending, the internal queue is cleared - Requires "users" consent; omitted if consent is not given - Also called automatically by the SDK before event recording and session begin/update
For instance method clear
CountlyInstance.clear() // Logic Discards all queued user property changes without sending them. Does not affect properties already persisted on the server.
Internal limits:
Custom property key length: truncated to maxKeyLength (default 128 chars) Custom property value length: truncated to maxValueSize (default 256 chars) Exception: "picture" URL allows up to 4096 chars regardless of maxValueSize User property cache limit: max number of user property changes held in memory before a forced flush (default 100, configurable via server setting "upcl")
SDK Behavior Settings:
upb (user property blacklist) — String array, default: empty Property keys in this list are silently dropped before queuing. If both upb and upw are present, upb takes precedence. upw (user property whitelist) — String array, default: empty Only property keys in this list are accepted; all others are silently dropped. Has no effect if upb is also set. upcl (user property cache limit) — Integer, default: 100 Maximum number of user property operations to accumulate in memory before triggering an automatic save().
Consent
The feature depends on "users" consent.
Orientation change tracking also requires "users" consent. If consent is revoked, orientation events stop being recorded.
Networking and Params
User profile data is sent as a single user_details parameter containing a JSON object. The request is added to the standard request queue and sent to the /i endpoint.
user_details: JSON object containing predefined fields and a nested "custom" object
Example (decoded):
{
"name": "Jane Doe",
"email": "jane@example.com",
"byear": 1990,
"custom": {
"level": 5,
"$inc": { "score": 10 },
"$push": { "badges": "gold" },
"$addToSet": { "tags": "beta" },
"$pull": { "tags": "alpha" }
}
}curl --request POST \
--url ‘https://YOUR_SERVER/i’ \
--header ‘Content-Type: application/x-www-form-urlencoded’ \
--data ‘user_details={"name":"Jane Doe","email":"jane@example.com","custom":{"level":5}}’ \
--data ‘...remaining common params’Modifier operations ($inc, $mul, $max, $min, $setOnce, $push, $addToSet, $pull) are always nested under the custom key as sub-objects, where each modifier key maps to an object of { propertyKey: value } pairs. Array modifiers ($push, $addToSet, $pull) send their accumulated values as arrays.
Application Performance Monitoring
APM is no longer receiving new updates. Existing functionality will continue to work, but no new enhancements or features will be added. The documentation below reflects the current stable implementation.
Exposed Methods
Config Methods
CountlyConfig.apm.enableAppStartTimeTracking()
CountlyConfig.apm.enableForegroundBackgroundTracking()
CountlyConfig.apm.enableManualAppLoadedTrigger()
CountlyConfig.apm.setAppStartTimestampOverride(timestamp: long)
Instance Methods
CountlyInstance.startTrace(traceKey: String)
CountlyInstance.endTrace(traceKey: String, customMetrics: Map<String, Integer>)
CountlyInstance.cancelTrace(traceKey: String)
CountlyInstance.cancelAllTraces()
CountlyInstance.recordNetworkTrace(networkTraceKey: String, responseCode: int, requestPayloadSize: int, responsePayloadSize: int, requestStartTimestampMs: long, requestEndTimestampMs: long)
CountlyInstance.startNetworkRequest(networkTraceKey: String, uniqueId: String)
CountlyInstance.endNetworkRequest(networkTraceKey: String, uniqueId: String, responseCode: int, requestPayloadSize: int, responsePayloadSize: int)
CountlyInstance.setAppIsLoaded()
CountlyInstance.triggerForeground()
CountlyInstance.triggerBackground()
Implementation Details
For instance method startTrace
CountlyInstance.startTrace(traceKey: String) // Valid values traceKey is not nullable and not empty // Logic Starts a custom trace. The trace key and start timestamp are stored. - If a trace with the same key is already running, the call is ignored and warned.
For instance method endTrace
CountlyInstance.endTrace(traceKey: String, customMetrics: Map<String, Integer>) // Valid values traceKey is not nullable and not empty customMetrics is nullable // Logic Ends a previously started custom trace and records it. - Duration is calculated in ms from start to end timestamp - Trace key is truncated by maxKeyLength, then validated: - Max 2048 chars (truncated if longer) - Keys starting with '$' are warned (removed server-side) - Custom metric key validation: - Max 32 chars per key (entries with longer keys are removed) - Keys starting with '$' are warned - Keys containing '.' are warned (dots removed server-side) - Null/empty keys or null values are removed - Must match pattern /^[a-zA-Z][a-zA-Z0-9_]*$/ server-side - Reserved metric keys (response_time, response_payload_size, response_code, request_payload_size, duration, slow_rendering_frames, frozen_frames) are removed from custom metrics if provided - Custom metrics are subject to maxKeyLength and maxSegmentationValues limits
For instance method recordNetworkTrace
CountlyInstance.recordNetworkTrace(networkTraceKey: String, responseCode: int, requestPayloadSize: int, responsePayloadSize: int, requestStartTimestampMs: long, requestEndTimestampMs: long) // Valid values networkTraceKey is not nullable and not empty // Logic and validation - responseCode must be between 100 and 599 inclusive; invalid values are set to 0 - requestPayloadSize and responsePayloadSize must be = 0; negative values set to 0 - If startTimestamp endTimestamp, the values are swapped - Trace key is truncated by maxKeyLength, then validated (max 2048 chars) - The trace is sent as an immediate APM request to the server
For instance method cancelTrace
CountlyInstance.cancelTrace(traceKey: String) // Logic Cancels a previously started custom trace without recording it.
For instance method cancelAllTraces
CountlyInstance.cancelAllTraces() // Logic Cancels all currently running custom traces without recording them.
For instance method startNetworkRequest / endNetworkRequest
CountlyInstance.startNetworkRequest(networkTraceKey: String, uniqueId: String) CountlyInstance.endNetworkRequest(networkTraceKey: String, uniqueId: String, responseCode: int, requestPayloadSize: int, responsePayloadSize: int) // Valid values Both networkTraceKey and uniqueId are not nullable and not empty // Logic Alternative to recordNetworkTrace for tracking network requests in two steps. - Internally the trace is stored with a composite key: "traceKey|uniqueId" - This allows tracking multiple concurrent requests to the same endpoint - startNetworkRequest stores the start timestamp - endNetworkRequest retrieves the start timestamp, calculates duration, then calls the same recordNetworkTrace logic (with all its validation) - Same validation rules as recordNetworkTrace apply to the end call
For instance method setAppIsLoaded
CountlyInstance.setAppIsLoaded() // Logic Manually triggers the moment when the app has finished loading. Only functional when enableManualAppLoadedTrigger is set in config. Records the app start time from SDK init to this call.
For instance methods triggerForeground / triggerBackground
CountlyInstance.triggerForeground() CountlyInstance.triggerBackground() // Logic Manually reports that the app has entered the foreground or background. Used for platforms where app lifecycle detection is not automatic. Records foreground/background duration for performance monitoring.
General Information
Countly server supports multiple metrics for performance monitoring. They are divided into 3 groups:
- Custom traces — used to measure the performance of some running task. At the basic level, this function is similar to timed events. The default metric that gets tracked is the "duration" of the event. There should be a "startTrace" and "endTrace" call, which start and end the tracking. When ending the tracking, the developer has the option of providing additional metrics. Those metrics are String and Integer/Numeric pairs.
-
Network request traces — used to track network request performance including response time, payload sizes, and response codes. Can be recorded in one shot via
recordNetworkTraceor in two steps viastartNetworkRequest/endNetworkRequest. - Device traces — used to track device-level performance metrics such as app start time and foreground/background duration.
In some of the exposed functionality, developers can provide the metric key for identifying the tracked thing. There are some requirements that the key must meet. Those requirements will be enforced on the API endpoint, and if the key will be deemed invalid, the trace will be dropped. Therefore it's important to catch invalid keys and warn about them on the SDK side.
Metric keys have a maximum length of 32 characters.
Trace keys have a maximum length of 2048 characters.
Metric names can't start with "$" and they can't have "." in them. Trace names can't start with "$". Those values should still be accepted. But the SDK should print a warning that the server will strip those characters.
The following SDK internal limits apply to APM data. These are defined globally in the SDK Internal Limits section and enforced within this feature:
maxKeyLength (default: 128) Applies to: custom trace key names, custom metric keys. Keys exceeding this limit are truncated. maxSegmentationValues (default: 100) Applies to: custom APM metrics passed to endTrace. If the developer provides more than this many custom metric entries, excess entries are dropped.
In addition to the global limits, APM enforces its own feature-specific limits:
Trace key length — max 2048 characters Trace keys exceeding this length are truncated. Keys starting with "$" trigger a warning (the server strips the character). Metric key length — max 32 characters Custom metric keys exceeding this length cause the entry to be removed. Keys starting with "$" or containing "." trigger a warning. Reserved metric keys — automatically removed if provided by the developer: response_time, response_payload_size, response_code, request_payload_size, duration, slow_rendering_frames, frozen_frames
Networking and Params
APM requests are sent to the "/i" endpoint. There is no batching functionality for APM requests, every APM data point or trace is put in the request queue immediately after it is acquired. That means after a network request finishes, after a custom trace ends, and each time the device transitions between foreground and background.
APM data is combined into a single JSON object and set on the "apm" param. APM requests should be sent through the request queue.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'apm={"type":"device","name":"app_start","apm_metrics":{"duration":3200},"stz":1584698900000,"etz":1584699100000}' \
--data ...remaining common paramsSample custom trace request:
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'apm={"type":"device","name":"forLoopProfiling_1","apm_metrics":{"duration":10,"memory":200},"stz":1584698900000,"etz":1584699900000}'
--data ...remaining common paramsSample network trace request:
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'apm={"type":"network","name":"/count.ly/about","apm_metrics":{"response_time":1330,"response_payload_size":120,"response_code":300,"request_payload_size":70},"stz":1584698900000,"etz":1584699900000}'
--data ...remaining common paramsSample device trace request:
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'apm={"type":"device","name":"app_start","apm_metrics":{"duration":15000},"stz":1584698900,"etz":1584699900}'
--data ...remaining common paramsStorage
APM data is not stored persistently beyond the standard request queue. Once a trace ends or a network request completes, the APM payload is formed immediately and added to the request queue as a regular queued request. No separate APM-specific persistence layer is used. In-progress custom traces (started but not yet ended) are held in memory only and are lost if the SDK is shut down before endTrace is called.
Consent
The feature depends on "apm" consent.
User Consent
Exposed Methods
Config Methods
CountlyConfig.setRequiresConsent(shouldRequireConsent: boolean)
CountlyConfig.setConsentEnabled(featureNames: String[])
Instance Methods
CountlyInstance.giveConsent(featureNames: String[])
CountlyInstance.removeConsent(featureNames: String[])
CountlyInstance.giveConsentAll()
CountlyInstance.removeConsentAll()
boolean CountlyInstance.getConsent(featureName: String)
CountlyInstance.setConsent(featureNames: String[], isConsentGiven: boolean)
CountlyInstance.createFeatureGroup(groupName: String, features: String[])
CountlyInstance.setConsentFeatureGroup(groupName: String, isConsentGiven: boolean)
CountlyInstance.checkAllConsent()
Implementation Details
The full list of valid consent feature names is:
sessions, events, views, location, crashes, attribution, users, push, star-rating, remote-config, apm, feedback, clicks, scrolls, content, metrics, forms // IMPORTANT: Wire-format names use HYPHENS, not camelCase: // "star-rating" (not "starRating"), "remote-config" (not "remoteConfig")
For config method setRequiresConsent
CountlyConfig.setRequiresConsent(shouldRequireConsent: boolean) // Logic Enables consent enforcement for the SDK. - When true: all features are initialized to consent=false and will not function until explicit consent is given - When false (default): all getConsent() calls return true silently and the SDK operates without consent checks
For config method setConsentEnabled
CountlyConfig.setConsentEnabled(featureNames: String[]) // Valid values featureNames must contain valid feature name strings (see list above) // Logic Provides initial consent during SDK initialization. - Equivalent to calling giveConsent() immediately after init - Features not listed start with consent=false when requiresConsent is true
For instance method giveConsent
CountlyInstance.giveConsent(featureNames: String[]) // Valid values featureNames must contain valid feature name strings; groups are also accepted // Logic Gives consent for one or more features. - Accepts a single feature name, an array, or variable arguments - If consent is already given for a feature, the call is ignored for that feature - The SDK immediately begins collecting data for the consented features - A consent update request is queued to notify the server
For instance method removeConsent
CountlyInstance.removeConsent(featureNames: String[]) // Valid values featureNames must contain valid feature name strings; groups are also accepted // Logic Removes consent for one or more features. - Accepts the same parameter options as giveConsent - If consent was not given for a feature, the call is ignored for that feature - The SDK immediately stops collecting data for the affected features - A consent update request is queued to notify the server - Depending on SDK structure, existing queued requests may be kept or discarded
For instance methods giveConsentAll / removeConsentAll
CountlyInstance.giveConsentAll() CountlyInstance.removeConsentAll() // Logic Convenience methods that give or remove consent for all known features at once.
For instance method getConsent
boolean CountlyInstance.getConsent(featureName: String) // Logic Returns the current consent status for a single feature. - Returns true if consent is given, false if not - Returns true unconditionally if requiresConsent is false - For a feature group name, returns true only if ALL underlying features have consent
For instance method setConsent
CountlyInstance.setConsent(featureNames: String[], isConsentGiven: boolean) // Logic Convenience method combining giveConsent and removeConsent. - Calls giveConsent if isConsentGiven is true, removeConsent if false
For instance methods createFeatureGroup / setConsentFeatureGroup
CountlyInstance.createFeatureGroup(groupName: String, features: String[]) CountlyInstance.setConsentFeatureGroup(groupName: String, isConsentGiven: boolean) // Logic createFeatureGroup stores a named mapping from groupName to a list of feature names. setConsentFeatureGroup calls giveConsent or removeConsent for all features in the group.
For instance method checkAllConsent
CountlyInstance.checkAllConsent() // Logic Returns true only if consent is given for every known feature. Returns true unconditionally if requiresConsent is false.
Two-phase consent notification:
- consentWillChange() is called BEFORE consent values are updated - onConsentChanged() is called AFTER consent values are updated This ordering is critical for modules that must stop activity before consent is removed (e.g. Views must stop tracking before consent is set to false).
Module reactions on consent change:
- "location" revoked: location erasure request is sent to the server - "content" revoked: content zone is exited and any displayed content is removed - "sessions" given after init: a session begins (if automatic session triggers are enabled) - "remote-config" given after init: RC values are downloaded (if automatic triggers are enabled)
SDK Behavior Settings:
cr (consent required) — Boolean, default: false If the server sends cr=true, the SDK must enforce consent requirements as if setRequiresConsent(true) had been configured locally. The developer-set value takes precedence if it is already true; the server can only enable consent enforcement, not disable it.
Device ID change behavior:
When device ID changes without merge: - All consents are reset to false - Modules are notified via ConsentChangeSource.DeviceIDChangedNotMerged
General Information
GDPR compatibility is about dividing the SDK functionality into different features and allowing SDK integrators to ask for consent when using these features. Once consent has been given, the SDK may only send newly collected (after consent is given) data to the server.
The user may change their mind during the app run and opt-out of some features. The SDK must be able to enable or disable features at runtime accordingly.
Consent management in the SDK is done in 2 steps:
- consent has to first be required in the app — otherwise the SDK works as if all consent is given
- if consent is required, it has to explicitly be given for each targeted feature
The config object accepts an initial set of consents at startup. After init, consent can be given or removed at any time using the instance methods. The SDK does not persistently store consent state — the host application is responsible for persisting user choices and re-providing them on each SDK initialization.
Feature grouping allows existing features to be collected into named groups so that consent can be given or removed for the entire group at once:
Countly.createFeatureGroup("activity", ["sessions", "events", "views"]);
Countly.setConsentFeatureGroup("activity", true); // gives consent for all threeCommon integration flow when requiresConsent = true:
- Set
requiresConsent = truein config — the SDK starts but collects and sends nothing. - Show your consent UI; persist the user's choices in your app storage.
- Call
giveConsent(featureNames)for the approved features — the SDK activates those features and notifies the server. - On every subsequent app launch, re-provide consent from your stored choices.
- If the user later opts out, call
removeConsent(featureNames)— the SDK stops those features and notifies the server.
Exposing Available Features for Consent
The SDK should expose all the features it supports for consent in the form of a method, static properties, or constant strings. The developer may check which features are available during development or when creating a consent form UI.
Developers shouldn't have to write the consent feature strings themselves. They should be provided either as constants or even as enums thereby eliminating the need for strings.
The following are the currently available features:
-
sessions- tracking when, how often, and how long users use your app/website -
events- allow events to be sent to the server (doesn't apply to other features using event mechanisms) -
location- allows location information to be sent. If consent has not been given, the SDK should force send an empty location uponbegin_sessionto prevent the server from determining location via IP addresses -
views- allow tracking of which views/pages a user visits -
scrolls- allow user scrolls for scroll heatmap to be tracked -
clicks- allow user clicks for heatmaps as well as link clicks to be tracked -
forms- allow user's form submissions to be tracked -
crashes- allow crashes, exceptions, and errors to be tracked -
attribution- allows the campaign from which a user came to be tracked -
users- allow collecting/providing user information, including custom properties -
push- allows push notifications -
star-rating- allows their rating and feedback to be sent -
apm- allows usage of application performance monitoring features -
remote-config- allows downloading of remote configs from the server -
feedback- allows showing things like surveys and NPS -
content- allows server-driven content (Content Zone) to be fetched and displayed -
metrics- allows device and environment metrics to be collected and reported
Note that the available features may change depending on the platform.
Networking and Params
Consent change requests are sent to the standard request endpoint using the common request parameters. They are added to the request queue and processed asynchronously — they are not sent directly like content fetch requests.
The consent state is sent as a single consent parameter containing a JSON object with the full current state of all features — not just the changed ones. Every update is a complete snapshot. Both giving and removing consent can be expressed in a single request.
consent: JSON object — full current state of all features
Example (decoded): {"sessions":true,"events":true,"views":false,"crashes":true,...}
All known feature names are included as keys with boolean values.Consent changes are sent to the standard /i endpoint.
# Consent given for sessions and events, removed for views
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'consent={"sessions":true,"events":true,"views":false,...}' \
--data '...remaining common params'Multiple rapid consent changes may be coalesced into a single request before being queued (implementation-dependent — some platforms debounce by ~1 second).
Storage
Consent state is not persisted by the SDK across sessions. The host application is responsible for storing the user's consent choices and calling the appropriate consent methods on every SDK initialization.
The SDK only holds consent state in memory during the current session. State is reset when:
- The app restarts and consent is not re-provided
- Device ID changes without merge, all consents are cleared
Exception: Push notification consent may be persisted by the platform's push module (where supported) because the OS-level notification permission state must survive app restarts. This is platform-specific and should not be relied upon for other features.
Security and Privacy
Parameter Tampering
This is one of the preventive measures of Countly. If someone in the middle intercepts the request, it would be possible to change the data in the request and make another request with other data to the server or simply make random requests to the server through the retrieved app_key.
To prevent this from happening, the SDK should provide the option to send a checksum alongside the request data. To do so, it should be possible for the developer to provide some random string as SALT to the SDK as parameters or configuration options.
If this SALT is provided, right before making the request, the SDK should take all the payload it is about to send (all the data after the ‘?’ symbol in GET requests, including the app_key and device_id or query string encoded body of POST requests) and make a sha256 hash of this data. You should also provide SALT, and append it as checksum256={hash}.
if(salt){
data += "&checksum256=" + sha256Hash(data + salt);
}If SALT is not provided, the SDK should make ordinary requests without any checksums.
Other Features
Attribution
Attribution allows attributing installs from specific campaigns.
There are 2 forms of attribution: directAttribution and indirectAttribution.
There should be a way to provide them during init and there should be a way to provide them after init.
These values should be sent when they are provided in a separate request.
This requires attribution consent.
Direct Attribution
With this the dev is able to provide 2 String values: "Campaign type" and "Campaign data". The "type" determines for what purpose the attribution data is provided. Depending on the type, the expected data will differ, but usually that will be a string representation of a json object.
Currently there is only one type "countly". That type expected the data to look like following: '{cid:"[PROVIDED_CAMPAIGN_ID]", cuid:"[PROVIDED_CAMPAIGN_USER_ID]"}'. The inserted values would be retrieved from install attribution.
This feature is currently setup in a way to give more flexibility in the future. For now it will be only possible to record install attribution by handling twp special cases. In the future this feature will be generalised and a new param will be added.
If the provided type is "countly" then the first special case will be executed. The data string is an stringified json that has 2 values "cid" or Campaign ID and "cuid" or Campaign user ID.
Non valid or empty string should produce an error log.
The "Campaign ID" value is mandatory. If this has no valid value, an error log should be printed and execution should be aborted.
The "Campaign user ID" value is optional and if it is missing or invalid, only the "Campaign ID" value should be sent.
The call to record this value should be named something similar to "recordDirectAttribution".
The param for the campaign ID should be added as:
"&campaign_id=[PROVIDED_CAMPAIGN_ID]"
The param for the campaign user ID should be added as:
"&campaign_user=[PROVIDED_CAMPAIGN_USER_ID]"
If the provided type is "_special_test" then the second special case will be executed. If the provided data is not null or empty then it will be processed.
A request will be created. The provided value should be HTTP encoded and then set to the parameter "attribution_data" and then sent.
"&attribution_data=[ENCODED_CAMPAIGN_DATA]"
Indirect Attribution
With this the dev is able to provide a map/dictionary of String to String values. This allows multiple values to be provided.
Common values that would be provided here would be IDFA (for iOS) and AdvertisingId (for Android).
Each usable value will have a predefined key that has to be used. IDFA will need to be provided with the "idfa" key and Advertising ID will need to be provided with the "adid" key. These keys have to the be provided by the SDK as "constant" variables or some other convenient way where the developer is not setting the final key manually.
The pseudo code for recording indirect attribution would look something like this:
Map<String, String> attributionValues = new HashMap<>(); attributionValues.put(AttributionIndirectKey.AdvertisingID, getAdvertisingID()); Countly.recordIndirectAttribution(attributionValues);
The map/dictionary with valid key-value pairs will then be transformed into a json object which will set to the "aid" param and then immedietelly sent to the server.
Each key-value pair should be validated. If the key or value is either null, undefined or empty string, that key-value pair should be removed from the map/dictionary and an error message should be printed.
It should not be validated if the provided keys are part of our officially supported ones ("idfa" and "adid" at the time of writing). Just that the keys and their values are legitamate values.
If after the validation no valid value is left another error log should be printed and the execution of this call should not continue.
The call to record this value should be named something similar to "recordIndirectAttribution".
The param in the request would look something like like:
&aid={"adid":[PROVIDED_ATTRIBUTION_ID], "idfa":[PROVIDED_IDFA_VALUE]}Or:
&aid={"adid":[PROVIDED_ATTRIBUTION_ID]}Or:
&aid={"idfa":[PROVIDED_IDFA_VALUE]}Or:
&aid={"rndid":[SOME_OTHER_ID_VALUE]}SDK Internal Limits
The SDK should have the following limits:
- "maxKeyLength" - 128 chars
Limits the maximum size of all string keys.
"Keys" include:
- event names
- view names
- custom trace key name (APM)
- custom metric key (APM)
- segmentation key (for all features)
- custom user property
- custom user property keys that are used for property modifiers (mul, push, pull, set, increment, etc)
- "maxValueSize" - 256 chars
Limits the size of all values in our key-value pairs.
"Value" fields include:
- segmentation value in case of strings (for all features)
- custom user property string value
- user profile named key (username, email, etc) string values. Except the "picture" field, which has a limit of 4096 chars
- custom user property modifier string values. For example, for modifiers like "push," "pull," "setOnce", etc.
- breadcrumb text
- manual feedback widget reporting fields (reported as an event)
- rating widget response (reported as an event)
- "maxSegmentationValues" - 100 developer-supplied entries
Max amount of custom (dev-provided) segmentation in:
- Event segmentation
- View segmentation
- Global view segmentation
- Custom (Global) crash segmentation
- Crash segmentation
- Custom APM Metrics
- "maxBreadcrumbCount" - 100 entries
Maximum amount of breadcrumbs that can be recorded before the oldest one is deleted
- "maxStackTraceLinesPerThread" - default value of 30
limits how many stack trace lines would be recorded per thread
- "maxStackTraceLineLength" - default value of 200
limits how many characters are allowed per stack trace line. This also limits the crash message length.
Besides those 2 exposed tweakable crash-related values, there would also be an internal one for "maxStackTraceThreadCount." Which would limit the maximum number of recorded threads by a default of 50. This would be mostly just a sanity check, as that has to be capped somehow. In cases where stack traces can be provided as a string, the maximum line count would be 50*30 = 1500. That string would have to be split into lines and then checked accordingly.
Crash information like PLC crashes for iOS, and native Android crashes do not have any limits applied to them.
Backoff Mechanism
The SDK uses a backoff mechanism to pause requests when the server is slow or unresponsive.
Exposed Methods
Config Methods
CountlyConfig.disableBackoffMechanism()
Implementation Details
The backoff mechanism should be applied where the SDK handles network traffic.
It uses a 3-stage check to determine whether to initiate backoff:
- Response Time Check: If the response time of the most recent request is greater than or equal to the accepted timeout (default: 10 seconds).
- Queue Load Check: If the ratio of stored requests to the maximum request queue size is less than or equal to the accepted threshold (default: 50%).
- Request Age Check: If the age of the request is less than or equal to the accepted request age (default: 24 hours).
If all conditions are met, the SDK enters backoff mode and pauses new outgoing requests for a defined duration (default: 60 seconds). After that duration, SDK triggers sending requests.
The current request is still processed and removed from the queue since a response was received.
Note: Immediate requests are not subject to the Backoff Mechanism.
These parameters are configurable via the SDK Behavior settings for the Backoff Mechanism:
-
bom: Enable/disable the backoff mechanism (default: true) -
bom_at: Accepted timeout in seconds (default: 10) -
bom_rqp: Accepted request queue percentage (between 0 and 1) (default: 0.5) -
bom_ra: Accepted request age (default: 24) -
bom_d: Backoff duration in seconds (default: 60)
To observe SDK health and how backoff mechanism is doing, two health check parameters are logged for the backoff mechanism:
-
bom: Total number of requests that were backed off -
cbom: Maximum number of consecutive requests that were backed off
For more information about SDK Health Checks see here.
For config method disableBackoffMechanism
CountlyConfig.disableBackoffMechanism() // Logic Disables the backoff mechanism - After disabling, mechanism must not work.
SDK Behavior Settings
SDK Behavior Settings provide a way for server to provide a configuration. This configuration can effect which internal limits are used and which features are allowed to work in the SDKs.
Exposed Methods
Config Methods
CountlyConfig.setSDKBehaviorSettings(sdkBehaviorSettings: String)
CountlyConfig.disableSDKBehaviorSettingsUpdates()
Implementation Details
During init and every X hours (4 hours by default) SDK tries to acquire the behavior settings and stores it persistently locally. Afterwards, tries to reconfigure itself.
When initializing the SDK, if there is a persistent behavior settings stored, it will be used. The SDK will still try to get a up to date version of the behavior settings at the end of initialization. Once the up to date version has been acquired, it is stored persistently and the SDK reconfigures itself to reflect the new settings.
SDK's Default < Dev Set Settings < Dev Set Behavior Settings < Stored Behavior SettingsSBS = SDK Behavior Settings
| Init Time Status | Initial Behavior | ||
|---|---|---|---|
| Stored SBS | Provided SBS | Temp ID | |
| ● | Uses Stored SBS | ||
| ● | ● | Uses Stored SBS | |
| ● | ● | Uses Stored SBS | |
| ● | ● | ● | Uses Stored SBS |
| ● | Uses Provided SBS | ||
| ● | ● | Uses Provided SBS | |
| ● | Uses User Config | ||
| Uses User Config | |||
After this initial behavior SDK should get the SDK Behavior Settings again to be up-to-date and save it. If that request fails or the settings are invalid it should keep using what it has.
After fetching the settings, the module must notify dependent components to allow them to reconfigure themselves with the updated settings. The notification mechanism can be implemented as a single function or a callback, allowing features to handle updates themselves. The exact approach depends on SDK design and platform constraints.
If user changes a setting in server, response would include that option with new value. If no value is sent for a configuration (meaning c is empty), then that means that the SDK has to use its own default or the value provided by the developer.
If temporary id is enabled, SDK Behavior Settings fetches must be omitted.
For config method setSDKBehaviorSettings:
CountlyConfig.setSDKBehaviorSettings(sdkBehaviorSettings: String) // Valid values Provided value should be not empty and correcty structured JSON object like below. // Logic In some cases, the behavior settings may not be applied on the app’s first run, or the app might temporarily lack internet connectivity. Therefore, providing the behavior settings through a dedicated configuration function becomes necessary. - After the SDK behavior settings are supplied, they are among the first components processed during SDK initialization. Relevant parts of the system are notified of any changes before the rest of the initialization continues.
For config method disableSDKBehaviorSettingsUpdates:
CountlyConfig.disableSDKBehaviorSettingsUpdates() // Logic By default, SDKs fetch the behavior settings from the server. However, in some cases, network traffic may increase due to unintended factors. This configuration method disables behavior settings requests to help mitigate such issues. - After SDK behavior settings updates disabled, it will only disable server config fetch requests. Feature still continues to read the stored behavior settings and provided behavior settings.
The feature must expose an internal interface for other components to access the required settings. Here is an example interface for function signatures. Some functions might not be exposed if they are used only inside of the feature. It is up to SDK design and platform constraints.
interface ConfigurationProvider {
Boolean getNetworkingEnabled() //networking
Boolean getTrackingEnabled() //tracking
Boolean getSessionTrackingEnabled() //st
Boolean getViewTrackingEnabled() //vt
Boolean getCustomEventTrackingEnabled() //cet
Boolean getEnterContentZone() //ecz
Boolean getCrashReportingEnabled() //crt
Boolean getLocationTrackingEnabled() //lt
Boolean getRefreshContentZoneEnabled() //rcz
Integer getRequestQueueSize() // rqs
Integer getEventQueueSize() //eqs
Integer getSessionUpdateInterval() //sui
Integer getLimitKeyLength() //lkl
Integer getLimitValueSize() //lvs
Integer getLimitSegmentationValues() //lsv
Integer getLimitBreadcrumbCount() //lbc
Integer getLimitStackTraceLineCount() //ltlpt
Integer getLimitStackTraceLineLength() //ltl
Integer getContentZoneInterval() //czi
Boolean getConsentRequired() //cr
Integer getServerConfigUpdateInterval() //scui
Integer getDropOldRequestTime() //dort
Boolean getBOMEnabled() //bom
Integer getBOMAcceptedTimeoutSeconds() //bom_at
Double getBOMRQPercentage() //bom_rqp
Integer getBOMRequestAge() //bom_ra
Integer getBOMDuration() //bom_d
Integer getUserPropertyCacheLimit() //upcl
Boolean getLoggingEnabled() //log
FilterList<Set<String>> getEventFilterList() //eb or ew
FilterList<Set<String>> getUserPropertyFilterList() //upb or upw
FilterList<Set<String>> getSegmentationFilterList() //sb or sw
FilterList<Map<String, Set<String>>> getEventSegmentationFilterList() //esb or esw
Set<String> getJourneyTriggerEvents() //jte
}Listing Filters are key-value pairs that use either a blacklist or whitelist approach. Blacklist means the listed values are blocked, whitelist means only the listed values are allowed. If both a blacklist and whitelist key are present for the same category, the blacklist takes precedence.
The FilterList structure contains:
FilterList {
filterList: the set/map of filter values
isWhitelist: boolean - false for blacklist, true for whitelist
}Event Segmentation Filters (esb/esw) are structured as a JSON object where each key is an event name and each value is a JSON array of segmentation keys to filter for that specific event.
Journey Trigger Events (jte) is a JSON array of event keys that trigger a content zone refresh when recorded. When one of these events is recorded, the SDK should refresh the content zone after making sure SDK delivers event to the server. If the initial attempt fails, retries refreshing content zone up to 3 times with 1-second delays Only custom events are affected by this.
Filter Application Logic:
Event filter (eb/ew): - Applied before recording any event. If the event name is filtered out, the event is dropped entirely. - Whitelist: event is allowed only if its name is in the list - Blacklist: event is blocked if its name is in the list - Empty filter list = no filtering (everything allowed) User property filter (upb/upw): - Applied before recording any user property. If the property key is filtered out, it is not sent. - Same whitelist/blacklist logic as event filter Segmentation filter (sb/sw): - Applied to all event segmentation before recording. Filtered keys are removed from segmentation. - Whitelist: only keys in the list are kept - Blacklist: keys in the list are removed - Empty filter list = no filtering Event segmentation filter (esb/esw): - Applied per-event. Each event name maps to a set of segmentation keys to filter. - If no rule exists for a specific event name, all segmentation is allowed for that event. - Same whitelist/blacklist logic as segmentation filter
Networking and Params
Behavior settings fetch request might consist of only 1 parameter
method: this is always "sc"
method parameter is always sent with behavior settings fetch request
Behavior settings fetch request is sent to the "/o/sdk" endpoint.
Behavior settings fetch requests are directly sent to the server, they will not be added to the request queue.
# Common way to send curl --request POST \ --url 'https://YOUR_SERVER/o/sdk' \ --header 'Accept: application/json' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'method=sc' \ --data ...remaining common params
Response would look like this:
{
"v": 1,
"t": 1742459739383,
"c": {
"tracking": false,
"networking": false,
// ...
}
}Which are:
v - current schema version
t - timestamp (at the time of creation)
c - config object (key value pairs)
Storage
The newly fetched server configuration should be merged with the stored configuration. It must not override all existing values only update the keys present in the new configuration.
Values are preserved in their original format as received from the response. Here is a full list of server config key value pairs and defaults:
| Feature Name | Key | Default Val |
|---|---|---|
| Allow all tracking | tracking | TRUE |
| Allow sending requests | networking | TRUE |
| Request queue size | rqs | 1000 requests |
| Event queue size (or batch) | eqs | 100 events |
| Session update interval | sui | 60 seconds |
| Allow session tracking | st | TRUE |
| Allow crash tracking | crt | TRUE |
| Allow location tracking | lt | TRUE |
| Allow view tracking | vt | TRUE |
| Key length limit | lkl | 128 chars |
| Value size limit | lvs | 256 chars |
| Segmentation values count limit | lsv | 100 key/values |
| Breadcrumb count limit | lbc | 100 breadcrumbs |
| Stack trace line count limit | ltlpt | 30 lines |
| Stack trace line length limit | ltl | 200 chars |
| Allow custom event tracking | cet | TRUE |
| Enter Content Zone after init | ecz | FALSE |
| Content Zone request interval | czi | 30 seconds |
| Consent required | cr | FALSE |
| Server config update interval | scui | 4 hours |
| Allow Refresh Content Zone | rcz | TRUE |
| Drop old request time | dort | 0 (disabled) |
| Backoff mechanism enabled | bom | TRUE |
| Backoff mechanism accepted timeout | bom_at | 10 seconds |
| Backoff mechanism request queue % | bom_rqp | 0.5 (between 0 and 1) |
| Backoff mechanism request age | bom_ra | 24 hours |
| Backoff mechanism duration | bom_d | 60 seconds |
| User property cache limit | upcl | 100 |
| Enable/disable logging | log | FALSE |
| Event blacklist (JSON array) | eb | [] (empty) |
| Event whitelist (JSON array) | ew | [] (empty) |
| User property blacklist (JSON array) | upb | [] (empty) |
| User property whitelist (JSON array) | upw | [] (empty) |
| Segmentation blacklist (JSON array) | sb | [] (empty) |
| Segmentation whitelist (JSON array) | sw | [] (empty) |
| Event segmentation blacklist (JSON object) | esb | {} (empty) |
| Event segmentation whitelist (JSON object) | esw | {} (empty) |
| Journey trigger events (JSON array) | jte | [] (empty) |
Consent
This feature does not need any consent.
Health Checks
The SDK Health Monitoring feature automatically tracks internal SDK health indicators and reports them to the server once per SDK initialization. This gives server-side visibility into whether the SDK is operating correctly: whether requests are succeeding, whether the backoff mechanism is being triggered excessively, and whether any internal warnings or errors have occurred since the last successful report.
The health check tracking, serialization, deserialization, and request logic should be contained within an independent, self-contained module that can be easily tested in isolation.
Exposed Methods
Config Methods
CountlyConfig.disableHealthCheck()
There are no instance methods for this feature. Health monitoring is fully automatic and does not expose any runtime API to the developer.
Implementation Details
For config method disableHealthCheck
CountlyConfig.disableHealthCheck() // Logic Disables the SDK health check feature entirely. - No health check request will be sent during SDK initialization. - The internal health counter object is still created and metrics still accumulate, but no report is sent to the server. - This is a permanent setting for the lifetime of the SDK instance.
For internal behavior sendHealthCheck
// Triggered automatically after SDK init completes (initFinished lifecycle hook) // Logic Sends a single immediate (out-of-queue) health check request to the /i endpoint. - Aborted if health check is disabled via config. - Aborted if temporary device ID mode is active. - Aborted if a health check has already been sent in this SDK lifecycle. - Marks the health check as sent (one-shot flag). - Prepares device metrics containing only the app version (with metric override support). - Prepares the base request data (common params + metrics). - Appends the serialized health counters as the &hc param. - Creates a connection processor and reads the current networking-enabled state. - Sends the request via the immediate request mechanism (not the request queue). - On successful server response (response JSON contains "result" key): - Counters are cleared in memory and persisted state is wiped (clearAndSave). - On network failure, no response, or missing "result" key: - Counters are retained in memory and storage. - They will be re-sent on the next SDK initialization.
For internal behavior counter accumulation
The health check module should expose the following methods for tracking. These are called by other SDK modules during normal operation:
logWarning()
- Increments the warning log counter ("wl") by 1.
- Called whenever the SDK emits an internal warning-level log entry.
logError()
- Increments the error log counter ("el") by 1.
- Called whenever the SDK emits an internal error-level log entry.
logFailedNetworkRequest(statusCode: integer, errorResponse: string)
- Records the HTTP status code of a failed request ("sc").
- Status code must be > 0 and < 1000.
- Records the error response body ("em"), truncated to 1000 characters if longer.
- Only the most recent failed request is stored; previous values are overwritten.
logBackoffRequest()
- Increments the total backoff request counter ("bom") by 1.
- Increments a temporary consecutive backoff run counter by 1.
- Called when a request is backed off, excluding immediate requests.
logConsecutiveBackoffRequest()
- Compares the temporary consecutive backoff run counter against the stored
maximum ("cbom") and keeps the higher value.
- Resets the temporary consecutive backoff run counter to 0.
- Called when a request completes without backing off, ending a consecutive run.
Excludes immediate requests.For internal behavior saveState / clearAndSave
saveState() - Calls logConsecutiveBackoffRequest() first to finalize the consecutive backoff maximum before persisting. - Serializes all current counter values to a JSON object. - Persists the JSON string to the platform key-value store. - Called on platform-specific app lifecycle events (e.g. activity stop, session update timer, session end) to survive process death. clearAndSave() - Resets all in-memory counter values to their defaults: - countLogWarning = 0 - countLogError = 0 - statusCode = -1 - errorMessage = "" - countBackoffRequest = 0 - consecutiveBackoffRequest = 0 - consecutiveBackoffRequestCounter = 0 - Clears the persisted storage entry (sets it to an empty string). - Called after a successful health check server acknowledgement.
For internal behavior initialization and state recovery
// On construction, the health counter reads the persisted state from storage. // Logic - Reads the stored JSON string from the platform key-value store. - If the string is null or empty, counters start at zero defaults. - If the string is valid JSON, each counter is restored from the stored values using safe fallback defaults (0 for numbers, -1 for status code, "" for error message). - If the stored string is malformed (JSON parse failure), counters are reset to zero defaults and the stored state is cleared via clearAndSave().
General Information
Health monitoring is enabled by default and requires no explicit opt-in. The developer may disable it during configuration via disableHealthCheck(). Even when disabled, the internal counter object is still created and metrics continue to accumulate (since other SDK modules may still call the tracking methods), but no request is ever sent.
The health check request is sent once, immediately after SDK initialization completes (in the initFinished lifecycle hook), as an immediate (out-of-queue) request. It is a one-shot per SDK lifecycle: only one health check request is sent per SDK initialization.
The request is skipped if any of the following conditions are true at init time:
- Health check is disabled via config
- Temporary device ID mode is active
- A health check has already been sent during this SDK lifecycle
The feature tracks the following indicators since the last successful health check submission:
- Number of SDK-internal warning log entries
- Number of SDK-internal error log entries
- HTTP status code of the last failed network request
- Error message body of the last failed network request (capped at 1000 characters)
- Total count of backoff requests (requests delayed due to server-side backoff signals)
- Maximum observed consecutive backoff request count
Counters accumulate across SDK sessions and are persisted to storage. Health metrics should not be saved after every change to a metric. They should be saved after the following triggers:
- Session update timer (for periodic persistence)
- Session ended (as a proxy that the app or page is about to be closed)
- Any other platform-specific mechanism that indicates the app is about to be closed or killed (e.g. activity stop on Android)
When the health check request is successfully acknowledged by the server (the response JSON contains a result key), all counters are cleared in memory and the persisted state is wiped. If the request fails (e.g. no network connectivity, or the response does not contain a result key), the counters are retained and will be included in the next health check attempt on a subsequent SDK initialization.
Consent
The SDK Health Monitoring feature does not require any consent. It is an internal SDK diagnostic mechanism and does not track user behavior or personally identifiable information. It operates independently of the consent framework and is not gated by any consent group.
Networking and Params
The health check is sent as an immediate request to the /i endpoint. It bypasses the standard request queue and is fired directly after initialization completes. It respects the SDK's networking-enabled state: if networking is disabled, the immediate request mechanism will not send the request.
The request consists of the base common params (app_key, device_id, timestamp, hour, dow, sdk_version, sdk_name), the &metrics param containing only the app version, and the &hc param containing the serialized health counters.
curl --request POST \
--url 'https://YOUR_SERVER/i' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'metrics={"_app_version":"1.0.0"}' \
--data 'hc={"el":5,"wl":12,"sc":503,"em":"Service Unavailable","bom":3,"cbom":2}' \
--data ...remaining common paramsThe &metrics param is a URL-encoded JSON object containing device metrics. For health check requests, only the app version is included:
&metrics={"_app_version": "1.0.0"}If metric overrides are configured and include _app_version, the overridden value is used instead of the platform-detected app version.
The &hc param is a URL-encoded JSON object containing the health counters:
&hc={
"el": <number>, // error log count since last successful health check
"wl": <number>, // warning log count since last successful health check
"sc": <number>, // HTTP status code of last failed network request (-1 if none)
"em": <string>, // error message of last failed network request ("" if none)
"bom": <number>, // total backoff request count since last successful health check
"cbom": <number> // maximum consecutive backoff request count observed
}The server is expected to respond with a JSON object containing a result field. The SDK checks only for the presence of this key to determine success. Any response without result is treated as a failure and counters are not cleared.
Storage
Health check counters are persisted to the platform's key-value store under the key HEALTH_CHECK. The stored value is a JSON string containing the current counter snapshot.
The persisted JSON schema is:
{
"LErr": <number>, // error log count
"LWar": <number>, // warning log count
"RStatC": <number>, // HTTP status code of last failed request (-1 default)
"REMsg": <string>, // error message of last failed request ("" default)
"BReq": <number>, // total backoff request count
"CBReq": <number> // maximum consecutive backoff count
}State is written on platform-specific app lifecycle events (session update timer, session end, activity stop, or equivalent) and cleared on successful health check submission. If the stored state is missing or empty on SDK startup, counters start at zero defaults. If the stored state is malformed (JSON parse failure), counters are reset to zero defaults and the stored state is cleared.
Changing the Server URL
This feature adds the ability to change the server URL after the SDK is initialised.
This should in memory overwrite the current URL. Basic URL validation should be performed on the provided URL.
After the URL is changed, all previously saved events and requests should be sent to the new URL.
Queue Operations
The SDK should provide queue management methods for advanced use cases:
CountlyInstance.flushQueues() // Logic Clears both the request queue and event queue. All pending data is discarded. Started timed events are not affected.
CountlyInstance.replaceAllAppKeysInQueueWithCurrentAppKey() // Logic Replaces the app key in all queued requests with the current app key. Useful when the app key changes and old requests should be associated with the new app.
CountlyInstance.removeDifferentAppKeysFromQueue() // Logic Removes all queued requests whose app key differs from the current app key. Useful for cleaning up after an app key change.
CountlyInstance.addDirectRequest(requestParameters: Map<String, String>) // Logic Adds a custom request directly to the queue using given key-value pairs. If consent is required and no consents are given, the call is ignored.
Offline Mode
Some SDKs (particularly Web) support an offline mode where the SDK does not send any data to the server. All data is stored locally and sent when offline mode is disabled.
CountlyInstance.enableOfflineMode() // Logic Puts the SDK into offline mode. All requests are queued but not sent. No network requests will be made while in offline mode.
CountlyInstance.disableOfflineMode(deviceId: String) // Logic Disables offline mode and optionally sets a new device ID. All queued requests are sent to the server. If a device ID is provided, it is applied before sending.
Markdown Linting
To ensure the formatting of the code is uniform across all platforms, certain linting tools can be used. Currently for markdown files "markdownlint" would be used.
Installation
You can add markdownlint to your project with the following line of code using the npm:
npm install markdownlint --save-dev
Another way to add it to your project would be to download its extension in VSC.
Rules
Markdownlint, has multiple rules that can be modified/defined from a config file called ".markdownlint.json". This file must be created at the root of the project and should have a structure similar to this:
{
"MD001": true, /*Heading levels should increase one at a time*/
"MD002": false, /*First heading should be h1*/
"MD003": true, /*Use only one heading style in a document*/
"MD004": true, /*Only one unordered list style should be used*/
"MD005": true, /*List items should have same indentation at same level*/
"MD006": true, /*Top level list items should not be indented*/
"MD007": { "indent": 2 }, /*Indent level as space*/
"MD009": false, /*No white space at the end*/
"MD010": false, /*No hard tab indentation*/
"MD011": true, /*link syntax should not be reversed like (a)[a.com]*/
"MD012": { "maximum": 1 }, /*No more than 1 blank line*/
"MD013": { "line_length": 300 }, /*Max line length*/
"MD014": false, /*Dollar sign should not be used consecutively for shell commands*/
"MD018": true, /*There should be a space after heading hash*/
"MD019": true, /**There should not be multiple spaces after heading hash*/
"MD020": true, /*Closed atx style heading should have 1 space inside hashes*/
"MD021": true, /*Closed atx style heading should note have multiple space inside hashes*/
"MD022": false, /*Before and after a heading should be a blank line*/
"MD023": true, /*Heading should not be indented*/
"MD024": true, /*No duplicate sibling headings*/
"MD025": true, /*Only one h1*/
"MD026": true, /*No punctuation at the end of a heading except '?'*/
"MD027": true, /*No more than 1 space after blockquote symbol*/
"MD028": true, /*No separation of blockquotes with a blank line*/
"MD029": true, /*Ordered list should be ordered and start with or 1*/
"MD030": true, /*Only one space between the list marker and text*/
"MD031": true, /*Before and after a fenced code block should be a blank line*/
"MD032": false, /*Before and after a list should be a blank line*/
"MD033": false, /*No raw HTML*/
"MD034": false, /*URL should be surrounded with brackets*/
"MD035": true, /*No inconsistent horizontal rules; ---, *** */
"MD036": true, /*No emphasis instead of heading*/
"MD037": true, /*No space between emphasis market and the text*/
"MD038": true, /*No space between backtick and text*/
"MD039": true, /*No space inside link text*/
"MD040": true, /*Fenced code blocks should have a language declared*/
"MD041": false, /*First line in a file should be h1*/
"MD042": true, /*No empty links*/
"MD043": false, /*Declare a heading structure*/
"MD044": true, /*Proper names should have the correct capitalization*/
"MD045": true, /*Images should have alt text*/
"MD046": true, /*Use indent or code fence alone*/
"MD047": true, /*Files should end with a single newline character */
"MD048": true, /*Code fence style should be uniform*/
"MD049": true, /*Emphasis style should be consistent*/
"MD050": true, /*Strong style should be consistent*/
"MD051": true, /*Link fragments should correspond to a heading*/
"MD052": true, /*Reference links and images should use a label that is defined*/
"MD053": true /* Link and image reference definitions should be needed*/
}
To exclude certain files from being analyzed you can create a ".markdownlintignore" at the project root and add directories that you want to exclude from the analysis:
// to exclude node_modules folder /node_modules/
Usage
Then you can add the following text to your npm scripts in your package.json file:
"lintMD": "markdownlint **/*.md --fix"
This way you can use the markdown linter with the following code at your project root:
npm run lintMD
Instead, if you have only installed the VSC extension you can simply right click on your markdown file and press 'Format Document' for ease of use. Or if you want to automatically format when saving or pasting into a Markdown document, configure Visual Studio Code's editor.formatOnSave or editor.formatOnPaste settings like so:
"[markdown]": { "editor.formatOnSave": true, "editor.formatOnPaste": true },
Backend Mode
Backend mode is a special SDK operating mode designed for server-to-server (backend) usage. It allows a single SDK instance to record data on behalf of multiple applications and multiple devices simultaneously. This is useful for scenarios such as data migrations, server-side event recording, and batch processing where no real device context exists.
When backend mode is enabled, the SDK disables most client-oriented features (automatic session tracking, location, consent management, device ID changes, view recording, crash breadcrumbs, etc.) and instead exposes a dedicated interface where every call requires an explicit device_id and optionally a separate app_key.
Backend mode and normal (client) mode are mutually exclusive. When backend mode is enabled, all standard SDK calls (events, sessions, views, crashes, location, consent, device ID changes) become no-ops and return immediately. Only the methods exposed through the Backend Mode interface are functional.
The backend mode interface can be extended with additional functions as needed. If a use case requires recording data that is not covered by the current set of methods (e.g. feedback, remote config, or custom endpoints), new methods can be added following the same pattern: accept an explicit device_id, optional app_key and timestamp, build the appropriate request, and add it to the request queue or event pool.
Exposed Methods
Config Methods
CountlyConfig.enableBackendMode()
CountlyConfig.setMaxRequestQueueSize(maxSize: int)
CountlyConfig.setEventQueueSizeToSend(threshold: int)
CountlyConfig.setBackendModeAppEQSizeToSend(appEQSize: int)
CountlyConfig.setBackendModeServerEQSizeToSend(serverEQSize: int)
Instance Methods (accessed via BackendMode interface)
BackendMode.RecordEvent(deviceId: String, eventKey: String, segmentations: Map, count: int, sum: double, duration: long, appKey: String, timestamp: long)
BackendMode.RecordUserProperties(deviceId: String, userProperties: Map<String, Object>, appKey: String, timestamp: long)
BackendMode.RecordException(deviceId: String, error: String, stackTrace: String, breadcrumbs: List<String>, customInfo: Map<String, Object>, metrics: Map<String, String>, unhandled: boolean, appKey: String, timestamp: long)
BackendMode.BeginSession(deviceId: String, appKey: String, metrics: Map<String, String>, location: Map<String, String>, timestamp: long)
BackendMode.UpdateSession(deviceId: String, duration: int, appKey: String, timestamp: long)
BackendMode.EndSession(deviceId: String, duration: int, appKey: String, timestamp: long)
BackendMode.StartView(deviceId: String, name: String, segmentations: Map, segment: String, appKey: String, firstView: boolean, timestamp: long)
BackendMode.StopView(deviceId: String, name: String, duration: long, segmentations: Map, segment: String, appKey: String, timestamp: long)
BackendMode.ChangeDeviceIdWithMerge(newDeviceId: String, oldDeviceId: String, appKey: String, timestamp: long)
BackendMode.RecordDirectRequest(deviceId: String, parameters: Map<String, String>, appKey: String, timestamp: long)
Implementation Details
For config method enableBackendMode
CountlyConfig.enableBackendMode() // Logic Enables backend mode for the SDK instance. - When enabled, the SDK skips loading persisted events, sessions, exceptions, and user details from storage during initialization. - A dedicated BackendMode module is created after init completes. - The session timer is started (used to periodically flush the event pool). - All standard client-side feature calls become no-ops.
For config method setMaxRequestQueueSize
CountlyConfig.setMaxRequestQueueSize(maxSize: int) // Valid values maxSize must be a positive integer. Default is 1000. // Logic Sets the maximum number of requests the request queue can hold. When the queue is full, the oldest request is dropped (FIFO eviction) to make room for the new one. This prevents unbounded memory growth in long-running backend processes.
For config method setEventQueueSizeToSend
CountlyConfig.setEventQueueSizeToSend(threshold: int) // Valid values threshold must be a positive integer. Default is 10. // Logic Sets the per-device event queue threshold. When the number of events for a specific device ID reaches this threshold, the events are automatically flushed into a request and added to the request queue.
For config method setBackendModeAppEQSizeToSend
CountlyConfig.setBackendModeAppEQSizeToSend(appEQSize: int) // Valid values appEQSize must be a positive integer. Default is 1000. // Logic Sets the per-app-key event queue size limit. When the total number of events across all device IDs for a single app key reaches this limit, all events for that app key are flushed into requests.
For config method setBackendModeServerEQSizeToSend
CountlyConfig.setBackendModeServerEQSizeToSend(serverEQSize: int) // Valid values serverEQSize must be a positive integer. Default is 10000. // Logic Sets the global (server-wide) event queue size limit. When the total number of events across all app keys and all device IDs reaches this limit, all events across every app key are flushed into requests.
For instance method RecordEvent
BackendMode.RecordEvent(deviceId: String, eventKey: String, segmentations: Map, count: int, sum: double, duration: long, appKey: String, timestamp: long) // Valid values - deviceId: required, must not be null or empty - eventKey: required, must not be null or empty - count: defaults to 1, negative values are reset to 1 - sum: optional, defaults to null - duration: optional, defaults to null - segmentations: optional key-value pairs, values must be bool, int, long, string, double, or float - appKey: optional, defaults to the app key provided during init - timestamp: optional, defaults to current UTC time as Unix milliseconds // Logic Adds the event to the internal event pool, keyed by device ID and app key. The event is not immediately sent; it is batched and flushed when: - The per-device event threshold is reached - The per-app event limit is reached - The global event limit is reached - The periodic timer fires (dumps all buffered events)
For instance method BeginSession
BackendMode.BeginSession(deviceId: String, appKey: String, metrics: Map<String, String>, location: Map<String, String>, timestamp: long) // Valid values - deviceId: required - appKey: optional, defaults to init app key - metrics: optional device metrics; if null, internal SDK metrics are used - location: optional location parameters (country_code, city, location, ip_address) - timestamp: optional, defaults to current time // Logic Creates a begin_session request with &begin_session=1 and the provided or default metrics. If location data is provided, it is appended as additional query parameters. The request is immediately added to the request queue and an upload is triggered.
For instance method UpdateSession
BackendMode.UpdateSession(deviceId: String, duration: int, appKey: String, timestamp: long) // Valid values - deviceId: required - duration: required, must be = 1 (in seconds); values less than 1 are rejected - appKey: optional - timestamp: optional // Logic Creates a session update request with &session_duration=[duration]. Used to signal that the session is still active. The caller is responsible for sending periodic updates (e.g. every 60 seconds).
For instance method EndSession
BackendMode.EndSession(deviceId: String, duration: int, appKey: String, timestamp: long) // Valid values - deviceId: required - duration: session duration in seconds - appKey: optional - timestamp: optional // Logic Creates an end_session request with &end_session=1 and &session_duration=[duration].
For instance method RecordUserProperties
BackendMode.RecordUserProperties(deviceId: String, userProperties: Map<String, Object>, appKey: String, timestamp: long)
// Valid values
- deviceId: required, must not be null or empty
- userProperties: required, must not be null or empty
- appKey: optional
- timestamp: optional
// Logic
Separates properties into predefined keys (name, username, email,
organization, phone, gender, byear, picture) and custom keys.
Custom keys are placed under a "custom" sub-object.
Values must be valid data types (bool, int, long, string, double, float);
invalid types are removed with a warning.
String values that start with '{' are parsed as JSON objects to support
user property modifiers (e.g. $inc, $push).
The result is URL-encoded JSON set as the &user_details parameter.For instance method RecordException
BackendMode.RecordException(deviceId: String, error: String, stackTrace: String, breadcrumbs: List<String>, customInfo: Map<String, Object>, metrics: Map<String, String>, unhandled: boolean, appKey: String, timestamp: long) // Valid values - deviceId: required - error: required, must not be null or empty (used as _name in crash data) - stackTrace: optional (used as _error in crash data) - breadcrumbs: optional, joined with newlines (used as _logs) - customInfo: optional key-value pairs (used as _custom), invalid data types are removed - metrics: optional, only predefined crash metric keys are accepted: _os, _os_version, _ram_total, _ram_current, _disk_total, _disk_current, _online, _muted, _resolution, _app_version, _manufacture, _device, _orientation, _run - unhandled: defaults to false; when true, _nonfatal is set to false - appKey: optional - timestamp: optional // Logic Builds a crash JSON object and sends it as the &crash parameter. The request is immediately added to the queue and an upload is triggered.
For instance method StartView
BackendMode.StartView(deviceId: String, name: String, segmentations: Map, segment: String, appKey: String, firstView: boolean, timestamp: long) // Valid values - deviceId: required - name: required, the view name - segment: platform or domain identifier; defaults to a platform-specific value if empty - firstView: if true, adds a "start":"1" segmentation entry - segmentations: optional additional segmentation - appKey: optional - timestamp: optional // Logic Records a [CLY]_view event with "visit":"1" in the segmentation. The "name" and "segment" values are added to segmentation automatically. If firstView is true, "start":"1" is also added. The event is batched through the event pool (same batching as RecordEvent).
For instance method StopView
BackendMode.StopView(deviceId: String, name: String, duration: long, segmentations: Map, segment: String, appKey: String, timestamp: long) // Valid values - deviceId: required - name: required - duration: required, must not be negative - segment: platform or domain identifier - segmentations: optional - appKey: optional - timestamp: optional // Logic Records a [CLY]_view event with the given duration. Unlike StartView, "visit" is not added and "start" is not added. The duration is passed as the event duration parameter.
For instance method ChangeDeviceIdWithMerge
BackendMode.ChangeDeviceIdWithMerge(newDeviceId: String, oldDeviceId: String, appKey: String, timestamp: long) // Valid values - newDeviceId: required, must not be null or empty - oldDeviceId: required, must not be null or empty - newDeviceId must not equal oldDeviceId // Logic Sends a request with &old_device_id=[oldDeviceId] using newDeviceId as the device_id. The server will merge data from the old device ID into the new one.
For instance method RecordDirectRequest
BackendMode.RecordDirectRequest(deviceId: String, parameters: Map<String, String>, appKey: String, timestamp: long) // Valid values - deviceId: required - parameters: required, must not be null or empty - appKey: optional - timestamp: optional // Logic Sends a raw request with the provided key-value pairs appended as query parameters, plus &dr=1 to mark it as a direct request. This is a catch-all method for sending arbitrary data to the server that is not covered by other backend mode methods.
General Information
Backend mode is intended for server-side integrations where the SDK runs without a real device context. A typical use case is a backend service that collects analytics data on behalf of many users/devices and forwards it to the Countly server. Another common scenario is data migration, where historical data from a different system needs to be replayed into Countly.
Key behavioral differences from normal mode:
- No automatic device ID generation. Every call must provide an explicit
device_id. - No automatic session management. Sessions must be manually started, updated, and ended via the backend mode interface.
- Multi-app support. Each call can optionally specify an
app_keythat differs from the one provided during initialization. If omitted, the init-time app key is used. - No persistent storage. Events, sessions, exceptions, and user details are not written to disk. Request queue overflow is handled by dropping the oldest request (FIFO eviction).
- Consent is ignored. Since backend mode is server-side, feature consent checks are bypassed.
- Standard client-side calls (RecordEvent, RecordView, SessionBegin, SetLocation, ChangeDeviceId, SetConsent, AddCrashBreadCrumb, etc.) become no-ops and log a warning.
Networking and Params
All backend mode requests are sent to the /i endpoint. Every request includes a base set of parameters regardless of the specific method:
curl --request POST \ 'https://your.server.ly/i' \ --data 'app_key=YOUR_APP_KEY\ &device_id=DEVICE_ID\ &sdk_version=SDK_VERSION\ &sdk_name=SDK_NAME\ &hour=HOUR\ &dow=DAY_OF_WEEK\ &tz=TIMEZONE_OFFSET\ ×tamp=UNIX_TIMESTAMP_MS\ &t=0\ &av=APP_VERSION'
The &t=0 parameter is always included in backend mode requests. This signals to the server that the request originates from a non-device (backend) context, which may affect how the server processes certain data (e.g. skipping device-specific aggregations).
Feature-specific parameters are appended to this base. For example, an event request adds:
curl --request POST \ 'https://your.server.ly/i' \ --data 'app_key=YOUR_APP_KEY\ &device_id=DEVICE_ID\ &sdk_version=SDK_VERSION\ &sdk_name=SDK_NAME\ &hour=HOUR\ &dow=DAY_OF_WEEK\ &tz=TIMEZONE_OFFSET\ ×tamp=UNIX_TIMESTAMP_MS\ &t=0\ &av=APP_VERSION\ &events=URL_ENCODED_JSON_ARRAY'
Session requests append session-specific params:
// Begin session &begin_session=1&metrics=URL_ENCODED_JSON // Update session &session_duration=SECONDS // End session &end_session=1&session_duration=SECONDS
User properties are sent as:
&user_details=URL_ENCODED_JSON
Crash data is sent as:
&crash=URL_ENCODED_JSON
Device ID merge requests include:
&old_device_id=URL_ENCODED_OLD_ID
Direct requests include:
&dr=1&custom_param1=value1&custom_param2=value2
Event Batching
In backend mode, events are not sent individually. They are accumulated in a multi-level event pool organized by app key and device ID. Three thresholds control when events are flushed into requests:
- Per-device threshold (default 10): When the event count for a specific device ID within an app key reaches this limit, those events are flushed.
- Per-app threshold (default 1000): When the total event count across all device IDs for a single app key reaches this limit, all events for that app key are flushed.
- Global threshold (default 10000): When the total event count across all app keys and device IDs reaches this limit, all events are flushed.
Additionally, the SDK's periodic timer (same interval as the session update timer) triggers a full dump of all buffered events regardless of thresholds.
Storage
Backend mode disables persistent storage entirely. Events, sessions, exceptions, user details, and the request queue are held only in memory:
- Events are not saved to disk. They exist in the in-memory event pool until flushed into requests.
- The request queue is not persisted. If the process terminates before requests are sent, queued data is lost.
- User details changes are not written to storage.
- Session state is not persisted. Session files are not loaded during init and not saved during operation.
This design is intentional: backend mode is expected to run in server environments where disk I/O for analytics state is unnecessary and where the process lifecycle is managed externally.
Experimental
This section describes features that are under development and in a prototype phase
Visibility Tracking
CountlyConfig.experimental.enableVisibilityTracking() // Logic Enables visibility tracking for events and views. When enabled, the SDK tracks whether the app is in the foreground or background at the time an event or view is created, and records this as part of the data.
Previous Name Recording
CountlyConfig.experimental.enablePreviousNameRecording() // Logic Enables recording of the previous view name for view events and the current view name for custom events. This adds contextual navigation information to the recorded data.
Legacy Features
Star Rating (Legacy)
The legacy star rating and rating widget APIs predate the unified feedback widget system. They are retained for backward compatibility but new integrations should prefer the feedback widget API in the User Feedback section.
Exposed Methods
Config Methods
CountlyConfig.setStarRatingSessionLimit(limit: int)
CountlyConfig.setStarRatingCallback(callback: StarRatingCallback)
CountlyConfig.setStarRatingTextTitle(title: String)
CountlyConfig.setStarRatingTextMessage(message: String)
CountlyConfig.setStarRatingTextDismiss(text: String)
CountlyConfig.setIfStarRatingDialogIsCancellable(isCancellable: boolean)
CountlyConfig.setIfStarRatingShownAutomatically(isShown: boolean)
CountlyConfig.setStarRatingDisableAskingForEachAppVersion(isDisabled: boolean)
Instance Methods
CountlyInstance.recordRatingWidgetWithID(widgetId: String, rating: int, email: String, comment: String, userCanBeContacted: boolean)
CountlyInstance.presentRatingWidgetWithID(widgetId: String, closeButtonText: String, callback: FeedbackRatingCallback)
CountlyInstance.showStarRating(callback: StarRatingCallback)
General Information
The legacy star rating is a native 1-to-5 dialog that can be shown manually via showStarRating or automatically when the session count reaches a configured threshold. The threshold is tracked per app version by default (configurable to once per app lifetime via setStarRatingDisableAskingForEachAppVersion).
The legacy rating widget methods (recordRatingWidgetWithID, presentRatingWidgetWithID) interact with server-created rating widgets using the older /o/feedback/widget API. The presentRatingWidgetWithID method checks device type targeting (phone/tablet/desktop) before display.
Implementation Details
For instance method recordRatingWidgetWithID
CountlyInstance.recordRatingWidgetWithID(widgetId: String, rating: int, email: String, comment: String, userCanBeContacted: boolean) // Consent Requires "star-rating" consent. // Validation widgetId must be a non-empty string (reject otherwise). rating is clamped to the range [1, 5]. // Logic Records an event with key "[CLY]_star_rating" and the following segmentation: - widget_id: provided widget ID - rating: clamped rating value - contactMe: userCanBeContacted value - email: provided email - comment: provided comment - platform: current SDK platform - app_version: current app version
For instance method presentRatingWidgetWithID
CountlyInstance.presentRatingWidgetWithID(widgetId: String, closeButtonText: String, callback: FeedbackRatingCallback) // Consent Requires "star-rating" consent. // Logic 1. Fetches the widget configuration from /o/feedback/widget?widget_id=[widgetId] with common params. 2. Checks the widget's device type targeting (phone, tablet, desktop) against the current device. If the current device type is not in the target list, the widget is not shown. 3. If targeting passes, displays the widget in a WebView with the provided closeButtonText for the native close button. 4. Calls the callback on completion or error.
For instance method showStarRating
CountlyInstance.showStarRating(callback: StarRatingCallback)
// Consent
Requires "star-rating" consent.
// Logic
Displays a native 1-to-5 star rating dialog.
The dialog title, message, and dismiss button text are configurable via CountlyConfig
(defaults: title="App Rating", message="How would you rate the app?", dismiss="Dismiss").
If the user selects a rating:
- Records an event with key "[CLY]_star_rating" and segmentation:
platform, app_version, rating (1-5).
- Calls the callback with the selected rating.
If the user dismisses without rating, no event is recorded.Consent
All legacy star rating and rating widget operations require "star-rating" consent:
recordRatingWidgetWithIDpresentRatingWidgetWithIDshowStarRating- Automatic star rating display on session count threshold
Networking and Params
Legacy rating widget config
Fetches the rating widget configuration to check device type targeting before display.
curl --request POST \ --url 'https://YOUR_SERVER/o/feedback/widget' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'widget_id=WIDGET_ID' \ --data 'app_key=YOUR_APP_KEY' \ --data 'device_id=DEVICE_ID' \ --data 'sdk_version=SDK_VERSION' \ --data 'sdk_name=SDK_NAME'
Storage
Star rating preferences are persisted locally using platform-specific storage (e.g., SharedPreferences on Android, localStorage on Web). The following values are stored:
- Session count for automatic display threshold tracking
- Whether the star rating has been shown for the current app version
- The app version for which the star rating was last shown
- Custom dialog text (title, message, dismiss button text)
Remote Config (Legacy)
First off, interaction with the Countly Server for the Remote Config feature should be done after you have checked the available API information. The Remote Config API documentation for legacy remote config API includes information about an earlier implementation. This legacy API uses 'method=fetch_remote_config' inside the request URL while fetching the remote config object and enrolling the user to A/B testing automatically.
The latest API, on the other hand, fetches the remote config object while giving us the ability to enroll the user in the A/B testing or not. This can be done by utilizing 'method=rc' and 'oi=1'(opting in/enrolling the user) or 'oi=0'(opting out/not enrolling the user) in the request URL.
There is also another API that is used for enrolling the user to A/B testing for the selected remote config keys without fetching the remote config object. This can be done by using the 'method=ab' in your request URL.
Their usage can be seen below:
// legacy API o/sdk?method=fetch_remote_config&metrics=...&app_key=app_key&device_id=device_id...(optional params: keys, omit_keys) // latest API for remote config o/sdk?method=rc&metrics=...&app_key=app_key&device_id=device_id...(optional params: keys, omit_keys, oi) // enrolling users o/sdk?method=ab&keys=...&app_key=app_key&device_id=device_id
There are currently 2 init time config flag associated with these APIs: rcAutoOptinAb (true by default) and useExplicitRcApi (false by default). These flags enable users to change between APIs and control the A/B testing enrolling process. useExplicitRcApi lets the user use the latest API if it is set to true. rcAutoOptinAb decides if auto enrolling (oi=1) must be on or not (oi=1) when using the latest API. Setting it to false should automatically trigger the usage of the latest API and should log a warning to the user.
Automatic Fetch
The Remote Config feature allows app developers to change the behavior and appearance of their applications at any time by creating or updating custom key-value pairs on the Countly Server.
There should be a flag upon initial config to enable the automatic fetching of the remote config upon SDK start. If this flag is set, the SDK will automatically fetch the remote config from the server and store it locally. A locally stored remote config should reflect the server response as is, overwriting any existing remote config. No merging or partial updating. Automatic fetching will be performed only upon SDK start, not with every begin session. There should also be a callback on the initial config to inform the developer about the results of automatic fetching the remote config.
e.g. config.enableRemoteConfig = true;
Manual Fetch
There should be a method/function to fetch the remote config manually anytime the developer would like. Just like with automatic fetch, this method will fetch the remote config from the server and store it locally. A locally stored remote config should reflect the server response as is, overwriting any existing remote config. No merging or partial updating. This method should take a callback argument to inform the developer about the results of manually fetching the remote config. Callback on initial config should not be affected by manual fetchings, as it is for automatic fetchings only.
e.g. updateRemoteConfig(callback(){ })
Getting Values
There should be a method to get remote config values for a given key. It will return the value for a given key. If the key does not exist, or the remote config has yet to be fetched, this method should return nil or null or however the platform handles the absence of values. If the server is not reachable, this method should return the last fetched and locally stored value if available.
e.g. remoteConfigValueForKey(key)
Keys and Omit Keys
There should be 2 additional methods for manual fetching: one for specifying which keys will be updated and one for specifying which keys will be ignored.
These methods should take an array of keys as an argument, in addition to callbacks, and send requests with keys= or omit_keys= query strings. For the result of these requests, only the keys in the response should be updated in local storage, not a complete overwrite as with an automatic or standard manual fetch.
e.g. updateRemoteConfigForKeysOnly(keys, callback(){ }) e.g. updateRemoteConfigExceptKeys(keys, callback(){ })
Example case: Local storage reflecting server as is (after an automatic or manual fetch):
{
"a": "x",
"b": "y",
"c": "z",
}Calling update for specified keys only:
updateRemoteConfigForKeysOnly(["a"], callback(){ });
Response:
{
"a": "xx",
}
Local storage:
{
"a": "xx",
"b": "y",
"c": "z",
}
A/B Testing
There should be a call to enroll users in the A/B testing without triggering any other action. This call should be named something similar to enrollUserToAb and should have one parameter named keys. This parameter should be an array of string key values. If an empty array or no value at all is provided the function should be terminated and should give an error log to the developer. Else the keys should be stringified and added to the request as 'keys=stringified_values_here'. The response can be parsed and put out as a log. If all is good the response's result field should return something like "Successfully enrolled the user to given keys".
Consents
In case where the consentRequiredflag is set upon initial config, remote config actions should only be executed if the "remote-config" consent is given. Additionally, if sessions consent is given, the remote config requests will have metrics info, similar to begin session requests.
Device ID Change
After a device ID change, the locally stored remote config should be cleaned, and an automatic fetch should be performed if enabled upon initial config.
Salt
Remote config requests need to include the checksum if enabled upon initial config. As with all other requests, only the query string part will be used to calculate hash.