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.

Initialization

To start making requests to the server, SDK needs 3 things: the URL of the server where you will be making requests, the app_key of the app for which you will be reporting, and your current device_id to uniquely identify this device.

Server URL

The SDK needs to provide the ability for the user to specify the URL for the server where their Countly instance is installed.

App Key

The App key should be provided by the SDK user. 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).

Other parameters

Of course, you may have other platform-specific parameters, such as adding a debug parameter, which provides different ways for device IDs to be generated. This allows for any other variables or settings upon initialization to be set.

However, there are some cross-platform parameters that you will need to allow to set for the SDK users:

  • country_code - (optional) ISO country code for the user's country
  • city - (optional) name of the user's city
  • location - (optional) comma separate lat and lng values.

These parameters, if provided, should be added to all begin_session requests.

More information can be found here.

Countly Code Generator

If you would like to understand how SDKs work by generating mobile or web code for custom 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.

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.

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

 

Per AppKey storage

All SDK storage (queued network requests, temporary event queue, device ID, etc) should be segmented by appKey.

The first time the SDK is launched with this feature migration of old storage should be performed. All data from the previous general stores should be moved into their appKey specific storage.

A list with all appKeys used in the storage should be kept. I

AppKey can be changed after the SDK has been initialised. When that is done, the event queue should be emptied into the request queue before the appKey change has finished.

There is functionality to erase the request and event queue for a specific appKey.
There is functionality to erase the request and event queue for all appKeys except the current one.

There is a call that returns storage stats per appKey which includes the "appKey", list of entries in the request queue, list of entries in the event queue.

 

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.

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, 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 user has an internet connection
  2. The HTTP response code was successful (which is any 2xx code or code between 200 <= x < 300)

Additionally, the server replies with a JSON object, which has a property result with a success value. There may be different scenarios, such as when blocking a specific device, the requests from the server configuration with the case "Success" may change to some other value. Therefore, do not rely on the success value wholeheartedly. However, if there is no way to check the HTTP response code, you may check if the response contains JSON with the result property.

{"result":"Success"}

If the response code is within the required interval and the response text is a JSON object which has only one key "result" (the value of that entry does not matter), 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.

Session flow

App Initialization

When an app is initialized, the SDK should then send the begin_session=1 request. 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.

Session update

Each minute of the session should be extended by sending the session_duration request with the number of seconds that passed since the previous session request (begin_session or session_duration, whichever was last).

Ending session

When the app exits, the SDK should send the end_session=1 request, including the session_duration parameter with how many seconds passed since the last session request (begin_session or session_duration, whichever was last).

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.

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.

API of the SDK

Depending on the SDK’s environment/language there could be a different set of features supported. Some of these features may be supported on any platform, whereas others are quite platform-specific. For example, a desktop app type may not be providing 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.

Here is a list of things your SDK could support:

Core features

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 which 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, ...)

Session Flows

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.

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

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 ar "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.

Events

Events (or custom events) are the basic Countly reporting tool that reports when something has happened in the app. Someone clicked a button, performed a specific action, etc. All of these examples could be recorded as an event.

Events should be provided by the SDK user who knows what's important for the app to log. Also, events may be used to report some internal Countly events starting with the [CLY]_ prefix, which vary per feature implementation on different platforms.

An event must contain key and count properties. If the count is not provided, it should default to 1. Optionally, a user may also provide the sum property (for example, in-app purchase events), the dur property for recording some duration/period of time and segmentation as a map with keys and values for segmentation.

More on event formatting may be found in the API Reference.

Here are a few examples of events:

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.event(string key)
  • Countly.event(string key, int count)
  • Countly.event(string key, double sum)
  • Countly.event(string key, double duration)
  • Countly.event(string key, int count, double sum)
  • Countly.event(string key, map segmentation)
  • Countly.event(string key, map segmentation, int count)
  • Countly.event(string key, map segmentation, int count, double sum)
  • Countly.event(string key, map segmentation, int count, double sum, double duration)

Note: count value defaults to 1 internally if not specified.

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.

User Details

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.

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.

Custom Device ID / Changing Device ID

There are 3 main cases where the SDK user might like to provide a custom device ID to identify a device/user.

1. Tracking the same user across multiple devices

In this case, the app developers will need to provide their own way to identify users upon initialization. The Countly SDK needs to provide a way to set the custom device ID upon initialization and store it persistently for the next session use. If there is an already stored device ID, the Countly SDK should primarily use the stored one, unless forced to do otherwise.

2. Tracking multiple users on the same device

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

3. Tracking an unauthenticated user who later becomes authenticated

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 Case 2, 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

To summarize, the Countly SDK should provide a proper way to change device IDs for all 3 cases.

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

Recording location

There are 4 location related parameters that can be set in a Countly SDK. It is "country code", "city", "location"(GPS coordinates), "IP" address.

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

Init should have a way to set all location values: location(GPS), city, country, ipAddress. If there is session consent, location 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, location values set in init should be sent as a separate location request.

Location set after init and after the first "begin_session" request should be sent in a separate request and update the internal location 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".

If location is disabled or no location consent is given, the SDK adds an empty location entry 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, is interpreted as "disable location tracking for this device ID".

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

Some sample situations for handling location:

1) dev sets location some time 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

 

Additional parameters

There are also optional, additional parameters. If you can get them on your platform, then you may append them to any/every request. However, if you can’t, it might be a good idea to allow the SDK user to optionally provide such values.

If values are not provided, the Countly server will try to determine them automatically, based on all the other provided data.

Here is more information on possible additional API parameters.

Reserved Segmentation keys

Currently, there are 9 segmentation keys that are reserved for Countly’s internal use. They should be ignored when the SDK user provides them as segmentation for any functionality. The list of keys is as follows:

  • name
  • segment
  • visit
  • start
  • bounce
  • exit
  • view
  • domain
  • dur

Attribution

Attribution allows attributing installs from specific campaigns. They are tied together with sessions. Attribution information should be sent in the following situations, depending on which comes first:

  1. With the first begin session call
  2. as soon as the information is available in a separate request (this is relevant when there is no session consent)

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()

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 just as with other similar functions, this is also optional. 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)

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. Here is more information on view-tracking APIs.

Let's start with manual view tracking, as it should be available on any platform. 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.

Orientation changes

Orientation change tracking requires "users" consent.

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

Ratings and Surveys

Star Rating

If possible, the SDK should provide a simple 1 through 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 in the event the developer would like to use the user's 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;

Surveys (WIP)

Showing surveys or performing any of the survey related features require that the "feedback" consent is given.

Currently there are 2 kinds of surveys: basic surveys and NPS. Both are shown using a very similar API and basicly the same processing.

First step to showing a feedback widget is getting a list of the available surveys for this deviceID. That would be done with a function call named similar to "getAvailableFeedbackWidgets". That call takes a callback. That callback returns 2 values. The second is the error string. The first one a list of available survey 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 2 values, widget id and widget type. Potential type values are currently "nps" and "survey".

The url to acquire all available widhets 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, sha256 param should be added with the checksum.

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

{"result":[{"_id":"5f8c6f959627f99e8e7de746","type":"survey"},{"_id":"5f8c6fd81ac8659e8846acf4","type":"nps"}]}

"result" contains a JSON array of objects. The json object contains a 'id' and 'type'. Possible type values are "survey" and "nps". Both are used to mark the surveys type. The id is used to construct the web view url.

 

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

If the developer has decided on a survey to present, he would call "presentFeedbackWidget" and pass the chosen "CountlyFeedbackWidget" object.

Using information from that object, a widget url will be constructed and presented in a webView or other similar mechanism. That webview will perform further survey interraction.

Using that id we contstruct a url that looks like:

//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]

//web SDK would also pass "origin"

The created url contains params for: widget_id, device_id, app_key, sdk_version, sdk_name, app_version, platform, origin (for  web SDK). Even if parameter tamper protection is enabled, this url does not use the checksum param.

That url then should be provided to a webview and shown as a alert dialog similar to the rating widget. 

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

GDPR compatibility

GDPR compatibility is about dividing the SDK functionality into different features and allowing SDK users to ask for consent when using these features. Once consent has been given, only then may the SDK 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 disable these features on run time.

Mostly persistence consent settings should be handled by the client and not by the SDK. However, it may do so if the SDK is required to handle it.

Initial Configuration

Upon initial configuration, 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. Even if specific SDK methods (reporting errors, recording custom events, etc.) are manually called, these calls should be ignored until consent is given.

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.

The following are the currently available features:

  • sessions - tracking when, how often, and how long users use your app/website
  • events - allow custom events to be sent to the server
  • 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 basic surveys and NPS

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

Feature Grouping (optional)

The SDK may also provide features 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 should have a method for giving consent, and 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 app run, a user may give consent to more features after starting the SDK.

Upon receiving consent, 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. For example, if features are crashes and users, then the request should contain consent = {"crashes":true,"users":true}. 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.

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.

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} . For example, if the features are crashes and users, then the request should contain consent={"crashes":false,"users":false}.

This may be a separate request, or it may be attached to any other 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.

Remote Config

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.

First off, interaction with the Countly Server for the Remote Config feature should be done after you have checked the Remote Config API documentation.

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

Consents

In cases where the consentRequiredflag is set upon initial config, fetching the remote config (automatic or manual) will be performed only when there is at least one consent 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.

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
Was this article helpful?
0 out of 0 found this helpful

Looking for help?