Countly Documentation

Countly Resources

Here you'll find comprehensive guides to help you start working with Countly as quickly as possible.

Plugin API side

Since Countly API side is basically a REST API, then plugin mechanism attached to that also works in a similar way, by passing events with api paths.

File that will handle api requests should be named api.js and located in your plugins directory api folder as {plugin}/api/api.js

It should require at least plugins manager to hook to its events and common.js from Countly api to connect to database and use other useful methods.

Then you need to start registering to specific events and act on them accordingly.
Each event will also provide its own object with some variable like request parameters, etc.

Example api.js file could look like this:

var plugin = {},
	common = require('../../../api/utils/common.js'),
  plugins = require('../../pluginManager.js');

(function (plugin) {
	//write api call
	plugins.register("/i", function(ob){
    //get request parameters
    var params = ob.params;
    
		//check if it has data we need
    if(params.qstring.user_details){
      //if it is string, but we expect json, lets parse it
			if (typeof params.qstring.ourplugin == "string") {
        try {
            params.qstring.ourplugin = JSON.parse(params.qstring.ourplugin);
        } catch (SyntaxError) {
            console.log('Parse JSON failed');
            //we are not doing anything with request
						return false;
        }
        //start doing something with request
        
        //and tell core we are working on it, by returning true
        return true;
      }
      
      //we did not have data we were interested in
      return false;
    }
	});
}(plugin));

module.exports = plugin;

Params object

There is a common object passed through many methods on api side, and it is usually stored in a variable named "params". Contens of this object may depend based on type of api request as well as phase of processing this request

Property name
What it contains
When it is added

href

Full request url

From the start

qstring

Object of the query string or body passed with request

From the start

res

Response object

From the start

req

Request object

From the start

apiPath

Two level path string

From the start

fullPath

String of full api endpoint path

From the start

files

Files uploaded with request

From the start for POST request

cancelRequest

If contains true, then request should be ignored and not processed

Can be set at any time by any plugin, but API only checks for it in beggining after / and /sdk events, so that is when plugins should set it if needed

bulk

True if this SDK request is processed from the bulk method

When using /i/bulk endpoint

promises

Array of the promises by different events

When all promises are fulfilled, request ended

ip_address

IP address of the device submitted request

on all SDK requests

user

Data with some user info, like country geolcoation, etc from the request

on all SDK requests

app_user_id

ID of app_users document for the user

on all SDK requests

member

All data about dashboard user

on all requests containing api_key, after validation through validation methods

app

Document for the app

on all SDK requests and after validateUserForDataReadAPI validation

app_user

Document for the app_user

on all SDK requests

time

Time object containing:
now - moment object for request time
nowUTC - moment object for request time in UTC
nowWithoutTimestamp - moment object or current time
timestamp - request timestamp
mstimestamp - request milisecond timestamp
yearly - moment.format("YYYY"),
monthly - moment.format("YYYY.M"),
daily - moment.format("YYYY.M.D"),
hourly - moment.format("YYYY.M.D.H"),
weekly - Math.ceil(moment.format("DDD") / 7),
month - moment.format("M"),
day - moment.format("D"),
hour - moment.format("H")

on all SDK requests

Available event paths and parameters

Currently supported event names or basically paths that plugin can listen to are:

Path
Description
Usable
Properties

/master

Dispatched from a master cluster managing workers

When you want to launch background tasks to do in parallel with countly server workers

Has no parameters passed to it

/worker

Initialization of the worker, basically executed only once when nodejs server starts

To accomplish tasks needed to do once per start, as create connection pools to database, if you are not using Countly default connections

common

Has common js as another way of getting db connection and other common utilities

/

Any HTTP request to the Countly API

When need to modify data before Countly core processes it or when handling file uploads for specific URL

params - params object

apiPath
string with path request was made to

urlParts string with url parts

And validation methods
validateAppForWriteAPI, validateUserForDataReadAPI, validateUserForDataWriteAPI, validateUserForGlobalAdmin

/sdk

HTTP request from SDK

When you want to cancel a request from SDK

params - params object

/sdk/end

When finished processing request from SDK

When you want to post process some inserted information

params - params object

/i

Default write path retrieving basically all standard data from SDK

To retrieve new data from SDKs. This event is already validated, meaning you can write the data you received

params - params object

app
containing information about the for which data is meant to

/o

Default read path. You should treat this path as custom path, meaning you should return true in event if you are handling this request and write output for this request.

To read some basic data by supplying specific method query string,
This event is not validated and you need to validate to see if the user has any permission to read data

params - params object

validateUserForDataReadAPI
function to validate if user has at least user role for requested app

validateUserForDataWriteAPI
function to validate if user has write permission to app

/validation/user

If your plugin does some validation about user having access to some api endpoints or specific data

Allow or prohibit user access by returning false if allowed and true if prohibited

params - params object

/o/validate

Default read request with validated data, meaning user API_KEY exists and he has at least user role for this app.

Usually to read data for some existing system predefined read methods and not custom ones

params - params object

app
containing information about app for which data is meant to

/i/events

Processing each event

For getting or modifying event data

params - params object

currEvent
event data

/session/begin

When user session begins

To record that new session started or new user was added

params - params object

isNewUser - bool if user is new

/session/extend

When user extends session

To record that session was extended

params - params object

/session/end

When session end request received and session possibly ended

To record the end of the session

params - params object
dbAppUser
information about user

/session/post

When session actually ended, after cooldown period passed or received new session after colldown, without closing previous session

Use it as session end, when there is no guarantee that SDK will send any end_session requests

params - params object

end_session true if session was ended by end_session request or false if new session started without closing previous session

/session/duration

When total session duration was calculated after session ended

To record session duration of ended session

params - params object

session_duration
session duration in seconds

/session/user

Information about user which session started

To record or update user information

params - params object
dbAppUser
information about user

/session/metrics

Processing metrics

To add new metric data to process

params - params object

predefinedMetrics
object to which add your metrics that should be processed

user
nformation about user

isNewUser - bool if user is new

/session/retention

When user's session count and fs, ls timestamps and metric data was updated

If you need to post process user's metric data

params - params object

isNewUser true if user is new, false if already had any session

/o/method/total_users

When outputting total user count for metric values

To provide your metric name to get total users for it

shortcodesForMetrics - collection to metric name mapping
match - query part to get users from app_users collection

/i/apps/create

When new app is created

Modify data or add indexes to app specific collections

params - params object

appId - id of created app
data - app data

/i/apps/update

When app info is updated

Log changes or notify 3rd party services

params - params object

appId - id of created app
data - updated data

/i/apps/reset

When app's and app's analytics data is deleted

Delete analytics data related to this app

params - params object

appId - id of reseted app
data - app data

/i/apps/delete

When app is delete

Delete all app and app analytics related data

params - params object

appId - id of deleted app

data - app data

/i/apps/clear_all

When all app's analytics data is deleted

Delete all app analytics related data

params - params object

appId - id of deleted app
data - app data

/i/users/create

When new user is created

Add default settings or indexes

params - params object

data - some created user data

/i/users/update

When user info is updated

Log changes or notify 3rd party services

params - params object

data - some updated user data

member - member document

/i/users/delete

When user is deleted

Delete any specific user settings or data

params - params object

data - deleted user data

/i/device_id

When app user's device_id is changed by SDK

Change your plugins data if anything stored to specific app user

params - params object

oldUser old app user document

newUser new app user document

/systemlogs

Listens to register user actions in system logs

When you want to record an action for dashboard user

params - params object

action - action to record

data - data to record in json format

/plugins/drill

Listens by drill when someone wants to record events

To record events in drill, usually internal events that are not received by SDK, as SDK events are automatically processed by drill

params - params object

dbAppUser app user document

events - array of event objects, same as received by requests from SDK

/view/duration

Listens to update view's duration

To report view duration when view ended

params - params object

duration - duration of the view

{custom paths}

Any API path that is not handled by core or listed in this table

To create new plugin specific paths

params - params object

validateUserForDataReadAPI
function to validate if user has at least user role for requested app

validateUserForDataWriteAPI
function to validate if user has write permission to app

Canceling requests

In two cases you can cancel the request, so it won't be processed by core any further.

One of them when listening to / path (any request), the other when listening /sdk path (any SDK request). To cancel this request, all your plugin has to do is to set cancelRequest of params object to true.

plugins.register("/", function(ob){
  ob.params.cancelRequest = true;
});

Then further processing of the request will be ignored by core

User Validation

Not all data should be publicly available to add, edit, delete or even view for anyone making requests to Countly API. Thus you need to validate if user's provided API_KEY has permission to view of modify data.

As some of the events for plugins are made by system directly or are validated before passing to plugins, thus they don't need validation, there are only few cases where you need to validate user's request.

"/" - any http request made before data is processed

"/o" - path when reading basic metrics

{custom paths} - you define for your plugin

For plugin events on this paths, Countly provides validation functions you can choose to use for validation.

ob.validateUserForDataReadAPI - function to validate if user has at least user role for requested app

ob.validateUserForDataWriteAPI - function to validate if user has write permission to app, as being added as admin or being global admin

ob.validateUserForGlobalAdmin - function to validate if user is global admin, no need for app id here.

ob.validateAppForWriteAPI - function to validate if APP_KEY belongs to any app and add additional parameters to params object, which every SDK /i request has. As:

params.app_id - app id

params.app_cc - app country

params.app_name - app name

params.appTimezone - app timezone

params.time - request time

Example validation:

var plugin = {},
	common = require('../../../api/utils/common.js'),
  plugins = require('../../pluginManager.js');

(function (plugin) {
  
  //handling some custom path
  plugins.register("/i/ourplugin", function(ob){
    
    //get parameters
    var params = ob.params; //request params
		var validate = ob.validateUserForDataWriteAPI; //user validation
    
    validate(params, function (params) {
      //user is validated 
      //you can process request
    });
    
    //need to return true, so core does not repond that path does not exist
    return true;
  });
}(plugin));

module.exports = plugin;

Handling custom paths

Additionally to predefined event paths, any path that is unhandled by core api.js (basically not listed in reference api and above table), will be passed on for plugins.

For example if you call path like http://count.ly/o/foobar it will firstly dispatch it to plugins and if none of the plugins will say that they use this path, the error message will be returned by the api, that it is an incorrect path.

To say to the core that plugin uses this path or is doing something in the event asynchronously, you must simply return true from your event handler.

Also when you are handling your own custom paths, your plugin is responsible for providing any output to the client, or else such HTTP request would just timeout.

Custom /o path methods

Custom method that is not handled by the core to /o path is basically considered as a custom path, thus should be handled as one, by returning bool if request is used by plugin or not and outputting information if request is made for plugin

Also note, that in the event you only need to register single subpath, as /o/foo and all requests to /o/foo/bar1 and /o/foo/bar2 etc will be directed to /o/foo event, where you can retrieve paths array and process request.

Example of handling custom paths could look like:

var plugin = {},
	common = require('../../../api/utils/common.js'),
  plugins = require('../../pluginManager.js');

(function (plugin) {
  //handling custom path
  plugins.register("/i/ourplugin", function(ob){
    //get parameters
    var params = ob.params; //request params
		var validate = ob.validateUserForDataWriteAPI; //user validation
		var paths = ob.paths;
    validate(params, function (params) {
      //user is validated process request
      switch (paths[3]) {
      	case 'create':
          //create new object
          var data = params.qstring;
          //validate data if needed and write object to db
          common.db.collection('ourplugin').insert(data, function(err, app) {
						if(err)
							common.returnMessage(params, 200, err);
						else
							common.returnMessage(params, 200, "Success");
					});
        	break;
        case 'update':
          //update existing object
          var id = params.qstring.id;
          var data = params.qstring;
          //validate data if needed and write object to db
          common.db.collection('ourplugin').update({_id:id}, data, function(err, app) {
						if(err)
							common.returnMessage(params, 200, err);
						else
							common.returnMessage(params, 200, "Success");
					});
          break;
				case 'delete':
          //delete existing object
          var id = params.qstring.id;
          common.db.collection('ourplugin').remove({_id:id}, function(err, app) {
						if(err)
							common.returnMessage(params, 200, err);
						else
							common.returnMessage(params, 200, "Success");
					});
					break;
        default:
					common.returnMessage(params, 400, 'Invalid path, must be one of /create, /update or /delete');
          break;
       }
    });
    //need to return true, so core does not repond that path does not exist
    return true;
  });
}(plugin));

module.exports = plugin;

Processing metrics

One of the most common plugin usages for Countly could be adding new metrics to track data and display on dashboard like existing metrics: resolutions, carriers, etc. So we tried to make this process quite easy to achieve.

First thing to do on the API side when adding new metric, is to listen to /session/metric event and modify metric objects from which to collect data. After that Countly core would automatically handle processing metric data and storing it in database.

Then of course at some point you would need to listen to read path /o?method=mymetric and return your metric data for this request.

After validating user, outputing metric data can be done by requiring Countly fetch module and using its helper method fetch.fetchTimeObj

Only other things to consider is that you would need to delete your metric data when app either resets or gets deleted.

And that's it on the API side, here's an example of adding metric called my metric:

var plugin = {},
	common = require('../../../api/utils/common.js'),
  plugins = require('../../pluginManager.js'),
	fetch = require('../../../api/parts/data/fetch.js');

(function (plugin) {
  
  //waiting for metrics to be received
	plugins.register("/session/metrics", function(ob){
		var predefinedMetrics = ob.predefinedMetrics;
    
    //tell countly to process our metric
		predefinedMetrics.push({
            db: "mymetric", //collection name
            metrics: [
                { name: "_mymetric", //what to wait for in query string
                 set:"mymetric", // metric mymetric
                 short_code:"mymetric" } //optionally can provide short name
            ]
        });
	});
  
  //waiting for read request
	plugins.register("/o", function(ob){
		var params = ob.params;
		var validateUserForDataReadAPI = ob.validateUserForDataReadAPI;
    
    //if user requested to read our metric
		if (params.qstring.method == "mymetric") {
      
      //validate user and output data using fetchTimeObj method
			validateUserForDataReadAPI(params, fetch.fetchTimeObj, 'mymetric');
      
      //return true, we responded to this request
			return true;
		}
    
    //else we are not interested in this request
		return false;
	});
	
  //waiting for app delete event
	plugins.register("/i/apps/delete", function(ob){
		var appId = ob.appId;
    
    //delete all app data from our metric collection
		common.db.collection('mymetric').remove({'_id': {$regex: appId + ".*"}},function(){});
	});
	
  //waiting for app reset event
	plugins.register("/i/apps/reset", function(ob){
		var appId = ob.appId;
    
    //delete all app data from our metric collection
		common.db.collection('mymetric').remove({'_id': {$regex: appId + ".*"}},function(){});
    
	});
}(plugin));

module.exports = plugin;

And that is it. Now you can make standard metrics request to your Countly setup with your own metric inside and it will be tracked like every other metric.

As for this example:

/i?
begin_session=1
&app_key=YOUR-APP-KEY
&device_id=YOUR_DEVICE_ID
&metrics={
      "_os": "Android",
      "_os_version": "4.1",
      "_device": "Samsung Galaxy",
      "_resolution": "1200x800",
      "_carrier": "Vodafone",
      "_app_version": "1.2",
      "_mymetric": "myvalue"
}

/i/bulk requests

You don't need to process bulk requests specifically, Countly core does it for you, splitting bulk requests into single /i requests that are processed by your plugin as usual ones.

Plugin API side