Java

Follow

This documentation is for the Countly Java SDK version 24.1.X. The SDK source code repository can be found here.

Click here, to access the documentation for older SDK versions.

The Countly Java SDK minimum supported target version is Java 8.

To examine the example integrations please have a look here.

Adding the 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:24.1.0"
}

Or as:

<dependency>
  <groupId>ly.count.sdk</groupId>
  <artifactId>java</artifactId>
  <version>24.1.0</version>
  <type>pom</type>
</dependency>

SDK Integration

Minimal Setup

Before you can use any functionality, you have to initiate the SDK.

The shortest way to initiate the SDK is with this code snippet:

File targetFolder = new File("d:\\__COUNTLY\\java_test\\");

Config config = new Config("http://YOUR.SERVER.COM", "YOUR_APP_KEY", targetFolder)
  .enableTestMode()
  .setLoggingLevel(Config.LoggingLevel.DEBUG)
  .enableFeatures(Config.Feature.Events, Config.Feature.Sessions, Config.Feature.CrashReporting, Config.Feature.UserProfiles)
  .setDeviceIdStrategy(Config.DeviceIdStrategy.UUID);

Countly.instance().init(config);

This code will initiate the SDK in test mode with logging enabled. Here you would also need to provide your application key and server URL. Please check here for more information on how to acquire your application key (APP_KEY) and server URL.

If you are in doubt about the correctness of your Countly SDK integration you can learn about the verification methods from here.

SDK Data Storage

Countly SDK stores serialized versions of the following classes: InternalConfig, SessionImpl, EventQueue, 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 RuntimeExceptions whenever is in an inconsistent state. Once you remove Config.enableTestMode() call from your initialization sequence, SDK stops raising any Exceptions 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.

SDK Logging Mode

The first thing you should do while integrating our SDK is enable logging. If logging is enabled, the Countly Java SDK will print out debug messages about its internal state and encountered problems. The SDK will print these debug messages to the console.

Set setLoggingLevel on the config object to enable logging:

File targetFolder = new File("d:\\__COUNTLY\\java_test\\");

Config config = new Config("http://YOUR.SERVER.COM", "YOUR_APP_KEY", targetFolder)
  .setLoggingLevel(Config.LoggingLevel.DEBUG)
  .enableFeatures(Config.Feature.Events, Config.Feature.Sessions, Config.Feature.CrashReporting, Config.Feature.UserProfiles)
  .setDeviceIdStrategy(Config.DeviceIdStrategy.UUID);

This logging level would not influence the log listener. That will always receive all the printed logs event if the logging level is "OFF."

Log Listener

To listen to the SDK's internal logs, you can call setLogListener on the Config Object. If set, SDK will forward its internal logs to this listener regardless of SDK's loggingLevel .

config.setLogListener(new LogCallback() {
  @Override
  public void LogHappened(String logMessage, Config.LoggingLevel logLevel) {
    //print log
  }
});

Crash Reporting

The Countly Java SDK has the ability to collect crash reports, which you may examine and resolve later on the server. The SDK can collect unhandled exceptions by default if the consent for crash reporting is given. You can reach all crash-related functionality from the returned interface on:

Countly.instance().crashes()

Automatic Crash Handling

Automatic crash handling is enabled by default. To disable it call this method on the config object during initialization:

config.disableUnhandledCrashReporting();

Handled Exceptions

You might catch an exception or similar error during your app’s runtime. To report them use the following method:

Countly.instance().crashes().recordHandledException(Throwable t);

// Or you can also add segment to be recorded with the error
Countly.instance().crashes().recordHandledException(Throwable t, Map<String, Object> segment);

If you have handled an exception and it turns out to be fatal to your app, you may use this call:

Countly.instance().crashes().recordUnhandledException(Throwable t);

// Or you can also add segment to be recorded with the error
Countly.instance().crashes().recordUnhandledException(Throwable t, Map<String, Object> segment);

Crash Breadcrumbs

Throughout your app you can leave crash breadcrumbs which would describe previous steps that were taken in your app before the crash. After a crash happens, they will be sent together with the crash report.

Following command adds crash breadcrumb:

Countly.instance().crashes().addCrashBreadcrumb(String record);

The maximum breadcrumb limit is 100. To change the maximum limit use this method during initialization:

config.setMaxBreadcrumbCount(int maxBreadcrumbCount);

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, Object> which can be filled with arbitrary data like {"category": "Pants", "size": "M"}. The valid data types for segmentation are: "String", "Integer", "Double", "Boolean", "Long" and "Float". All other types will be ignored.

Recording Events

The standard way of recording events is through your Countly instance's events interface:

Map<String, Object> segmentation = new HashMap<String, Object>();
segmentation.put("Time Spent", 60);
segmentation.put("Retry Attempts", 60);

Countly.instance().events().recordEvent("purchase", segmentation, 2, 19.98, 35);

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.instance().events().recordEvent("purchase", 1);

2. Event key, count, and sum

Countly.instance().events().recordEvent("purchase", 1, 20.3);

3. Event key and count with segmentation(s)

HashMap<String, Object> segmentation = new HashMap<String, Object>();
segmentation.put("country", "Germany");
segmentation.put("app_version", 1.0);

Countly.instance().events().recordEvent("purchase", segmentation, 1);

4. Event key, count, and sum with segmentation(s)

HashMap<String, Object> segmentation = new HashMap<String, Object>();
segmentation.put("country", "Germany");
segmentation.put("app_version", 1.0);

Countly.instance().events().recordEvent("purchase", segmentation, 1, 34.5);

5. Event key, count, sum, and duration with segmentation(s)

HashMap<String, Object> segmentation = new HashMap<String, Object>();
segmentation.put("country", "Germany");
segmentation.put("app_version", 1.0);

Countly.instance().events().recordEvent("purchase", segmentation, 1, 34.5, 5.3);

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.instance().events().startEvent("LevelTime") to start tracking how much time a user spends on this level. Also keep your segmentation values in a map like
HashMap<String, Object> segmentation = new HashMap<String, Object>();
segmentation.put("level", 37);
  • Then, something happens when the user is at that level; for example, the user buys some coins. Along with the regular "Purchase" event, you decide you want to segment the "LevelTime" event with purchase information. While ending the event, you also pass the sum value as the purchase amount to the function call.
  • Once the user stops playing, you need to stop this event and call: Countly.instance().events().endEvent("LevelTime",segmentation, 1, 9.99)
  • If you decide to cancel the event you can call: Countly.instance().events().cancelEvent("LevelTime")

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 endEvent("LevelTime") call, nothing will happen.

Sessions

Session tracking is a tool to track the specific time period that the end user is using the application. It is a timeframe with a start and end.

Manual Sessions

Session tracking does not happen automatically. It has to be started. After that the elapsed session duration will be sent every 60 seconds.

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 automatically. It can also be called more often manually.
  • session.end() must be called to mark the end of the session. All the data recorded since the last session.update() or since session.begin() in case no updates have been sent yet, is sent in this request as well.

View Tracking

You can track the views of your application with the Java SDK. With views feature, you can also create flows to see view transitions. Public interface of the views can be accessed via:

Countly.instance().views()

Manual View Reporting

The SDK provides various ways to track views. You can have a single view at a given time or track multiple views according to your needs. Each view would have its own unique view ID which could be used for manipulating the view further.

Auto Stopped Views

An easy way to track views is with using the auto stopped views. These views would stop if another view starts. You can start an auto stopped view with or without segmentation like this:

// without segmentation, use view id for further manipulation of views
String viewID = Countly.instance().views().startAutoStoppedView("View Name");
  
// Or with segmentation
Map<String, Object> viewSegmentation = new HashMap<>();
viewSegmentation.put("Cats", 123);
viewSegmentation.put("Moons", 9.98d);
viewSegmentation.put("Moose", "Deer");

// use view id for further manipulation of views
String view2ID = Countly.instance().views().startAutoStoppedView("View Name", viewSegmentation);

Regular Views

As opposed to the "auto stopped views", with regular views you can have multiple of them started at the same time, and then you can control them independently.

You can start a view that would not close when another view starts, like this:

Countly.instance().views().startView("View Name");

While manually tracking views, you may add your custom segmentation to them like this:

Map<String, Object> viewSegmentation = new HashMap<>();
viewSegmentation.put("Cats", 123);
viewSegmentation.put("Moons", 9.98d);
viewSegmentation.put("Moose", "Deer");

Countly.instance().views().startView("View Name", viewSegmentation);

These views would also return a string view ID when they are called.

Stopping Views

You can stop a view with its name or its view ID. To stop it with its name:

Countly.instance().views().stopViewWithName("View Name");

You can provide a segmentation while doing so:

Map<String, Object> viewSegmentation = new HashMap<>();
viewSegmentation.put("Cats", 123);
viewSegmentation.put("Moons", 9.98d);
viewSegmentation.put("Moose", "Deer");

Countly.instance().views().stopViewWithName("View Name", viewSegmentation);

If there are multiple views with the same name then stopViewWithName would close the one that has started first. These views would have different identifiers even though their names are same.

To stop a view with its view ID:

Countly.instance().views().stopViewWithID("View ID");

You can provide a segmentation while doing so:

Map<String, Object> viewSegmentation = new HashMap<>();
viewSegmentation.put("Cats", 123);
viewSegmentation.put("Moons", 9.98d);
viewSegmentation.put("Moose", "Deer");

Countly.instance().views().stopViewWithID("View ID", viewSegmentation);

You can also stop all running views at once with a segmentation:

Map<String, Object> viewSegmentation = new HashMap<>();
viewSegmentation.put("Cats", 123);
viewSegmentation.put("Moons", 9.98d);
viewSegmentation.put("Moose", "Deer");

Countly.instance().views().stopAllViews(viewSegmentation); // pass null if no segmentation

Pausing and Resuming Views

If you are starting multiple views at the same time it might be necessary for you to pause some views while others are still continuing. This can be achieved by using the unique identifier you get while starting a view.

To pause a view with its ID:

Countly.instance().views().pauseViewWithID("View ID");

To resume a view with its ID:

Countly.instance().views().resumeViewWithID("View ID");

Adding Segmentation to Started Views

You can add segmentation values to a view before it ends. This can be done as many times as desired and the final segmentation that will be send to the server would be the cumulative sum of all segmentations. However if a certain segmentation value for a specific key has been updated, the latest value will be used.

To add segmentation to a view using its view ID:

Map<String, Object> viewSegmentation = new HashMap<>();
viewSegmentation.put("Cats", 123);
viewSegmentation.put("Moons", 9.98d);
viewSegmentation.put("Moose", "Deer");

Countly.instance().views().addSegmentationToViewWithID("View ID", viewSegmentation);

To add segmentation to a view using its name:

Map<String, Object> viewSegmentation = new HashMap<>();
viewSegmentation.put("Cats", 123);
viewSegmentation.put("Moons", 9.98d);
viewSegmentation.put("Moose", "Deer");

Countly.instance().views().addSegmentationToViewWithName("View Name", viewSegmentation);

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.

Retrieving Current Device ID

You may want to see what device id and device id type Countly is assigning for the specific device. For that, you may use the following calls. Current device id types are 'DEVELOPER_SUPPLIED', 'SDK_GENERATED'.

Countly.instance().deviceId().getID() // will return String
Countly.instance().deviceId().getType() // will return DeviceIdType enum

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.instance().deviceId().changeWithMerge("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.instance().deviceId().changeWithoutMerge("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.

Device ID Generation

When initializing Countly, If no custom ID is provided, the Countly Java SDK generates a unique device ID. The SDK uses a random UUID string for the device ID generation. For example, after you init the SDK without providing a custom id, this call will return something like this:

Countly.instance().deviceId().getID(); // CLY_1930183b-77b7-48ce-882a-87a14056c73e

User Location

You can track your users' location with Countly Java SDK. This information can then be used for various tasks in your Countly server, like creating cohorts or sending push notifications depending on the location. You can only provide these four parameters regarding a user's location:

  • Country code in the two-letter, ISO standard, e.g. "en-US", "zh-CN"
  • City name (must be set together with the country code), e.g. "Reykjavik"
  • Latitude and longitude values, separated by a comma, e.g. "56.42345,123.45325"
  • Your user’s IP address, e.g. "192.168.1.1"

Setting Location

If you set the user location during SDK initialization it will be sent to the server during the start of the user session:

config.setLocation(countryCode, city, gpsCoordinates, ipAddress);

As server side location calculations depends on the location info coming at the beginning of a user session, providing this info at init time is recommended.

If you get your users' location info after SDK initialization, you can still provide them with the following call:

//set user location
String countryCode = "us";
String city = "Houston";
String latitude = "29.634933";
String longitude = "-95.220255";
String ipAddress = null; // IP address must only be provided during init.
Countly.instance().location().setLocation(countryCode, city, latitude + "," + longitude, ipAddress);

Here you if you don't want to set specific fields, you should set them to null.

When these values are set, a separate request will be created to send them to the server and these values would be cached for location tracking later, so at the start of the next user session they would be used in server side calculations. 

Disabling Location

To turn off location tracking during init, use this method. Otherwise the location tracking is enabled by default:

config.disableLocation();

To turn off location tracking after init you can use this method:

Countly.instance().location().disableLocation();

This action will erase the cached location data from the device and the server.

Remote Config

Remote config allows you to modify the app by requesting key-value pairs from the Countly server. The returned values can be changed based on the users. For more details, please see the Remote Config documentation. It is accessible through Countly.instance().remoteConfig() interface. Remote config values are stored when downloaded unless they are deleted. Also, if values downloaded with full update, stored values are overwritten by newly downloaded values.;

Downloading values

Automatic Remote Config Triggers

Automatic remote config triggers are disabled by default so there is no need for disable action. If you enable it by enableRemoteConfigAutomaticTriggers, the remote config values are going to be fully updated.

Config config = new Config(COUNTLY_SERVER_URL, COUNTLY_APP_KEY, sdkStorageRootDirectory);
config.enableRemoteConfigAutomaticTriggers();
...

Remote configs are going to be downloaded from scratch in these triggers:

  • Just after initialization of the Countly Java SDK
  • After a device id change

Manual Calls

There are three ways to trigger remote config value download manually:

  • Manually downloading all keys
  • Manually downloading specific keys
  • Manually downloading, omitting (everything except) keys.

Each of these calls also has an optional RCDownloadCallback callback parameter which would be called when the download has finished.

dowloadAllKeys is the same as the automatic update - it replaces all stored values with the ones from the server (all locally stored values are deleted and replaced with new ones).

Or downloading values of only specific keys might be needed. To do so, calling downloadSpecificKeys to download new values for the specific keys would update only those keys which are provided with a String array.

Or downloading values of only a few keys might not be needed. To do so, calling downloadOmittingKeys would update all values except the provided keys. The keys are provided with a String array.

All Keys Include Keys Omit Keys
Countly.instance().remoteConfig().downloadAllKeys((requestResult, error, fullValueUpdate, downloadedValues) -> {
  if(requestResult.equals(RequestResult.Success){
    //do sth
  }
});

When making requests with an "keysToInclude" or "keysToOmit" array, if those arrays are empty or null, they will function the same as a dowloadAllKeys request and will update all the values. This means it will also erase all keys not returned by the server.

Accessing Values

There is two way to access remote config values. Either all values can be gathered, or a value can be obtained for a specific key.

//Which will return map of all stored remote config values
Map<String,RCData> allValues = Countly.instance().remoteConfig().getValues();
Object valueOfKey = allValues.get("key").value;

RCData looks like this:

class RCData {
  //value that is downloaded from the server for that key
  Object value;
  //metadata about ownership of the value
  //it is false when device id changed but that key's value is not updated
  boolean isCurrentUsersData;
}

Why value is in Object class? Because all data types supported by JSON can be a stored. For example it can be a JSONArray, JSONObject, Integer, Boolean, Float, String, Double, Long. So it is needed to cast to appropriate data type. If value is null then there is no value for that key found.

To get a value's of a specific key:

RCData data = Countly.instance().remoteConfig().getValue("key");
RCData data1 = Countly.instance().remoteConfig().getValue("key1");
RCData data2 = Countly.instance().remoteConfig().getValue("key2");
JSONObject json = (JSONObject) data.value;
JSONArray jArray = (JSONArray) data1.value;
Integer intValue = (Integer) data2.value;

Clearing Stored Values

Clearing the remote config values might be needed at some case, so by this call you can clean the remote config values:

Countly.instance().remoteConfig().clearAll();

Global Download Callbacks

A callback function might be needed after remote config values downloaded. Remote config download callback functions can be registered during initialization of Countly or via remoteConfig interface.

//during initialization
Config config = new Config(COUNTLY_SERVER_URL, COUNTLY_APP_KEY, sdkStorageRootDirectory);
config.remoteConfigRegisterGlobalCallback(RCDownloadCallback callback);
...

RCDownloadCallback is called when the remote config download request is finished, and it would have the following parameters:

  • rResult: RequestResult Enum (either Error, Success or NetworkIssue)
  • error: String (error message. "null" if there is no error)
  • fullValueUpdate: boolean ("true" - all values updated, "false" - a subset of values updated)
  • downloadedValues: Map<String, RCData> (the whole downloaded remote config values from server)
RCDownloadCallback {
  void callback(RequestResult rResult, String error, boolean fullValueUpdate, Map<String, RCData> downloadedValues);
}

Via remoteConfig interface, already registered callback can be also removed

//register a download callback
Countly.instance().remoteConfig().registerDownloadCallback(RCDownloadCallback callback);

//remove already registered download callback Countly.instance().remoteConfig().removeDownloadCallback(RCDownloadCallback callback);

A/B Testing

User's participation in A/B tests can be accessed from the Remote Config feature in multiple ways. Possible ways to enroll or remove your users for A/B tests are listed below.

Enrollment on Download

Enrollment to A/B tests can be done automatically when downloading remote config values. This can be enabled on the initialization of the Countly.

//during initialization
Config config = new Config(COUNTLY_SERVER_URL, COUNTLY_APP_KEY, sdkStorageRootDirectory);
config.enrollABOnRCDownload();
...

Enrollment on Access

A/B tests can be enrolled while getting remote config values from storage. If there is no key exists functions does not enroll user to A/B tests:

//This call returns RCData, works same way with non-enrolling variant getValue 
Countly.instance().remoteConfig().getValueAndEnroll(String key); 
  
//This call returns Map<String,RCData>, works same way with non-enrolling variant getValues 
Countly.instance().remoteConfig().getAllValuesAndEnroll();

Enrollment on Action

To enroll a user into the A/B tests for the given keys following method should be used:

Countly.instance().remoteConfig().enrollIntoABTestsForKeys(String[] keys);

Keys array is the required parameter. If it is not given this function does nothing.

Exiting A/B Tests

To remove users from A/B tests of certain keys, following function should be used:

Countly.instance().remoteConfig().exitABTestsForKeys(String[] keys);

Keys array is the required parameter. If it is not given, user is going to be removed from all tests.

User Feedback

You can receive feedback from your users with nps, survey and rating feedback widgets.

The rating feedback widget allows users to rate using the 1 to 5 rating system as well as leave a text comment. Survey and nps feedback widgets allow for even more textual feedback from users.

Feedback Widget

It is possible to display 3 kinds of feedback widgets: nps, survey and rating. All widgets have their generated URL to be shown in a web viewer and should be approached using the same methods.

Before any feedback widget can be shown, you need to create them in your Countly dashboard.

Getting Available Widgets

After you have created widgets at your dashboard you can reach their related information as a list, corresponding to the current user's device ID, by providing a callback to the getAvailableFeedbackWidgets method, which returns the list as the first parameter and error as the second:

Countly.instance().feedback().getAvailableFeedbackWidgets((retrievedWidgets, error) -> {
  // handle error
  // do something with the returned list here like pick a widget and then show that widget etc...
});

The objects in the returned list would look like this:

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

Here all the values are same with the values that can be seen at your Countly server like the widget ID, widget type, widget name and the tags you have passed while creating the widget. Tags can contain information that you would like to have in order to keep track of the widget you have created. Its usage is totally left to the developer.

Potential 'type' values are:

FeedbackWidgetType {survey, nps, rating}

Presenting A Widget

After you have decided which widget you want to show, you would provide that object to the following function as the first parameter. Second is a callback with constructed url to show and error message in case an error occurred:

Countly.instance().feedback().constructFeedbackWidgetUrl(chosenWidget, (constructedUrl, error) -> {
  // handle error and the constructed url
});

Manual Reporting

There might be some cases where you might want to use the native UI or a custom UI you have created. At those times you would want to request all the information related to that widget and then report the result manually. You can see more information about manual reporting from here.

For a sample integration, have a look at our sample code at our github repo.

Getting Feedback Widget Data

Initial steps for manually reporting your widget results, first you would need to retrieve the available widget list with the getAvailableFeedbackWidgets method. After that you would have a list of possible CountlyFeedbackWidget objects. You would pick the widget you want to display and pass that widget object to the function below as the first parameter. Second parameter is a callback that would return the widget data as first parameter and the error as second:

Countly.instance().feedback().getFeedbackWidgetData(chosenWidget, (retrievedWidgetData, error) -> {
  // handle data and error here
});

Here the retrievedWidgetData would yield to a JSON Object with all of the information you would need to present the widget by yourself.

For how this retrievedWidgetData would look like and in depth information on this topic please check our detailed article here.

Reporting Widget Result Manually

After you have collected the required information from your users with the help of the retrievedWidgetData you have received, you would then package the responses into a Map<String, Object>, and then pass it (reportedResult) together with the widget object you picked from the retrieved widget list (widgetToReport) and the retrievedWidgetData to report the feedback result with the following call:

//this contains the reported results
Map<String, Object> reportedResult = new HashMap<>();

//
// You would fill out the results here. That step is not displayed in this sample check the detailed documentation linked above
//

//report the results to the SDK
Countly.instance().feedback().reportFeedbackWidgetManually(widgetToReport, retrievedWidgetData, reportedResult);

If the user would have closed the widget, you would report that by passing a "null" as the reportedResult.

User Profiles

For information about User Profiles, review this documentation. You can access user profiles via Countly.instance().userProfile()

Setting User Properties

Setting Custom Values

To set custom properties, call setProperty(). To send modification operations, call the corresponding methods and ensure to call Countly.instance().userProfile().save() to send the configured user properties to the server after setting them:

Countly.instance().userProfile().setProperty("mostFavoritePet", "dog");
Countly.instance().userProfile().increment("phoneCalls"); // increments by 1
Countly.instance().userProfile().pushUnique("tags", "fan");
Countly.instance().userProfile().pushUnique("skill", "singer");
Countly.instance().userProfile().save();

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 and ensure to call Countly.instance().userProfile().save() to send the configured user properties to the server after setting them:

Countly.instance().userProfile().setProperty(PredefinedUserPropertyKeys.NAME, "Firstname Lastname");
Countly.instance().userProfile().setProperty(PredefinedUserPropertyKeys.EMAIL, "test@test.com");
Countly.instance().userProfile().setProperty(PredefinedUserPropertyKeys.USERNAME, "nickname");
Countly.instance().userProfile().setProperty(PredefinedUserPropertyKeys.ORGANIZATION, "Tester");
Countly.instance().userProfile().setProperty(PredefinedUserPropertyKeys.PHONE, "+123456789");
Countly.instance().userProfile().save();

Setting User Picture

You can either upload a profile picture by this call:

Countly.instance().userProfile().setProperty(PredefinedUserPropertyKeys.PICTURE, BYTE_IMAGE)

or you can provide a picture url or local file path to set (only JPG, JPEG files are supported by the Java SDK):

Countly.instance().userProfile().setProperty(PredefinedUserPropertyKeys.PICTURE_PATH, String)

User Property Modificators

Here is the list of property modificators:

//set a custom property
Countly.instance().userProfile().setProperty("money", 1000);
//increment money by 50
Countly.instance().userProfile().increment("money", 50);
//multiply money with 2
Countly.instance().userProfile().multiply("money", 2);
//save maximum value
Countly.instance().userProfile().saveMax("score", 400);
//save minimum value
Countly.instance().userProfile().saveMin("time", 60);
//add property to array which can have unique values
Countly.instance().userProfile().pushUnique("currency", "dollar");
//add property to array which can be duplicate
Countly.instance().userProfile().push("currency", "dollar");
//remove value from array
Countly.instance().userProfile().pull("currency","dollar");
//set only a value
Countly.instance().userProfile().setOnce("bank","TestBank");
//save changes
Countly.instance().userProfile().save();

Security and Privacy

Parameter Tamper Protection

You may set the optional salt to be used for calculating the checksum of requested data, which will be sent with each request, using the checksum field. You will need to set the same salt on the Countly server. If the salt on the Countly server is selected, all requests will be checked for the validity of the checksum field before being processed.

Config config = new Config(COUNTLY_SERVER_URL, COUNTLY_APP_KEY, sdkStorageRootDirectory);
config.enableParameterTamperingProtection("salt");
Countly.instance().init(config);

Other Features and Notes

SDK Config Parameters Explained

These are the methods that lets you set values in your Countly config object:

  • setUpdateSessionTimerDelay(int delay) - Sets the interval for the automatic session update calls. The delay can not be smaller than 1 sec.
  • setEventQueueSizeToSend() - Sets the threshold for event grouping.
  • setSdkPlatform(String platform) - Sets the SDK platform if it is used in multiple platforms. Default value is OS name.
  • setRequiresConsent(boolean requiresConsent) - Enable GDPR compliance by disallowing SDK to record any data until corresponding consent calls are made. Default is false.
  • setMetricOverride(Map<String, String> metricOverride) - Mechanism for overriding metrics that are sent together with "begin session" requests and remote.
  • setApplicationVersion(String version) - Change application version reported to Countly server.
  • setApplicationName(String name) - Change application name reported to Countly server.
  • setLoggingLevel(LoggingLevel loggingLevel) - Logging level for Countly SDK. Default is OFF.
  • enableParameterTamperingProtection(String salt) - Enable parameter tampering protection.
  • setRequestQueueMaxSize(int requestQueueMaxSize) - In backend mode set the in memory request queue size. Default is 1000.
  • enableForcedHTTPPost() - Force usage of POST method for all requests.
  • setCustomDeviceId(String customDeviceId) - Set device id to specific string and strategy to DeviceIdStrategy.CUSTOM_ID.
  • setLogListener(LogCallback logCallback) - Add a log callback that will duplicate all logs done by the SDK.
  • enrollABOnRCDownload() - Enables A/B tests enrollment when remote config keys downloaded
  • remoteConfigRegisterGlobalCallback(RCDownloadCallback callback) - Register a callback to be called when remote config values is downloaded
  • enableRemoteConfigValueCaching() - Enable caching of remote config values
  • enableRemoteConfigAutomaticTriggers() - Enable automatic download of remote config values on triggers
  • disableLocation() - Disable location tracking
  • setLocation(String countryCode, String city, String geoLocation, String ipAddress) - Set location parameters to be sent with session begin
  • setMaxBreadcrumbCount(int maxBreadcrumbCount) - To change maximum limit of crash breadcrumb
  • disableUnhandledCrashReporting() - To disable unhandled crash reporting

Example Integrations

app-java module contains example use cases for the Countly Java SDK

- Example is a java application that covers most of the functionalities.

- BackendModeExample is a java application of an example usage of the BackendMode

SDK storage and Requests

Setting Event Queue Threshold

Events get grouped together and are sent either every minute or after the unsent event count reaches a threshold. By default it is 10. If you would like to change this, call:

config.setEventQueueSizeToSend(6);

Setting Maximum Request Queue Size

The request queue is flushed when the session timer delay exceeds. If network or server are not reachable, If the number of requests in the queue reaches the setRequestQueueMaxSize limit, the oldest requests in the queue will be dropped, and the newest requests will take their place. by default request queue size is 1000. You can change this by:

config.setRequestQueueMaxSize(56);

Forcing HTTP POST

You can force HTTP POST request for all requests by:

config.enableForcedHTTPPost();

Custom Metrics

This functionality is available since SDK version 22.09.1.

During some specific circumstances, like beginning a session or requesting remote config, the SDK is sending device metrics.

It is possible for you to either override the sent metrics (like the application version for some specific variant) or provide either your own custom metrics. If you are providing your own custom metrics, you would need your own custom plugin server-side which would interpret it appropriately. If there is no plugin to handle those custom values, they will be ignored.

Map<String, String> metricOverride = new HashMap<>();
metricOverride.put("SomeKey", "123");
metricOverride.put("_locale", "xx_yy");

Config config = new Config(COUNTLY_SERVER_URL, COUNTLY_APP_KEY)
  .setMetricOverride(metricOverride);
  
Countly.init(targetFolder, config);

For more information on the specific metric keys used by Countly, check here.

Backend Mode

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 enableBackendModeon 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 Configobject before the SDK init.

Recording Data

In order to record data using the SDK, users are required to provide a device ID and may optionally include a timestamp, specified in milliseconds. It is important to note that the device ID is a mandatory field and cannot be set to null or omitted.

It is also worth noting that if a timestamp value is not provided or is less than 1, the SDK will automatically update the value to the current time, specified in milliseconds. This ensures that all recorded data is accurately timestamped and prevents data duplication.

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>();
segment.put("Time Spent", "60");
segment.put("Retry Attempts", "60");

Countly.instance().backendM().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>();
segmentation.put("visit", "1");
segmentation.put("segment", "Windows");
segmentation.put("start", "1");

Countly.instance().backendM().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>();
segmentation.put("login page", "authenticate request");

Map<String, String> crashDetails = new HashMap<String, String>();
crashDetails.put("_os", "Windows 11");
crashDetails.put("_os_version", "11.202");
crashDetails.put("_logs", "main page");

Countly.instance().backendM().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>();
segmentation.put("login page", "authenticate request");

Map<String, String> crashDetails = new HashMap<String, String>();
crashDetails.put("_os", "Windows 11");
crashDetails.put("_os_version", "11.202");
crashDetails.put("_logs", "main page");

try {
  int a = 10 / 0;
} catch(Exception e) {
  Countly.instance().backendM().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>();
metrics.put("_os", "Android");
metrics.put("_os_version", "10");
metrics.put("_app_version", "1.2");

Map<String, String> location = new HashMap<String, String>();
location.put("ip_address", "192.168.1.1");
location.put("city", "Lahore");
location.put("country_code", "PK");
location.put("location", "31.5204,74.3587");

Countly.instance().backendM().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.instance().backendM().sessionUpdate("device-id", duration, null);

Session end:

double duration = 20;
Countly.instance().backendM().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.instance().backendM().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.instance().backendM().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.instance().backendM().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.instance().backendM().getQueueSize();

It will return the number of requests in the memory request queue.

FAQ

Where Does the SDK Store the Data?

The Countly Java SDK stores data in a directory/file structure. All SDK-related files are stored inside the directory given with sdkStorageRootDirectory parameter to the Config class during init. The SDK creates files for sessions, users, event queues, requests, crashes, and JSON storage to keep the device ID, migration version, etc.

What Information Is Collected by the SDK?

The data that SDKs gather to carry out their tasks and implement the necessary functionalities is mentioned in here. It is saved locally before any of it is transferred to the server.

When events are recorded, the time of when the event is recorded, will be collected

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.

Looking for help?