Skip to content

Commit 9643709

Browse files
authored
fix(medic#8778): skip over invalid telemetry values
1 parent 59c5fee commit 9643709

File tree

4 files changed

+101
-20
lines changed

4 files changed

+101
-20
lines changed

webapp/src/ts/modules/reports/reports-add.component.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,21 @@ export class ReportsAddComponent implements OnInit, OnDestroy, AfterViewInit {
204204
const form = await this.formService.render(formContext);
205205
this.form = form;
206206
this.globalActions.setLoadingContent(false);
207-
if (!model.doc || !model.doc._id) {
208-
return;
207+
if (model?.doc?._id) {
208+
await this.ngZone.runOutsideAngular(() => this.renderAttachmentPreviews(model));
209209
}
210-
211-
await this.ngZone.runOutsideAngular(() => this.renderAttachmentPreviews(model));
212-
213-
this.telemetryData.postRender = Date.now();
214-
this.telemetryData.action = model.doc ? 'edit' : 'add';
215-
this.telemetryData.form = model.formInternalId;
216-
this.telemetryService.record(
217-
`enketo:reports:${this.telemetryData.form}:${this.telemetryData.action}:render`,
218-
this.telemetryData.postRender - this.telemetryData.preRender
219-
);
220210
} catch (err) {
221211
this.setError(err);
222212
console.error('Error loading form.', err);
223213
}
214+
215+
this.telemetryData.postRender = Date.now();
216+
this.telemetryData.action = model.doc ? 'edit' : 'add';
217+
this.telemetryData.form = model.formInternalId;
218+
this.telemetryService.record(
219+
`enketo:reports:${this.telemetryData.form}:${this.telemetryData.action}:render`,
220+
this.telemetryData.postRender - this.telemetryData.preRender
221+
);
224222
}
225223

226224
private setError(err) {

webapp/src/ts/services/telemetry.service.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,35 @@ export class TelemetryService {
145145
};
146146
}
147147

148+
/**
149+
* Emit the value of the doc.
150+
* Skip over values that aren't numeric because they will cause an Error in the "_stats" reduce function.
151+
* Exposed for unit testing.
152+
* @param doc The db doc to map.
153+
* @param emit A function called with the key and value to map the doc to.
154+
*/
155+
_aggregateMap(doc, emit) {
156+
const val = doc.value;
157+
if (typeof val === 'number' && !Number.isNaN(val)) {
158+
emit(doc.key, val);
159+
}
160+
}
161+
162+
private aggregateMapReduce(db) {
163+
return db.query(
164+
{
165+
map: this._aggregateMap,
166+
reduce: '_stats',
167+
},
168+
{ group: true }
169+
);
170+
}
171+
148172
private async aggregate(db, dbName) {
149173
const [ metadata, dbInfo, reduceResult ] = await Promise.all([
150174
this.generateMetadataSection(dbName),
151-
this.dbService
152-
.get()
153-
.info(),
154-
db.query(
155-
{ reduce: '_stats', map: (doc, emit) => emit(doc.key, doc.value) },
156-
{ group: true },
157-
)
175+
this.dbService.get().info(),
176+
this.aggregateMapReduce(db),
158177
]);
159178

160179
const aggregateDoc = {
@@ -287,6 +306,10 @@ export class TelemetryService {
287306
if (value === undefined) {
288307
value = 1;
289308
}
309+
if (typeof value !== 'number' || Number.isNaN(value)) {
310+
console.error(new Error(`Invalid telemetry value "${value}" for key "${key}"`));
311+
return;
312+
}
290313

291314
try {
292315
const today = this.getToday();

webapp/tests/karma/ts/modules/reports/reports-add.component.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('Reports Add Component', () => {
3434
let geoHandle;
3535
let formService;
3636
let router;
37+
let telemetryService;
3738
let route;
3839

3940
beforeEach(waitForAsync(() => {
@@ -54,6 +55,7 @@ describe('Reports Add Component', () => {
5455
params: new Subject(),
5556
};
5657
router = { navigate: sinon.stub() };
58+
telemetryService = { record: sinon.stub() };
5759

5860
const mockedSelectors = [
5961
{ selector: Selectors.getLoadingContent, value: false },
@@ -85,7 +87,7 @@ describe('Reports Add Component', () => {
8587
{ provide: FormService, useValue: formService },
8688
{ provide: ActivatedRoute, useValue: route },
8789
{ provide: Router, useValue: router },
88-
{ provide: TelemetryService, useValue: { record: sinon.stub() }},
90+
{ provide: TelemetryService, useValue: telemetryService },
8991
],
9092
})
9193
.compileComponents()
@@ -201,6 +203,9 @@ describe('Reports Add Component', () => {
201203
component.ngAfterViewInit();
202204
tick();
203205

206+
expect(telemetryService.record.callCount).to.equal(1);
207+
expect(telemetryService.record.args[0][0]).to.equal('enketo:reports:my_form:add:render');
208+
expect(telemetryService.record.args[0][1]).to.not.be.undefined;
204209
expect(geolocationService.init.calledOnce).to.be.true;
205210
expect(openReportContentStub.calledOnce).to.be.true;
206211
expect(openReportContentStub.args[0]).to.deep.equal([{ formInternalId: 'my_form' }]);

webapp/tests/karma/ts/services/telemetry.service.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ describe('TelemetryService', () => {
167167
expect(telemetryDb.destroy.calledOnce).to.be.true;
168168
});
169169

170+
it('should log an error and abort if value is non-numeric', async () => {
171+
await service.record('test', 'bad-value');
172+
expect(consoleErrorSpy.called).to.be.true;
173+
expect(telemetryDb.post.called).to.be.false;
174+
});
175+
170176
const setupDbMocks = () => {
171177
telemetryDb.query.resolves({
172178
rows: [
@@ -423,4 +429,53 @@ describe('TelemetryService', () => {
423429
expect(telemetryDb.close.notCalled).to.be.true;
424430
});
425431
});
432+
433+
describe('aggregateMap', () => {
434+
435+
describe('should emit numerical value: ', () => {
436+
437+
for (const val of [
438+
45,
439+
4.5,
440+
-20,
441+
0
442+
]) {
443+
it(JSON.stringify(val), () => {
444+
const doc = {
445+
key: 'mykey',
446+
value: val
447+
};
448+
const emit = sinon.stub();
449+
service._aggregateMap(doc, emit);
450+
expect(emit.callCount).to.equal(1);
451+
expect(emit.args[0][0]).to.equal('mykey');
452+
expect(emit.args[0][1]).to.equal(val);
453+
});
454+
}
455+
456+
});
457+
458+
describe('should NOT emit non-numerical value: ', () => {
459+
460+
for (const val of [
461+
'astring',
462+
undefined,
463+
null,
464+
{ an: 'object' },
465+
Number.NaN
466+
]) {
467+
it('' + val, () => {
468+
const doc = {
469+
key: 'mykey',
470+
value: val
471+
};
472+
const emit = sinon.stub();
473+
service._aggregateMap(doc, emit);
474+
expect(emit.called).to.be.false;
475+
});
476+
}
477+
478+
});
479+
480+
});
426481
});

0 commit comments

Comments
 (0)