Plugin API Side

Follow

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;

Plugin event flow

Below you can see a general view of how plugin event flow works on the server side API. In this figure you can see all core hooks a plugin can listen to.

(Right click and download for a bigger image)

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 beginning 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, the 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

/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

 

/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

 

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.

  • validateUser -  to check if the user exists
  • validateUserForRead -  to check if the user has read rights for the provided app
  • validateUserForWrite - to check if the user has write rights for the provided app
  • validateGlobalAdmin - to check if the user is Global Admin

Prerequisites for validation methods are that user credentials must be provided as:

  • params.qstring.api_key or
  • params.qstring.auth_token or
  • params.req.headers["countly-token"]

For the methods that also validate the access to the app, it required app_id to be provided as:

  • params.qstring.app_id

If validation fails, those methods will respond to request themselves and nothing else is required on the developer part.

After validation is completed, additional parameters are added to params object, as:

  • params.app - app document
  • params.member - user document
  • params.time - request time
  • 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'),
{validateUserForWrite} = require('../../../api/utils/rights.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 validateUserForWrite(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;

For more information about rights validation methods, checkout rights module documentation.

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'),
{validateUserForWrite} = require('../../../api/utils/rights.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 paths = ob.paths; validateUserForWrite(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'),
{validateUserForRead} = require('../../../api/utils/rights.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; //if user requested to read our metric if (params.qstring.method == "mymetric") { //validate user and output data using fetchTimeObj method validateUserForRead(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.

Looking for help?