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:
|
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.