This document will guide you through the process of Countly SDK installation and it applies to version 20.11.X
To access the documentation for version 19.09-sdk2-rc, click here.
The process of setting up Countly Java SDK includes 2 simple steps: adding SDK as a dependency to your project and initializing SDK. Once those are done, you'll have basic analytics on your server like users, sessions, devices, etc.
Adding SDK to the project
SDK is hosted on MavenCentral, more info can be found here and here. To add it, you first have to add the MavenCentral repository. For Gradle you would do it something like this:
buildscript {
repositories {
mavenCentral()
}
}
The dependency can be added as:
dependencies {
implementation "ly.count.sdk:java:20.11.1"
}
Or as:
<dependency>
<groupId>ly.count.sdk</groupId>
<artifactId>java</artifactId>
<version>20.11.1</version>
<type>pom</type>
</dependency>
SDK Integration
Minimal Setup
To start Countly SDK, you need to create a config class and pass it to the init
method. To that method, you also pass the path where countly can store its things.
Config config = new Config("http://YOUR.SERVER.COM", "YOUR_APP_KEY")
.enableTestMode()
.setLoggingLevel(Config.LoggingLevel.DEBUG)
.enableFeatures(Config.Feature.Events, Config.Feature.Sessions, Config.Feature.CrashReporting, Config.Feature.UserProfiles)
.setDeviceIdStrategy(Config.DeviceIdStrategy.UUID);
File targetFolder = new File("d:\\__COUNTLY\\java_test\\");
Countly.init(targetFolder, config);
In our Config
instance we:
- Told SDK not to use HTTPS (note http:// in URL) and to send data to Countly server located at http://YOUR.SERVER.COM. We also specified the app key (YOUR_APP_KEY).
- Enabled test mode (read - crash whenever in an inconsistent state, don't forget to disable it in Production!).
- Set logging level to DEBUG to make sure everything works as expected.
- Enabled crash reporting feature and tell SDK to use UUID strategy, that is random UUID string, as device id.
Providing the application key
Also called "AppKey" as shorthand. The application key is used to identify for which application this information is tracked. You receive this value by creating a new application in your Countly dashboard and accessing it in its application management screen.
Note: Ensure you are using the App Key (found under Management -> Applications) and not the API Key. Entering the API Key will not work.
Providing the server URL
If you are using Countly Enterprise trial servers, use https://try.count.ly
, https://us-try.count.ly
or https://asia-try.count.ly
It is basically the domain from which you are accessing your trial dashboard.
If you use both Countly Lite and Countly Enterprise, use your own domain name or IP address, such as https://example.com or https://IP (if SSL has been set up).
If you are in doubt about the correctness of your Countly SDK integration you can learn about methods to verify it from here.
SDK logging / debug mode
The first thing you should do while integrating our SDK is enabling logging. If logging is enabled, then our SDK will print out debug messages about its internal state and encountered problems.
Set setLoggingLevel
on the config object to enable logging:
Config config = new Config("http://YOUR.SERVER.COM", "YOUR_APP_KEY")
.setLoggingLevel(Config.LoggingLevel.DEBUG)
.enableFeatures(Config.Feature.Events, Config.Feature.Sessions, Config.Feature.CrashReporting, Config.Feature.UserProfiles)
.setDeviceIdStrategy(Config.DeviceIdStrategy.UUID);
SDK data storage
Countly SDK stores serialized versions of the following classes: InternalConfig
, SessionImpl
, RequestImpl
, CrashImpl
, UserImpl
& TimedEvents
. All those are stored in device memory, in binary form, in separate files with filenames prefixed with [CLY]_
.
SDK notes
Test mode
To ensure correct SDK behavior, please use Config.enableTestMode()
when your app is in development and testing. In test mode, Countly SDK raises RuntimeException
s whenever is in an inconsistent state. Once you remove Config.enableTestMode()
call from your initialization sequence, SDK stops raising any Exception
s and switches to logging errors instead (if logging wasn't specifically turned off). Without having test mode on during development you may encounter some important issues with data consistency in production.
Events
Events in Countly represent some meaningful event user performed in your application within a Session
. Please avoid recording everything like all taps or clicks users performed. In case you do, it will be very hard to extract valuable information from generated analytics.
An Event
object contains the following data types:
-
name
, or event key. Required. A unique string that identifies the event. -
count
- number of times. Required, 1 by default. Like a number of goods added to the shopping basket. -
sum
- sum of something, amount. Optional. Like a total sum of the basket. -
dur
- duration of the event. Optional. For example how much time users spent checking out. -
segmentation
- some data associated with the event. Optional. It's a Map<String, String> which can be filled with arbitrary data like {"category": "Pants", "size": "M"}.
Recording events
The standard way of recording events is through your Session
instance:
Countly.session().event('purchase')
.setCount(2)
.setSum(19.98)
.setDuration(35)
.addSegments("category", "pants", "size", "M")
.record();
Please note the last method in that call chain, .record()
call is required for the event to be recorded.
The example above results in a new event being recorded in the current session. The event won't be sent to the server right away. Instead, Countly SDK will wait until one of the following happens:
-
Config.sendUpdateEachSeconds
seconds passed since begin or last update request in case of automatic session control. -
Config.eventsBufferSize
events have been already recorded and not sent yet. -
Session.update()
have been called by the developer. -
Session.end()
have been called by the developer or by Countly SDK in case of automatic session control.
We have provided an example of recording a purchase event below. Here is a quick summary of the information with which each usage will provide us:
- Usage 1: how many times the purchase event occurred.
- Usage 2: how many times the purchase event occurred + the total amount of those purchases.
- Usage 3: how many times the purchase event occurred + from which countries and application versions those purchases were made.
- Usage 4: how many times the purchase event occurred + the total amount, both of which are also available, segmented into countries and application versions.
- Usage 5: how many times the purchase event occurred + the total amount, both of which are also available, segmented into countries and application versions + the total duration of those events.
1. Event key and count
Countly.session().events("purchase").setCount(1).record();
2. Event key, count, and sum
Countly.session().events("purchase").setCount(1).setSum(20.3).record();
3. Event key and count with segmentation(s)
HashMap<String, String> segmentation = new HashMap<String, Object>();
segmentation.put("country", "Germany");
segmentation.put("app_version", "1.0");
Countly.session().events("purchase").setCount(1).setSegmentation(segmentation).record();
4. Event key, count, and sum with segmentation(s)
HashMap<String, String> segmentation = new HashMap<String, Object>();
segmentation.put("country", "Germany");
segmentation.put("app_version", "1.0");
Countly.session().events("purchase").setCount(1).setSum(34.5).setSegmentation(segmentation).record();
5. Event key, count, sum, and duration with segmentation(s)
HashMap<String, String> segmentation = new HashMap<String, Object>();
segmentation.put("country", "Germany");
segmentation.put("app_version", "1.0");
Countly.session().events("purchase").setCount(1).setSum(34.5).setDuration(5.3).setSegmentation(segmentation).record();;
Those are only a few examples of what you can do with events. You may extend those examples and use Country, app_version, game_level, time_of_day, and any other segmentation that will provide you with valuable insights.
Timed events
There is also a special type of Event
supported by Countly - timed events. Timed events help you to track long continuous interactions when keeping an Event
instance is not very convenient.
The basic use case for timed events is following:
- User starts playing a level "37" of your game, you call
Countly.session().timedEvent("LevelTime").addSegment("level", "37")
to start tracking how much time a user spends on this level. - Then something happens when the user is at that level, for example, the user bought some coins. Along with regular "Purchase" event, you decide you want to segment the "LevelTime" event with purchase information:
Countly.session().timedEvent("LevelTime").setSum(9.99)
. - Once the user stopped playing, you need to stop recording this event:
Countly.session().timedEvent("LevelTime").endAndRecord()
Once this event is sent to the server, you'll see:
- how much time users spend on each level (duration per
level
segmentation); - which levels are generating the most revenue (sum per
level
segmentation); - which levels are not generating revenue at all since you don't show ad there (0 sums in
level
segmentation).
With timed events, there is one thing to keep in mind: you have to end timed event for it to be recorded. Without endAndRecord()
call, nothing will happen.
Sessions
Manual sessions
Session in Countly is a single app launch or several app launches if the time between them is less than 30 seconds (by default). Of course, you can override this behavior.
Session
lifecycle methods include:
-
session.begin()
must be called when you want to send begin session request to the server. This request contains all device metrics: device, model, carrier, etc. -
session.update()
can be called to send a session duration update to the server along with any events, user properties, and any other data types supported by Countly SDK. Called each Config.sendUpdateEachSeconds seconds in auto session mode. -
session.end()
must be called to mark the end of the session. All the data recorded since the lastsession.update()
or sincesession.begin()
in case no updates have been sent yet, is sent in this request as well.
User profiles
For information about User Profiles, review this documentation
Setting predefined values
The Countly Java SDK allows you to upload specific data related to a user to the Countly server. You may set the following predefined data for a particular user:
- Name: Full name of the user.
- Username: Username of the user.
- Email: Email address of the user.
- Organization: Organization the user is working in.
- Phone: Phone number.
- Picture: Picture path for the user’s profile.
- Gender: Gender of the user (use only single char like ‘M’ for Male and ‘F’ for Female).
- BirthYear: Birth year of the user.
The SDK allows you to upload user details using the methods listed below.
To set standard properties, call respective methods of UserEditor
:
Countly.user(getApplicationContext()).edit()
.setName("Firstname Lastname")
.setUsername("nickname")
.setEmail("test@test.com")
.setOrg("Tester")
.setPhone("+123456789")
.commit();
Setting custom values
To set custom properties, call set(). To send modification operations, call the corresponding method:
Countly.user(getApplicationContext()).edit()
.set("mostFavoritePet", "dog")
.inc("phoneCalls", 1)
.pushUnique("tags", "fan")
.pushUnique("skill", "singer")
.commit();
Device ID management
A device ID is a unique identifier for your users. You may specify the device ID yourself or allow the SDK to generate it. When providing one yourself, keep in mind that it has to be unique for all users. Some potential sources for such an id may be the users username, email or some other internal ID used by your other systems.
Changing device ID
The SDK allows you to change the Device ID at any point in time. You can use any of the following two methods to changing the Device ID, depending on your needs.
Changing Device ID with server merge
In case your application authenticates users, you might want to change the ID to the one in your backend after he has logged in. This helps you identify a specific user with a specific ID on a device he logs in, and the same scenario can also be used in cases this user logs in using a different way. In this case, any data stored in your Countly server database associated with the current device ID will be transferred (merged) into the user profile with the device id you specified in the following method call:
Countly.session().changeDeviceIdWithMerge("New Device Id");
Changing Device ID without server merge
You might want to track information about another separate user that starts using your app (changing apps account), or your app enters a state where you no longer can verify the identity of the current user (user logs out). In that case, you can change the current device ID to a new one without merging their data. You would call:
Countly.session().changeDeviceIdWithoutMerge("New Device Id");
Doing it this way, will not merge the previously acquired data with the new id.
Do note that every time you change your deviceId without a merge, it will be interpreted as a new user. Therefore implementing id management in a bad way could inflate the users count by quite a lot.
Retrieving current device ID
You may want to see what device id Countly is assigning for the specific device. For that, you may use the following calls.
Countly.session().getDeviceId()
Other features
Backend Mode
The minimum SDK version requirement for this feature is 20.11.2.
The SDK provides a special mode to transfer data to your Countly Server, called 'Backend Mode'. This mode disables the regular API of the SDK and offers an alternative interface to record user data. This alternative approach would be useful when integrated in backend scenarios or when importing data into countly from a different source.
Data recorded with this mode is kept in memory queues and is not stored persistently. This means that any data, that was not yet sent to the server when the app is closed/killed, will be lost.
Enabling Backend Mode
To enable Backend Mode you should create a config class and call enableBackendMode
on this object, and later you should pass it to the init
method.
Config config = new Config("http://YOUR.SERVER.COM", "YOUR_APP_KEY")
.enableBackendMode()
.setRequestQueueMaxSize(500)
.setLoggingLevel(Config.LoggingLevel.DEBUG);
Countly.init(targetFolder, config);
If the Backend Mode is enabled the SDK stores up to a maximum of 1000 requests by default. Then when this limit is exceeded the SDK will drop the oldest request from the queue in the memory. To change this request queue limit, call setRequestQueueMaxSize
on the Config
object before the SDK init.
Recording Data
Users have to provide a device id, and optionally, the time in milliseconds every time they record any data. The device id is mandatory, so you can not set it null or not provide it while recording data.
Note: If the provided timestamp is null or less than 1, SDK updates its value to the current time in milliseconds.
Recording an event
You may record as many events as you want.
There are a couple of values that can be set when recording an event.
- deviceID- Device id is mandatory, it can not be empty or null.
- key- This is the main property which would be the identifier/name for that event. It is mandatory and it can not be empty or null.
- count - A whole positive numerical number value that marks how many times this event has happened. It is optional and if it is provided and its value is less than 1, SDK will automatically set it to 1.
- sum - This value will be summed up across all events in the dashboard. It is optional you may set it null.
- duration - This value is used for recording and tracking the duration of events. Set it to 0 if you don't want to report any duration.
- segmentation - A map where you can provide custom data for your events to track additional information. It is not a mandatory field, so you may set it to null or empty. It is a map that consists of key and value pairs. The accepted data types for the values are "String", "Integer", "Double", and "Boolean". All other types will be ignored.
Example:
Map<String, String> segment = new HashMap<String, String>() {{
put("Time Spent", "60");
put("Retry Attempts", "60");
}};
Countly.backendMode().recordEvent("device-id", "Event Key", 1, 10.5, 5, segment, 1646640780130L);
Note: Device ID and 'key' both are mandatory. The event will not be recorded if any of these two parameters is null or empty.
Recording a view
You may record views by providing the view details in segmentation with a timestamp.
There are a couple of values that can be set when recording an event.
- deviceID - Device id is mandatory, if it is null or empty data will not be recorded.
- name - It is the name of the view and it must not be empty or null.
- segmentation - A map where you can provide custom data for your view to track additional information. It is not a mandatory field, you may set it to null or leave it empty. It is a map of key/value pairs and the accepted data types are "String", "Integer", "Double", and "Boolean". All other types will be ignored.
- timestamp - It is time in milliseconds. It is not mandatory, and you may set it to null.
Example:
Map<String, String> segmentation = new HashMap<String, String>() {{
put("visit", "1");
put("segment", "Windows");
put("start", "1");
}};
Countly.backendMode().recordView("device-id", "SampleView", segmentation, 1646640780130L);
Note: Device ID and 'name' both are mandatory. The view will not be recorded if any of these two parameters is null or empty.
Recording a crash
To report exceptions provide the following detail:
- deviceID - Device id is mandatory, and if it is null or not provided no data will be recorded.
- message - This is the main property which would be the identifier/name for that event. It should not be null or empty.
- stacktrace - A string that describes the contents of the call stack. It is mandatory, and should not be null or empty.
- segmentation - A map where you can provide custom data for your view to track additional information. It is not a mandatory field, so you may set it to null or leave it empty. It is a map of key/value pairs and the accepted data types are "String", "Integer", "Double", and "Boolean". All other types will be ignored.
- crashDetail - It is not a mandatory field, so you may set it to null or leave it empty. It is a map of key/value pairs. To know more about crash parameters, click here.
- timestamp - It is time in milliseconds. It is not mandatory, and you may set it to null.
Map<String, String> segmentation = new HashMap<String, String>() {{
put("login page", "authenticate request");
}};
Map<String, String> crashDetails = new HashMap<String, String>() {{
put("_os", "Windows 11");
put("_os_version", "11.202");
put("_logs", "main page");
}};
Countly.backendMode().recordException("device-id", "message", "stacktrace", segmentation, crashDetails, null);
You may also pass an instance of an exception instead of the message and the stack trace to record a crash.
For example:
Map<String, String> segmentation = new HashMap<String, String>() {{
put("login page", "authenticate request");
}};
Map<String, String> crashDetails = new HashMap<String, String>() {{
put("_os", "Windows 11");
put("_os_version", "11.202");
put("_logs", "main page");
}};
try {
int a = 10 / 0;
} catch (Exception e) {
Countly.backendMode().recordException("device-id", e, segmentation, crashDetails, null);
}
Note: Throwable is a mandatory parameter, the crash will not be recorded if it is null.
Recording sessions
To start a session please provide the following details:
- deviceID - Device id is mandatory, if it is null or empty data will not be recorded.
- metrics - It is a map that contains device and app information as key-value pairs. It can be null or empty and the accepted data type for the pairs is "String".
- location - It is not a mandatory field, so you may set it to null or leave it empty. It is a map of key/value pairs and the accepted keys are "city", "country_code", "ip_address", and "location".
- timestamp - It is time in milliseconds. It is not mandatory, and you may set it to null.
Example:
Map<String, String> metrics = new HashMap<String, String>() {{
put("_os", "Android");
put("_os_version", "10");
put("_app_version", "1.2");
}};
Map<String, String> location = new HashMap<String, String>() {{
put("ip_address", "192.168.1.1");
put("city", "Lahore");
put("country_code", "PK");
put("location", "31.5204,74.3587");
}};
Countly.backendMode().sessionBegin("device-id", metrics, location, 1646640780130L);
Note: In above example '_os', '_os_version' and '_app_version' are predefined metrics keys. To know more about metrics, click here.
To update or end a session please provide the following details:
- deviceID - Device id is mandatory, if it is null or empty no action will be taken.
- duration - It is the duration of a session, you may pass 0 if you don't want to submit a duration.
- timestamp - It is time in milliseconds. It is not mandatory, and you may set it to null.
Session update:
double duration = 60;
Countly.backendMode().sessionUpdate("device-id", duration, null);
Session end:
double duration = 20;
Countly.backendMode().sessionEnd("device-id", duration, 1223456767L);
Note: Java SDK automatically sets the duration to 0 if you have provided a value that is less than 0.
Recording user properties
If you want to record some user information the SDK lets you do so by passing data as user details and custom properties.
- deviceID - Device id is mandatory, if it is null or empty no data will be recorded.
- userProperties - It is a map of key/value pairs and it should not be null or empty. The accepted data types as a value are "String", "Integer", "Double", and "Boolean". All other types will be ignored.
- timestamp - It is time in milliseconds. It is not mandatory, and you may set it to null.
For example:
Map<String, Object> userDetail = new HashMap<>();
userDetail.put("name", "Full Name");
userDetail.put("username", "username1");
userDetail.put("email", "user@gmail.com");
userDetail.put("organization", "Countly");
userDetail.put("phone", "000-111-000");
userDetail.put("gender", "M");
userDetail.put("byear", "1991");
//custom detail
userDetail.put("hair", "black");
userDetail.put("height", 5.9);
userDetail.put("marks", "{$inc: 1}");
Countly.backendMode().recordUserProperties("device-id", userDetail, 0);
You may also perform certain manipulations to your custom property values, such as incrementing the current value on a server by a certain amount or storing an array of values under the same property.
For example:
Map<String, Object> operation = new HashMap<>();
userDetail.put("fav-colors", "{$push: black}");
userDetail.put("marks", "{$inc: 1}");
Countly.backendMode().recordUserProperties("device-id", userDetail, 0);
The keys for predefined modification operations are as follows:
Key | Description |
---|---|
$inc | increment used value by 1 |
$mul | multiply value by the provided value |
$min | minimum value |
$max | maximal value |
$setOnce | set value if it does not exist |
$pull | remove value from an array |
$push | insert value to an array |
$addToSet | insert value to an array of unique values |
Recording direct requests
The SDK allows you to record direct requests to the server. To record a request you should provide the request data along with the device id and timestamp. Here are the details:
- deviceID - Device id is mandatory, so if it is null or empty no data will be recorded.
- requestData - It is a map of key/value pairs and it should not be null or empty. The accepted data type for the value is "String".
- timestamp - It is time in milliseconds. It is not mandatory, and you may set it to null.
For example:
Map<String, String> requestData = new HashMap<>();
requestData.put("device_id", "device-id-2");
requestData.put("timestamp", "1646640780130");
requestData.put("key-name", "data");
Countly.backendMode().recordDirectRequest("device-id-1", requestData, 1646640780130L);
Values in the 'requestData' map will override the base request's respective values. In the above example, 'timestamp' and 'device_id' will be overridden by their respective values in the base request.
Note: 'sdk_name', 'sdk_version', and 'checksum256' are protected by default and their values will not be overridden by 'requestData'.
Getting the request queue size
In case you would like to get the size of the request queue, you can use:
int queueSize = Countly.backendMode().getQueueSize();
It will return the number of requests in the memory request queue.