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:
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”:
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:
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:
- Downloading the list of available widgets from the server
- Selecting a widget from that list
- 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:
- A call to fetch the widget list from the server.
- A call to fetch a single widget's data from the server.
- 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'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(11));//put a random rating
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:
- What information is the server looking for?
- What API endpoint should you use?
- 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 an ID called the 'device ID.' This is attached to every request (which contains events and other data) that is sent to the Countly server. This ID consists of String characters.
This ID is normally (by default) generated in the environment the Countly SDK has been integrated into (e.g., a smartphone, a web browser, or a desktop application). But how you handle this ID would depend on how you define a user in your platform specifically.
What happens if there are several users and several devices that are used interchangeably? What happens if a user can log in and log out, hence transitioning between a known and an anonymous user? In such cases, you should experiment and decide on the correct user tracking strategy before going into production to minimize the negative effects. For an overview on how these different situations could be handled, look below.
Available Mechanisms For Interacting With Device ID
Countly SDKs try to be configurable and flexible, and handling device IDs is no exception.
Device ID During Init
Countly SDKs behave differently in the first on a device compared to subsequent init's.
On your first init, when integrating a Countly SDK, Countly will try to acquire a device ID. By default, Countly will generate a random value (for the device_id) to identify the user or use some platform-specific value; for example, IDFV for iOS, and then store it in the local storage.
On subsequent inits, the SDK would fetch and use this same value as the device ID. It will not generate a new one. By default, the SDK would ignore any provided device ID values.
There are some configuration options during initialization. During the first init, it is possible to do the following actions:
- Provide a custom device ID- SDK will use the provided ID and will not generate one.
- Tell the SDK which device ID generation method to use- in some SDKs, it is possible to influence the ID generator and pick a specific method.
- Enable temporary device ID mode- while in this mode, the SDK will not send anything to the Countly server until a device ID has been provided.
- Provide a device ID with a URL parameter- this exists only in the Web SDK. This provides a way to "inject" a device ID on a first run.
Some SDKs might have a "clear stored device ID" flag that can be set during init. If this is done, then the SDK will clear it's stored value and will try to reacquire a device ID value. It would then behave like on the first init. It is generally not advised to use this flag as it can cause user count inflation issues.
For a deeper overview of how the SDK would behave in different situations, have a look at this table.
Changing Device ID
Countly SDKs provide two ways to change the device_id after the SDK initialization:
1. Change device_id without merging. That will simply end the session of the old device_id, sync all the left data, and start a new session for the new device_id.
This is handy, for example, when multiple users use the same device, and you want to track them without sharing their data individually.
2. Change device_id with merging. This will create a new user with a new device_id, and start a new session. Then, merge the data of the anonymous user with the old device_id into this new ID. And afterward, delete the anonymous user with the old device_id from Countly and only keep this user's information under the new developer-given ID.
This is handy when, for example, you are firstly tracking an anonymous user with a Countly generated device_id, but then the user authenticates, so you retrieve the ID for this user and change it in the SDK, allowing to merge both users on the server. This means that everything that the anonymous user had, all events and properties, will now be assigned to an identified user, and the old user will be deleted.
You can implement different strategies that utilize these two options with the help of device ID type information. 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.
Offline / Temporary ID Mode
It is possible to launch the Countly SDK in an offline/temporary ID mode during the first initialization. This mode can also enabled after initialization with the use of special calls exposed by the SDK.
If this mode is enabled, no data will be sent to the server until a real device ID value is provided by the host app. After that is done, all stored requests will be marked as created by this device ID and then sent to the server and assigned to this user.
Device ID Type
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.
Different User Tracking Strategies
Default User Tracking
As mentioned in the "Device ID during init" section. With no additional configuration, the SDK will generate a random device ID on the first init and then use it.
Generally speaking, this way assigns a unique ID to a particular user who is the owner of that device (phone, browser, PC, or tablet).
Pros
- This is the easiest and fastest implementation, with no additional steps needed other than undertaking the default SDK implementation.
Cons
- If multiple different users use the same device, they will be identified as a single user in the Countly dashboard and will have a single profile under User Profiles.
- If the same user uses multiple devices, each device will be identified as a separate user in the Countly dashboard; hence the same user will have separate user profiles.
- Depending on the platform, if app storage is reset (erased) or the app is uninstalled and re-installed again, this user most likely will be identified as a new user, and a new user profile will be created. This highly depends on how the platform behaves. Check here to understand what happens in such cases.
Tracking Known Users
This method, as opposed to the first one, helps Countly identify and track users if they are known to you. It is used when tracking the same user across multiple devices or different users on the same device, as the default tracking method is not appropriate. In this case, you need to provide your user identifier as the device ID. This unique identifier can be a user email address or an internal customer ID — or simply anything unique to that user. The Countly SDK can then use this String as the device_id. From this point on, Countly will know precisely what user it is, and the same device_id will be used even across different devices.
To accomplish that, you need to provide a String value as the device_id upon the SDK initialization inside the config object. The user authentication page is a good candidate for implementing this method. So this might fit applications that can identify their users right away during the SDK init, or have little or no actions before authenticating users.
This would be the case when the user inside the Countly dashboard directly corresponds to your customer (e.g., 1 Countly user = 1 company customer, regardless of the device or platform they use).
Pros
- Each of your customers will be exactly one single user inside Countly and have one user profile.
Cons
- If you do not know your user ID right away and would know it only after the user authenticates, you will miss all the actions that were made before authentication.
Known User With Pre-Tracking
To tackle the problem of missing out on data before user authentication, it is possible to launch the Countly SDK in an offline/temporary ID mode. This mode is described in the Offline / Temporary ID mode section.
This way, you can track everything needed, before knowing the user's identity. When the user finally authenticates, you get your user’s identifier and use that to exit the Offline / Temporary ID mode
For the definition of the user, nothing changes- it still directly corresponds to your customer.
Pros
- Each of your customers will be exactly one single user inside Countly and have one user profile.
- You will have the opportunity to be able to collect and visualize data before the user authenticates, but only after authentication.
Cons
- If your user does not authenticate (and so be known), you will never receive any data from this user.
Managing Anonymous and Known Users Together
It is also possible to collect data of both user states (before login/known and after login/known) and manage the ID using the functionality discussed in the above, changing device ID section.
You can implement different strategies that utilize these two options with the help of the device ID type information that has been discussed in the device ID type section.
So with this knowledge, for example, you can start tracking a user as anonymous with a Countly generated ID. Then, upon authentication, change the device_id to your own ID by merging. And then, when the user logs out, you can change it back to the anonymous generated ID without user merge. The problem is that when the user logs out, it will create a new user inside the Countly dashboard again, and there will be two different user profiles: one with your provided ID and the other with a random ID.
So the user on the Countly dashboard represents both your customer and anonymous user before authentication. And in some cases, it could be the same user but with two different user profiles inside Countly. For applications like banking, where the user must log in and log out every time, that can double the user count, thus skewing the data.
You can try to tweak this strategy to minimize double-user creation. For example, upon logout, let’s not change the device_id at all and keep using your provided one. Instead, upon authentication, we check if the type of device_id provided is yours, if it is, we switch the device_id without merging. But if the type of device_id is Countly generated, then we change the device_id with user merging.
In such a case, the scenario would look like this:
- The app starts for the first time, and a Countly-generated ID is created.
- Upon authentication, you confirm that the current device_id is Countly generated; it means we need to do merging when switching to your provided device_id.
- When the user logs out, we do not do anything.
- When the user logs in, we check the current device_id, and we see that it was provided by you. So we switch to the authenticated user’s device_id without merging. If it is the same user and the same ID the SDK currently has, nothing will happen. But if it is a different ID, then it is probable that another user logged in, and the SDK will stop the current session and start a new one for the new user.
These are just a couple of examples of how you could manage tracking data for both known users and anonymous ones. The actual implementations may differ based on your application specifics. But an example integration of the mentioned method can be reached from here.
Pros
- You get to track data for users both before and after authentication.
Cons
- In some cases, aggregated data may be skewed and may over-report users and new users due to many anonymous users getting created.
- Merging can be quite a performance-intensive process, especially if the user that is merged has a lot of data or there are lots of users to merge.
- In some cases, the same user may have a user profile for both states: a known user and an anonymous one.
- It requires SDK integration and customization, which is slightly more difficult.
Other Known Strategies
We have seen our customers using their own different implementations, and one of them was quite effective, which is why we have included it here. This strategy involves dividing the onboarding (pre-authenticated users) and authenticated users into separate Countly apps.
In this scenario, the customer had quite a long and complicated onboarding process with the registration form. But once that was done, there was nothing else to do before the login screen. So they wanted to utilize one user profile per one customer inside the Countly dashboard. But they also wanted to track how a user onboards, how long it takes, and where they would drop off if registration was not finished.
That is why sending onboarding data to one app and then sending data of known users to the other app made perfect sense for them.
Another strategy could be you also leaving a custom property with your user ID once registration is complete on the onboarding app, just to be able to tie both users together. This approach would require changing app_key in the running SDK, and currently, not all SDKs support that. You would need to consult Countly or make modifications yourself on certain SDKs.
Conclusion
There are different user tracking strategies available. Each one has its own pros and cons. You need to understand what kind of data you want to collect and what you want the word user to mean exactly for you in the Countly dashboard. Make sure you know the options, and then you will be able to find the best way that fits you with all its trade-offs.
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.
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.
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 Secure.ANDROID_ID as the generated ID
- The iOS SDK uses:
- On iOS and tvOS, default device ID is Identifier For Vendor (IDFV).
- On watchOS and macOS, default device ID is 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:
- 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.
- 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 FCM Key
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.
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.
Clicking on that icon should reveal a link called "Manage API in Google Cloud Console.", go ahead and click on it.
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.
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.