Countly Documentation

Countly Resources

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

Plugin structure

Inside plugins folder the structure is quite similar to Countly itself, having files for api part and for frontend part.

But there are also other files available. Example plugin structure look like this:

│   install.js
│   package.json
│   tests.js
│   uninstall.js
│
├───api
│       api.js
│
└───frontend
    │   app.js
    │
    └───public
        ├───images
        │   └───empty
        │           image1.png
        │           image2.png
        │
        ├───javascripts
        │       countly.models.js
        │       countly.views.js
        │
        ├───localization
        │       empty.properties
        │
        ├───stylesheets
        │       main.css
        │
        └───templates
                tempalte2.html
                template1.html

Diagram below shows a high level architecture of Countly plugins. It shows a clear visualization of how Countly SDK, Core and database connects with each other and how different components do what.

Countly High Level Plugin Architecture. Click on image to see a bigger version.

Countly High Level Plugin Architecture. Click on image to see a bigger version.

package.json

This file is like a standard nodejs package file containing information about plugin and also dependencies which should be installed in plugin directory and information to display in Countly dashboar when enabling/disabling plugins.

Example contents of package.json are:

{
  "name": "countly-empty",
  "title": "Plugin template",
  "version": "0.0.0",
  "description": "Empty plugin template for creating new plugins",
  "author": "Count.ly",
  "homepage": "https://count.ly/marketplace/",
  "repository" :{ "type" : "git", "url" : "http://github.com/Countly/countly-server.git"},
  "bugs":{ "url" : "http://github.com/Countly/countly-server/issues"},
  "keywords": [
    "countly",
    "analytics",
    "mobile",
    "plugins",
	"template"
  ],
  "dependencies": {
  },
  "private": true
}

install.js

This file will be executed when plugin is enabled. Couple of things you need to consider when writing install.js file.

  1. It can be executed multiple times, when enabling and disabling plugin multiple times
  2. You should also use this file when upgrading a plugin to apply changes for a previous version
  3. Countly might not run at the time when this file will be executed (during installation for example), so you must manage your own connection to db and other stuff
  4. This file will be executed in a separate node process, so the file must also end correctly to end the node process. For example, don't forget to close database connection when you are done modifying it.

Example contents of install.js is as follows:

var mongo = require('../../frontend/express/node_modules/mongoskin'),
	async = require('../../api/utils/async.min.js'),
	fs = require('fs'),
	path = require("path"),
	countlyConfig = require('../../frontend/express/config');

console.log("Installing plugin");


console.log("Creating needed directories");
var dir = path.resolve(__dirname, '');
fs.mkdir(dir+'/../../frontend/express/public/folder', function(){});

console.log("Modifying database");
var dbName;
if (typeof countlyConfig.mongodb === "string") {
    dbName = countlyConfig.mongodb;
} else if ( typeof countlyConfig.mongodb.replSetServers === 'object'){
	countlyConfig.mongodb.db = countlyConfig.mongodb.db || 'countly';
	//mongodb://db1.example.net,db2.example.net:2500/?replicaSet=test
    dbName = countlyConfig.mongodb.replSetServers.join(",")+"/"+countlyConfig.mongodb.db;
} else {
    dbName = (countlyConfig.mongodb.host + ':' + countlyConfig.mongodb.port + '/' + countlyConfig.mongodb.db);
}
if(dbName.indexOf("mongodb://") !== 0){
	dbName = "mongodb://"+dbName;
}

var countlyDb = mongo.db(dbName);

countlyDb.collection('apps').find({}).toArray(function (err, apps) {

    if (!apps || err) {
		console.log("No apps to upgrade");
		countlyDb.close();
        return;
    }
	function upgrade(app, done){
		console.log("Adding indexes to " + app.name);
		countlyDb.collection('app_users' + app._id).ensureIndex({"name":1},done);
	}
	async.forEach(apps, upgrade, function(){
		console.log("Plugin installation finished");
		countlyDb.close();
	});
});

uninstall.js

Similar to install.js, only this file is executed when plugin is being disabled. Same rules that apply to install.js also apply to uninstall.js file

tests.js or folder tests with index.js

This file will be executed when all tests are launched using npm test command from Gruntfile.js

During test execution all plugin files will be quality checked using JSHint and then tests executed.

Upon test you will have access to created APP_ID, APP_KEY and API_KEY and can perform any related tests with it on frontend or api.

After test is done, you must reset app data to be clean.

For tests you will have superagent module available for requests and shouldjs module available for assertion. As well as testUtils module for settings, data and helpful methods.

Testing frontend is a little more complicated than api part, because we need to authenticate user and then retrieve csrf for making any post requests to server. But this process is made easier with testutils methods

Check out this example of covering different basics of testing:

var request = require('supertest');
var should = require('should');
var testUtils = require("../../../test/testUtils");
//request with url
request = request(testUtils.url);

//data will use in tests
var APP_KEY = "";
var API_KEY_ADMIN = "";
var APP_ID = "";
var DEVICE_ID = "1234567890";

describe('Testing plugin', function(){
  
  //Simple api test
  describe('Empty plugin', function(){
    it('should have no data', function(done){
      
      //on first test we can retrieve settings
			API_KEY_ADMIN = testUtils.get("API_KEY_ADMIN");
			APP_ID = testUtils.get("APP_ID");
			APP_KEY = testUtils.get("APP_KEY");
      
      //and make a request
			request
			.get('/o?api_key='+API_KEY_ADMIN+'&app_id='+APP_ID+'&method=ourplugin')
			.expect(200)
			.end(function(err, res){
				if (err) return done(err);
				var ob = JSON.parse(res.text);
				ob.should.be.empty;
				done();
			});
		});
  });
  
  //Testing frontend
  describe('Posting data to front end', function(){
    //first we authenticate
		before(function( done ){
			testUtils.login( request );
			testUtils.waitLogin( done );
		});
		it('should have no live data', function(done){
			request
			.post("/events/iap")
			.send({
        app_id:APP_ID, 
        somedata:"data", 
        //getting csrf
        _csrf:testUtils.getCSRF()})
			.expect(200)
			.end(function(err, res){
				if (err) return done(err);
				done();
			});
		});
	});
  
  //Reset app data
  describe('reset app', function(){
		it('should reset data', function(done){
			var params = {app_id:APP_ID};
			request
			.get('/i/apps/reset?api_key='+API_KEY_ADMIN+"&args="+JSON.stringify(params))
			.expect(200)
			.end(function(err, res){
				if (err) return done(err);
				var ob = JSON.parse(res.text);
				ob.should.have.property('result', 'Success');
        //lets wait some time for data to be cleared
				setTimeout(done, 5000)
			});
		});
	});
  
  //after that we can also test to verify if data was cleared
  describe('Verify empty plugin', function(){
    it('should have no data', function(done){
			request
			.get('/o?api_key='+API_KEY_ADMIN+'&app_id='+APP_ID+'&method=ourplugin')
			.expect(200)
			.end(function(err, res){
				if (err) return done(err);
				var ob = JSON.parse(res.text);
				ob.should.be.empty;
				done();
			});
		});
  });
});

Plugin structure