Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tidepool-uploader",
"productName": "tidepool-uploader",
"version": "2.67.0-add-model-rollbar.4",
"version": "2.67.0-one-button-bolus.1",
"description": "Tidepool Project Universal Uploader",
"main": "./main.prod.js",
"author": {
Expand Down
10 changes: 8 additions & 2 deletions app/utils/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ export function addInfoToError(err, props) {
if (!_.isEmpty(v) && v !== err.message &&
k !== 'utc' &&
k !== 'code' &&
k !== 'version' &&
k !== 'data'
k !== 'version'
) {
if (k === 'data') {
// the device data is needed for the debug download links on failed
// uploads, but has to stay non-enumerable so it is never serialized
// into the metrics query string or error reports
Object.defineProperty(err, k, { value: v, writable: true, configurable: true });
return;
}
err[k] = v;
if (
k !== 'uuid' &&
Expand Down
25 changes: 18 additions & 7 deletions lib/drivers/medtronic600/NGPHistoryParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1770,7 +1770,20 @@ class NGPHistoryParser {

buildSuspendResumeRecords(events) {
for (const event of this.eventsOfType(NGPHistoryEvent.EVENT_TYPE.INSULIN_DELIVERY_STOPPED)) {
const resumeEvent = this.findResumeForSuspend(event);
let resumeEvent = this.findResumeForSuspend(event);

let duration = resumeEvent == null ? 0 :
(resumeEvent.timestamp.rtc - event.timestamp.rtc) * 1000;
if (duration < 0) {
duration = resumeEvent.timestamp.toDate().valueOf() - event.timestamp.toDate().valueOf();
}
if (duration < 0) {
// We can't reliably pair this resume with the suspend, so treat the
// suspend as unresumed rather than failing the whole upload.
debug('Resume event precedes suspend event; treating suspend at', sundial.formatDeviceTime(event.timestamp.toDate()), 'as unresumed');
resumeEvent = null;
duration = 0;
}

const reason = {
suspended: 'automatic',
Expand All @@ -1797,12 +1810,6 @@ class NGPHistoryParser {
},
};

const duration = resumeEvent == null ? 0 :
(resumeEvent.timestamp.rtc - event.timestamp.rtc) * 1000;
if (duration < 0) {
throw new Error('Suspend event duration cannot be less than zero');
}

const suspendResumeEvent = this.cfg.builder.makeDeviceEventSuspendResume()
.with_reason(reason)
.with_duration(duration)
Expand Down Expand Up @@ -1845,6 +1852,10 @@ class NGPHistoryParser {
const bolus = this.cfg.builder.makeNormalBolus()
.with_normal(event.deliveredAmount);

if (event.bolusSource === NGPUtil.NGPConstants.BOLUS_SOURCE.EASY_BOLUS) {
bolus.with_deliveryContext('oneButton');
}

if (event.programmedAmount !== event.deliveredAmount) {
bolus.with_expectedNormal(event.programmedAmount);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/drivers/tandem
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tidepool-uploader",
"version": "2.67.0-add-model-rollbar.4",
"version": "2.67.0-one-button-bolus.1",
"engines": {
"node": "24.15.0"
},
Expand Down
55 changes: 55 additions & 0 deletions test/app/utils/errors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* == BSD2 LICENSE ==
* Copyright (c) 2026, Tidepool Project
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the associated License, which is identical to the BSD 2-Clause
* License as published by the Open Source Initiative at opensource.org.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the License for more details.
*
* You should have received a copy of the License along with this program; if
* not, you can obtain one from Tidepool Project at tidepool.org.
* == BSD2 LICENSE ==
*/

import { expect } from 'chai';

import { addInfoToError } from '../../../app/utils/errors';

describe('errors', () => {
describe('addInfoToError', () => {
test('should copy device data onto the error for the debug download links', () => {
const err = new Error('Oops!');
const data = { post_records: [], compressed: new Uint8Array([31, 139]) };
addInfoToError(err, { details: 'something broke', data });

expect(err.data).to.equal(data);
});

test('should not expose device data to serialization of the error', () => {
const err = new Error('Oops!');
addInfoToError(err, {
details: 'something broke',
data: { post_records: [{ type: 'bolus' }] },
});

// the metrics middleware serializes the error's enumerable properties
// into a GET query string, which must not include the device data
expect(Object.keys(err)).to.not.contain('data');
expect(JSON.stringify(err)).to.not.contain('bolus');
});

test('should not include device data in the debug details string', () => {
const err = new Error('Oops!');
addInfoToError(err, {
details: 'something broke',
data: { post_records: [{ type: 'bolus' }] },
});

expect(err.debug).to.equal('Details: something broke');
});
});
});
104 changes: 104 additions & 0 deletions test/lib/medtronic600/testNGPHistoryParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,42 @@ describe('NGPHistoryParser.js', () => {
});
});

describe('easy bolus', () => {
test('should set delivery context on a one-button (easy) bolus', () => {
// same fixtures as the wizard bolus above, but with bolus source (byte 0x0B)
// set to 2 (EASY_BOLUS) and no wizard record
const bolusProgrammedData = '150016822dff2e9e029f8e02aa0000014dfc000032c8';
const bolusCompleteData = 'dc001a822dff189e029f8e02aa0000014dfc00014dfc000032c8';
const historyParser = new NGPHistoryParser(
cfg, settings,
[bolusProgrammedData + bolusCompleteData],
);
const events = [];

const expected = {
clockDriftOffset: 0,
conversionOffset: 0,
deliveryContext: 'oneButton',
deviceTime: '2017-02-10T15:54:36',
index: 2184052526,
jsDate: new Date('2017-02-10T15:54:36.000Z'),
normal: 8.55,
payload: {
logIndices: [
2184052526,
],
},
subType: 'normal',
time: '2017-02-10T15:54:36.000Z',
timezoneOffset: 0,
type: 'bolus',
};

historyParser.buildNormalBolusRecords(events);
expect(events[0]).to.deep.equal(expected);
});
});

describe('suspend', () => {
test('should calculate the correct suspend duration', () => {
const suspendData = '1e000c81ee52f6a092886601';
Expand Down Expand Up @@ -133,6 +169,74 @@ describe('NGPHistoryParser.js', () => {
historyParser.buildSuspendResumeRecords(events);
expect(events[0]).to.deep.equal(expected);
});

test('should fall back to clock-corrected timestamps when the RTC resets during a suspend', () => {
// suspend at 2019-03-01T12:00:00, then the pump loses its clock
// (TIME_RESET, so the RTC counter restarts lower), then resume at
// 2019-03-01T12:30:00 wall-clock time
const suspendData = '1e000c82000000a20bdb4001';
const resumeData = '1f000c80000100a40be14802';
const historyParser = new NGPHistoryParser(cfg, settings, [suspendData + resumeData]);
const events = [];

const expected = {
time: '2019-03-01T12:00:00.000Z',
timezoneOffset: 0,
clockDriftOffset: 0,
conversionOffset: 0,
deviceTime: '2019-03-01T12:00:00',
type: 'deviceEvent',
subType: 'status',
status: 'suspended',
reason: { suspended: 'automatic', resumed: 'manual' },
duration: 1800000,
payload: {
suspended: { cause: 'Alarm suspend' },
resumed: { cause: 'User cleared alarm' },
logIndices: [2181038080],
},
index: 2181038080,
jsDate: new Date('2019-03-01T12:00:00.000Z'),
};

historyParser.buildSuspendResumeRecords(events);
expect(events[0]).to.deep.equal(expected);
expect(events[1].duration).to.equal(1800000);
});

test('should treat a suspend as unresumed when the resume precedes it even in clock-corrected time', () => {
// resume wall-clock time (11:50:00) is before the suspend (12:00:00)
// and the RTC delta is also negative, so the pair is unusable
const suspendData = '1e000c82000000a20bdb4001';
const resumeData = '1f000c80000100a40bd7e802';
const historyParser = new NGPHistoryParser(cfg, settings, [suspendData + resumeData]);
const events = [];

const expected = {
time: '2019-03-01T12:00:00.000Z',
timezoneOffset: 0,
clockDriftOffset: 0,
conversionOffset: 0,
deviceTime: '2019-03-01T12:00:00',
type: 'deviceEvent',
subType: 'status',
status: 'suspended',
reason: { suspended: 'automatic', resumed: 'automatic' },
duration: 0,
payload: {
suspended: { cause: 'Alarm suspend' },
resumed: { cause: 'not_resumed' },
logIndices: [2181038080],
},
annotations: [{ code: 'status/incomplete-tuple' }],
index: 2181038080,
jsDate: new Date('2019-03-01T12:00:00.000Z'),
};

historyParser.buildSuspendResumeRecords(events);
expect(events[0]).to.deep.equal(expected);
expect(events[1].duration).to.equal(0);
});
});

describe('temp basal', () => {
Expand Down