From b4ac2d1d7118ec01b347b94d05228802ada8d74d Mon Sep 17 00:00:00 2001 From: Mariano Ruiz Date: Tue, 1 Jun 2021 11:07:25 -0300 Subject: [PATCH] 6915 - Telemetry daily frequency (#7095) * Telemetry frequency changed from monthly to daily. * The id of a telemetry record now looks like `telemetry-----`. Also in the metadata section a new field `day` is added with the day of the month the data belongs to. * Bug fix: when `record()` was called for the first time in the period (a period was a month, now is a day), the record was added to the telemetry DB and then the aggregation executed with the data that was stored previously, so the aggregation process use to aggregate all the records from the previous period + a record that was actually from the current period. Now the last record is not taken into account in the aggregation submitted, instead is stored to be used in the following aggregation. * Add more telemetry tests. Refactor. * Improvements in the script to collect meta data from a terminal: - Output errors in the standard error stream to see the errors in the console while piping right results to a JSON file. - Add Unix exec permission and header to execute the script without the `node ` prefix. --- scripts/get_users_meta_docs.js | 10 +- webapp/src/ts/services/telemetry.service.ts | 44 +- .../ts/services/telemetry.service.spec.ts | 389 +++++++++++------- 3 files changed, 278 insertions(+), 165 deletions(-) mode change 100644 => 100755 scripts/get_users_meta_docs.js diff --git a/scripts/get_users_meta_docs.js b/scripts/get_users_meta_docs.js old mode 100644 new mode 100755 index fe7b906cfde..82f01fe4e42 --- a/scripts/get_users_meta_docs.js +++ b/scripts/get_users_meta_docs.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + const inquirer = require('inquirer'); const PouchDB = require('pouchdb-core'); const fs = require('fs'); @@ -86,7 +88,7 @@ const actionQuestions = [{ } docs.forEach(doc => console.log(JSON.stringify(doc, null, 2) + ',')); } else if (i === 0) { - console.log('\x1b[31m%s\x1b[0m', `There are no documents of type ${type}`); + console.error('\x1b[31m%s\x1b[0m', `There are no documents of type ${type}`); break; } else { console.log('{}]'); @@ -100,7 +102,7 @@ const actionQuestions = [{ let docIndex = 0; if (docs.length === 0) { - console.log('\x1b[31m%s\x1b[0m', `There are no documents of type ${type}`); + console.error('\x1b[31m%s\x1b[0m', `There are no documents of type ${type}`); } else { console.log(JSON.stringify(docs[docIndex], null, 2)); @@ -125,7 +127,7 @@ const actionQuestions = [{ console.log(JSON.stringify(docs[docIndex], null, 2)); if (printMessage) { - console.log('\x1b[31m%s\x1b[0m', `No next document. This is the last one.`); + console.error('\x1b[31m%s\x1b[0m', `No next document. This is the last one.`); } } else if (response.action === 'save_current') { const filePath = path.join(path.resolve(__dirname), docs[docIndex]._id + '.json'); @@ -154,6 +156,6 @@ const actionQuestions = [{ } } } catch(err) { - console.log(err); + console.error(err); } })(); diff --git a/webapp/src/ts/services/telemetry.service.ts b/webapp/src/ts/services/telemetry.service.ts index 228352801ef..e6f8f3fafbf 100644 --- a/webapp/src/ts/services/telemetry.service.ts +++ b/webapp/src/ts/services/telemetry.service.ts @@ -9,13 +9,13 @@ import { SessionService } from '@mm-services/session.service'; providedIn: 'root' }) /** - * TelemetryService: Records, aggregates, and submits telemetry data + * TelemetryService: Records, aggregates, and submits telemetry data. */ export class TelemetryService { // Intentionally scoped to the whole browser (for this domain). We can then tell if multiple users use the same device private readonly DEVICE_ID_KEY = 'medic-telemetry-device-id'; private DB_ID_KEY; - private LAST_AGGREGATED_DATE_KEY; + private FIRST_AGGREGATED_DATE_KEY; private queue = Promise.resolve(); @@ -27,7 +27,7 @@ export class TelemetryService { // Intentionally scoped to the specific user, as they may perform a // different role (online vs. offline being being the most obvious) with different performance implications this.DB_ID_KEY = ['medic', this.sessionService.userCtx().name, 'telemetry-db'].join('-'); - this.LAST_AGGREGATED_DATE_KEY = ['medic', this.sessionService.userCtx().name, 'telemetry-date'].join('-'); + this.FIRST_AGGREGATED_DATE_KEY = ['medic', this.sessionService.userCtx().name, 'telemetry-date'].join('-'); } private getDb() { @@ -54,15 +54,21 @@ export class TelemetryService { return uniqueDeviceId; } - private getLastAggregatedDate() { - let date = parseInt(window.localStorage.getItem(this.LAST_AGGREGATED_DATE_KEY)); + /** + * Returns a Moment object when the first telemetry record was created. + * + * This date is computed and stored in milliseconds (since Unix epoch) + * when we call this method for the first time and after every aggregation. + */ + private getFirstAggregatedDate() { + let date = parseInt(window.localStorage.getItem(this.FIRST_AGGREGATED_DATE_KEY)); if (!date) { date = Date.now(); - window.localStorage.setItem(this.LAST_AGGREGATED_DATE_KEY, date.toString()); + window.localStorage.setItem(this.FIRST_AGGREGATED_DATE_KEY, date.toString()); } - return date; + return moment(date); } private storeIt(db, key, value) { @@ -73,11 +79,17 @@ export class TelemetryService { }); } + // moment when the aggregation starts (the beginning of the current day) + private aggregateStartsAt() { + return moment().startOf('day'); + } + + // if there is telemetry data from previous days, aggregation is performed and the data destroyed private submitIfNeeded(db) { - const monthStart = moment().startOf('month'); - const dbDate = moment(this.getLastAggregatedDate()); + const startOf = this.aggregateStartsAt(); + const dbDate = this.getFirstAggregatedDate(); - if (dbDate.isBefore(monthStart)) { + if (dbDate.isBefore(startOf)) { return this .aggregate(db) .then(() => this.reset(db)); @@ -97,6 +109,7 @@ export class TelemetryService { 'telemetry', metadata.year, metadata.month, + metadata.day, metadata.user, metadata.deviceId, ].join('-'); @@ -109,7 +122,7 @@ export class TelemetryService { this.dbService.get().query('medic-client/doc_by_type', { key: ['form'], include_docs: true }) ]) .then(([ddoc, formResults]) => { - const date = moment(this.getLastAggregatedDate()); + const date = this.getFirstAggregatedDate(); const version = (ddoc.deploy_info && ddoc.deploy_info.version) || 'unknown'; const forms = formResults.rows.reduce((keyToVersion, row) => { keyToVersion[row.doc.internalId] = row.doc._rev; @@ -120,6 +133,7 @@ export class TelemetryService { return { year: date.year(), month: date.month() + 1, + day: date.date(), user: this.sessionService.userCtx().name, deviceId: this.getUniqueDeviceId(), versions: { @@ -204,7 +218,7 @@ export class TelemetryService { private reset(db) { window.localStorage.removeItem(this.DB_ID_KEY); - window.localStorage.removeItem(this.LAST_AGGREGATED_DATE_KEY); + window.localStorage.removeItem(this.FIRST_AGGREGATED_DATE_KEY); return db.destroy(); } @@ -225,7 +239,7 @@ export class TelemetryService { * metric_b: { sum: -16, min: -4, max: -4, count: 4, sumsqr: 64 } * } * - * See: https://wiki.apache.org/couchdb/Built-In_Reduce_Functions#A_stats + * See: https://docs.couchdb.org/en/stable/ddocs/ddocs.html#_stats * * This single month aggregate document is of type 'telemetry', and is * stored in the user's meta DB (which replicates up to the main server) @@ -255,14 +269,14 @@ export class TelemetryService { let db; this.queue = this.queue .then(() => db = this.getDb()) - .then(() => this.storeIt(db, key, value)) .then(() => this.submitIfNeeded(db)) + .then(() => db = this.getDb()) // db is fetched again in case submitIfNeeded dropped the old reference + .then(() => this.storeIt(db, key, value)) .catch(err => console.error('Error in telemetry service', err)) .finally(() => { if (!db || db._destroyed || db._closed) { return; } - try { db.close(); } catch (err) { diff --git a/webapp/tests/karma/ts/services/telemetry.service.spec.ts b/webapp/tests/karma/ts/services/telemetry.service.spec.ts index 29ee09fb1b7..c6898aff9d3 100644 --- a/webapp/tests/karma/ts/services/telemetry.service.spec.ts +++ b/webapp/tests/karma/ts/services/telemetry.service.spec.ts @@ -8,17 +8,19 @@ import { DbService } from '@mm-services/db.service'; import { SessionService } from '@mm-services/session.service'; describe('TelemetryService', () => { - const NOW = new Date(2018, 10, 10, 12, 33).getTime(); + const NOW = new Date(2018, 10, 10, 12, 33).getTime(); // -> 2018-11-10T12:33:00 let service: TelemetryService; let dbService; - let dbInstance; + let metaDb; let sessionService; let clock; - let pouchDb; + let telemetryDb; let storageGetItemStub; let storageSetItemStub; let consoleErrorSpy; + const windowPouchOriginal = window.PouchDB; + const windowScreenOriginal = { availWidth: window.screen.availWidth, availHeight: window.screen.availHeight @@ -28,18 +30,83 @@ describe('TelemetryService', () => { hardwareConcurrency: window.navigator.hardwareConcurrency }; + function defineWindow() { + Object.defineProperty( + window.navigator, + 'userAgent', + { value: 'Agent Smith', configurable: true } + ); + Object.defineProperty( + window.navigator, + 'hardwareConcurrency', + { value: 4, configurable: true } + ); + Object.defineProperty( + window.screen, + 'availWidth', + { value: 768, configurable: true } + ); + Object.defineProperty( + window.screen, + 'availHeight', + { value: 1024, configurable: true } + ); + } + + function restoreWindow() { + Object.defineProperty( + window.navigator, + 'userAgent', + { value: windowNavigatorOriginal.userAgent, configurable: true } + ); + Object.defineProperty( + window.navigator, + 'hardwareConcurrency', + { value: windowNavigatorOriginal.hardwareConcurrency, configurable: true } + ); + Object.defineProperty( + window.screen, + 'availWidth', + { value: windowScreenOriginal.availWidth, configurable: true } + ); + Object.defineProperty( + window.screen, + 'availHeight', + { value: windowScreenOriginal.availHeight, configurable: true } + ); + } + + function subtractDays(numDays) { + return moment() + .subtract(numDays, 'days') + .valueOf() + .toString(); + } + + function sameDay() { + return moment() + .valueOf() + .toString(); + } + beforeEach(() => { - dbInstance = { + defineWindow(); + metaDb = { info: sinon.stub(), put: sinon.stub(), get: sinon.stub(), query: sinon.stub() }; - dbService = { get: () => dbInstance }; + dbService = { get: () => metaDb }; consoleErrorSpy = sinon.spy(console, 'error'); - pouchDb = { + telemetryDb = { post: sinon.stub().resolves(), - close: sinon.stub() + close: sinon.stub(), + query: sinon.stub(), + destroy: sinon.stub().callsFake(() => { + telemetryDb._destroyed = true; + return Promise.resolve(); + }) }; sessionService = { userCtx: sinon.stub().returns({ name: 'greg' }) }; storageGetItemStub = sinon.stub(window.localStorage, 'getItem'); @@ -54,128 +121,62 @@ describe('TelemetryService', () => { service = TestBed.inject(TelemetryService); clock = sinon.useFakeTimers(NOW); - window.PouchDB = () => pouchDb; + window.PouchDB = () => telemetryDb; }); afterEach(() => { clock.restore(); sinon.restore(); window.PouchDB = windowPouchOriginal; - Object.defineProperty( - window.navigator, - 'userAgent', - { value: windowNavigatorOriginal.userAgent, configurable: true } - ); - Object.defineProperty( - window.navigator, - 'hardwareConcurrency', - { value: windowNavigatorOriginal.hardwareConcurrency, configurable: true } - ); - Object.defineProperty( - window.screen, - 'availWidth', - { value: windowScreenOriginal.availWidth, configurable: true } - ); - Object.defineProperty( - window.screen, - 'availHeight', - { value: windowScreenOriginal.availHeight, configurable: true } - ); + restoreWindow(); }); describe('record()', () => { it('should record a piece of telemetry', async () => { - pouchDb.post = sinon.stub().resolves(); - pouchDb.close = sinon.stub(); - storageGetItemStub - .withArgs('medic-greg-telemetry-db') - .returns('dbname'); - storageGetItemStub - .withArgs('medic-greg-telemetry-date') - .returns(Date.now().toString()); + storageGetItemStub.withArgs('medic-greg-telemetry-db').returns('dbname'); + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(Date.now().toString()); await service.record('test', 100); expect(consoleErrorSpy.callCount).to.equal(0); - expect(pouchDb.post.callCount).to.equal(1); - expect(pouchDb.post.args[0][0]).to.deep.include({ key: 'test', value: 100 }); - expect(pouchDb.post.args[0][0].date_recorded).to.be.above(0); - expect(storageGetItemStub.callCount).to.equal(2); - expect(pouchDb.close.callCount).to.equal(1); + expect(telemetryDb.post.callCount).to.equal(1); + expect(telemetryDb.post.args[0][0]).to.deep.include({ key: 'test', value: 100 }); + expect(telemetryDb.post.args[0][0].date_recorded).to.be.above(0); + expect(storageGetItemStub.callCount).to.equal(3); + expect(storageGetItemStub.args[0]).to.deep.equal(['medic-greg-telemetry-db']); + expect(storageGetItemStub.args[1]).to.deep.equal(['medic-greg-telemetry-date']); + expect(storageGetItemStub.args[2]).to.deep.equal(['medic-greg-telemetry-db']); + expect(telemetryDb.close.callCount).to.equal(1); }); it('should default the value to 1 if not passed', async () => { - pouchDb.post = sinon.stub().resolves(); - pouchDb.close = sinon.stub(); - storageGetItemStub - .withArgs('medic-greg-telemetry-db') - .returns('dbname'); - storageGetItemStub - .withArgs('medic-greg-telemetry-date') - .returns(Date.now().toString()); + storageGetItemStub.withArgs('medic-greg-telemetry-db').returns('dbname'); + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(Date.now().toString()); await service.record('test'); expect(consoleErrorSpy.callCount).to.equal(0); - expect(pouchDb.post.args[0][0].value).to.equal(1); - expect(pouchDb.close.callCount).to.equal(1); - }); - - it('should set localStorage values', async () => { - pouchDb.post = sinon.stub().resolves(); - pouchDb.close = sinon.stub(); - storageGetItemStub - .withArgs('medic-greg-telemetry-db') - .returns(undefined); - storageGetItemStub - .withArgs('medic-greg-telemetry-date') - .returns(undefined); - - await service.record('test', 1); - - expect(consoleErrorSpy.callCount).to.equal(0); - expect(storageSetItemStub.callCount).to.equal(2); - expect(storageSetItemStub.args[0][0]).to.equal('medic-greg-telemetry-db'); - expect(storageSetItemStub.args[0][1]).to.match(/medic-user-greg-telemetry-/); // ends with a UUID - expect(storageSetItemStub.args[1][0]).to.equal('medic-greg-telemetry-date'); - expect(storageSetItemStub.args[1][1]).to.equal(NOW.toString()); + expect(telemetryDb.post.args[0][0].value).to.equal(1); + expect(telemetryDb.close.callCount).to.equal(1); }); - it('should aggregate once a month and resets the db', async () => { - storageGetItemStub - .withArgs('medic-greg-telemetry-db') - .returns('dbname'); - storageGetItemStub - .withArgs('medic-greg-telemetry-date') - .returns( - moment() - .subtract(5, 'weeks') - .valueOf() - .toString() - ); - - pouchDb.post = sinon.stub().resolves(); - pouchDb.query = sinon.stub().resolves({ + function setupDbMocks() { + storageGetItemStub.returns('dbname'); + telemetryDb.query.resolves({ rows: [ - { key: 'foo', value: 'stats' }, - { key: 'bar', value: 'more stats' }, + { key: 'foo', value: {sum:2876, min:581, max:2295, count:2, sumsqr:5604586} }, + { key: 'bar', value: {sum:93, min:43, max:50, count:2, sumsqr:4349} }, ], }); - pouchDb.destroy = sinon.stub().callsFake(() => { - pouchDb._destroyed = true; - return Promise.resolve(); - }); - pouchDb.close = sinon.stub(); - - dbInstance.info.resolves({ some: 'stats' }); - dbInstance.put.resolves(); - dbInstance.get + metaDb.info.resolves({ some: 'stats' }); + metaDb.put.resolves(); + metaDb.get .withArgs('_design/medic-client') .resolves({ _id: '_design/medic-client', deploy_info: { version: '3.0.0' } }); - dbInstance.query.resolves({ + metaDb.query.resolves({ rows: [ { id: 'form:anc_followup', @@ -188,28 +189,28 @@ describe('TelemetryService', () => { } ] }); + } - Object.defineProperty(window.navigator, 'userAgent', { value: 'Agent Smith', configurable: true }); - Object.defineProperty(window.navigator, 'hardwareConcurrency', { value: 4, configurable: true }); - Object.defineProperty(window.screen, 'availWidth', { value: 768, configurable: true }); - Object.defineProperty(window.screen, 'availHeight', { value: 1024, configurable: true }); + it('should aggregate once a day and resets the db first', async () => { + setupDbMocks(); + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(subtractDays(5)); await service.record('test', 1); - expect(consoleErrorSpy.callCount).to.equal(0); - expect(pouchDb.post.callCount).to.equal(1); - expect(pouchDb.post.args[0][0]).to.deep.include({ key: 'test', value: 1 }); - expect(dbInstance.put.callCount).to.equal(1); + expect(telemetryDb.post.callCount).to.equal(1); + expect(telemetryDb.post.args[0][0]).to.deep.include({ key: 'test', value: 1 }); - const aggregatedDoc = dbInstance.put.args[0][0]; - expect(aggregatedDoc._id).to.match(/telemetry-2018-10-greg/); + expect(metaDb.put.callCount).to.equal(1); + const aggregatedDoc = metaDb.put.args[0][0]; + expect(aggregatedDoc._id).to.match(/^telemetry-2018-11-5-greg-[\w-]+$/); expect(aggregatedDoc.metrics).to.deep.equal({ - foo: 'stats', - bar: 'more stats', + foo: {sum:2876, min:581, max:2295, count:2, sumsqr:5604586}, + bar: {sum:93, min:43, max:50, count:2, sumsqr:4349}, }); expect(aggregatedDoc.type).to.equal('telemetry'); expect(aggregatedDoc.metadata.year).to.equal(2018); - expect(aggregatedDoc.metadata.month).to.equal(10); + expect(aggregatedDoc.metadata.month).to.equal(11); + expect(aggregatedDoc.metadata.day).to.equal(5); expect(aggregatedDoc.metadata.user).to.equal('greg'); expect(aggregatedDoc.metadata.versions).to.deep.equal({ app: '3.0.0', @@ -227,24 +228,131 @@ describe('TelemetryService', () => { }, deviceInfo: {} }); - expect(dbInstance.query.callCount).to.equal(1); - expect(dbInstance.query.args[0][0]).to.equal('medic-client/doc_by_type'); - expect(dbInstance.query.args[0][1]).to.deep.equal({ key: ['form'], include_docs: true }); - expect(pouchDb.destroy.callCount).to.equal(1); - expect(pouchDb.close.callCount).to.equal(0); + + expect(metaDb.query.callCount).to.equal(1); + expect(metaDb.query.args[0][0]).to.equal('medic-client/doc_by_type'); + expect(metaDb.query.args[0][1]).to.deep.equal({ key: ['form'], include_docs: true }); + expect(telemetryDb.destroy.callCount).to.equal(1); + expect(telemetryDb.close.callCount).to.equal(0); + + expect(consoleErrorSpy.callCount).to.equal(0); // no errors + }); + + it('should not aggregate when recording the day the db was created and next day it should aggregate', async () => { + setupDbMocks(); + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(sameDay()); + + await service.record('test', 10); + + expect(telemetryDb.post.callCount).to.equal(1); // Telemetry entry has been recorded + expect(telemetryDb.post.args[0][0]).to.deep.include({ key: 'test', value: 10 }); + expect(telemetryDb.query.called).to.be.false; // NO telemetry aggregation has + expect(metaDb.put.callCount).to.equal(0); // been recorded yet + + clock = sinon.useFakeTimers(moment(NOW).add(1, 'minutes').valueOf()); // 1 min later ... + await service.record('test', 5); + + expect(telemetryDb.post.callCount).to.equal(2); // second call + expect(telemetryDb.post.args[1][0]).to.deep.include({ key: 'test', value: 5 }); + expect(telemetryDb.query.called).to.be.false; // still NO aggregation has + expect(metaDb.put.callCount).to.equal(0); // been recorded (same day) + + let postCalledAfterQuery = false; + telemetryDb.post.callsFake(async () => postCalledAfterQuery = telemetryDb.query.called); + clock = sinon.useFakeTimers(moment(NOW).add(1, 'days').valueOf()); // 1 day later ... + await service.record('test', 2); + + expect(telemetryDb.post.callCount).to.equal(3); // third call + expect(telemetryDb.post.args[2][0]).to.deep.include({ key: 'test', value: 2 }); + expect(telemetryDb.query.callCount).to.equal(1); // Now aggregation HAS been performed + expect(metaDb.put.callCount).to.equal(1); // and the stats recorded + + // The telemetry record has been recorded after aggregation to not being included in the stats, + // because the record belong to the current date, not the day aggregated (yesterday) + expect(postCalledAfterQuery).to.be.true; + + const aggregatedDoc = metaDb.put.args[0][0]; + expect(aggregatedDoc._id).to.match(/^telemetry-2018-11-10-greg-[\w-]+$/); // Now is 2018-11-11 but aggregation + expect(telemetryDb.destroy.callCount).to.equal(1); // is from the previous day + + expect(consoleErrorSpy.callCount).to.equal(0); // no errors + }); + + it('should aggregate from days with records skipping days without records', async () => { + setupDbMocks(); + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(sameDay()); + + await service.record('datapoint', 12); + + expect(telemetryDb.post.callCount).to.equal(1); + expect(metaDb.put.callCount).to.equal(0); // NO telemetry has been recorded yet + + clock = sinon.useFakeTimers(moment(NOW).add(1, 'minutes').valueOf()); // 1 min later ... + await service.record('another.datapoint'); + + expect(telemetryDb.post.callCount).to.equal(2); // second call + expect(metaDb.put.callCount).to.equal(0); // still NO telemetry has been recorded (same day) + + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(sameDay()); + clock = sinon.useFakeTimers(moment(NOW).add(2, 'days').valueOf()); // 2 days later ... + await service.record('test', 2); + + expect(telemetryDb.post.callCount).to.equal(3); // third call + expect(metaDb.put.callCount).to.equal(1); // Now telemetry IS recorded + + let aggregatedDoc = metaDb.put.args[0][0]; + expect(aggregatedDoc._id).to.match(/^telemetry-2018-11-10-greg-[\w-]+$/); // Today 2018-11-12 but aggregation is + expect(telemetryDb.destroy.callCount).to.equal(1); // from from 2 days ago (not Yesterday) + + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(sameDay()); // same day now is 2 days ahead + + clock = sinon.useFakeTimers(moment(NOW).add(7, 'days').valueOf()); // 7 days later ... + await service.record('point.a', 1); + + expect(telemetryDb.post.callCount).to.equal(4); // 4th call + expect(metaDb.put.callCount).to.equal(2); // Telemetry IS recorded again + aggregatedDoc = metaDb.put.args[1][0]; + expect(aggregatedDoc._id).to.match(/^telemetry-2018-11-12-greg-[\w-]+$/); // Now is Nov 19 but agg. is from Nov 12 + + // A new record is added ... + clock = sinon.useFakeTimers(moment(NOW).add(2, 'hours').valueOf()); // ... 2 hours later ... + await service.record('point.b', 0); // 1 record added + // ...the aggregation count is the same because + // the aggregation was already performed 2 hours ago within the same day + expect(telemetryDb.post.callCount).to.equal(5); // 5th call + expect(metaDb.put.callCount).to.equal(2); // Telemetry count is the same + + expect(consoleErrorSpy.callCount).to.equal(0); // no errors }); + }); + + describe('getDb()', () => { + it('should set localStorage values', async () => { + storageGetItemStub + .withArgs('medic-greg-telemetry-db') + .returns(undefined); + storageGetItemStub + .withArgs('medic-greg-telemetry-date') + .returns(undefined); + + await service.record('test', 1); + + expect(consoleErrorSpy.callCount).to.equal(0); + expect(storageSetItemStub.callCount).to.equal(3); + expect(storageSetItemStub.args[0][0]).to.equal('medic-greg-telemetry-db'); + expect(storageSetItemStub.args[0][1]).to.match(/^medic-user-greg-telemetry-[\w-]+$/); + expect(storageSetItemStub.args[1][0]).to.equal('medic-greg-telemetry-date'); + expect(storageSetItemStub.args[1][1]).to.equal(NOW.toString()); + }); + }); + + describe('storeConflictedAggregate()', () => { it('should deal with conflicts by making the ID unique and noting the conflict in the new document', async () => { storageGetItemStub.withArgs('medic-greg-telemetry-db').returns('dbname'); - storageGetItemStub.withArgs('medic-greg-telemetry-date').returns( - moment() - .subtract(5, 'weeks') - .valueOf() - .toString() - ); - - pouchDb.post = sinon.stub().resolves(); - pouchDb.query = sinon.stub().resolves({ + storageGetItemStub.withArgs('medic-greg-telemetry-date').returns(subtractDays(5)); + + telemetryDb.query = sinon.stub().resolves({ rows: [ { key: 'foo', @@ -256,36 +364,25 @@ describe('TelemetryService', () => { }, ], }); - dbInstance.info.resolves({ some: 'stats' }); - dbInstance.put.onFirstCall().rejects({ status: 409 }); - dbInstance.put.onSecondCall().resolves(); - dbInstance.get.withArgs('_design/medic-client').resolves({ + metaDb.info.resolves({ some: 'stats' }); + metaDb.put.onFirstCall().rejects({ status: 409 }); + metaDb.put.onSecondCall().resolves(); + metaDb.get.withArgs('_design/medic-client').resolves({ _id: '_design/medic-client', deploy_info: { version: '3.0.0' } }); - dbInstance.query.resolves({ rows: [] }); - pouchDb.destroy = sinon.stub().callsFake(() => { - pouchDb._destroyed = true; - return Promise.resolve(); - }); - pouchDb.close = sinon.stub(); - - Object.defineProperty(window.navigator, 'userAgent', { value: 'Agent Smith', configurable: true }); - Object.defineProperty(window.navigator, 'hardwareConcurrency', { value: 4, configurable: true }); - Object.defineProperty(window.screen, 'availWidth', { value: 768, configurable: true }); - Object.defineProperty(window.screen, 'availHeight', { value: 1024, configurable: true }); + metaDb.query.resolves({ rows: [] }); await service.record('test', 1); expect(consoleErrorSpy.callCount).to.equal(0); - expect(dbInstance.put.callCount).to.equal(2); - expect(dbInstance.put.args[1][0]._id).to.match(/conflicted/); - expect(dbInstance.put.args[1][0].metadata.conflicted).to.equal(true); - expect(pouchDb.destroy.callCount).to.equal(1); - expect(pouchDb.close.callCount).to.equal(0); + expect(metaDb.put.callCount).to.equal(2); + expect(metaDb.put.args[1][0]._id).to.match(/^telemetry-2018-11-5-greg-[\w-]+-conflicted-[\w-]+$/); + expect(metaDb.put.args[1][0].metadata.conflicted).to.equal(true); + expect(telemetryDb.destroy.callCount).to.equal(1); + expect(telemetryDb.close.callCount).to.equal(0); }); }); - });