SDK Development Guide

Follow

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 URLThe 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.

App KeyThe 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 here.

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, for example event keys or for simple numerical values.

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 Countly Code Generator, 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 /i endpoint Server API reference.

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

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:

  1. Initiating request - either a new event reported or session call, etc.
  2. 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
  3. This payload is inserted into the queue (First In, First Out)
  4. All updates to the queue should be persistently stored. Based on the environment, you may directly use storage for the queue
  5. 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:

  1. The HTTP response code was successful (which is any 2xx code or code between 200 <= x < 300)
  2. The returned request is a JSON object
  3. 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.

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)
  • dowCurrent 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, ...)

Crash Reporting

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. Here is 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).

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.

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)

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.setCrashFilterCallback(callback);
Countly.sharedInstance().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.

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.

Symbolication (WIP)

Events

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

More on Event formatting can be found in the API Reference.

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.

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:

  1. event consent is given, but 'view' consent is not given
  2. dev calls "recordEvent('[CLY]_view')
  3. event is not recorded

Example 2:

  1. event consent is given, and 'view' consent is also given
  2. dev calls "recordEvent('[CLY]_view')
  3. event is recorded

Example 3:

  1. event consent is not given, but 'view' consent is given
  2. dev calls "recordEvent('[CLY]_view')
  3. 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

Metrics should only be reported together with the begin_session=1 parameter on every session start. Collect as many metrics as possible or allow some values to be provided by the user upon initialization. Possible metrics are listed in the API Reference.

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

Session Flow

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

Automatic Session Tracking

Automatic tracking should be done according to the platforms lifecycle. For apps that have a visual component (not command line or server call tracking), sessions should be tracked when the app is visible and in the foreground. That would mean starting the session when the app comes into foreground and ending the it goes to the background.

Manual Session Tracking (WIP)

Most of the official SDKs implement automatic session handling, meaning SDK users don't need to separately bother with session calls. However, it is good practice to provide a way to disable automatic session handling and allow SDK users to make session calls themselves through methods such as:

  • Countly.begin_session()
  • Countly.session_duration(int seconds)
  • Countly.end_session(int seconds)

Here is the documentation showing how you may report sessions through our API.

Session API

All these session requets are built uppon the base request which includes all the base params, therefore those won't be explicitly mentioned.

Starting a Session

The SDK should then send the begin_session=1 param. This same request should also contain metrics parameters with the maximum metrics described on /i page, which may be collected from this SDK-specific environment/language. It might look something like:

"&begin_session=1&metrics={...}"

Session 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:

"&session_duration=60"

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:

"&end_session=1&session_duration=15"

Sample Uses

Here are a few example requests generated by different session lengths:

2 min 30 second session 30 second session
begin_session=1&metrics={...}
session_duration=60
session_duration=60
end_session=1&session_duration=30

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.

View Tracking

Reporting views would allow you to analyze which views/screens/pages were visited by the app user as well as how long they spent on a specific view. If it is possible to automatically determine when a user visits a specific view in your platform, then you should provide an option to automatically track views. Also, it is important to provide a way to track views manually. 

View Structure

View information is packaged into events. There are 2 kinds of events: 

  • an event to indicate that a view was entered
  • an event to send the duration of the view and indicate that the view was exited

All view-related events use the event key "[CLY]_view" and are dependent on the consent key "views".

All view events are sent with a "count" of 1.

The duration field "dur" is used when reporting view duration. It should contain the view duration in seconds.

There are 5 potential segmentation values:

  • "name" - string value of the view name, for example, "View_1"
  • "segment" - string value of the SDK platform name, for example, "Android"
  • "visit" - if the view was entered, this property should be set with the value "1"
  • "start" - if the view was entered and it was the first view of a session this property should be set with "1"
  • "_idv" - the unique identifier of this view-session. This should be set to the String concatenation of 8 base64 characters created from 6 bytes of randomness (ideally crypto safe) and timestamp in ms.

A sample event for reporting the first view would look like this:

events=[
{
"key": "[CLY]_view",
"count": 1,
"segmentation": {
"name": "view1",
"segment": "Android",
"visit": 1,
"start": 1,
"_idv": "f0e8f5db5e5d9e7b9a45d3916b93e43dd091153fdfb6c9a6f"
}
}
]

Sample event for reporting this view's duration:

events=[
{
"key": "[CLY]_view",
"count": 1,
"dur": 30,
"segmentation": {
"name": "view1",
"segment": "Android",
"_idv": "f0e8f5db5e17ad5ce5cf53916b93e43dd091153fdfb6c9a6f"
}
}
]

Here is more information on view-tracking APIs.

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

During the first SDK initilization, 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.

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 bellow table.

Whatever value is acquired, it should be stored persistently. 

If another value was already acquired before, that one should be used unless a "clear stored device ID" flag is used.

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. In this case, the Countly SDK needs to provide a way to change the device ID at any point while the app is running.

This change can be done with either with a server side merge or without it.

Note: If a new and current device ID is exactly the same, then the Countly SDK must ignore this change call.

When changing device ID, it has to be done to a valid value. Making the SDK regenerate a new device ID should not be possible by providing an invalid value. If an invalid value (empty or null) is provided, the request is ignored and it prints a warning.

Changing ID Without Merging

It should replace the internally used device ID with the new one, and use it for all new requests, persistently storing it for further sessions. The Countly SDK should follow these 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 for further session use
  • Begin a new session with the new device ID

Changing ID With Merging

Developers may need to change a device ID to their own internal user ID and merge the server-side data previously generated by a user while he/she was unauthenticated. It is similar to "Changing ID without merging", but the Countly SDK will need to merge the data on the server as well. In order to make a proper transition, the Countly SDK should follow these steps:

  • Temporarily keep the current device ID
  • Change the device ID and store it persistently for further session use
  • Use the old_device_id API with the temporarily kept, old device ID to merge the data on the server
  • No need to end and 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. It would be named similar to "GetDeviceId()".

There should also be a call that returns the current device ID type. A "type" identifies the source of that device ID. The call would be named similar to "GetDeviceIdType()".

There are 3 device ID types:
* SDK_GENERATED - this ID was generated by the SDK.
* DEVELOPER_SUPPLIED - this ID was provided by the developer. Either during init or by changing the device ID after init.
* TEMPORARY_ID - the SDK is in the temporary ID mode.

Temporary ID Mode (WIP)

It's a mode that the SDK can enter. While in it, no requests, that are created under it, would be sent to the server. All data would be recorded under a temporary ID tag. Once a real device ID is acquired, that temporary tag would be replaced with the new device ID and they would be sent to the server.

Push Notifications

Push notifications are platform-specific and not all platforms have them. However, if your platform does, you would need to register your device to the push notification server and send the token to the Countly server. For more information, please click here for API calls.

From the SDK API point of view, there could be one simple function to enable push notifications for the Countly server:

Countly.enable_push()

Actioned Events

When recording actioned events, one of the segmentation values recorded is the platform value. It is recorded with the key "p". The platform is then recorded with one unique character. Each platform needs to have it's own unique value. The currently used ones are the following:

  • "a" - Android
  • "i" - iOS
  • "m" - macOS

Platform Specific Notes

Additional Intent Redirection Checks (Android)

To increase platform security and limit exploits, google has enforced additional requirements for push notification that require additional checks for push intents. More info can be found here.

These additional checks should be optional and there should be a way to enable the during init/push setup. Something like this:

CountlyPush.useAdditionalIntentRedirectionChecks = true;

If these are enabled then the SDK will enforce additional security checks.

As additional parameters there would a one or multiple allow lists to provide details of what kind of packages or activities are allowed. There should be a way to enable the during init/push setup.

Providing that information could look something like this:

List<String> allowedClassNames = new ArrayList<>();
allowedClassNames.add("MainActivity");
List<String> allowedPackageNames = new ArrayList<>();
allowedPackageNames.add(getPackageName());

CountlyConfigPush countlyConfigPush = new CountlyConfigPush(this, Countly.CountlyMessagingMode.PRODUCTION)
.setAllowedIntentClassNames(allowedClassNames)
.setAllowedIntentPackageNames(allowedPackageNames);
CountlyPush.init(countlyConfigPush);

Recording Location

There are 4 location related parameters that can be set in a Countly SDK. It is "country code" (in ISO format), "city", "location_gps" (GPS coordinates), "IP" address. Their params in API requests are: "country_code", "city", "location", "ip".

Location information is not stored persistently (in a way that would survive SDK shutdowns un further inits). Location information is stored only in memory as variables. The SDK should cache only the latest location information.

The SDK has a single "setLocation" request where the dev can pick and choose which values he wants to set. All set values are sent in a single request. The provided values overwrite the previously cached values. If some field was left out of the "setLocation" request, that means that the previously cached value should be erased. Only the params that are not null and not empty strings should be sent.

Init should have a way to set all location values (through config): location_gps, city, country, ipAddress. Init config should also have a call to disable location. If during init location is disabled and location values have been set, the disable location call should take precedence.

If there is session consent, location values set in init should be sent in the first "begin_session" request and cached internally. Location values set after init but before the first begin session request would overwrite the values set in init and should be sent with the first begin_session request. If there is no session consent or automatic session recording is disabled, location values set in init should be sent as a separate request that contains only location values and they should be cached internally.

If during init location tracking is disabled and session consent is not given, empty "location" param should be sent in a independent request.

Location values set after init and after the first "begin_session" request should be sent in a separate request and update the internal location value cache. This internal location cache should be added to every non-first "begin_session" request. Values set in newer "setLocation" calls overwrite the cached values from previous calls.

If the location feature gets disabled or location consent is removed, the SDK sends a request with an empty "location" parameter.

If location is disabled or no location consent is given, the SDK adds an empty location parameter to every "begin_session" request.

If location consent is given and location gets reenabled (previously was disabled), we send that set location information in a separate request and save it in the internal location cache.

If location consent was removed and is given back, no special action needs to be taken.

If city is not paired together with country, a warning should be printed that they should be set together.

When an empty "location" entry is sent to the server, it is interpreted as "disable location tracking for this device ID".

Empty country code, city and IP address can not be sent.

Init Config Options

Feature Calls

Storage

Consent

The location feature depends on the "location" consent.

Networking and Params

There are 4 location related parameters that can be set in a Countly SDK. It is "country code" (in ISO format), "city", "location_gps" (GPS coordinates), "IP" address. Their params in API requests are: "country_code", "city", "location", "ip".

Location requests are sent to the "/i" endpoint.

Location requests should be sent through the request queue.

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 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 send into the event queue, if click or/and scroll tracking is enabled. Automatic click and scroll tracking should be able to be enabled after init:

Asynchronous Synchronous
Countly.q.push(['track_scrolls']);
Countly.q.push(['track_clicks']);

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. Server would provide 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. Message starts with 'cly:' and includes 'app_key','token','purpose' and 'url' properties as a JSON object.

To prevent any XSS attempts, the SDK should provide an option to the developer to give a list of trustable domains (a whitelist per se) for which the SDK would load the provided scripts from and would reject to load scripts from domains out of this list. By default the Countly server URL would be included in this list. List should be provided by the user during init, under the 'heatmap_whitelist' flag as an array of stings.

Asynchronous Synchronous
Countly.app_key = "YOUR_APP_KEY";
Countly.url = "https://try.count.ly";
Countly.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.

Consent

Both click and scroll tracking is subject to provided consent, namely "clicks" and "scrolls". If consents are enabled, user actions like clicks and scrolls should only be collected if the consent is provided, else 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.

Users can participate in AB tests. For that to happen, they must enroll in those tests (keys) with specific requests.

All of these calls should be behind a "RemoteConfig" interface if possible. If that is impossible, all function calls should start with "RemoteConfig."

Remote Config API

Downloaded remote config values have to be stored persistently.

Consent

This feature depends on the "remote-config" consent if consents are enabled.

When it is given, it would trigger RC values to download (if enabled).

When it is removed, the RC storage structure should be cleared.

Data Structures, Notes

RCDownloadCallback

The RCDownloadCallback callback is called when RC values are downloaded. Its return values:

  • Result/error: Enum (RequestResult)
  • Error message: String. "null" if there is no error.
  • Full Update: Boolean ("true" - all values updated, "false" - a subset of values updated)
  • Updated values: Map<String, Object> (the whole downloaded RC set, the delta)
RCDownloadCallback {
  void callback(RequestResult rResult, String error, boolean fullValueUpdate, Map<String, RCData> downloadedValues)
}

RCData

RCData Class is returned when retrieving downloaded values. The isCurrentUsersData field indicates if it is the cached data of the previous user or an up-to-date value of the current user:

Class RCData {
  Object value;
  Boolean isCurrentUsersData;
}

Other Enums

RequestResult:

Enum RequestResult { Error, Success, NetworkIssue }

Automatic Download Triggers

Certain events and features can trigger the download/re-download or caching/erasure of RC values if automatic RC triggers are enabled: 

SDK Init - We download the RC values at init.

Temp ID - Entering temp ID mode should clear the RC cache. Coming out of temp ID mode should download RC values again.

Consent - Receiving "remote-config" consent should trigger RC values to download. Removing that consent should clear the stored values.

Device ID Change - Changing the device ID without merge should clear cached RC values. And re-download them for the new ID.

Value Caching Mechanism

The value caching mechanism tracks which values belong to which user in case of a device ID change.

If the device ID was changed, but the RC values were not re-downloaded or partially re-downloaded, this mechanism shows to which user (current or previous) the values belong.

Init Time Configuration

Config flag enables RC values to be downloaded automatically on specific automatic triggers. Disabled by default.

config.enableRemoteConfigAutomaticTriggers()

There should be a config flag that would automatically enroll a user to available experiments while RC values are downloaded. Disabled by default.

config.enrollABOnRCDownload()

Registering the callback function that would be called when the RC is downloaded. This can be called multiple times, all callbacks would be notified.

config.remoteConfigRegisterGlobalCallback(RCDownloadCallback callback)

Enable/disable caching of previous user's RC values. It is disabled by default. If enabled, old values must be kept after the device ID change, and the metadata must be updated to show isCurrentUsersData as false . 

config.enableRemoteConfigValueCaching()

All cached values must be cleared when receiving results for the next full update. In case of a partial update only the downloaded values must be updated and the rest would still have their isCurrentUsersData as false.

Usage

SDK fetches all or a partial amount of RC keys from the Server.

Downloaded values are saved persistently.

Downloading Keys Manually

Downloading has three modes: fetch all keys, fetch given keys, or fetch except given keys:

"Download all keys" makes the main API call without optional parameters and stores the downloaded values:

Countly.RemoteConfigDownloadKeys(RCDownloadCallback callback)

"Download given keys" makes the main API call with the optional parameter 'keys.' The developer should provide these keys as String Array. Then these values must be passed as params to the request URL (&keys=["key1", "key2"...])

Countly.RemoteConfigDownloadSpecificKeys(String[] keys, RCDownloadCallback callback)

"Download except for given keys" makes the main API call with the optional parameter 'omit_keys.' The developer should provide these keys as String Array. Then these values must be passed as params to the request URL (&omit_keys=["key1", "key2"...])

Countly.RemoteConfigDownloadOmittingKeys(String[] omitKeys, RCDownloadCallback callback)

All of those calls target the following server endpoint, but if auto enrolling flag is set (enrollABOnRCDownload) then the opt in parameter must be set to 1 and added the the URL formed (like '&oi=1'):

o/sdk?method=rc&metrics=...&app_key=app_key&device_id=device_id...(optional params: keys, omit_keys, oi)

When working correctly returns a JSON Object of key-value pairs like this:

{
  "key1": "val1",
  "key2": "val2"
  ....
}

Add/Remove Download Callback Listeners

Dev should be able to register new callbacks:

Countly.RemoteConfigRegisterDownloadCallback(RCDownloadCallback callback)

Or remove a registered callback:

Countly.RemoteConfigRemoveDownloadCallback(RCDownloadCallback callback)

Getting Values

Gets the map that contains all values from the storage, it returns Map<String, RCData>.

Countly.RemoteConfigGetAllKeys()

Gets values for a specific key from the map of all values downloaded. It returns a RCData. If the value doesn't exist then 

Countly.RemoteConfigGetKey(String key)

Clear All Values

A call to wipe the persistently stored RC values.

Countly.RemoteConfigClearAll()

A/B Testing API

Consent, Data Structures, Notes

This feature depends on the "remote-config" consent.

When it is given, nothing should be done.

When it is removed, nothing should be done.

Usage

To enroll a user into an AB test there should be a call that takes a list of keys that were used in the app. This would indicate that if any of those were used for an AB test, that the user should be enrolled in them.

Countly.RemoteConfigEnrollIntoABTestsForKeys(String[] keys)

A user can be enrolled to some keys (["key1","key2"...]) with this API:

o/sdk?method=ab&keys=...&app_key=app_key&device_id=device_id

This should return a JSON Object like this:

{"result": "Successfully enrolled in ab tests"}

There should be a call that would remove the user from any AB tests that would involve the given keys.

Countly.RemoteConfigExitABTestsForKeys(String[] keys)

To remove a user from some or all A/B tests, the following API is used:

o/sdk?method=ab_opt_out&app_key="APP_KEY"&device_id=DEVICE_ID (optional param: keys)

If an optional parameter with keys (["key1", "key2"...]) is not provided, the user is removed from all experiments.

A/B Testing Variant Control API

The intended use for this API is only for app testing. It is not intended to be used in production environments. The function names have a "Testing" prefix to indicate that they should be only used for testing.

Downloaded variant values are stored only in memory.

Consent, Data Structures, Notes

This feature depends on the "remote-config" consent.

When it is given, nothing should be done.

When it is removed, the variant storage structure should be cleared.

The RCVariantCallback callback is called when AB variants are downloaded. Its return values:

  • Result/error: Enum (RequestResult). This enum's values are described in the RemoteConfig section.
  • Error message: String. "null" if there is no error.
RCVariantCallback {
  void callback(RequestResult rResult, String error)
}

Init Time Configuration

There are no init time configuration options.

Usage

Download Variant Information

Initiate the network request for fetching the variant values from the server. The end result would be stored in memory.

Countly.RemoteConfig.TestingDownloadVariantInformation(RCVariantCallback completionCallback)

The main server API endpoint:

o/sdk?method=ab_fetch_variants&app_key="APP_KEY"&device_id=DEVICE_ID

This would return a JSON Object like this:

{
  "key1": [
    {
      "name": "variant1",
      "value": "value1"
    },
    {
      "name": "variant2",
      "value": "value2"
    }
  ],
  "key2": [
    {
      "name": "variant1",
      "value": "value1"
    }
  ]
}

Here this object should be parsed into a Map<String, String[]> like this and saved into memory:

{
  "key1": ["variant1", "variant2"],
  "key2": ["variant1"]
}

Get All Variants

A call that returns all variant information (stored in memory) parsed as Map<String, String[]>.

Countly.RemoteConfig.TestingGetAllVariants()

Get Variants For Key

Returns variant information (stored in memory) for a specific key. The return type is String[]. If there is no entry for that key, it returns 'null'.

Countly.RemoteConfig.TestingGetVariantsForKey(String valueKey)

Enroll User Into Variant

Dev should provide the key and the variant strings for that to happen:

Countly.RemoteConfig.TestingEnrollIntoVariant(String keyName, String variantName, RCVariantCallback completionCallback)

A user can be enrolled into a variant, and a key is given with this API:

o/sdk?method=ab_enroll_variant&app_key="APP_KEY"&key=...&variant=..&device_id=DEVICE_ID

This should return a JSON Object like this:

{"result": "success"}

User Feedback

Star Rating

If possible, the SDK should provide a simple 1 to 5 star-rating interface for receiving user feedback about the application. The interface will have a simple message explaining its purpose, a 1 through 5-star meter for receiving users’ ratings, and a dismiss button in case the user does not wish to give a rating. This star rating has nothing to do with App Store/Google Play Store ratings and reviews. It is just for getting brief feedback from users to be displayed on the Countly dashboard.

After a user gives a rating, a reserved event will be recorded with [CLY]_star_ratingas the key and following as the segmentation dictionary:

  • platform: on which the application runs
  • app_version: application's version number
  • rating: user's 1-to-5 rating

If a user dismisses the star-rating dialog without giving a rating, an event will not be recorded. The star-rating dialog's message and dismiss button title may be customized using the properties on the initial configuration object.

CountlyConfiguration.starRatingMessage = "Custom Message";
CountlyConfiguration.starRatingDismissButtonTitle = "Custom Dismiss Button Title";

If not explicitly set, the message should read, "How would you rate the app?" and the dismiss button title will read, "Dismiss" or one of the corresponding localized versions depending on the device’s language.

The star-rating dialog may be displayed in 2 ways:

1. Manually by the Developer

The star-rating dialog will be displayed when the developers call the specified method, such as askForStarRating. Optionally, there will be a callback method indicating the user's 1-to-5 rating value for the developer if the developer wants to use the rating.

Countly.askForStarRating(callback);

There is no limit on how many times the star-rating dialog may be manually displayed.

2. Automatically, Depending on the Session Count

The star-rating dialog will be displayed when the application's session count reaches a specified limit, once for each new version of the application. The SDK should keep track of the session count for each app version locally and compare it to the specified count on each app launch. This session count limit may be specified upon initial configuration.

CountlyConfiguration.starRatingSessionCount = 5;

Once the star-rating dialog has been displayed automatically, it will not be displayed again unless there is a new app version.

Upon initial configuration, there should be an optional flag called starRatingDisableAskingForEachAppVersion to force the star-rating dialog to be displayed only once per app lifetime instead of for each new version.

CountlyConfiguration.starRatingDisableAskingForEachAppVersion = false;

Rating Widgets

Automatic Rating Widgets

Automatic widgets are integrated with custom HTML. That is either injected into the page (web) or shown in a webView (mobile).

To present such a widget, you would have the following call:

presentRatingWidgetWithID(String widgetId, String closeButtonText, RatingWidgetCallback callback)

It takes the ID of the widget, the custom close button text, and a callback.

Manual Rating Widgets

In case a developer wants to use their custom UI, they can report the result manually.

They would use the following call:

That function should be called "recordRatingWidgetWithID" and it should have the following parameters:
"(String widgetId, int rating, String email, String comment, boolean userCanBeContacted)"

"recordRatingWidgetWithID" should record an event with the internal key "[CLY]_star_rating". The event should have the following segmentation:

  • "platform" - current platform
  • "app_version" - current app version
  • "rating" - provided rating result. In the range from 1 to 5. (Mandatory)
  • "widget_id" - provided widget ID. (Mandatory)
  • "contactMe" - provided value.
  • "email" - provided value.
  • "comment" - provided value.

Basic filtering (type checks) on the provided values should be performed. Mandatory values must be provided. Invalid widget ID's (non string or empty values) should not be accepted. Rating value should be modified, if necessary, so that it lies within the acceptable range of [1,5].

Feedback Widgets

The Feedback Widgets feature is a way to gather insights and opinions from users. Showing Feedback Widgets or performing any of the Feedback Widget-related features requires feedback consent.

Before any Feedback Widgets can be shown, they need to be created in the Countly Dashboard.

The Feedback Widgets API provides access to three types of widgets:

  • Surveys: A list of questions supporting various answer formats such as multiple-choice, textbox, and rating scales.
  • Ratings: Collects user feedback using a 1-5 rating scale. Has an option to leave a comment, and lea to leave an email for future contact.
  • NPS (Net Promoter Score): Questions on a scale from 0-10, with customization options for follow-up questions.

They are shown using a very similar server API and basically the same processing. This is also an alternative method to use rating widgets, which are also now included in this newer SDK API. Refer to the Feedback User Guide for more detailed information about Feedback Widgets.

Feedback widgets can be used through three methods:

  • Automatic Server Rendered Widget: The server rendered widget is inserted in the web page or the UI, using a WebView, by the SDK.

  • Manually Rendered and Reported Widget: The client app builds a custom UI, and the results are reported to the SDK manually. Used in cases where the developer wants to use a custom UI.

  • Server Rendered Widget in a Custom WebView: The SDK builds a required URL to be used in a WebView. This would then be used in the WebView of the client app of choice.

Retrieving the List of Eligible Widgets

The first step to showing a feedback widget is getting a list of the available widgets for this device ID. That would be done with a function named similar to getAvailableFeedbackWidgets. That call takes a callback. That callback returns 2 values. The second is the error string. The first one is a list of available widget objects (or any other mechanism that allows the grouping of this data). If a class is used for grouping, it should be named similar to CountlyPresentableFeedback. That object contains 4 core values (widget id (_id), widget type (type), widget name (name), tags (tg)) and 1 optional value (UI info (appearance)). Potential type values are currently "nps", "survey" and "rating".

The URL to acquire all available widgets in a list is:

/o/sdk?method=feedback&app_key=[appKey]&device_id=[deviceID]&sdk_version=[sdkVersion]&sdk_name=[sdkName]

If parameter tampering is enabled, the sha256 param should be added with the checksum.

If a temporary device ID is enabled, feedback widgets can't be shown, so an empty list of available widgets is returned.

The server side will determine if there are any valid surveys for this device ID and return something similar to:

{
  "result":[
      {
        "_id":"614871419f030e44be07d82f",
        "type":"rating",
        "appearance":{
          "position":"mleft",
          "bg_color":"#fff",
          "text_color":"#ddd",
          "text":"Feedback"
          },
        "tg":["/"],
        "name":"Leave us a feedback"
      },
      {
        "_id":"614871419f030e44be07d839",
        "type":"nps",
        "name":"One response for all",
        "tg":[]
      },
      {
        "_id":"614871429f030e44be07d83d",
        "type":"survey",
        "appearance":{
          "position":"bLeft",
          "show":"uSubmit",
          "color":"#0166D6",
          "logo":null,
          "submit":"Submit",
          "previous":"Previous",
          "next":"Next"
          },
        "name":"Product Feedback example",
        "tg":[]
      }
    ]
  }

result contains a JSON Array of widget Objects. The _id is used to construct the web view URL. Tags (the tg key) returns an Array of String values. This is information provided by the creator of the widget and can be used for various reasons. However the main goal is to provide versioning or a whitelist of domains to present the widget, and its implementation is left to the developer.

The idea is that the developer would retrieve this list of potential widgets and decide any further action he would want to take with them with respect to the information provided.

Constructing WebView URL

Constructing a WebView URL requires calling the related method by passing a CountlyFeedbackWidget object. Using information from that object, a widget URL will be constructed.

Constructed URL using the widget ID (_id value) looks like this:

//for nps
/feedback/nps?widget_id=[widgetID]&device_id=[deviceID]&app_key=[appKey]&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]

//for basic surveys
/feedback/survey?widget_id=[widgetID]&device_id=[deviceID]&app_key=[appKey]&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]

//for ratings
/feedback/rating?widget_id=[widgetID]&device_id=[deviceID]&app_key=[appKey]&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]

//web SDK would also pass "origin"
//web SDK also passes "widget_v=Web"

The created URL contains params for widget_id, device_id, app_key, sdk_version, sdk_name, app_version, platform, and origin (for Web SDK).

Even if parameter tamper protection is enabled, this URL does not use the checksum param!

That URL should then be provided to a WebView and shown as an alert dialog similar to the rating widget.

Automatic Feedback Widgets

Automatic Feedback Widget reporting has 3 steps:

  1. Retrieve a list of available widgets and pick one.
  2. Presenting the widget with presentFeedbackWidget call.
  3. Handling the callbacks.

As explained in the 'Retrieving the List of Eligible Widgets' section, the first step uses the getAvailableFeedbackWidgets function to communicate with the Countly server and obtain a list of available widgets based on the specific device. This list contains information about each widget, such as its ID, type, name, and tags.

Once the list is retrieved and the developer decides upon the widget they are going to use, as explained in the 'Constructing the WebView URL' section, they would call presentFeedbackWidget method and pass the chosen widget (CountlyFeedbackWidget) object.

It should be possible to provide 2 optional callbacks to the presentFeedbackWidget call:

  1. a callback (potentially named widgetShown) that is called when the widget is successfully presented (currently that would mean that there were no issues/errors while trying to show the dialog with the WebView). Also, we aren't verifying if the WebView is showing a working widget. If there are any issues during the display of the widget, this callback will return an error message.
  2. a callback (potentially named widgetClosed) that is called when the widget/dialog is closed (for mobile SDKs and for the web SDK that would mean slightly different things, but the main point is to have the host app/site notified of when the "feedback process" is over). If there are any issues during the closing of the widget, this callback will return an error message.

Manual Feedback Widgets

Manual feedback widget reporting has 3 steps:

  1. Retrieve a list of available widgets and pick one. This is the same initial step with the automatic feedback widgets and reuses the same call to retrieve them.
  2. Download widget data from the server (for that single widget with the information that has been retrieved from the previous step).
  3. Report the feedback result (for that single widget according to that data from the previous step).

As mentioned before, the first step uses getAvailableFeedbackWidgets function, which is also used for automatic feedback widgets. After inspecting the returned list, the developer would select one widget he would want to report from the list of widgets, and then he would use this as the CountlyFeedbackWidget object.

The second step uses the CountlyFeedbackWidget object from the previous step and calls getFeedbackWidgetData function. This function call accepts the CountlyFeedbackWidget object and a callback. That callback returns 2 values - the retrieved CountlyWidgetData JSON and an error message string. The string is used in case there are some issues with this call.

The returned CountlyWidgetData JSON is the response from making a server request. That request is done outside of the request queue (so a direct request). Using the widget ID and widget type information from the provided CountlyFeedbackWidget object, we construct a URL similar to:

//for nps
/o/surveys/nps/widget?widget_id=[widgetID]&shown=1&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]

//for basic surveys
/o/surveys/survey/widget?widget_id=[widgetID]&shown=1&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]

//for rating widgets
/o/surveys/rating/widget?widget_id=[widgetID]&shown=1&sdk_version=[sdkVersion]&sdk_name=[sdkName]&app_version=[appVersion]&platform=[platform]

Performing a request on that URL should return JSON describing the widget, which should then be returned as a parsed JSON object.

After this step, the developer has all the information he needs to create the widget and all the information required to prepare a response (namely the widgetResult object) to "report" the filled widget.

The developer would look at this document to better understand how to interpret the widget type and data to fill out the widgetResult object. Depending on the type of the widget being reported this object would have different key/value pairs.

At the third step, the developer would call the reportFeedbackWidgetManually function to report the result. This call requires 3 fields/values/parameters: CountlyFeedbackWidget object from the first step, CountlyWidgetData object from the second step and the ,widgetResult object that has been formed and provided by the developer. If the widgetResult is set to "null" then that means that the widget was closed without filling it out (this still requires an event to be created).

CountlyFeedbackWidget object is used for obtaining widget id and type, while CountlyWidgetData object is used to verify the correctness of the reported widgetResult. For now, this second step is optional, but might become mandatory in the future, therefore both fields should be required from the start.

The reported widget result should be recorded as an event and must be put into the event queue. This event must have the "[CLY]_nps"(for nps), "[CLY]_survey"(for survey) or "[CLY]_star_rating"(for rating) key respective to the type of the reported widget. That event should have the following segmentation:

  • platform - SDK platform
  • app_version - host app version
  • widget_id - respective widget ID

In addition to this segmentation which identifies the widget, the contents of the provided widgetResult object should be merged into this event's segmentation. If the widgetResult was provided null and so the widget was reported as closed, you should additionally add the following segmentation value:

  • closed - with the value of "1"

After this event has been added to the event queue, the event queue should be forcefully combined into a request, even if the event count is under the threshold. This way the event is sent as soon as possible to the server and marks the widget as "completed" for that specific user.

API and Data Structures Exposed by the SDK

Provided functionality should require Feedback consent.

Not all platforms can support methods to present the widget.

If supported by the platform, the 'Feedback' interface should expose the following functions:

// Retrieves the list of available Feedback Widgets
void getAvailableFeedbackWidgets(RetrieveFeedbackWidgets callback);
  
// Displays a specific widget
void presentFeedbackWidget(CountlyFeedbackWidget widgetInfo, Context context, String closeButtonText, FeedbackCallback devCallback);
  
// Retrieves a specific widget's data as a JSONObject
void getFeedbackWidgetData(CountlyFeedbackWidget widgetInfo, RetrieveFeedbackWidgetData callback);
  
// Manually reports a widget's results
void reportFeedbackWidgetManually(CountlyFeedbackWidget widgetInfo, JSONObject widgetData, Map<string, object> widgetResult);
  
// Construct URL for the chosen feedback widget
string constructFeedbackWidgetUrl(CountlyFeedbackWidget chosenWidget);

Retrieved CountlyFeedbackWidget object would look like this:

class CountlyFeedbackWidget {
  public String widgetId;
  public FeedbackWidgetType type;
  public String name;
  public String[] tags;
}

The RetrieveFeedbackWidgetscallback (used for getAvailableFeedbackWidgets) returns a list of CountlyFeedbackWidget objects as the first parameter and an error string as the second.

The RetrieveFeedbackWidgetDatacallback (used for the getFeedbackWidgetData) returns the widget data as the first parameter and an error string as the second parameter. The data is returned in a JSON Object. Check here for example data structures returned.

The FeedbackCallback callback (used for presentFeedbackWidget) returns an error string. The error is "null" in case there were no issues.

There are no Init time config options for this feature.

User Profiles

Your SDK does not need to have a platform-specific way to receive user data if it isn’t possible on your platform. However, you will need to provide a way for a developer to pass this information to the SDK and send it to the Countly server.

To do so, you may create a method to accept an object with key/regarding the user, which are described here, or provide a parameterized method to pass the information regarding the user. Note that all fields are optional.

Additionally, there could be custom key values added to the user details. In this case, you would need to provide a means to set them:

  • Countly.user_details(map details)
  • Countly.user_custom_details(map custom_details)

You may find more information on what data may be set for a user by following this link.

If a "null" value is set to a user property, the SDK should ignore the value and print a warning.

If an empty string is provided as a value then that should lead to the deletion of the user property on the server side. This should trigger the SDK to send a JSON null value assigned to the property.

Modifying Custom Data Properties

You should also provide an option to modify custom user data, such as by increasing the value on the server by 1, etc. Since there are many operations you could perform with that data, it is recommended to implement a subclass for this API, which may be retrieved through the Countly instance.

The standard methods that should be provided by the SDK are as follows (provided as pseudo-code, naming conventions may differ from platform to platform):

  • Countly.userData.set(string key, string value)
  • Countly.userData.setOnce(string key, string value)
  • Countly.userData.increment(string key)
  • Countly.userData.incrementBy(string key, double value)
  • Countly.userData.multiply(string key, double value)
  • Countly.userData.max(string key, double value)
  • Countly.userData.min(string key, double value)
  • Countly.userData.push(string key, string value)
  • Countly.userData.pushUnique(string key, string value)
  • Countly.userData.pull(string key, string value)
  • Countly.userData.save() //send data to server

Notewhen reporting to the server, assure the push, pushUnique, and pull parameters can provide multiple values for the same property as an array.

Here is more information on how to report this data to the server.

Orientation Changes

This feature sends an event of the current orientation. It is sent when the first screen loads and every time the orientation changes.

Orientation tracking is enabled by default if the required consent is given.

Orientation tracking can be disabled during init. The config variable would be named similar to "enableOrientationTracking" which then would receive a "false" value to turn orientation tracking off.

Orientation change tracking requires "users" consent.

When recording the orientation event, you should use the key "[CLY]_orientation". You would set the count to "1" and provide a single segmentation value. The key for that value is "mode" and the value is "portrait" if the current orientation is portrait mode or "landscape" if the current orientation is landscape mode.

Application Performance Monitoring

Countly server supports multiple metrics for performance monitoring. They are divided into 3 groups:

  • Custom traces
  • Network request traces
  • Device traces

Trace / Metric keys

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.

API Calls

Currently, there is no batching functionality for APM requests. Every APM data point/trace should be put in the request queue immediately after it is acquired. So that would be after a network request is finished, after a custom trace has ended, every time the device goes to the background or foreground, etc.

APM data is combined into a single JSON object which is set to the "apm" param. So a basic request would look similar to:

/i?app_key=app_key
&device_id=device_id
&dow=dow
&hour=hour
&timestamp=timestamp
&apm={ _apm_params }

Custom Traces

These are used as a tool 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.

 Sample custom trace API request:

/i?app_key=xyz
&device_id=pts911
&apm={"type":"device",
"name":"forLoopProfiling_1",
"apm_metrics":{"duration": 10, “memory”: 200},
"stz": 1584698900000,
"etz": 1584699900000}
&timestamp=15847000900000

Network Traces

Sample network trace request:

/i?app_key=xyz
&device_id=pts911
&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}
&timestamp=1584698900000

Device Traces

Sample device trace request:

/i?app_key=xyz
&device_id=pts911
&apm={"type":"device",
"name":"
app_start",
"apm_metrics":{"duration": 15000},
"stz": 1584698900,
"etz": 1584699900}
&timestamp=1584698900

 

User Consent

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.

Additionally, the user may change his/her mind during the app run and opt-out of some features. Therefore, the SDK should be able to enable or disable these features on run time.

Consent persistence is handled by the host app and not by the SDK. In exceptional circumstances (in cases it is needed) it is possible to handle some consent values persistently inside of the SDK.

Consent management in the SDK is done in 2 steps

  1. consent has to first be required in the app otherwise, the SDK works as if all consent is given
  2. if consent is required, it has to explicitly be given for each targeted feature

Initial Configuration

During SDK init there should be a flag (e.g. requiresConsent) to inform the SDK that it requires consent before doing anything.

If this configuration is set, the SDK should not send any data to the server without consent. Even if specific SDK methods (reporting errors, recording events, etc.) are manually called, these calls should be ignored until consent is given.

Init should also have a function to provide an array consent that is given during startup.

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 themself. 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)
  • locationallows location information to be sent. If consent has not been given, the SDK should force send an empty location upon begin_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
  • accessory-devices - allow accessories or wearable devices, such as Apple Watches, etc. to be detected
  • 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

Note that the available features may change depending on the platform.

Feature Grouping (optional)

The SDK may also provide feature grouping, allowing existing features to be put into groups and the use of these groups to give, cancel, and check consent.

For example, a client may put "sessions", "events," and "views" into one group called "activity". After which, they give their consent to "activity", the SDK should then automatically give consent to all underlying features.

Countly.group_features({
    activity:["sessions","events","views"],
    interaction:["scrolls","clicks","forms"]
});

Giving Consent

The SDK initial consent should be sent through the config object.

After init there should be a method for giving consent. This method should have feature names or groups as parameters. It may accept a single feature or group as well as multiple features or groups in the form of an array or variable arguments, depending on the SDK language and environment.

At any time during the app run, a user may give consent to more features after starting the SDK.

Upon receiving consent (also during init), the SDK should immediately begin collecting data allowed by the provided feature(s) and also begin sending the consent approval to the server in the form of consent= {"feature":true}. For the exact feature names, refer to the list above.

When consent is given, the update request should sent the current state of consent values and not only the given "delta". This may be a separate request, or it may be attached to any other SDK request.

If someone attempts to give consent for a second time, the SDK should ignore it as the consent is already given and nothing changes.

Checking Consent Status

There should also be a method to check the current consent status for the SDK, returning true if consent was given, and false if not. Checking status for groups should return true only if all the underlying features return true.

This check should also return true if consent is not required.

Removing Consent

The SDK also needs to provide a method to remove consents. It should support the same parameter options as the consent giving method.

Upon receiving the request to remove consent, the SDK should immediately stop collecting data allowed by the provided feature(s) and also send consent removal to the server in the form of consent= {"feature":false} .

When consent is removed, the update request should sent the current state of consent values and not only the given "delta". This may be a separate request, or it may be attached to any other SDK request.

Depending on the SDK structure, the SDK may sync existing requests in the queue. Or, it may ignore requests in the queue and never send them or remove them from the queue.

Both giving consent and removing consent may be combined in a single request as well. If, for example, consent was given for crashes but removed from users, then the request should contain consent={"crashes":true,"users":false}.

Common Flow with Required Consent

1) The Developer sets requiresConsent upon initial configuration: config.requiresConsent = true;.

2) The Developer starts the Countly SDK, but the Countly SDK does nothing related to user tracking, no information is queued or sent to the server.

3) The Developer handles permissions, such as showing the popup to the user and asking for consent as well as persistently storing user choices.

4) Upon receiving consent from a user or storage, the Developer calls the giveConsent method of the Countly SDK, feature names, or groups, depending on the permissions they managed to get.

5) The Countly SDK starts relevant features and also sends a request to the Countly Server (consent={"feature":true}).

6) The Countly SDK checks if FeatureNamehas already been passed to the giveConsent method, and it ignores all repetitive calls.

7) The Countly SDK does not persistently store the status of given consents and expects the developer to call the giveConsent method on each app launch, just as with starting the SDK.

8) If the app user changes his/her mind about consents at a later time, the developer may reflect this to the Countly SDK using the removeConsentmethod, passing feature names or groups.

9) The Countly SDK stops the relevant features and also sends a request to the Countly Server (consent={"feature":false}).

10) The Countly SDK checks if feature names or groups have already been passed to the removeConsent method, and it ignores all repetitive calls. Or, it attempts to cancel consents never given at all.

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.

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.

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 },

Experimental

This section describes features that are under development and in a prototype phase

SDK Server Configuration

Server UI URL for the UI (Management -> SDK):
https://xxx.count.ly/dashboard#/manage/sdk/configurations
Sample Response:
{
"v":1,
"t":1681808287464,
"c":{
"tracking":false,
"networking":false,
"crashes":false,
"views":false,
"heartbeat":61,
"event_queue":11,
"request_queue":1001
}
}
v - current schema version
t - timestamp
c - config object
Server has default values for every field.
If the currently set value differs from the default, it will be sent by the server. If no value is sent for a configuration, then that means that the SDK has to use its own default or the value provided by the developer.
SDK side the precedence of configs will be:
SDK default < Dev set value < server config value

Once per init the SDK tries to acquire the config and stores it persistently locally.

When initializing the SDK, if there is a persistent config stored, it will be used. The SDK will still try to get a up to date version of the config. Once the up to date version has been acquired, it is stored persistently and the SDK reconfigures itself to reflect the new configuration.

SDK Health Checks

These are a collection of different metrics and helpers that would give better insight into the state of the SDK integration.

Health Information with Instant Request

During SDK operation, various metrics regarding its usage should be collected. Those collected metrics should then be periodically sent.

The trigger for sending the collected metrics is the end of SDK initialization. At that point, the SDK should send overall health metrics collected since the last time the health metrics were successfully sent.

Metrics should be stored persistently. Upon successful transmission, the stored metrics should be cleared.

Metrics should be sent as an instant request.

There is no way to disable health checks.

The health check tracking, serialization, deserialization, etc., functionality should be contained within an independent module designed so it can be easily tested.

Health metrics should not be saved after every change to a metric. They should be changed after the following triggers:

  • Session update timer - for a periodic save
  • Session ended - as a proxy that the app/page is about to be closed
  • Any other platform mechanisms that would indicate that the app is about to be closed or killed

Here is a list of metrics that need to be tracked. They are identified by the JSON key that should be used when sending these health metrics:

  • el (Integer) - The amount of error-level SDK logs triggered

  • wl (Integer) - The amount of warning-level SDK logs triggered 

  • sc (Integer) - The status code of the last failed request 

  • em (String) - The first 1000 characters of the response returned by the last failed request

The module should expose the following methods for tracking, saving, and creating the param:

//increments the "wl" counter
void logWarning()

//increment the "el" counter
void logError()

//update the "sc" and "em" values
void logFailedNetworkRequest(integer statusCode, string errorResponse)

//counters should be cleared, and the state should be saved
void clearAndSave()

//state should be saved
void saveState()

The health check request contains the base params, the regular metric param with only the app version, and the metric information under the "hc" param. The request should be sent to the "/i" endpoint. Health check metrics should be sent as a JSON. Each metric would be setting its key-value pair in there.

// the relevant parts:
https://countly.server/i?hc={"el":12,"wl": 22,"sc":300,"em": "some_error" }&metrics={app_version:2}...

The request is considered sent successfully as per the regular SDK interpretations of success.

Request Parameters for Health Check

This gives insight into how full is the request queue for the specific device.

With every request sent, the SDK also adds a parameter showing how many requests are in the stored request queue.

The integer value of this is set under the param "rr." This param is not stored in the RQ but is added just before sending the request:

// the relevant parts:
https://countly.server/*?...&rr=23...

If tamper protection (salting) is enabled, this parameter should also be included in the checksum calculation.

Legacy Features

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.

Looking for help?