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;
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)
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
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
Currently supported event names or basically paths that plugin can listen to are:
/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
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
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;
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;
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.