A Deeper Look at SDK Concepts

Sessions

Session, in its most basic definition, is a group of interactions a user engages in your application/website in a given timeframe. It can be used to keep track of user-specific states like user identity, views, and events. Again, it can be seen as hits to the server by a single user, grouped in a certain way. Countly has a specific internal logic to group these hits and calls them as sessions.

In Countly, there are two main methods you can use to track sessions in your application or website. These are automatic session tracking and manual session tracking. They both obey the same session flow where a session starts with a “begin session” signal, a session is updated/extended with a “session duration” signal (which is configurable and by default is every 60 seconds), and a session end mark is given by “end session” signal. While the automatic session tracking follows this flow with a certain logic and triggers, as explained below, in the case of the manual tracking, the triggers are set by the developers who integrate the SDK into their application or website.

In automatic session tracking, sessions always start with a “begins session” signal when a user connects to your website/app, session is updated/extended by “session update” signal as long as the user is active and the session ends with an “end session” request when the app or site is closed.

However, for Web SDK, after the session has started, as long as the user is active (like clicking or scrolling through a page), the inactivity timer will be reset to its initial value (which is 20 minutes by default). This way, the inactivity timer will only start counting when a user is inactive for the last minute (this default value can be changed during the initialization.). And if the user stays inactive for the whole duration of the inactivity timer, only then, the session will come to an end.

In case a user becomes active just at the end of the inactivity timer or just at the end of the session, Countly servers provide a grace period to extend the session instead of terminating it. This “session cooldown” value is 15 seconds by default and can be changed from the Countly dashboard under the settings section:

001.png

So if a user becomes active from inactivity at the end of the inactivity timer or opens your site/app again after closing it, as long as it is within the session cooldown time, the user session will be extended instead of the creation of a new session.

Another option you can change from your dashboard’s settings section is the “maximal session duration”:

002.png

This is a limit for your session update signals, where, if you were updating your session in a longer time frame than this value (which is 2 minutes by default), then Countly servers would cap them to this default value. To provide a better session flow logic, as a tandem to the session update signal, the SDK also sends recorded events until that time as a request to the server, and these events are tied to the begin session signal coming before them.

Session information of a user can be observed from their user profile under the “session history” section with their corresponding events, duration, and starting time:

003.png

 

So the total duration of a session will be calculated by summing up these session duration signals, and these signals would only be sent as long as the user is active or, if not, at least inactive while keeping the app/site open within the inactivity timer’s activity, for automatic session tracking.

Manual session tracking works on the same principle as the automatic tracking. However, the developer has total control over when and where to send “begin session,” “session update,” and “end session” signals. Also, other inner logic that is ingrained into automatic session tracking must be handled by the developer in order to achieve a proper integration. Which are:

  • Sending metrics and location information with the begin session signal.
  • Sending begin session only once per app use/ site visit
  • Handling inactivity timer logic (optional, for web SDK)
  • Sending session update signals with time elapsed regularly (with an internal logic).
  • Sending end session signal at a proper occasion like tab or app close.

User metrics and location information should also be sent with the begin session signal. Things like an inactivity timer and other time-related matters have to be handled by the developer to record accurate session information, considering the limitations of the platform that they are working with and keeping in mind the behavior and the expectations of the Countly server.

It should also be kept in mind that, if consents are enabled and “session” consent was not provided, during the initialization,  session tracking will not work.

Reporting "Feature Data" Manually with Events

Views

Currently, SDK doesn't have any direct mechanism to record views. You may record views by using RecordEvent method. 

There are a couple of other values that can be set when recording a view. 

  • key- [CLY]_view is predefined by SDK, do not change it while recording a view.
     
  • segmentation- A map where you can provide the view's name and other information related to views. You may also provide custom data for your view to track additional information. It is a mandatory field, you may not set it to null.
  • count- It defines how many times this event occurred. Set it to 1.

Example:

std::map<std::string, std::string> segmentation;
segmentation["name"] = "view-name";
segmentation["visit"] = "1";
segmentation["segment"] = "Windows";
segmentation["start"] = "1";

Countly::getInstance().RecordEvent("[CLY]_view", segmentation, 1);

Note: 'name,' 'visit,' 'start', and 'segment' are internal keys to record a view.

Working with Feedback Widgets

Most SDKs provide an easy way to display a widget on the screen. But if you have an elaborate logic which needs you to check and select among multiple widgets that can not be satisfied with the convenience methods SDKs offer manual methods for you to achieve that too.

You can present any widget you want in 3 steps:

  1. Downloading the list of available widgets from the server
  2. Selecting a widget from that list
  3. Displaying the selected widget

Most of the SDKs provides a method to fetch the widget list from the server and another method for you to present a widget from that list. You can refer to each SDK's respective documentation to check the exact method calls and examples:

Interpreting Retrieved Feedback Widget Lists

When working with feedback widgets, at some point, the available feedback widget list has to be retrieved from the Countly server. The SDK will expose a method for that, and it will be named similar to getAvailableFeedbackWidgets. The return value will be a list of objects that describe the available widgets. The objects will have the following fields, the brackets contain their key in the retrieved JSON:

  • widget id (_id)- This is the respective widget ID that you can also see in your dashboard.
  • widget type (type)- This describes the widget type. It would be either 'nps,' 'survey', or 'rating'.
  • widget name (name)- This is the widget name from your dashboard.
  • widget tags (tg)- This is an Array of String tag values from the widget creation (this would be an empty Array if no tags were assigned).
  • appearance information (appearance)- (not exposed in all SDKs) This is some UI information about the widget (currently, only rating and survey widgets would have these values).

In our Web SDK, you would not receive a parsed result. Instead, you would receive the JSON as it would be returned from the server. The returned array would look something like the following:

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

Reporting a Feedback Widget Manually

This guide will go into the reporting of feedback widgets (nps, surveys, and ratings) manually. It will give more context into how the widget data should be interpreted and how the response should be structured when reporting back to the SDK. Also, it must be noted that not all SDKs contain the functionality to do manual feedback widget reporting. 

The SDK should provide 3 calls to perform this process:

  1. A call to fetch the widget list from the server.
  2. A call to fetch a single widget's data from the server.
  3. A call to report a single widget to the server.

The widget list received would be a JSON array of objects corresponding to the widgets. By selecting one of these objects (widgets) and providing it in the second call developer can fetch its data from the server. When receiving the data, it would be packaged in a JSON type object. Their structure would slightly differ depending on the type of widget being reported.

In case of a survey, widget data would look something like this:

 

{
   "_id":"601345cf5e313f747656c241",
   "app_id":"5e3356e07b96b63120334842",
   "name":"Survey name",
   "questions":[
      {
         "type":"multi",
         "question":"Multi answer question",
         "required":true,
         "choices":[
            {
               "key":"ch1611875792-0",
               "value":"Choice A"
            },
            {
               "key":"ch1611875792-1",
               "value":"Choice B"
            },
            {
               "key":"ch1611875792-2",
               "value":"Choice C"
            },
            {
               "key":"ch1611875792-3",
               "value":"Choice D"
            }
         ],
         "randomize":false,
         "id":"1611875792-0"
      },
      {
         "type":"radio",
         "question":"Radio button question",
         "required":false,
         "choices":[
            {
               "key":"ch1611875792-0",
               "value":"First"
            },
            {
               "key":"ch1611875792-1",
               "value":"Second"
            },
            {
               "key":"ch1611875792-2",
               "value":"Third"
            },
            {
               "key":"ch1611875792-3",
               "value":"Fourth"
            }
         ],
         "randomize":false,
         "id":"1611875792-1"
      },
      {
         "type":"text",
         "question":"Text input question",
         "required":true,
         "id":"1611875792-2"
      },
      {
         "type":"dropdown",
         "question":"Question with a dropdown",
         "required":false,
         "choices":[
            {
               "key":"ch1611875792-0",
               "value":"Value 1"
            },
            {
               "key":"ch1611875792-1",
               "value":"Value 2"
            },
            {
               "key":"ch1611875792-2",
               "value":"Value 3"
            }
         ],
         "randomize":false,
         "id":"1611875792-3"
      },
      {
         "type":"rating",
         "question":"Rating type question",
         "required":false,
         "id":"1611875792-4"
      }
   ],
   "msg":{
      "thanks":"Thanks for your feedback!"
   },
   "appearance":{
      "show":"uClose",
      "position":"bLeft",
      "color":"#2eb52b"
   },
   "type":"survey"
}

 

In case of an NPS widget, the JSON internally would look something like this:

{
   "_id":"60186d8b3687037dbb058d80",
   "app_id":"5e3356e07b96b63120334842",
   "name":"test3",
   "msg":{
      "mainQuestion":"How likely are you to recommend this product to a friend?",
      "followUpAll":"",
      "followUpPromoter":"We're glad you like us. What do you like the most about our product?",
      "followUpPassive":"Thank you for your feedback. How can we improve your experience?",
      "followUpDetractor":"We're sorry to hear it. What would you like us to improve on?",
      "thanks":"Thanks for your feedback!"
   },
   "followUpType":"score",
   "appearance":{
      "show":"uSubmit",
      "color":"#027aff",
      "style":"full"
   },
   "type":"nps"
}

And in case of a rating widget, it would look something like this:

{
 "_id":"62222d125852e20462481193",
 "popup_header_text":"What&#39;s your opinion about this page?",
 "popup_comment_callout":"Add comment",
 "popup_email_callout":"Contact me via e-mail",
 "popup_button_callout":"Submit feedback",
 "popup_thanks_message":"Thank you for your feedback",
 "trigger_position":"mright",
 "trigger_bg_color":"13B94D",
 "trigger_font_color":"FFFFFF",
 "trigger_button_text":"Feedback",
 "target_devices":{
 "phone":true,
 "desktop":true,
 "tablet":true
 },
 "target_page":"all",
 "target_pages":["/"],
 "is_active":"true",
 "hide_sticker":false,
 "app_id":"12345687af5c256b91a6345f",
 "contact_enable":"true",
 "comment_enable":"true",
 "trigger_size":"m",
 "type":"rating",
 "ratings_texts":[
 "Very dissatisfied",
 "Somewhat dissatisfied",
 "Neither satisfied Nor Dissatisfied",
 "Somewhat Satisfied",
 "Very Satisfied"
 ],
 "status":true,
 "targeting":null,
 "ratingsCount":116,
 "ratingsSum":334
}

These describe all server-side configured information that would be used to visualize a widget manually. Starting from some style and color-related fields and, finally all questions and their potential answers. In the case of surveys, it also shows the required id's to report survey results.

 

When reporting these widget's results manually, the filled-out response is reported through the segmentation field of the reporting event. So depending on the type of widget you are reporting, you have to construct a widgetResult object, specific to that widget, which would then be utilized in the third call that has been mentioned at the top of this section. In this third call, the developer is expected to provide the widget object obtained from the first call, the widget's data object that has been obtained from the second call, and a properly formed widgetResult object that has been created with respect to the type of widget that is being reported. More information on how to form this object is provided below.

Reporting NPS Widgets Manually

To report the results of an NPS widget manually, no information from the widget's data JSON is needed. These widgets can report only two pieces of information - an Integer rating, ranging from 0 to 10, and a String comment, representing the user's comment.

Therefore when reporting these results, you need to set two segmentation values, one with the key of "rating" and an Integer value and the other with the "comment" key and a String value.

Android Sample Code

The following sample code would report the result of an NPS widget:

Countly.sharedInstance().feedback().getFeedbackWidgetData(chosenWidget, new RetrieveFeedbackWidgetData() {
    @Override public void onFinished(JSONObject retrievedWidgetData, String error) {
        Map<String, Object> segm = new HashMap<>();
        segm.put("rating", 3);//value from 0 to 10
        segm.put("comment", "Filled out comment");

        Countly.sharedInstance().feedback().reportFeedbackWidgetManually(widgetToReport, retrievedWidgetData, segm);
    }
});

Web Sample Code

The following code shows what the expected widgetResult objects look like for the NPS widget:

var widgetResult = {
         rating: 3, // between 0 to 10
         comment: "any comment" // string
    };

Reporting Rating Widgets Manually

To report the results of a Rating widget manually, again, no information from the obtained widget data is needed. These widgets have similar reporting capabilities to NPS widgets. Similarly there would be an Integer rating, ranging from 1 to 5, and a String comment, representing the user's comment. Then, in addition to these, there would be an email String for the user email and a contactMe Boolean (true or false) if the user gave consent to be contacted again or not.

Web Sample Code

The following code shows what the expected widgetResult objects look like for the Rating widget:

var widgetResult = {
         rating: 3, // between 1 to 5
         comment: "any comment", // string
         email: "email@any.mail", // string
         contactMe: true // boolean
    };

Reporting Survey Widgets Manually

To report survey widgets manually, an investigation of the widget data received from the second call is needed. Each question has a question type, and depending on the question type, the answer needs to be reported in a different way. Each question also has it's own ID, which needs to be used as part of the segmentation key when reporting the result.

The question id can be seen in the "id" field. For example, in the above posted survey JSON the first question has the ID of "1611875792-0". When trying to report the result for a question, you would append "answ-" to the start of an ID and then use that as a segmentation key. For example, for the first survey questions, you would have the result segmentation key of "answ-1611875792-0".

At the end, your widgetResult would look something like this (Web example):

var widgetResult = {
       "answ-1602694029-0": "answer", // for text input fields
       "answ-1602694029-1": 7, // for rating picker
       "answ-1602694029-2": "ch1602694029-0", // there is a question with choices. It is a choice key
       "answ-1602694029-3": "ch1602694030-0,ch1602694030-1" // in case 2 choices selected
      };

The specific value would depend on the question type. Here is a description of how to report results for different question types:

Multiple Answer Question

It has the type "multi". In the question description, there is a field "choices," which describes all valid options and their keys.

Users can select any combination of all answers.

You would prepare the segmentation value by concatenating the keys of the chosen options and using a comma as the delimiter.

For example, the above survey has four options in its first question. If a user chooses the first, third, and fourth options as the result, the resulting value for the answer would be: "ch1611875792-0,ch1611875792-2,ch1611875792-3".

Radio Buttons

It has the type "radio". In the question description, there is a field "choices," which describes all valid options and their keys.

Only one option can be selected.

You would use the chosen options key value as the value for your result segmentation.

Dropdown Value Selector

It has the type "dropdown". In the question description, there is a field "choices," which describes all valid options and their keys.

Only one option can be selected.

You would use the chosen options key value as the value for your result segmentation.

Text Input Field

It has the type "text".

You would provide any String you want as the answer.

Rating Picker

It has the type "rating"

You would provide any Integer value from 1 to 10 as the answer.

Android Sample Code

The following sample code would go through all of the received Survey widgets questions and choose a random answer to every question. It then reports the results:

Countly.sharedInstance().feedback().getFeedbackWidgetData(chosenWidget, new RetrieveFeedbackWidgetData() {
    @Override public void onFinished(JSONObject retrievedWidgetData, String error) {
        JSONArray questions = retrievedWidgetData.optJSONArray("questions");

        Map<String, Object> segm = new HashMap<>();
        Random rnd = new Random();

        //iterate over all questions and set random answers
        for (int a = 0; a < questions.length(); a++) {
            JSONObject question = null;
            try {
                question = questions.getJSONObject(a);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            String wType = question.optString("type");
            String questionId = question.optString("id");
            String answerKey = "answ-" + questionId;
            JSONArray choices = question.optJSONArray("choices");

            switch (wType) {
                //multiple answer question
                case "multi":
                    StringBuilder sb = new StringBuilder();

                    for (int b = 0; b < choices.length(); b++) {
                        if (b % 2 == 0) {//pick every other choice
                            if (b != 0) {
                                sb.append(",");
                            }
                            sb.append(choices.optJSONObject(b).optString("key"));
                        }
                    }
                    segm.put(answerKey, sb.toString());
                    break;
                //radio buttons
                case "radio":
                //dropdown value selector
                case "dropdown":
                    int pick = rnd.nextInt(choices.length());
                    segm.put(answerKey, choices.optJSONObject(pick).optString("key"));//pick the key of random choice
                    break;
                //text input field
                case "text":
                    segm.put(answerKey, "Some random text");
                    break;
                //rating picker
                case "rating":
                    segm.put(answerKey, rnd.nextInt(10) + 1);//put a random rating from 1 to 10
                    break;
            }
        }

        Countly.sharedInstance().feedback().reportFeedbackWidgetManually(widgetToReport, retrievedWidgetData, segm);
    }
});

There Is No SDK That I Can Integrate for My Use Case. What Are the Options?

Countly SDKs provide you with many options to track your users with the least amount of code in a way that fits your use case. Behind the scene, the SDK would do various tasks to gather information, reshape this information in a way the Countly servers can understand, and prevent the possibility of data loss as much as possible. With the help of the SDKs, you only need to write a single line of code while the SDK does hundreds of lines of work behind the hood. However, the core principles behind these operations are simple even though they are tedious. So if you come across a situation where you can not integrate a Countly SDK into your project, you can still be able to track your users and inform that information to your Countly instance as long as you can send API calls following the core rules and structures that is shared among all Countly SDKs.

To be able to track your users manually and to share this information you have gathered with your Countly instance you will need to know three things:

  1. What information is the server looking for?
  2. What API endpoint should you use?
  3. How should you structure your requests?

As long as you have the answers to these questions, you can track your users and gather information in any way you want as long as you form and send correct requests to your Countly server. Documentations that would be useful to find the answers to these questions are the Countly glossary to understand the Countly terminology, the API documentation to see the endpoints and the data structure, the SDK Development Guide to see the scope of the endpoints, and the specific documentation of the SDK of your platform to see the capabilities and the features.

Handling the Device ID in Your Integrations

Countly tracks your users through a parameter called the 'device ID.' This is attached to every request that is sent to the Countly server. This ID consists of String characters.

Interacting With Device ID

During Initialization

To check SDK initialization logic and for a quick overview regarding device IDs please have a look here.

During initialization, you can:

  • Provide a device ID - SDK uses your ID instead of generating one
  • Enable temporary ID mode - Delay data sending until a real ID is provided
  • Use URL parameters (Web SDK only) - Inject device ID via URL
  • Clear stored device ID - To act like first init each time (can inflate user count if you don't provide ID)

After Initialization

Basic Method

Most current SDKs provides a single method called 'setID' (or its equivalent) for changing device ID after SDK initialization. 

Check quick access documentation for checking how you can use this method.

This method will have this logic:

Here merge and non merge indicates:

  • Merge: Meaning you are just changing the ID (alias, nickname in a way) of the same user
  • Non Merge: You are creating a new user

SDK detects if current ID is provided by developer or not by its internal device ID type tracking.

If you are using Consent feature, you should provide consent again after changing device ID.

Advanced Method

If you have a unique scenario where you have to choose when to merge and when to not merge while changing device ID you can use another method called 'changeID' (or its equivalent).

Check the SDK Spec documentation of the SDK you are using for Extended Device ID Management article to learn how you can use this method.

Most Countly SDKs provide calls to see the current device ID and the device ID type. The main types you would like to check for device ID management are to see if the ID was SDK-generated or developer-supplied.

  1. Change device ID without merging. That will simply end the session of the current user, sync all the left data, and start a new session for the new user (device ID). Basically creating a new user.
  2. Change device ID with merging. This will change the old device ID into new device ID, and start a new session. This will basically changing the ID of the old user. If this new ID was already being used before, then in server data under the old device ID will be merged into the new ID you provide.

Merging is useful when you want to migrate from SDK generated IDs to your own user identifier. However it is a costly operation in server so it should not be spammed.

Offline / Temporary ID Mode

If this mode is enabled, no data will be sent to the server until a new device ID value is provided while still tracking user data.

After new ID is provided, all stored data so far will be sent to the server and assigned to this user (device ID).

Implementing Strategies

A quick guide to implementing the strategies mentioned at Strategy Guide.

Strategy 1: Device-Based Tracking (Default)

This is how SDK tracks normally. You do not need to change anything.

Strategy 2: Known User Tracking (after Auth tracking)

This strategy is only for tracking after a user authenticates:

  • Clear stored IDs with init config option
  • Provide device ID during SDK init in config
  • Make sure you are initializing only after user information is available

Strategy 3: Known User Tracking (pre and after Auth tracking)

This strategy starts tracking directly when an application or website open, even before authentication however it refrains from sending this data until user authenticates:

  • Start with temporary ID mode with init config option
  • When user authenticates change device ID with 'setID' method
  • When user logs out enter temporary ID mode again

Strategy 4: Anonymous and Known User Tracking

With this strategy you will start tracking the user directly without waiting for authentication to send data:

  • Clear stored IDs with init config option
  • When user authenticates change the device ID with 'setID' method

Setting Custom User Metrics

User metrics are sent when starting a session or requesting remote config. Some SDKs expose functionality to override the SDK set metric values or provide custom ones.

Not all keys are used by all SDKs. Here is a list of metric keys used by Countly:

  • _os- The name of the platform/operating system.
  • _os_version- Version of platform/operating system.
  • _app_version- sets the application version. For some platforms, this is retrieved from the app configuration.
  • _device- Device model name.
  • _device_type- sets what kind of device is using the app. The potential values are: "console," "mobile," "tablet," "smart tv," "wearable," "embedded," "desktop".
  • _resolution- The screen resolution of the device.
  • _density- Screen density of the device.
  • _locale- Locale or language of the device in ISO format.
  • _store- (Mobile SDK) A source where the user came from.
  • _carrier- (Mobile SDK) Carrier or operator used for connection.
  • _has_watch- (iOS SDK).
  • _installed_watch_app- (iOS SDK). 
  • _browser- (Web SDK) Browser name.
  • _browser_version- (Web SDK) Browser version.
  • _ua- (Web SDK) User agent String.

Preparing Your App for Symbolication

This section will guide you through the Android, iOS, and JavaScript symbolication processes.

Android

Android's official tool for code shrinking and obfuscation is called ProGuard. A detailed description of its usage can be found here. There is also a paid tool with additional features called DexGuard. Both ProGuard and DexGuard can be used for Countly crash symbolication. Currently, we do not support any other Android obfuscation libraries.

If you are using Android Studio for development, the mapping files will not be produced when you make instant runs. For them to appear, you will either need to generate a signed APK or choose the Build APK option.

After the build is complete, the symbol file called mapping.txt can be found under <module-name>/build/outputs/mapping/release/ or <module-name>/build/outputs/mapping/debug/, depending on how you initiate the build process.

ProGuard Rules

You can add some rules to ProGuard (or DexGuard) and modify how it runs. These rules should be added to the proguard-rules.pro file. You may find more information in the ProGuard manual.

For the best symbolication results, you should include this line in your ProGuard rule file:

-keepattributes SourceFile,LineNumberTable

To include also the source file name in the symbolication results, we recommend not to have the following line in your proguard file:

-renamesourcefileattribute SourceFile

iOS

The symbol file is a dSYM file for iOS.

  • A dSYM file is Apple's standard Mach-O file which contains debug symbols for a given build.
  • It includes debug symbols for all architectures used for the build (e.g. armv7, arm64).
  • Its size may vary depending on the original source code and libraries used in the project.
  • It is a file-like folder structure, and actual dSYM data is in the binary file AppName.app.dSYM/Contents/Resources/DWARF/AppName.

dSYM Location

First of all, for each executable target (main app, extensions, and Cocoa Touch Frameworks) in the project, a separate dSYM file is generated when the project is built. The dSYM location depends on the build settings. By default, it is defined by $DWARF_DSYM_FOLDER_PATH Xcode Environment Variable.

An example location is: ~/Library/Developer/Xcode/DerivedData/AppName-abcdef0123456789/Build/Products/Release-iphoneos/AppName.app.dSYM

In addition, if you use the Product > Archive option in Xcode to create the .xcarchive of your app, you may find a dSYM already created inside the .xcarchive. By default, its location is: ~/Library/Developer/Xcode/Archives/YYYY-MM-DD/AppName DD-MM-YYYY, HH.mm.xcarchive/dSYMs

Automatic dSYM Upload

For automatic dSYM, please see the Countly iOS SDK documentation here.

JavaScript

In order to symbolicate errors, we need a source map file. We will try to lay out how you can produce a source map file for builds that use Webpack, you can also consult your build tool's documentation on how to generate one for your builds.

So, for this demonstration, we will use the sample frontend application that is available in the Countly Web SDK repository. It is a simple project with just one source file src/index.js that imports a third-party dependency, the Countly SDK, and binds a couple of buttons to throw errors.

import Countly from "countly-sdk-web"

Countly.init({
  app_key: "YOUR_APP_KEY",
  app_version: "1.0",
  url: "https://try.count.ly",
  debug: true
});

//track sessions automatically
Countly.track_sessions();

//track pageviews automatically
Countly.track_pageview();

//track any clicks to webpages automatically
Countly.track_clicks();

//track link clicks automatically
Countly.track_links();

//track form submissions automatically
Countly.track_forms();

//track javascript errors
Countly.track_errors();

//let's cause some errors
function cause_error(){
  undefined_function();
}

window.onload = function() {
  document.getElementById("handled_error").onclick = function handled_error(){
    Countly.add_log('Pressed handled button'); 
    try {
      cause_error();
    } catch(err){
      Countly.log_error(err)
    }
  };

  document.getElementById("unhandled_error").onclick = function unhandled_error(){
    Countly.add_log('Pressed unhandled button'); 
    cause_error();
  };
}

We also have a webpack configuration that takes this source file, resolves its import, and minifies the resulting file, creating a single minified file dist/main.js. Also, note that we've set the devtool option to hidden-source-map which means webpack will generate a source map file but will not reference it in the main.js file. This is typically ideal for production environments where you don't need to inspect the underlying code in the browser. You might want to go with source-map in development environments, it will reference the source map file in main.js and ease debugging in the browser.
 

const path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'development',
  plugins: [new webpack.ProgressPlugin()],
  devtool: "hidden-source-map",

  module: {
    rules: [{
      test: /\.(js|jsx)$/,
      include: [path.resolve(__dirname, 'src')],
      loader: 'babel-loader'
    }]
  },

  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },

  output: {
    devtoolModuleFilenameTemplate: '[resource-path]',
    devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]'
  }
};

Here is a screenshot of how you would run the webpack with npx and use the webpack package that would be installed along with your project.

1612759824.png

Then, you can see that webpack produced themain.js file along with its source map file main.js.map. Now, we just need to upload that source map file to our Countly instance.

1612759876.png

Acquiring Public Key or Certificate for SSL Pinning

You can use the "openssl" command line utility to get this information.

Acquiring the SSL Public Key from a Server

To get the current public key from your server you can use the following snippet (replace xxx.server.ly with your server name):

#get the public key
openssl s_client -connect xxx.server.ly:443 | openssl x509 -pubkey -noout

That command would produce output similar to the following:

depth=2 C = CC, O = XXX Server Service, CN = X1 Z1
verify return:1
depth=1 C = CC, O = XXX Server Service, CN = Y2 Z34
verify return:1
depth=0 CN = *.xxx.server.ly
verify return:1
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqBv1G1nbGTlZa5ARZYSy
x+ZfVZLaWJlHrIm8crPjj6X/LXfPj/K/bBeVwB2VrpEfLr/vJsv4AC3Dp+YZzchv
zOQUBIiJW0xZ3DXaV9arok9vUk1Srdphr6AO8gixS8Hv7Jnd6B+GszZxtE1tTxjg
5mzm67V1gg0dKSEKEvX49YdVsjj6HKSzqK8J1GS/gllgUuACZMMtxi3L9eBtOkZZ
ihgtHLuL5kf6i5sKb0nZUvCh5cxInNnDE3NohzHacC0p8Uah4WvnJ6nNLFD76xYG
fEn98nqOp9Lw+T4UWuH9A5D+uR4L/6rcHvNvqGlgKX0uZkKuyZnhVdjXeunQzwmX
UwIDAQAB
-----END PUBLIC KEY-----

The public key for xxx.server.ly is returned in the pem format, its bytes are between the -----BEGIN PUBLIC KEY----- and-----END PUBLIC KEY----- tags. You would not copy the tags and just the characters between them when providing this information to the SDK. Remember to not add any newlines when providing this to the SDK.

Acquiring the SSL Certificate Information from a Server

To get the whole certificate from your server you can use the following snippets (replace xxx.server.ly with your server name):

#get the list of certificates
openssl s_client -connect xxx.server.ly:443 -showcerts

The command would produce output similar to the following (for improving readability, the certificate related bytes are cut short):

CONNECTED(00000003)
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = xxx.server.ly
verify return:1
---
Certificate chain
 0 s:/CN=xxx.server.ly
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
-----BEGIN CERTIFICATE-----
MIIEKDCCAxCgAwIBAgISA+z3u2fRWjX3pN/gVx3hU69zMA0GCSqGSIb3DQEBCwUA
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbm...
-----END CERTIFICATE-----
 1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
   i:/O=Digital Signature Trust Co./CN=DST Root CA X3
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIQAqxcJmoA5OoRVjRk4VSSbzAN...
-----END CERTIFICATE-----
---
Server certificate
subject=/CN=xxx.server.ly
issuer=/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
No client certificate CA names sent
Peer signing digest: SHA256
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 3072 bytes and written 460 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: B049D3E8126B5421704F7F793EBF78E2B595A7B4820341F169F5C394D177697A4
    Session-ID-ctx:
    Master-Key: 05F08C1C9B9E5EDC01A3A51DA3B656E715E1173186C3167EDC758BFBB7603A80
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1642277969
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

The produced output would contain information about multiple certificates, it shows the certificate chain of trust to the respective root certificate authority. The specific certificate information for your server would be at the top and that is the one you would need to copy.

You would copy the string/bytes between the first ---BEGIN CERTIFICATE--- and -----END CERTIFICATE----- tags and paste them to init block of the SDK that you are using. Remember to not add any newlines when providing this to the SDK.

Common SSL Certificate Problems

Problems might be encountered related to SSL or certificate exceptions. Here is a list of common reasons for issues in Android.

Sometimes a good way of exploring the cause of the problem is the same openssl certificate command:

openssl s_client -connect xxx.server.ly:443 -showcerts

Example output might look like this:

0056931101000000:error:10080002:BIO routines:BIO_lookup_ex:system lib:crypto/bio/bio_addr.c:738:nodename nor servname provided, or not known
connect:errno=0

Its error codes may lead to a solution.

A common issue, which can be encountered is that the server's certificate does not contain the full chain of trust in it and it shows only 1 entry. In such cases, this may be helpful.

Tracking Events in a WebView

Incase you are using a WebView in your application, you can establish communication between the WebView and the native application. This will allow you to send data from the WebView to the native application and vice versa.

This can be used to track information that happens inside the WebView and send it to the native application. For example, if you have a WebView that is used to display a web page, you can track the page views inside the WebView and send them to the native application. The native application can then send this information to Countly.

Tracking from Host or Child

There are two scenarios which can shape the nature of this communication:

The first is when you want to track everything from the SDK running in the native application. In this case, the WebView would be sending information(events) to the native application and the native application would be using this information to send events.

The second scenario is when you want to track events happening inside the WebView from an SDK running inside the web app on that view. In this case, the native application would send the current device ID to the WebView and the WebView would use this device ID to send events directly to the server. In this scenario if session tracking is desired it should only be done in the native application.

Here we will demonstrate two methods to establish communication between the WebView and the native application. Each for different platforms and scenarios mentioned above.

Tracking Depending on the Native Platform

Android

Tracking Events from the WebView

To track events happening inside the web app, that the user is interacting with through the WebView, with the SDK running on that web app (it is assumed to be Web SDK here) you would need to send device ID of the user to that SDK from the native app and you would need to modify the init configuration of the SDK running on that web app. 

Assuming you have already created a WebView in your native application, next you would need to enable the JavaScript and local storage access on that WebView:

// for example
private WebView webView;
webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true); // Enable JS
webSettings.setDomStorageEnabled(true); // Enable local storage access
webView.setWebViewClient(new WebViewClient());

Then you would need to load the URL of the web app by adding device ID to the search query:

String deviceID = Countly.sharedInstance().deviceId().getID(); // get device ID
webView.loadUrl("https://your.web.app.address?cly_device_id=" + deviceID);

This way when the SDK is initialized inside the WebView it would use the device ID you provided. During the initialization of the SDK of your web app you would need to set 'clear_stored_id' to true or set the 'storage' to 'none':

Countly.init({
   app_key: "YOUR_APP_KEY",
   url: "https://xxx.count.ly",
   clear_stored_id: true,
   // OR you can use:
   // storage: "none"
});

From this point on all events you send with the web SDK on that web app would be registered for the same user that was using the native app. A key point here is to not track sessions with the Web SDK if you are already tracking it at the native app.

Tracking Events from the Native App

There are three things that need to be done to establish communication between the WebView and the native application on Android:

  • To enable JavaScript in the WebView
  • Add a JavaScript Interface that expects a message (and sends an event with it)
  • Use the method that sends a message to native side at the web app

Here is an example of how to send events from WebView to the native application assuming you have set a WebView:

// for example you have set up a WebView like this
private WebView webView;
webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true); // Enable JS here
webSettings.setDomStorageEnabled(true);
webView.addJavascriptInterface(new JSBridge(), "JSBridge"); // Add JS Interface
webView.setWebViewClient(new WebViewClient());

We will need a class where we define the method we will use to send a message from web app:

// Define the class we will add with JS Interface
class JSBridge {
    @JavascriptInterface
    public void communicate(String key) { // method that will get message 
        Countly.sharedInstance().events().recordEvent(key); // record an event with message
    }
}

Then on the WebView side, you can send messages to the native application using the method we created in our class at native side. Here is an example of how to do this:

// create a function that uses the JSBridge we have declared at native side
function sendMessage(message) {
  JSBridge.communicate(message);
}

Calling this method with a message you provide would result in that message to be used as a key of an event and recorded at the native side by the SDK running there.

iOS

Tracking Events from the WebView

To track events inside the WebView the only thing you need to do at the native side is to pass the device ID of the user:

// Lets assume you have a WebView like this
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {
  var webView: WKWebView!

  override func loadView() {
    let webConfiguration = WKWebViewConfiguration()
    webView = WKWebView(frame: .zero, configuration: webConfiguration)
    webView.configuration.preferences.javaScriptEnabled = true
    webView.uiDelegate = self
    view = webView
  }
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // get device ID
    let id = Countly.sharedInstance().deviceID()
    
    // your web app url + device ID
    let myURL = URL(string:"https://your.app.url?cly_devide_id=" + id)
    let myRequest = URLRequest(url: myURL!)
    webView.load(myRequest)
  }
}

At your web app initialize the SDK like this:

// Making sure that SDK does not try to use the device ID from storage
Countly.init({
   app_key: "YOUR_APP_KEY",
   url: "https://xxx.count.ly",
   clear_stored_id: true,
   // OR you can use:
   // storage: "none"
});

Now you can use the SDK inside the web app as normal and all events would be registered under the user from the native app. Session tracking should not be enabled inside the web app if it was enabled at the native side.

Tracking Events from the Native App

To track events from your native app you can use a simple userContentController and pass a message from the WebView which then you can use for sending events or more:

// Lets assume you have a WebView like this
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {
  
  // Controller logic here
  func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    // You can create an event with message from JS side here
    print(message.body)
    Countly.sharedInstance().recordEvent(message.body)
  }

  var webView: WKWebView!
  var webHandler: String = "jsHandler" // handler name. We will use this at WebView

  override func loadView() {
    let webConfiguration = WKWebViewConfiguration()
    webView = WKWebView(frame: .zero, configuration: webConfiguration)
    webView.configuration.preferences.javaScriptEnabled = true
    
    // Assign controller here
    let contentController = WKUserContentController()
    webView.configuration.userContentController = contentController
    webView.configuration.userContentController.add(self, name: webHandler)
  
    webView.uiDelegate = self
    view = webView
  }
  override func viewDidLoad() {
    super.viewDidLoad()
    let myURL = URL(string:"https://your.app.url") // your web app url
    let myRequest = URLRequest(url: myURL!)
    webView.load(myRequest)
  }
}

At your web app create a function like this:

// You can call this function inside your web app to send a message to your native app
function sendMessage(param) {
  try {
    window.webkit.messageHandlers.jsHandler.postMessage(param); // we send message here
  } catch (error) {
    // log or sth else
  }
}

Using Web SDK Inside a Flutter Web App

One of the ways to execute JavaScript in Dart is to use Dart:js library (another way would be to use its superset, the 'js' library). Using this library you can use the Countly methods inside your Dart code. However it depends on you defining the functions you will use before hand. So an example strategy to use Web SDK inside a Flutter Web App would go through these steps:

  • Add a new '.js' file in 'web' folder of your project
  • Inside that file set some methods to call later:
// lets say inside the my_methods.js file you have created
function sendEvent(key) {
    Countly.add_event({key: key});
}
  • At the 'head' tag of index.html add this file and Countly script as a source:
<script type='text/javascript' src='https://cdn.jsdelivr.net/npm/countly-sdk-web@latest/lib/countly.min.js' defer></script>
<script src="my_methods.js" defer></script>
<body>
<script>
// ... Flutter related code here

// initialize Countly
Countly.init({
   app_key: "YOUR_APP_KEY",
   url: "https://xxx.count.ly"
});
</script>
</body>
  • Now in flutter import dart:js
  • And use js.context.callMethod('method_name',['args']) to call those methods:
import 'dart:js' as js; // import the library

// ... your other code here

// lets say you have a button that triggers this function:
void webEvent() {
   js.context.callMethod('sendEvent',['some_key']); // call the method from my_methods.js
}

Now you should be able to send an event with a key you want in your code. You can change things according to your own project inspiring from these basic principles.

What Information Is Collected by the SDKs

The following description mentions data that is collected by SDK's to perform their functions and implement the required features. Before any of it is sent to the server, it is stored locally.

Parameters Sent With Every Request

When sending any network requests to the server, the following informations are sent in addition of the main data.

Parameter Name Description
timestamp timestamp that request is created at
hour hour that request is created at
tz timezone of the request that is created on
dow day of the week that request is created at (For Countly, the week commences on Sunday, designated as index 0)
sdk_version version of the SDK that request is created from
sdk_name name of the SDK that request is created from
app_key key for the app that is sending the request
device_id unique device identifier that request is created for
av application version if it is provided

 

Here is an example of a base request. To visualize things better it is URL decoded. When sending, data has to be URL encoded

https://xxx.server.ly/i?timestamp=1703164988058&hour=14&tz=180&dow=4&sdk_version=23.12.0&sdk_name=CountlySDK&app_key=APP_KEY&device_id=DEVICE_ID&av=1.0.0&rr=0

Parameters Specific to Certain Requests

Depending on the request prepared, some additional information might be added to make a deeper analysis. Those additional informations are specific informations related to device/sdk/platform that are reachable.

Common Metrics

Those additional informations are needed for Session, Crash Reporting and Remote Config requests. These are the common collected device metrics if they are available for the specific device/sdk/platform.

Parameter Name Description
_device name of the device
_os device OS
_os_version device OS version
_resolution resolution of the device/application
_app_version application version
_manufacturer device manufacturer
_carrier device carrier if extractable by the SDK
_orientation device orientation if exists
_has_hinge device has hinge sensor, foldable (Only used by the Android SDK)

 

Session Specific Metrics

The following metrics are additional to the common metrics that sent with every begin session request. They are collected if they are available for the specific device/sdk/platform.

Parameter Name Description
_locale locale of the device
_density density of the screen
_device_type device type
_store package name or store name if collected by the SDK
_ua User agent (Only used by the Web SDK)
_browser Browser name (Only used by the Web SDK)
_browser_version Browser version (Only used by the Web SDK)

 

Here is an example of session begin request. This is URL decoded, when sending, data has to be URL encoded

https://xxx.server.ly/i?timestamp=1703164988058&hour=14&tz=180&dow=4&sdk_version=23.12.0&sdk_name=CountlySDK&app_key=APP_KEY&device_id=DEVICE_ID&av=1.0.0&rr=0&end_session=1&session_duration=35&metrics={
"_device": "CountlyDevice",
"_os": "MacOS",
"_os_version": "1.0.0",
"_resolution": "1080x1080",
"_app_version": "1.0.0",
"_manufacturer": "Countly",
"_carrier": "Countly-Mobile",
"_density": "XXHDPI",
"_locale": "en_US",
"_device_type": "web",
"_store": "ly.count.sdk",
"_orientation": "Horizontal",
"_ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
"_browser": "Firefox",
"_browser_version": "42.0",
"_has_hinge": "true"
}

Crash Specific Metrics

These metrics are automatically collected when a crash is reported manually or automatically if they are available for the specific device/sdk/platform.

Parameter Name Description
_cpu CPU information of the device
_opengl OpenGL information if exists
_root Device root information if exists
_ram_total Total RAM of the device
_ram_current Current RAM of the device
_disk_current Current disk of the device
_bat Battery level of the device if exists
_run Running time of the SDK
_architecture CPU architecture if collected by the SDK
_online Device online status if collected by the SDK
_muted Device muted status if collected by the SDK
_background Application in background status if collected by the SDK
_executable_name Executable name (Only used by the iOS SDK)
_build_uuid Build UUID (Only used by the iOS SDK)
_app_build App build version (Only used by the iOS SDK)

 

Crash Data

These parameters are automatically collected when a crash is reported manually or automatically if they are available for the specific device/sdk/platform.

Parameter Name Description
_error error description
_nonfatal whether crash is fatal or not
_logs breadcrumbs if given
_type type of the crash if given
_name name of the crash if given
_native_cpp true if it is a native crash (Only used by the Android SDK)
_plcrash true if PL Crash Reporter enabled (Only used by the iOS SDK)
_binary_images binary stack trace (iOS SDK)

 

Here is an Example Crash Request:

https://xxx.server.ly/i?timestamp=1703164988058&hour=14&tz=180&dow=4&sdk_version=23.12.0&sdk_name=CountlySDK&app_key=APP_KEY&device_id=DEVICE_ID&av=1.0.0&rr=0&crash={
"_device":"Android SDK built for x86",
"_os":"Android",
"_os_version":"10",
"_resolution":"1080x2088",
"_app_version":"1.0.0",
"_manufacturer":"Google",
"_orientation":"Portrait",
"_carrier": "C-Mobile",
"_cpu":"x86",
"_opengl":"2",
"_root":"false",
"_ram_total":"1994",
"_ram_current":"213",
"_disk_total":"2162",
"_disk_current":"32",
"_bat":"100.0",
"_run":"6",
"_architecture":"arch",
"_online":"true",
"_muted":"false",
"_background":"false",
"_executable_name":"name",
"_build_uuid":"uuid",
"_app_build":"1.0",
"_error":"java.lang.Exception: RangeError (index): Invalid value: Not in inclusive range 0..2: 10\n\tat ly.count.dart.countly_flutter.CountlyFlutterPlugin.onMethodCall(CountlyFlutterPlugin.java:340)\n\tat io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:8)\n\tat io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295)\n\tat io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$io-flutter-embedding-engine-dart-DartMessenger(DartMessenger.java:322)\n\tat io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0.run(Unknown Source:12)\n\tat android.os.Handler.handleCallback(Handler.java:883)\n\tat android.os.Handler.dispatchMessage(Handler.java:100)\n\tat android.os.Looper.loop(Looper.java:214)\n\tat android.app.ActivityThread.main(ActivityThread.java:7356)\n\tat java.lang.reflect.Method.invoke(Native Method)\n\tat com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)\n\tat com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)\n",
"_nonfatal":"false"
"_logs":"logs",
"_type":"crash",
"_name":"error",
"_native_cpp":"true",
"_plcrash":"plcrash",
"_binary_images":"110001101"
}

Push Notifications

If push notifications are used:

- The device's push notification token

- If the user clicks on the notification, then the time of the click and on which button the user has clicked.

View Tracking

If automatic view tracking is enabled, it will collect:

- activity class name. (Only for the Android SDK)

User Feedback

If feedback or rating widgets are used, it will collect the users' input and the time of the widget's completion.

Events

* When events are recorded, the following information is collected:
- Time of event
- Current hour
- Current day of the week

User Consent

If the consent feature is used, the SDK will collect and send what consent has been given to the SDK or removed from the SDK.

Any other information like data in custom events, location, user profile information or other manual requests depends on what the developer decides to provide and is not collected by the SDK itself.

Device ID Sources

By default all of the Countly SDKs uses their implementation of device id generation method if no developer supplied custom id is given during initialization. Here are the device id generation methods for each SDK:

- The Android SDK uses a random UUID as the generated ID

- The iOS SDK use a persistently stored random NSUUID string

- The Windows SDK uses:

  • cpuId - [net35, net45] (we recommend against using this) uses the OS-provided CPU id info to generate a hash that is used as an id. It should be possible to generate the same id on a reinstall if the CPU stays the same. On virtual machines and Windows 10 devices are not guaranteed to be unique and generate the same id and therefore device id conflicts.
  • multipleWindowsFields - [net35, net45] uses multiple OS-provided fields (CPU id, disk serial number, windows serial number, windows username, mac address) to generate a hash that would be used as the device Id. This method should regenerate the same id on a reinstall, provided those source fields do not change.
  • windowsGUID - [all platforms] generates a random GUID that will be used as a device id. Very high chance of being unique. Will generate a new id on a reinstall.

- The Web SDK generates a random device id

- The Unity SDK uses SystemInfo.deviceUniqueIdentifier

- The Java SDK uses a random UUID

SDK Internal Limits

Countly SDKs have internal limits to prevent users from unintentionally sending large amounts of data to the server. If these limits are exceeded, the data will be truncated to keep it within the limit. Below, you can see these limits and which data they affect.

Key Length

It is 128 characters maximum by default.

SDKs limit the maximum size of all user-set keys:

- Event names and Event segmentation keys

- View names and View segmentation keys

- Custom APM and network trace keys

- Custom APM trace metric keys

- Custom Crash segmentation keys

- Global View and Crash segmentation keys

- Custom User Property keys

- Custom User Property keys used in modifications (with mul, push, pull, set, increment, etc)

Value Size

It is 256 characters maximum by default.

SDKs limit the size of all user-set string segmentation (or their equivalent) values:

- Event and View segmentation values

- Custom Crash segmentation values

- Custom APM trace metric values

- Global View and Crash segmentation values

- Custom User Property values

- Custom User Property values used in modifications (with mul, push, pull, set, increment, etc)

- User Profile named key (username, email, etc) values (except the "picture" field, which has a limit of 4096 chars)

- Breadcrumb value

- Manual Feedback and Rating Widgets reporting fields

Segmentation Value Count

It is 100 developer-provided entries maximum by default.

SDKs limit the amount of user-set segmentation key-value pairs:

- Event and View segmentation count

- Custom Crash segmentation count

- Custom APM trace metric count

- Global View and Crash segmentation count

Breadcrumb Count

It is 100 developer-provided entries maximum by default.

SDKs limit the amount of user-set breadcrumbs that can be recorded (exceeding deletes the oldest one)

Stack Trace Lines Per Thread

It is 30 lines maximum by default.

SDKs limit the stack trace lines that would be recorded per thread

Stack Trace Line Length

It is 200 characters maximum by default.

SDKs limit the characters that are allowed per stack trace line (which limits the crash message length)

Using the Countly SDK's with iOS and Android Widgets and Watches

With mobile devices, there are three different modalities that a user can interact with phone app, phone widget, and watch app. When designing a product that spans all three worlds, the goal is normally to track the user's lifecycle across all of them.

To perform this cross-modality tracking, care must be given that tracking is performed with the same device ID across all of them. If the device ID to which the event or session is attributed would not be the same across all devices and modalities, then the same user would be counted as a separate person on one of them.

This also means that if the user's device ID changes, this change needs to be synchronized across all modalities/apps.

This can be achieved in two general approaches:

  1. Performing data recording at each location separately - each modality integrates, configures, and initializes the Countly SDK separately. Device ID synchronization needs to be performed manually across all modalities. This would be accomplished either by using some platform-provided method to perform direct communication or using an external device (for example, a server) to perform a centralized exchange. Alternatively, every modality needs a way to create the same device ID without direct cross-synchronization.
  2. Performing data recording at a central location and proxying data recording from each non-app modality eliminates the need for synchronization, as only one instance of the SDK is initialized and configured. Device ID changes are immediately taken into account when they happen. Since cross-device recording is centralized, views and sessions must be recorded manually. The main issue with this approach is that there will be times when the main app might be out of reach for proxy recording or might not be running.

To differentiate from which modality the event came from, you can use a custom segmentation value that shows that. Every unique value would indicate a separate modality.

Notes for Native iOS Integration

The general recommendation is to have Countly integrated into each separate modality as that seems to present the least amount of issues. If the modalities used are from the same developer account, then an internal communication channel should be available to them. If they are not on the same developer account, an outside synchronization mechanism will need to be used.

For additional integration details, you will also have to look at this section.

Notes for Native Android Integration

Due to the way the SDK is currently designed, integrating the SDK in a widget will cause crashes. Therefore the recommendation is to proxy the tracking requests to the host application. Shared preferences could potentially be used to achieve this.

The SDK would be directly integrated with the watch modality, and then synchronization would be done with shared preferences.

Notes for Flutter Integration

If you have a Flutter app, there doesn't seem to be an easy way to integrate other modalities. To achieve that, you would need to create them using the native platform languages and integrate the native Countly SDKs. In that case, the recommendations from the previous sections would apply.

Push Notifications

How to Acquire Service Account File

For Firebase Cloud Messaging (FCM) integration, you need a service account file to authenticate your requests. Follow these steps to acquire the file:

1. Navigate to Google Cloud Console

  • Open Google Cloud Console.
  • Ensure you are logged in with the Google account that has access to your Firebase project.

2. Select Your Project

  • From the Project Selector dropdown in the top navigation bar, select your Firebase project.

3. Access Service Accounts

  • In the left-hand navigation menu, go to IAM & Admin Service Accounts.
  • Look for the service account named firebase-adminsdk-random-characters@project-id.iam.gserviceaccount.com.

4. Create a Private Key

  • Next to the service account, click the Actions menu (three dots).
  • Select Manage keys.
  • Click Add Key > Create new key.
  • Choose the JSON format and confirm by clicking Create.

5. Download and Secure the File

  • The service account file will automatically download to your system as a .json file.
  • Important: Store this file securely. Do not share it publicly or commit it to source control repositories.

How to Acquire FCM Key (Legacy)

This guide will show you how to acquire the FCM key of an existing project from your Firebase console:

At your console click on your application to open the popup with the settings icon and click on it.

004.png

This should open the project settings. Now, click on the "Cloud Messaging" section and you should be able to see "Cloud Messaging API (Legacy)" on your screen with a vertical "three dots icon" next to it.

005.png

Clicking on that icon should reveal a link called "Manage API in Google Cloud Console.", go ahead and click on it.

006.png

This will direct you to the "Cloud Messaging" API panel on Google Cloud Console. Now click on the "Enable" button to enable Legacy Cloud Messaging API.

007.png

After these steps, return back to the "Cloud Messaging" section of your project and reload it. Your FCM key should appear. Now, you can copy the "Server Key" and use it in your Countly server.

008.png

Backoff Mechanism

To ensure stable communication with the server, the SDK includes a backoff mechanism designed to temporarily pause outgoing requests when the server appears to be under strain or unresponsive.

The SDK automatically triggers the backoff mechanism when all of the following conditions are met:

  1. Slow Server Response
    The response time from the server exceeds an acceptable threshold. (default 10 seconds) (30 seconds is request timeout)
  2. Moderate Queue Size
    The number of requests currently stored in the queue is less than half of the maximum allowed capacity (default %50). This indicates that the queue is not congested but may grow if not controlled.
  3. Recent Request
    The request being evaluated is not too old. (default 24 hours)

Once triggered, the SDK temporarily pauses sending new requests for a short period (default 60 seconds). This helps prevent unnecessary retries and reduces the risk of overloading the server when it is not responding in a timely manner. After the backoff period ends, the SDK resumes sending requests automatically if conditions have improved.

This mechanism is enabled by default to help manage server load more gracefully. However, it can be disabled via configuration if needed, depending on your specific use case or infrastructure requirements.

Was this page helpful?
Reach out to us for any other questions.
Helpful?

Looking for more Help?