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.
- It can be executed multiple times, when enabling and disabling plugin multiple times
- You should also use this file when upgrading a plugin to apply changes for a previous version
- 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
- 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();
});
});
});
});