Plugin Structure

Follow

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.

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 dashboard 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 pluginManager = require('../pluginManager.js'),
    async = require('async'),
    fs = require('fs'),
    path = require("path");

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 countlyDb = pluginManager.dbConnection("countly");

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();
                });
        });
    });
});

Looking for help?