This document explains how to install Countly SDK for Windows desktop applications. It applies to version 23.02.X.
Click here, to access the documentation for older SDK versions.
The Countly Windows SDK implements the following explicit flavors:
- .NET Standard, Version=2.0
- .NET Portable, Version=4.5
- .NET Framework, Version=v3.5
The Countly GitHub page for this SDK contains also sample projects. You should be able to download them to test the basic functionality of this SDK and make sure you are using it correctly. In case you encounter any problems in your application,
The project page can be found here
Adding the SDK to the Project
To install the package, you can use either the NuGet Package Manager or the Package Manager Console. When you install a package, NuGet records the dependency, either in your project file or a packages.config
file (depending on the project format).
- In Solution Explorer, right-click References and choose Manage NuGet Packages.
- Choose "nuget.org" as the Package source, select the Browse tab, search for Countly, select that package in the list, and select Install:
-
Accept any license prompts.
SDK Integration
Before you can use any Countly functionality, you need to call Countly.Instance.Init
to initiate the SDK.
Minimal Setup
The shortest way to initiate the SDK is with this call:
//create the Countly init object
CountlyConfig cc = new CountlyConfig();
cc.serverUrl = "http://YOUR_SERVER";
cc.appKey = "YOUR_APP_KEY";
cc.appVersion = "1.2.3";
//initiate the SDK with your preferences
Countly.Instance.Init(cc);
In the CountlyConfig
object, you provide appKey and your Countly server URL. Please check here for more information on how to acquire your application key (APP_KEY) and server URL.
Note: The SDK targets multiple profiles. Therefore for some of them, there are feature differences. Either with additional function calls or with additional fields in the CountlyConfig object.
If you are in doubt about the correctness of your Countly SDK integration you can learn about the verification methods from here.
SDK Logging / Debug Mode
The first thing you should do while integrating our SDK is to enable logging. If logging is enabled, then our SDK will print out debug messages about its internal state and encounter problems. To enable logging you need to do the following two steps:
Step 1: Enable SDK logging using the following call:
Countly.IsLoggingEnabled = true;
You can turn it on and off in any place of your code.
Step 2: Go to project properties, select the 'Build' tab and make sure the following things are correct.
- Configuration: Debug
- "Define DEBUG constant" is checked
Log messages written in the application will show up in 'Output' windows.
SDK Data Storage
Cached requests and other SDK relevant information is stored in files in a named folder. All platform targets except .net40 call that folder "countly", .net40 calls that folder "countly_data".
All platform targets except .net35 will use IsolatedStorage for that. .net35 will store that named folder in the same folder as the executable by default.
If there are permission limitations for your app targeting .net35, then there is a call where you can change the path for the named storage folder. You can change that by using this:
Countly.SetCustomDataPath("C:\path\to\new\folder\");
SDK Notes
Additional Info for UWP Project Setup
It's possible to register an unhandled crash handler during SDK initialization. To do that, you need to provide a link to your application.
var cc = new CountlyConfig
{
serverUrl = "SERVER_URL",
appKey = "APP_KEY",
appKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXX
appVersion = "1.2.3",
application = referenceToApplication //provide link to your application
};
await Countly.Instance.Init(cc);
Crash Reporting
The Countly SDK for Windows can collect Crash Reports, which you may examine and resolve later on the server.
Automatic Crash Handling
Automatic crash handling is possible if the project type / platform target supports a unhandled exception handler.
In that case you would subscribe to that and report the collected errors/exception to the Countly SDK.
Exception details and device properties would be sent on the next app launch.
Handled Exceptions
You might catch an exception or similar error during your app’s runtime. You may also log these handled exceptions to monitor how and when they are happening. To log exceptions use the following code snippet:
Dictionary<string, string> customInfo = new Dictionary<string, string>
{
{ "customData", "importantStuff" }
};
try {
throw new Exception("It is an exception");
} catch (Exception ex) {
Countly.RecordException(ex.Message, ex.StackTrace, customInfo, false);
}
Here is the detail of the parameters:
- error - A string that contains a detailed description of the exception.
- stackTrace - A string that describes the contents of the call stack.
- customInfo - Custom key/values to be reported.
- unhandled - (bool) Set false if the error is fatal.
If you have handled an exception and it turns out to be fatal to your app, you may use the following calls:
Countly.RecordUnhandledException(ex.Message, ex.StackTrace, customInfo, true);
Crash Breadcrumbs
Throughout your app, you can leave crash breadcrumbs Mandatory that 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.
The following command adds a crash breadcrumb:
Countly.Instance.AddCrashBreadCrumb("breadcrumb");
Consent
This feature uses Crashes
consent. No additional crash logs will be recorded if consent is required and not given.
Events
An event is any type of action that you can send to a Countly instance, e.g. purchases, changed settings, view enabled, and so on, letting you get valuable information about your application.
There are a couple of values that can be set when recording an event. The main one is the key property which would be the identifier/name for that event. For example, in case a user purchased an item in a game, you could create an event with the key 'purchase'.
Optionally there are also other properties that you might want to set:
- Count - a whole numerical value that marks how many times this event has happened. The default value for that is 1.
- Sum - This value would be summed across all events in the dashboard. For example, in-app purchase events sum of purchased items. Its default value is null.
- Duration - Used to record and track the duration of events. The default value is null.
- Segmentation- A value where you can provide custom segmentation for your events to track additional information. It is a key and value map. The accepted data types for the value are "String".
Recording Events
Here is a quick way to record an event:
Countly.RecordEvent("event-key");
Based on the example below of an event recording a purchase, here is a quick summary of the information for each usage:
- Usage 1: how many times purchase event occured.
- Usage 2: how many times purchase event occured + the total amount of those purchases.
- Usage 3: how many times purchase event occured + which countries and application versions those purchases were made from.
- Usage 4: how many times purchase event occured + the total amount both of which are also available segmented into countries and application versions.
- Usage 5: how many times purchase event occured + the total amount both of which are also available segmented into countries and application versions.
1. Event key and count
await Countly.RecordEvent("purchase", 3);
2. Event key, count, and sum
await Countly.RecordEvent("purchase", 3, 0.99);
3. Event key and count with segmentation(s)
Segmentation segmentation = new Segmentation();
segmentation.Add("country", "Germany");
segmentation.Add("app_version", "1.0");
await Countly.RecordEvent("purchase", 3, segmentation);
4. Event key, count, and sum with segmentation(s)
Segmentation segmentation = new Segmentation();
segmentation.Add("country", "Germany");
segmentation.Add("app_version", "1.0");
await Countly.RecordEvent("purchase", 3, 2.97, segmentation);
5. Event key, count, sum, duration with segmentation(s)
Segmentation segmentation = new Segmentation();
segmentation.Add("country", "Germany");
segmentation.Add("app_version", "1.0");
await Countly.RecordEvent("purchase", 3, 2.97, 122.45, segmentation);
These are only a few examples of what you can do with Events. You may go beyond those examples and use country, app_version, time_of_day, and any other segmentation of your choice that will provide you with valuable insights.
Timed Events
Timed events are events which gives you the ability to calculate the duration it takes for an event to take place with respect to any arbitrary point you choose. Timed events must be handled manually, where they need a call to start the event and another call to end the event. First call is used to start an internal timer which would continue counting until the second call is used to stop it and record the duration. This second call would end the timer, create an event with the given name and the with the duration from the timer and send it to the event queue. If this second call has not been called or if the app has been closed before calling it, no event would be created. As the timer is stored at the memory of the device, if you close the app before ending the event, you will have to start all over when you open the app later again.
string eventName = "Some event";
//start some event with the event name "Some event"
Countly.Instance.StartEvent(eventName);
//wait some time
//end the event with the same event name "Some event"
Countly.Instance.EndEvent(eventName);
You may also provide additional information when ending an event. In that case, you can provide the segmentation, count, or sum values. The default values for those are "null", 1, and 0.
string eventName = "Some event";
//start some event
Countly.Instance.StartEvent(eventName);
//wait some time
Segmentation segmentation = new Segmentation();
segmentation.Add("wall", "orange");
//end the event while also providing segmentation information
Countly.Instance.EndEvent(eventName, segmentation);
Here are other options to end timed events:
//end the event while providing segmentation information and count
Countly.Instance.EndEvent("timed-event", segmentation, 4);
//end the event while providing segmentation information, count and sum
Countly.Instance.EndEvent("timed-event", segmentation, 4, 10);
You may cancel an already started timed event in case it is not needed anymore:
//start some event
Countly.Instance.StartEvent(eventName);
//wait some time
//cancel the event
Countly.Instance.CancelEvent(eventName);
Consent
This feature uses Events
consent. No additional events will be recorded if consent is required and not given.
When consent is removed, all previously started timed events will be cancelled.
Sessions
Manual Sessions
After you have initiated the SDK, call Countly.Instance.SessionBegin()
when you want to start tracking the user session. Usually, that is called in the entry point of the app.
Call Countly.Instance.SessionEnd()
before the app is closed to mark the end of the apps session.
The SDK will send an update request every 60 seconds, to track how long the session is.
That time is tracked using an internal timer. Some usage scenarios (like using the SDK in a console application) can prevent the timer from working. In those circumstances, you can manually call Countly.Instance.SessionUpdate(elapsedTime)
to track the passage of time. You should still call it about every minute.
//start the user session
Countly.Instance.SessionBegin();
//end the user session
Countly.Instance.SessionEnd();
//update the session manually
int elapsedTime = 60;//elapsed time in seconds
Countly.Instance.SessionUpdate(elapsedTime);
View Tracking
Manual View Recording
The SDK provides a call to record views in your application. More information about how to use them can be found here. You only need to provide the name for the view.
Countly.Instance.RecordView("Some View");
Device ID Management
To link events, sessions, crashes, etc to a user, a deviceId is used. It is usually generated by the SDK. It is then saved locally and then reused on every init unless the developer supplies its own device Id.
Ideally, the same device Id would be generated on the same device when using the same generation method. This way it would be possible to track users on reinstalls.
Device ID Generation
The SDK supports multiple ways for generating that ID, each with its pro's and cons and some limited to a specific compilation target:
- 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.
- developerSupplied - The device Id was provided by the developer. Used in cases where developers want to use an id tied to their internal systems/servers.
Device id and generation method can be provided during SDK init. Those values can also bet not set, then the default method for that target will be used.
//create the Countly init object
CountlyConfig cc = new CountlyConfig();
cc.serverUrl = "http://YOUR_SERVER";
cc.appKey = "YOUR_APP_KEY";
cc.appVersion = "1.2.3";
cc.developerProvidedDeviceId = "use@email.com";
//initiate the SDK with your preferences
Countly.Instance.Init(cc);
Changing Device ID
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 (e.g another tablet, another mobile phone, or web). 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.ChangeDeviceId("new-device-id", true);
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.ChangeDeviceId("new-device-id", false);
Doing it this way, will not merge the previously acquired data with the new id.
If the device ID is changed without merging and consent was enabled, all previously given consent will be removed. This means that all features will cease to function until new consent has been given again for that new device 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.
string usedId = await Countly.GetDeviceId();
User Location
While integrating this SDK into your application, you might want to track your user location. You could use this information to better know your app’s user base. There are 4 fields that can be provided:
- Country code (two-letter ISO standard).
- City name (must be set together with the country code).
- Latitude and longitude values separated by a comma, e.g. "56.42345,123.45325".
- Your user’s IP address.
Setting Location
Note that the IP address will only be updated if it was set during the init process.
During init, you can set location info in the configuration:
config.SetLocation(countryCode, city, gpsCoordinates, ipAddress);
After SDK initialization, this location info will be sent to the server at the start of the user session. Use SetLocation
method to disable or set the location at any time after the SDK Init call.
For example:
//set user location
String gpsLocation = "63.445821, 10.898868";
String ipAddress = "13.56.33.12";
String country_code = "us";
String city = null;
Countly.Instance.SetLocation(gpsLocation, ipAddress, country_code, city);
If there are fields you don't want to set, set them to null. If you're going to reset a field, set it to an empty string.
If no location is provided, it will be approximated by using GeoIP.
Disabling Location
Users might want to opt-out of location tracking. To do so call:
//disable location tracking
Countly.Instance.DisableLocation();
This will also erase all location info server side.
User Profiles
This feature is available with an Enterprise Edition subscription. For information about User Profiles, review this documentation.
Setting Predefined Values
The 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: Web-based Url 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.
You can save info about user tracking data is related to. Countly provides Countly.UserDetails
an object that exposes user-related properties
Each time you change a property value, Countly syncs it with a server. If you set value as null
, you will delete the property.
Example:
Countly.UserDetails.Name = "John";
// set name to JohnCountly.UserDetails.Name = "null";
// remove name
Setting Custom Values
The SDK gives you the flexibility to send only the custom data to Countly servers, even when you don’t want to send other user-related data.
You can provide custom properties for user using Custom
object
Countly.UserDetails.Custom.Add("city", "london");
Setting User Picture
Additionally, you can upload a picture of the user to the server. Accepted picture formats are .png, .gif and .jpeg and picture will be resized to maximal 150x150 dimensions.
Countly.UserDetails.UploadUserPicture(picture_stream);
Note: dots (.) and dollar signs ($) in key names will be stripped out.
User Consent
If you want to comply with GDPR or similar privacy requirements, there is functionality to manage user consent to features.
More information about GDPR can be found here.
Setup During Init
By default the requirement for consent is disabled. To enable it, you have to do it with the CountlyConfig object by setting consentRequired
to true
.
//create the Countly init object
CountlyConfig cc = new CountlyConfig();
cc.serverUrl = "http://YOUR_SERVER";
cc.appKey = "YOUR_APP_KEY";
//enable consent
cc.consentRequired = true;
//initiate the SDK with your preferences
Countly.Instance.Init(cc);
By default, no consent is given. That means that if no consent is enabled, Countly will not work and no network requests, related to features, will be sent. When the consent status of a feature is changed, that change will be sent to the Countly server.
Consent for features is not persistent and will have to be set every time while initializing countly. Therefore, the storage and persistence of given consent fall on the SDK integrator.
Feature names in this SDK are stored as an enum called ConsentFeatures
.
Features currently supported by this SDK are:
- sessions - tracking when, how often and how long users use your app
- events - allow sending events to the server
- location - allow sending location information
- crashes - allow tracking crashes, exceptions, and errors
- users - allow collecting/providing user information, including custom properties
In case consent is required, you may give consent to features before the SDK Init call. These features consents are not persistent and must be given on every restart.
//create the Countly init object
CountlyConfig cc = new CountlyConfig();
cc.serverUrl = "http://YOUR_SERVER";
cc.appKey = "YOUR_APP_KEY";
//enable consent
cc.consentRequired = true;
//set consent features
Dictionary<ConsentFeatures, bool> consent = new Dictionary<ConsentFeatures, bool>();
consent.Add(ConsentFeatures.Crashes, true);
consent.Add(ConsentFeatures.Events, false);
consent.Add(ConsentFeatures.Location, true);
consent.Add(ConsentFeatures.Sessions, false);
consent.Add(ConsentFeatures.Users, false);
cc.givenConsent = consent;
//initiate the SDK with your preferences
Countly.Instance.Init(cc);
Changing Consent
Consent can also be changed at any other moment in the app after init:
//preparing consent features
Dictionary<ConsentFeatures, bool> consent = new Dictionary<ConsentFeatures, bool>();
consent.Add(ConsentFeatures.Crashes, true);
consent.Add(ConsentFeatures.Events, false);
consent.Add(ConsentFeatures.Location, true);
//changing consent
Countly.Instance.SetConsent(consent);
Other Features and Notes
SDK Config Parameters Explained
To change the Configuration, update the values of parameters in the "CountlyConfig
object. Here are the details of the optional parameters:
developerProvidedDeviceId - (Optional, string) Your Device ID. It is an optional parameter. Example: f16e5af2-8a2a-4f37-965d-qwer5678ui98.
consentRequired- (Optional, bool) This is useful during the app run when the user wants to opt-out of SDK features.
sessionUpdateInterval - (Optional, int) Sets the interval (in seconds) after which the application will automatically extend the session. The default value is 60 (seconds).
SDK Internal Limits
SDK does have configurable fields to manipulate the internal SDK value and key limits. If values or keys provided by the user, would exceed the limits, they would be truncated. Here are the details of these configurable fields:
MaxKeyLength - (int) Maximum size of all string keys. The default value is 128.
MaxValueLength - (int) Maximum size of all values in our key-value pairs. The default value is 256.
MaxSegmentationValues - (int) Max amount of custom (dev provided) segmentation in one event. The default value is 256.
MaxStackTraceLinesPerThread - (int) Limits how many stack trace lines would be recorded per thread. The default value is 30.
MaxStackTraceLineLength - (int) Limits how many characters are allowed per stack trace line. The default value is 200.
MaxBreadcrumbCount - (int)maximum amount of breadcrumbs. The default value is 100.
FAQ
What Information Is Collected by the SDK
The following description mentions data that is collected by SDK to perform their functions and implement the required features. Before any of it is sent to the server, it is stored locally.
*When sending any network requests to the server, the following things are sent in addition to the main data:
- Timestamp of when the request is created
- Current hour
- Current day of week
- Current timezone
- SDK version
- SDK name
* If sessions are used then it would record the session start time, end time, and duration
* If sessions are used then also device metrics are collected which contains:
- Screen resolution
- Screen density
- OS name
- OS version
- App version
- Locale identifier
* When events are recorded, the following information collected:
- Time of event
- Current hour
- Current day of week
* If crash tracking is enabled, it will collect the following information at the time of the crash:
- OS name
- OS version
- Device resolution
- App version
- Time of the crash
- Crash stack trace
- Error description
- Total RAM
- If there is a network connection
Any other information like data in 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.
Is Windows SDK Compatible With .Net Maui
.NET Multi-platform App UI (.NET MAUI) is a cross-platform framework for creating native mobile and desktop apps with C# and XAML. It is compatible with .NET 6 runtime (or rather .NET Core 5). All .NET Core versions (2.0 and above) are compatible with .NET Standard 2.0 libraries.
As one of our Windows SDK flavors targets '.NET Standard' you should be able to use our Windows SDK in your .Net Maui applications without any hurdle.
However there a couple of issues to touch upon. On .NET MAUI applications, when an uncaught exception happens the AppDomain.UnhandledException
event occurs. As it is a cross-platform framework this effects the each platform you target differently and each one should need. For Android applications all exceptions should go through the Android.Runtime.AndroidEnvironment.UnhandledExceptionRaiser
event instead of the AppDomain.CurrentDomain.UnhandledException
event. And for iOS and Mac Catalyst, handling of the UnwindNativeCode
value is necessary.
So for reporting native crashes using this class in your project would provide a handler that would work on major platforms with the mentioned logic. To handle uncaught exceptions correctly we catch iOS and Android errors at lines 29 and 40 respectively. After catching them, we route all unhandled exceptions from different platforms through this event handler.
Usage:
MauiExceptions.UnhandledException += (sender, args) => { Countly.RecordException(args.ExceptionObject.ToString(), null, null, true).Wait(); };
Windows SDK GitHub page contains a sample project to test the basic functionality.