diff --git a/src/lib/db/client-applications-store.js b/src/lib/db/client-applications-store.js index be40d75d47..b7f40a38f1 100644 --- a/src/lib/db/client-applications-store.js +++ b/src/lib/db/client-applications-store.js @@ -5,6 +5,7 @@ const NotFoundError = require('../error/notfound-error'); const COLUMNS = [ 'app_name', 'created_at', + 'created_by', 'updated_at', 'description', 'strategies', @@ -20,6 +21,7 @@ const mapRow = row => ({ updatedAt: row.updated_at, description: row.description, strategies: row.strategies, + createdBy: row.created_by, url: row.url, color: row.color, icon: row.icon, @@ -29,6 +31,8 @@ const remapRow = (input, old = {}) => ({ app_name: input.appName, updated_at: input.updatedAt, description: input.description || old.description, + created_by: input.createdBy || old.createdBy, + announced: input.announced || old.announced || false, url: input.url || old.url, color: input.color || old.color, icon: input.icon || old.icon, @@ -118,6 +122,26 @@ class ClientApplicationsDb { ? this.getAppsForStrategy(filter.strategyName) : this.getAll(); } + + async getUnannounced() { + const rows = await this.db(TABLE) + .select(COLUMNS) + .where('announced', false); + return rows.map(mapRow); + } + + /** * + * Updates all rows that have announced = false to announced =true and returns the rows altered + * @return {[app]} - Apps that hadn't been announced + */ + async setUnannouncedToAnnounced() { + const rows = await this.db(TABLE) + .update({ announced: true }) + .where('announced', false) + .whereNotNull('announced') + .returning(COLUMNS); + return rows.map(mapRow); + } } module.exports = ClientApplicationsDb; diff --git a/src/lib/services/client-metrics/index.js b/src/lib/services/client-metrics/index.js index 1db03e1db2..4919210ee4 100644 --- a/src/lib/services/client-metrics/index.js +++ b/src/lib/services/client-metrics/index.js @@ -10,6 +10,7 @@ const { clientRegisterSchema } = require('./register-schema'); const { APPLICATION_CREATED } = require('../../event-type'); const FIVE_SECONDS = 5 * 1000; +const FIVE_MINUTES = 5 * 60 * 1000; module.exports = class ClientMetricsService { constructor( @@ -21,7 +22,11 @@ module.exports = class ClientMetricsService { clientInstanceStore, eventStore, }, - { getLogger, bulkInterval = FIVE_SECONDS }, + { + getLogger, + bulkInterval = FIVE_SECONDS, + announcementInterval: appAnnouncementInterval = FIVE_MINUTES, + }, ) { this.globalCount = 0; this.apps = {}; @@ -63,6 +68,7 @@ module.exports = class ClientMetricsService { }); this.seenClients = {}; setInterval(() => this.bulkAdd(), bulkInterval); + setInterval(() => this.announceUnannounced(), appAnnouncementInterval); clientMetricsStore.on('metrics', m => this.addPayload(m)); } @@ -78,10 +84,26 @@ module.exports = class ClientMetricsService { }); } + async announceUnannounced() { + if (this.clientAppStore) { + const appsToAnnounce = await this.clientAppStore.setUnannouncedToAnnounced(); + if (appsToAnnounce.length > 0) { + const events = appsToAnnounce.map(app => { + return { + type: APPLICATION_CREATED, + createdBy: app.createdBy, + data: app, + }; + }); + await this.eventStore.batchStore(events); + } + } + } + async registerClient(data, clientIp) { const value = await clientRegisterSchema.validateAsync(data); value.clientIp = clientIp; - this.logger.info(`${JSON.stringify(data)}`); + value.createdBy = clientIp; this.seenClients[this.clientKey(value)] = value; } diff --git a/src/migrations/20210304141005-add-announce-field-to-application.js b/src/migrations/20210304141005-add-announce-field-to-application.js new file mode 100644 index 0000000000..36e3f5e410 --- /dev/null +++ b/src/migrations/20210304141005-add-announce-field-to-application.js @@ -0,0 +1,24 @@ +'use strict'; + +exports.up = function(db, cb) { + db.runSql( + ` + ALTER TABLE client_applications ADD COLUMN announced boolean DEFAULT false; + UPDATE client_applications SET announced = true; + `, + cb, + ); +}; + +exports.down = function(db, cb) { + db.runSql( + ` + ALTER TABLE client_applications DROP COLUMN announced; + `, + cb, + ); +}; + +exports._meta = { + version: 1, +}; diff --git a/src/migrations/20210304150739-add-created-by-to-application.js b/src/migrations/20210304150739-add-created-by-to-application.js new file mode 100644 index 0000000000..1505d84b50 --- /dev/null +++ b/src/migrations/20210304150739-add-created-by-to-application.js @@ -0,0 +1,18 @@ +'use strict'; + +exports.up = function(db, cb) { + db.runSql( + ` + ALTER TABLE client_applications ADD COLUMN created_by TEXT; + `, + cb, + ); +}; + +exports.down = function(db, cb) { + db.runSql('ALTER TABLE client_applications DROP COLUMN created_by;', cb); +}; + +exports._meta = { + version: 1, +}; diff --git a/src/test/e2e/helpers/database.json b/src/test/e2e/helpers/database.json index 63cb102134..7f5325ce09 100644 --- a/src/test/e2e/helpers/database.json +++ b/src/test/e2e/helpers/database.json @@ -24,17 +24,20 @@ "applications": [ { "appName": "demo-app-1", - "strategies": ["default"] + "strategies": ["default"], + "announced": true }, { "appName": "demo-app-2", "strategies": ["default", "extra"], - "description": "hello" + "description": "hello", + "announced": true }, { "appName": "deletable-app", "strategies": ["default"], - "description": "Some desc" + "description": "Some desc", + "announced": true } ], "clientInstances": [ diff --git a/src/test/e2e/services/client-metrics-service.e2e.test.js b/src/test/e2e/services/client-metrics-service.e2e.test.js new file mode 100644 index 0000000000..ac19bdfe96 --- /dev/null +++ b/src/test/e2e/services/client-metrics-service.e2e.test.js @@ -0,0 +1,58 @@ +const test = require('ava'); +const faker = require('faker'); +const dbInit = require('../helpers/database-init'); +const getLogger = require('../../fixtures/no-logger'); +const ClientMetricsService = require('../../../lib/services/client-metrics'); +const { APPLICATION_CREATED } = require('../../../lib/event-type'); + +let stores; +let clientMetricsService; + +test.before(async () => { + const db = await dbInit('client_metrics_service_serial', getLogger); + stores = db.stores; + clientMetricsService = new ClientMetricsService(stores, { + getLogger, + bulkInterval: 500, + announcementInterval: 2000, + }); +}); + +test.after(async () => { + await stores.db.destroy(); +}); +test.serial('Apps registered should be announced', async t => { + t.plan(3); + const clientRegistration = { + appName: faker.internet.domainName(), + instanceId: faker.random.uuid(), + strategies: ['default'], + started: Date.now(), + interval: faker.random.number(), + icon: '', + description: faker.company.catchPhrase(), + color: faker.internet.color(), + }; + const differentClient = { + appName: faker.lorem.slug(2), + instanceId: faker.random.uuid(), + strategies: ['default'], + started: Date.now(), + interval: faker.random.number(), + icon: '', + description: faker.company.catchPhrase(), + color: faker.internet.color(), + }; + await clientMetricsService.registerClient(clientRegistration, '127.0.0.1'); + await clientMetricsService.registerClient(differentClient, '127.0.0.1'); + await new Promise(res => setTimeout(res, 1200)); + const first = await stores.clientApplicationsStore.getUnannounced(); + t.is(first.length, 2); + await clientMetricsService.registerClient(clientRegistration, '127.0.0.1'); + await new Promise(res => setTimeout(res, 2000)); + const second = await stores.clientApplicationsStore.getUnannounced(); + t.is(second.length, 0); + const events = await stores.eventStore.getEvents(); + const appCreatedEvents = events.filter(e => e.type === APPLICATION_CREATED); + t.is(appCreatedEvents.length, 2); +});