diff --git a/lib/client-metrics/client-metrics.test.js b/lib/client-metrics/client-metrics.test.js index 7beef4c9e4..3c74189365 100644 --- a/lib/client-metrics/client-metrics.test.js +++ b/lib/client-metrics/client-metrics.test.js @@ -4,15 +4,17 @@ const { test } = require('ava'); const UnleashClientMetrics = require('./index'); const sinon = require('sinon'); +const { EventEmitter } = require('events'); + const appName = 'appName'; const instanceId = 'instanceId'; test('should work without state', (t) => { - const metrics = new UnleashClientMetrics(); + const store = new EventEmitter(); + const metrics = new UnleashClientMetrics(store); - t.truthy(metrics.getMetricsOverview()); + t.truthy(metrics.getAppsWitToggles()); t.truthy(metrics.getTogglesMetrics()); - t.truthy(metrics.toJSON()); metrics.destroy(); }); @@ -20,7 +22,8 @@ test('should work without state', (t) => { test.cb('data should expire', (t) => { const clock = sinon.useFakeTimers(); - const metrics = new UnleashClientMetrics(); + const store = new EventEmitter(); + const metrics = new UnleashClientMetrics(store); metrics.addPayload({ appName, @@ -59,9 +62,10 @@ test.cb('data should expire', (t) => { t.end(); }); -test('addPayload', t => { - const metrics = new UnleashClientMetrics(); - metrics.addPayload({ +test('should listen to metrics from store', t => { + const store = new EventEmitter(); + const metrics = new UnleashClientMetrics(store); + store.emit('metrics', { appName, instanceId, bucket: { @@ -76,8 +80,6 @@ test('addPayload', t => { }, }); - t.truthy(metrics.clients[instanceId].appName === appName); - t.truthy(metrics.clients[instanceId].count === 123); t.truthy(metrics.apps[appName].count === 123); t.truthy(metrics.globalCount === 123); @@ -100,7 +102,6 @@ test('addPayload', t => { }, }); - t.truthy(metrics.clients[instanceId].count === 143); t.truthy(metrics.globalCount === 143); t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, { yes: 133, no: 10 }); t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 133, no: 10 }); @@ -108,50 +109,65 @@ test('addPayload', t => { metrics.destroy(); }); -test('addBucket', t => { - const metrics = new UnleashClientMetrics(); - metrics.addClient(appName, instanceId); - metrics.addBucket(appName, instanceId, { - start: new Date(), - stop: new Date(), - toggles: { - toggleX: { - yes: 123, - no: 0, +test('should build up list of seend toggles when new metrics arrives', t => { + const store = new EventEmitter(); + const metrics = new UnleashClientMetrics(store); + store.emit('metrics', { + appName, + instanceId, + bucket: { + start: new Date(), + stop: new Date(), + toggles: { + toggleX: { + yes: 123, + no: 0, + }, + toggleY: { + yes: 50, + no: 50, + }, }, }, }); - t.truthy(metrics.clients[instanceId].count === 123); - t.truthy(metrics.globalCount === 123); - t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 123, no: 0 }); + const appToggles = metrics.getAppsWitToggles(); + const togglesForApp = metrics.getSeenTogglesByAppName(appName); + + t.truthy(appToggles.length === 1); + t.truthy(appToggles[0].seenToggles.length === 2); + t.truthy(appToggles[0].seenToggles.includes('toggleX')); + t.truthy(appToggles[0].seenToggles.includes('toggleY')); + + t.truthy(togglesForApp.length === 2); + t.truthy(togglesForApp.includes('toggleX')); + t.truthy(togglesForApp.includes('toggleY')); metrics.destroy(); }); -test('addClient', t => { - const metrics = new UnleashClientMetrics(); - metrics.addClient(appName, instanceId); - metrics.addClient(appName, instanceId, new Date()); - t.truthy(metrics.clients[instanceId].count === 0); - t.truthy(metrics.globalCount === 0); +test('should handle a lot of toggles', t => { + const store = new EventEmitter(); + const metrics = new UnleashClientMetrics(store); + const toggleCounts = {}; + for (let i=0; i<100; i++) { + toggleCounts[`toggle${i}`] = {yes: i, no: i} + } + + store.emit('metrics', { + appName, + instanceId, + bucket: { + start: new Date(), + stop: new Date(), + toggles: toggleCounts, + }, + }); + + const seenToggles = metrics.getSeenTogglesByAppName(appName); + + t.truthy(seenToggles.length === 100); metrics.destroy(); -}); - -test('addApp', t => { - const metrics = new UnleashClientMetrics(); - - metrics.addApp(appName, instanceId); - t.truthy(metrics.apps[appName].clients.length === 1); - metrics.addApp(appName, 'instanceId2'); - t.truthy(metrics.apps[appName].clients.length === 2); - - metrics.addApp('appName2', 'instanceId2'); - t.truthy(metrics.apps.appName2.clients.length === 1); - metrics.addApp('appName2', instanceId); - t.truthy(metrics.apps.appName2.clients.length === 2); - - metrics.destroy(); -}); +}); \ No newline at end of file diff --git a/lib/client-metrics/index.js b/lib/client-metrics/index.js index 13a15ea50c..bf2df8cca2 100644 --- a/lib/client-metrics/index.js +++ b/lib/client-metrics/index.js @@ -4,10 +4,10 @@ const Projection = require('./projection.js'); const TTLList = require('./ttl-list.js'); module.exports = class UnleashClientMetrics { - constructor () { + constructor (clientMetricsStore) { + this.globalCount = 0; this.apps = {}; - this.clients = {}; this.lastHourProjection = new Projection(); this.lastMinuteProjection = new Projection(); @@ -15,6 +15,7 @@ module.exports = class UnleashClientMetrics { this.lastHourList = new TTLList({ interval: 10000, }); + this.lastMinuteList = new TTLList({ interval: 10000, expireType: 'minutes', @@ -31,18 +32,20 @@ module.exports = class UnleashClientMetrics { this.lastMinuteProjection.substract(toggleName, toggles[toggleName]); }); }); + clientMetricsStore.on('metrics', (m) => this.addPayload(m)); } - toJSON () { - return JSON.stringify(this.getMetricsOverview(), null, 4); + getAppsWitToggles () { + const apps = []; + Object.keys(this.apps).forEach(appName => { + const seenToggles = Object.keys(this.apps[appName].seenToggles); + const metricsCount = this.apps[appName].count; + apps.push({appName, seenToggles, metricsCount}) + }); + return apps; } - - getMetricsOverview () { - return { - globalCount: this.globalCount, - apps: this.apps, - clients: this.clients, - }; + getSeenTogglesByAppName(appName) { + return this.apps[appName] ? Object.keys(this.apps[appName].seenToggles) : []; } getTogglesMetrics () { @@ -53,16 +56,24 @@ module.exports = class UnleashClientMetrics { } addPayload (data) { - this.addClient(data.appName, data.instanceId); - this.addBucket(data.appName, data.instanceId, data.bucket); + const { appName, bucket } = data; + const app = this.getApp(appName) + this.addBucket(app, data.bucket); } - addBucket (appName, instanceId, bucket) { + getApp(appName) { + this.apps[appName] = this.apps[appName] || {seenToggles: {}, count: 0}; + return this.apps[appName]; + } + + addBucket (app, bucket) { let count = 0; // TODO stop should be createdAt const { stop, toggles } = bucket; - Object.keys(toggles).forEach((n) => { + const toggleNames = Object.keys(toggles); + + toggleNames.forEach((n) => { const entry = toggles[n]; this.lastHourProjection.add(n, entry); this.lastMinuteProjection.add(n, entry); @@ -72,49 +83,13 @@ module.exports = class UnleashClientMetrics { this.lastHourList.add(toggles, stop); this.lastMinuteList.add(toggles, stop); - this.addClientCount(appName, instanceId, count); + this.globalCount += count; + app.count += count; + this.addSeenToggles(app, toggleNames); } - addClientCount (appName, instanceId, count) { - if (typeof count === 'number' && count > 0) { - this.globalCount += count; - if (this.clients[instanceId]) { - this.clients[instanceId].count += count; - } - if (this.apps[appName]) { - this.apps[appName].count += count; - } - } - } - - addClient (appName, instanceId, started = new Date()) { - this.addApp(appName, instanceId); - if (instanceId) { - if (this.clients[instanceId]) { - this.clients[instanceId].ping = new Date(); - } else { - this.clients[instanceId] = { - appName, - count: 0, - started, - init: new Date(), - ping: new Date(), - }; - } - } - } - - addApp (appName, instanceId) { - if (appName && !this.apps[appName]) { - this.apps[appName] = { - count: 0, - clients: [], - }; - } - - if (instanceId && !this.apps[appName].clients.includes(instanceId)) { - this.apps[appName].clients.push(instanceId); - } + addSeenToggles (app, toggleNames) { + toggleNames.forEach(t => app.seenToggles[t] = true); } destroy () { diff --git a/lib/client-metrics/service.js b/lib/client-metrics/service.js deleted file mode 100644 index 231efbbe5f..0000000000 --- a/lib/client-metrics/service.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const { EventEmitter } = require('events'); - -module.exports = class UnleashClientMetrics extends EventEmitter { - constructor (metricsDb, interval = 10000) { - super(); - this.interval = interval; - this.db = metricsDb; - this.highestIdSeen = 0; - this.db.getMetricsLastHour().then(metrics => { - this.addMetrics(metrics); - this.startPoller(); - this.emit('ready'); - }); - this.timer = null; - } - - addMetrics (metrics) { - if (metrics && metrics.length > 0) { - this.highestIdSeen = metrics[metrics.length - 1].id; - } - this.emit('metrics', metrics); - } - - startPoller () { - this.timer = setInterval(() => { - this.db.getNewMetrics(this.highestIdSeen) - .then(metrics => this.addMetrics(metrics)); - }, this.interval); - this.timer.unref(); - } - - insert (metrics) { - return this.db.insert(metrics); - } - - destroy () { - try { - clearTimeout(this.timer); - } catch (e) {} - } -}; diff --git a/lib/client-metrics/service.test.js b/lib/client-metrics/service.test.js deleted file mode 100644 index bb0a541528..0000000000 --- a/lib/client-metrics/service.test.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -const { test } = require('ava'); -const MetricsService = require('./service'); -const sinon = require('sinon'); - -function getMockDb () { - const list = [{ id: 2 }, { id: 3 }, { id: 4 }]; - const db = { - getMetricsLastHour () { - return Promise.resolve([{ id: 1 }]); - }, - - getNewMetrics () { - return Promise.resolve([list.pop() || { id: 0 }]); - }, - }; - - return { - db, - }; -} - -test.cb('should call database on startup', (t) => { - const mock = getMockDb(); - const service = new MetricsService(mock.db); - t.plan(2); - - service.on('metrics', ([metric]) => { - t.true(service.highestIdSeen === 1); - t.true(metric.id === 1); - t.end(); - service.destroy(); - }); -}); - -test.cb('should poll for updates', (t) => { - const clock = sinon.useFakeTimers(); - - const mock = getMockDb(); - const service = new MetricsService(mock.db, 100); - - const metrics = []; - service.on('metrics', (_metrics) => { - _metrics.forEach(m => m && metrics.push(m)); - }); - - t.true(metrics.length === 0); - - service.on('ready', () => { - t.true(metrics.length === 1); - - clock.tick(300); - clock.restore(); - - process.nextTick(() => { - t.true(metrics.length === 4); - t.true(metrics[0].id === 1); - t.true(metrics[1].id === 4); - t.true(metrics[2].id === 3); - t.true(metrics[3].id === 2); - service.destroy(); - t.end(); - }); - }); -}); diff --git a/lib/client-metrics/ttl-list.test.js b/lib/client-metrics/ttl-list.test.js index 41a41729f0..fec8e922e2 100644 --- a/lib/client-metrics/ttl-list.test.js +++ b/lib/client-metrics/ttl-list.test.js @@ -27,10 +27,7 @@ test.cb('should slice off list', (t) => { expireType: 'milliseconds', }); - // console.time('4'); - // console.time('3'); - // console.time('2'); - // console.time('1'); + list.add({ n: '1' }, moment().add(1, 'milliseconds')); list.add({ n: '2' }, moment().add(50, 'milliseconds')); list.add({ n: '3' }, moment().add(200, 'milliseconds')); diff --git a/lib/db/client-instance-store.js b/lib/db/client-instance-store.js index 4a0db876ba..fd33b5b659 100644 --- a/lib/db/client-instance-store.js +++ b/lib/db/client-instance-store.js @@ -1,6 +1,7 @@ /* eslint camelcase: "off" */ 'use strict'; +const logger = require('../logger'); const COLUMNS = ['app_name', 'instance_id', 'client_ip', 'last_seen', 'created_at']; const TABLE = 'client_instances'; @@ -12,10 +13,24 @@ const mapRow = (row) => ({ createdAt: row.created_at, }); +const mapAppsRow = (row) => ({ + appName: row.app_name, + createdAt: row.created_at, +}); + class ClientInstanceStore { constructor (db) { this.db = db; + setTimeout(() => this._removeInstancesOlderThanTwoDays(), 10).unref(); + setInterval(() => this._removeInstancesOlderThanTwoDays(), 24 * 61 * 60 * 1000).unref(); + } + + _removeInstancesOlderThanTwoDays () { + this.db(TABLE) + .whereRaw('created_at < now() - interval \'2 days\'') + .del() + .then((res) => logger.info(`Deleted ${res} instances`)); } updateRow (details) { @@ -58,6 +73,24 @@ class ClientInstanceStore { .orderBy('last_seen', 'desc') .map(mapRow); } + + getByAppName (appName) { + return this.db + .select(COLUMNS) + .from(TABLE) + .where('app_name', appName) + .orderBy('last_seen', 'desc') + .map(mapRow); + } + + getApplications () { + return this.db + .distinct('app_name') + .select(['app_name']) + .from(TABLE) + .orderBy('app_name', 'desc') + .map(mapRow); + } }; module.exports = ClientInstanceStore; diff --git a/lib/db/client-metrics-db.js b/lib/db/client-metrics-db.js new file mode 100644 index 0000000000..0439cb9884 --- /dev/null +++ b/lib/db/client-metrics-db.js @@ -0,0 +1,58 @@ +'use strict'; + +const logger = require('../logger'); + +const METRICS_COLUMNS = ['id', 'created_at', 'metrics']; +const TABLE = 'client_metrics'; + +const mapRow = (row) => ({ + id: row.id, + createdAt: row.created_at, + metrics: row.metrics, +}); + +class ClientMetricsDb { + constructor (db) { + this.db = db; + + //Clear old metrics regulary + setTimeout(() => this.removeMetricsOlderThanOneHour(), 10).unref(); + setInterval(() => this.removeMetricsOlderThanOneHour(), 60 * 1000).unref(); + } + + removeMetricsOlderThanOneHour () { + this.db(TABLE) + .whereRaw('created_at < now() - interval \'1 hour\'') + .del() + .then((res) => logger.info(`Deleted ${res} metrics`)); + } + + // Insert new client metrics + insert (metrics) { + return this.db(TABLE).insert({ metrics }); + } + + // Used at startup to load all metrics last week into memory! + getMetricsLastHour () { + return this.db + .select(METRICS_COLUMNS) + .from(TABLE) + .limit(2000) + .whereRaw('created_at > now() - interval \'1 hour\'') + .orderBy('created_at', 'asc') + .map(mapRow); + } + + // Used to poll for new metrics + getNewMetrics (lastKnownId) { + return this.db + .select(METRICS_COLUMNS) + .from(TABLE) + .limit(1000) + .where('id', '>', lastKnownId) + .orderBy('created_at', 'asc') + .map(mapRow); + } +}; + +module.exports = ClientMetricsDb; diff --git a/lib/db/client-metrics-store.js b/lib/db/client-metrics-store.js index bafa031ed7..c2b042798b 100644 --- a/lib/db/client-metrics-store.js +++ b/lib/db/client-metrics-store.js @@ -1,55 +1,53 @@ 'use strict'; const logger = require('../logger'); -const METRICS_COLUMNS = ['id', 'created_at', 'metrics']; -const TABLE = 'client_metrics'; -const mapRow = (row) => ({ - id: row.id, - createdAt: row.created_at, - metrics: row.metrics, -}); +const { EventEmitter } = require('events'); -class ClientMetricsStore { +const TEN_SECONDS = 10 * 1000; - constructor (db) { - this.db = db; - setTimeout(() => this._removeMetricsOlderThanOneHour(), 10).unref(); - setInterval(() => this._removeMetricsOlderThanOneHour(), 60 * 60 * 1000).unref(); +class ClientMetricsStore extends EventEmitter { + + constructor (metricsDb, pollInterval = TEN_SECONDS) { + super(); + this.metricsDb = metricsDb; + this.highestIdSeen = 0; + this.timer; + + //Build internal state + metricsDb.getMetricsLastHour() + .then((metrics) => this._emitMetrics(metrics)) + .then(() => this._startPoller(pollInterval)) + .then(() => this.emit('ready')) + .catch((err) => logger.error(err)); } - _removeMetricsOlderThanOneHour () { - this.db(TABLE) - .whereRaw('created_at < now() - interval \'1 hour\'') - .del() - .then((res) => logger.info(`Deleted ${res} metrics`)); + _startPoller (pollInterval) { + this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval); + this.timer.unref(); + } + + _fetchNewAndEmit() { + this.metricsDb.getNewMetrics(this.highestIdSeen) + .then((metrics) => this._emitMetrics(metrics)) + } + + _emitMetrics (metrics) { + if (metrics && metrics.length > 0) { + this.highestIdSeen = metrics[metrics.length - 1].id; + metrics.forEach(m => this.emit('metrics', m.metrics)); + } } // Insert new client metrics insert (metrics) { - return this.db(TABLE).insert({ metrics }); + return this.metricsDb.insert(metrics) } - // Used at startup to load all metrics last week into memory! - getMetricsLastHour () { - return this.db - .select(METRICS_COLUMNS) - .from(TABLE) - .limit(2000) - .whereRaw('created_at > now() - interval \'1 hour\'') - .orderBy('created_at', 'asc') - .map(mapRow); - } - - // Used to poll for new metrics - getNewMetrics (lastKnownId) { - return this.db - .select(METRICS_COLUMNS) - .from(TABLE) - .limit(1000) - .where('id', '>', lastKnownId) - .orderBy('created_at', 'asc') - .map(mapRow); + destroy () { + try { + clearInterval(this.timer); + } catch (e) {} } }; diff --git a/lib/db/client-metrics-store.test.js b/lib/db/client-metrics-store.test.js new file mode 100644 index 0000000000..e4341ffbd0 --- /dev/null +++ b/lib/db/client-metrics-store.test.js @@ -0,0 +1,62 @@ +'use strict'; + +const { test } = require('ava'); +const ClientMetricStore = require('./client-metrics-store'); +const sinon = require('sinon'); + +function getMockDb () { + const list = [{ id: 4, metrics: {appName: 'test'} }, { id: 3, metrics: {appName: 'test'} }, { id: 2, metrics: {appName: 'test'} }]; + return { + getMetricsLastHour () { + return Promise.resolve([{ id: 1, metrics: {appName: 'test'} }]); + }, + + getNewMetrics (v) { + return Promise.resolve([list.pop() || { id: 0 }]); + } + }; +} + + +test.cb('should call database on startup', (t) => { + const mock = getMockDb(); + + const store = new ClientMetricStore(mock); + + t.plan(2); + + + store.on('metrics', (metrics) => { + t.true(store.highestIdSeen === 1); + t.true(metrics.appName === 'test'); + store.destroy(); + + t.end(); + }); +}); + + +test.cb('should poll for updates', (t) => { + const clock = sinon.useFakeTimers(); + + const mock = getMockDb(); + const store = new ClientMetricStore(mock, 100); + + const metrics = []; + store.on('metrics', (m) => metrics.push(m)); + + t.true(metrics.length === 0); + + store.on('ready', () => { + t.true(metrics.length === 1); + clock.tick(300); + process.nextTick(() => { + t.true(metrics.length === 4); + t.true(store.highestIdSeen === 4); + store.destroy(); + clock.restore(); + t.end(); + }); + }); +}); + diff --git a/lib/db/client-strategy-store.js b/lib/db/client-strategy-store.js index 0be6e0683e..0f59648cfe 100644 --- a/lib/db/client-strategy-store.js +++ b/lib/db/client-strategy-store.js @@ -49,6 +49,14 @@ class ClientStrategyStore { .from(TABLE) .map(mapRow); } + + getByAppName (appName) { + return this.db + .select(COLUMNS) + .where('app_name', appName) + .from(TABLE) + .map(mapRow); + } }; module.exports = ClientStrategyStore; diff --git a/lib/db/index.js b/lib/db/index.js index 19af24c75b..738fc9acea 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -5,12 +5,14 @@ const EventStore = require('./event-store'); const FeatureToggleStore = require('./feature-toggle-store'); const StrategyStore = require('./strategy-store'); const ClientInstanceStore = require('./client-instance-store'); +const ClientMetricsDb = require('./client-metrics-db'); const ClientMetricsStore = require('./client-metrics-store'); const ClientStrategyStore = require('./client-strategy-store'); module.exports.createStores = (config) => { const db = createDb(config); const eventStore = new EventStore(db); + const clientMetricsDb = new ClientMetricsDb(db); return { db, @@ -18,7 +20,7 @@ module.exports.createStores = (config) => { featureToggleStore: new FeatureToggleStore(db, eventStore), strategyStore: new StrategyStore(db, eventStore), clientInstanceStore: new ClientInstanceStore(db), - clientMetricsStore: new ClientMetricsStore(db), + clientMetricsStore: new ClientMetricsStore(clientMetricsDb), clientStrategyStore: new ClientStrategyStore(db), }; }; diff --git a/lib/routes/metrics.js b/lib/routes/metrics.js index 6212eafea8..bd11ee028d 100644 --- a/lib/routes/metrics.js +++ b/lib/routes/metrics.js @@ -2,10 +2,16 @@ const logger = require('../logger'); const ClientMetrics = require('../client-metrics'); -const ClientMetricsService = require('../client-metrics/service'); const joi = require('joi'); const { clientMetricsSchema, clientRegisterSchema } = require('./metrics-schema'); +/* +* TODO: +* - always catch errors and always return a response to client! +* - clean up and document uri endpoint +* - always json response (middleware?) +* - fix failing tests +*/ module.exports = function (app, config) { const { clientMetricsStore, @@ -13,17 +19,17 @@ module.exports = function (app, config) { clientInstanceStore, } = config.stores; - const metrics = new ClientMetrics(); - const service = new ClientMetricsService(clientMetricsStore); - - service.on('metrics', (entries) => { - entries.forEach((m) => { - metrics.addPayload(m.metrics); - }); - }); - - app.get('/metrics', (req, res) => { - res.json(metrics.getMetricsOverview()); + const metrics = new ClientMetrics(clientMetricsStore); + + /** + * [{ + "appName": "mfinn", + "toggles": ["toggle1", "toggle2"], + }] + */ + app.get('/client/seen-toggles', (req, res) => { + const seenAppToggles = metrics.getAppsWitToggles(); + res.json(seenAppToggles); }); app.get('/metrics/features', (req, res) => { @@ -32,11 +38,20 @@ module.exports = function (app, config) { app.post('/client/metrics', (req, res) => { const data = req.body; + const clientIp = req.ip; + joi.validate(data, clientMetricsSchema, (err, cleaned) => { if (err) { return res.status(400).json(err); } - service.insert(cleaned) + + clientMetricsStore + .insert(cleaned) + .then(() => clientInstanceStore.insert({ + appName: cleaned.appName, + instanceId: cleaned.instanceId, + clientIp, + })) .catch(e => logger.error('Error inserting metrics data', e)); res.status(202).end(); @@ -52,7 +67,8 @@ module.exports = function (app, config) { return res.status(400).json(err); } - clientStrategyStore.insert(cleaned.appName, cleaned.strategies) + clientStrategyStore + .insert(cleaned.appName, cleaned.strategies) .then(() => clientInstanceStore.insert({ appName: cleaned.appName, instanceId: cleaned.instanceId, @@ -66,12 +82,40 @@ module.exports = function (app, config) { }); app.get('/client/strategies', (req, res) => { - clientStrategyStore.getAll().then(data => res.json(data)); + const appName = req.query.appName; + if(appName) { + clientStrategyStore.getByAppName(appName) + .then(data => res.json(data)) + .catch(err => logger.error(err)); + } else { + clientStrategyStore.getAll() + .then(data => res.json(data)) + .catch(err => logger.error(err)); + } }); - app.get('/client/instances', (req, res) => { - clientInstanceStore.getAll() - .then(data => res.json(data)) + app.get('/client/applications/', (req, res) => { + clientInstanceStore.getApplications() + .then(apps => { + const applications = apps.map(({appName}) => ({ + appName: appName, + links: { + appDetails: `/api/client/applications/${appName}` + } + })) + res.json({applications}) + }) + .catch(err => logger.error(err)); + }); + + app.get('/client/applications/:appName', (req, res) => { + const appName = req.params.appName; + const seenToggles = metrics.getSeenTogglesByAppName(appName); + Promise.all([ + clientInstanceStore.getByAppName(appName), + clientStrategyStore.getByAppName(appName) + ]) + .then(([instances, strategies]) => res.json({appName, instances, strategies, seenToggles})) .catch(err => logger.error(err)); }); }; diff --git a/test/e2e/helpers/test-helper.js b/test/e2e/helpers/test-helper.js index 33c5d88d1f..8edd612292 100644 --- a/test/e2e/helpers/test-helper.js +++ b/test/e2e/helpers/test-helper.js @@ -78,6 +78,13 @@ function createClientInstance (stores) { started: Date.now(), interval: 10, }, + { + appName: 'demo-seed-2', + instanceId: 'test-2', + strategies: ['default'], + started: Date.now(), + interval: 10, + }, ].map(client => stores.clientInstanceStore.insert(client)); } diff --git a/test/e2e/metrics-api.test.js b/test/e2e/metrics-api.test.js index a4c91410d5..c407c3604f 100644 --- a/test/e2e/metrics-api.test.js +++ b/test/e2e/metrics-api.test.js @@ -51,14 +51,27 @@ test.serial('should get client strategies', async t => { .then(destroy); }); -test.serial('should get client instances', async t => { +test.serial('should get application details', async t => { const { request, destroy } = await setupApp('metrics_serial'); return request - .get('/api/client/instances') + .get('/api/client/applications/demo-seed') .expect('Content-Type', /json/) .expect((res) => { t.true(res.status === 200); - t.true(res.body.length === 1); + t.true(res.body.appName === 'demo-seed'); + t.true(res.body.instances.length === 1); + }) + .then(destroy); +}); + +test.serial('should get list of applications', async t => { + const { request, destroy } = await setupApp('metrics_serial'); + return request + .get('/api/client/applications') + .expect('Content-Type', /json/) + .expect((res) => { + t.true(res.status === 200); + t.true(res.body.applications.length === 2); }) .then(destroy); }); diff --git a/test/unit/routes/fixtures/fake-metrics-store.js b/test/unit/routes/fixtures/fake-metrics-store.js index 7a986d86a5..2f2e35fb50 100644 --- a/test/unit/routes/fixtures/fake-metrics-store.js +++ b/test/unit/routes/fixtures/fake-metrics-store.js @@ -3,4 +3,5 @@ module.exports = () => ({ getMetricsLastHour: () => Promise.resolve([]), insert: () => Promise.resolve(), + on: () => {} });