Skip to content
This repository has been archived by the owner on Jun 24, 2022. It is now read-only.

Commit

Permalink
Bug fix to ensure overlapping outbreak events do not result in more t…
Browse files Browse the repository at this point in the history
…han one exposure per checkin (#1604)
  • Loading branch information
smcmurtry authored May 12, 2021
1 parent d4ad1fe commit 902fe19
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 6 deletions.
5 changes: 5 additions & 0 deletions src/services/OutbreakService/OutbreakService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {unzip} from 'react-native-zip-archive';
import {readFile} from 'react-native-fs';
import {covidshield} from 'services/BackendService/covidshield';
import {EventTypeMetric, FilteredMetricsService} from 'services/MetricsService';
import {getRandomString} from 'shared/logging/uuid';

import {Observable} from '../../shared/Observable';
import {
Expand All @@ -31,6 +32,9 @@ export const HOURS_PER_PERIOD = 24;
export const EXPOSURE_NOTIFICATION_CYCLE = 14;

export interface OutbreakEvent {
// Don't use this for anything besides the dedup code.
// dedupeId will change each time we get new data from the server.
dedupeId: string;
locationId: string;
// ms
startTime: number;
Expand Down Expand Up @@ -212,6 +216,7 @@ export class OutbreakService {
convertOutbreakEvents = (outbreakEvents: covidshield.OutbreakEvent[]): OutbreakEvent[] => {
return outbreakEvents.map(event => {
return {
dedupeId: getRandomString(8),
locationId: event.locationId,
endTime: 1000 * Number(event.endTime?.seconds),
startTime: 1000 * Number(event.startTime?.seconds),
Expand Down
2 changes: 1 addition & 1 deletion src/shared/logging/uuid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {DefaultStorageService, StorageDirectory} from 'services/StorageService';

let currentUUID = '';

const getRandomString = (size: number) => {
export const getRandomString = (size: number) => {
const chars = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'];

return [...Array(size)].map(_ => chars[(Math.random() * chars.length) | 0]).join('');
Expand Down
73 changes: 72 additions & 1 deletion src/shared/qr.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {OutbreakEvent} from '../services/OutbreakService';

import {
CheckInData,
createOutbreakHistoryItem,
deduplicateMatches,
doTimeWindowsOverlap,
getMatchedOutbreakHistoryItems,
isExposedToOutbreak,
Expand All @@ -9,6 +12,7 @@ import {
getNewOutbreakExposures,
expireHistoryItems,
OutbreakHistoryItem,
MatchData,
} from './qr';

const getTimes = (startTimestamp, durationInMinutes: number) => {
Expand Down Expand Up @@ -110,7 +114,7 @@ describe('getMatchedOutbreakHistoryItems', () => {
});

it('returns not exposed if isIgnored or expired', () => {
const history = {
const history: OutbreakHistoryItem = {
outbreakId: '123-1612180800000',
isExpired: false,
isIgnored: false,
Expand All @@ -121,6 +125,7 @@ describe('getMatchedOutbreakHistoryItems', () => {
outbreakEndTimestamp: 1612195200000,
checkInTimestamp: 1612180800000,
notificationTimestamp: 1613758680944,
severity: 3,
};

expect(isExposedToOutbreak([history])).toStrictEqual(true);
Expand Down Expand Up @@ -324,6 +329,7 @@ describe('outbreakHistory functions', () => {
outbreakEndTimestamp: 1612195200000,
checkInTimestamp: 1612180800001,
notificationTimestamp: 1613758680944,
severity: 3,
};

const history = getMatchedOutbreakHistoryItems(checkIns, outbreaks);
Expand All @@ -343,5 +349,70 @@ describe('outbreakHistory functions', () => {
});
});

describe('deduplicateMatches', () => {
const locationId = 'abc123';
const checkIn1: CheckInData = {
id: locationId,
name: 'Burgers',
address: '123 King St',
timestamp: new Date(2021, 1, 10, 12).getTime(),
};
const checkIn2: CheckInData = {
id: locationId,
name: 'Burgers',
address: '123 King St',
timestamp: new Date(2021, 1, 11, 12).getTime(),
};
const outbreakEvent1: OutbreakEvent = {
dedupeId: 'outbreakEvent1',
locationId,
startTime: new Date(2021, 1, 10).getTime(),
endTime: new Date(2021, 1, 11).getTime(),
severity: 2,
};
const outbreakEvent2: OutbreakEvent = {
dedupeId: 'outbreakEvent2',
locationId,
startTime: new Date(2021, 1, 10).getTime(),
endTime: new Date(2021, 1, 11).getTime(),
severity: 3,
};
const outbreakEvent3: OutbreakEvent = {
dedupeId: 'outbreakEvent2',
locationId,
startTime: new Date(2021, 1, 11).getTime(),
endTime: new Date(2021, 1, 12).getTime(),
severity: 3,
};

it('filters out duplicate matches with a lower severity', () => {
const match1: MatchData = {
timestamp: checkIn1.timestamp,
checkIn: checkIn1,
outbreakEvent: outbreakEvent1,
};
const match2: MatchData = {
timestamp: checkIn1.timestamp,
checkIn: checkIn1,
outbreakEvent: outbreakEvent2,
};
expect(deduplicateMatches([match1, match2])).toStrictEqual([match2]);
});

it('does not filter out non duplicate matches', () => {
const match1: MatchData = {
timestamp: checkIn1.timestamp,
checkIn: checkIn1,
outbreakEvent: outbreakEvent1,
};
const match2: MatchData = {
timestamp: checkIn2.timestamp,
checkIn: checkIn2,
outbreakEvent: outbreakEvent3,
};
expect(deduplicateMatches([match1, match2])).toStrictEqual([match1, match2]);
});
});

// end
});
49 changes: 45 additions & 4 deletions src/shared/qr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,20 @@ interface MatchCalculationData {
checkIns: CheckInData[];
}

interface MatchData {
export interface MatchData {
timestamp: number;
checkIn: CheckInData;
outbreakEvent: OutbreakEvent;
}

interface MatchDeduplicationDict {
[checkInTimestamp: string]: {
outbreakEventId: string;
maxOutbreakEndTimestamp: number;
maxSeverity: OutbreakSeverity;
};
}

export interface OutbreakHistoryItem {
outbreakId: string /* unique to your checkin during the outbreak event */;
isExpired: boolean /* after 14 days the outbreak expires */;
Expand Down Expand Up @@ -134,11 +142,12 @@ export const getMatchedOutbreakHistoryItems = (
const matchedOutbreakIdsNotUnique = checkInLocationMatches.map(data => data.id);
const matchedOutbreakIds = Array.from(new Set(matchedOutbreakIdsNotUnique));

const matches = getMatches({outbreakEvents, checkInHistory, matchedOutbreakIds});
const allMatches = getMatches({outbreakEvents, checkInHistory, matchedOutbreakIds});

log.debug({message: 'outbreak matches', payload: {matches}});
log.debug({message: 'outbreak matches', payload: {allMatches}});

return matches.map(match => createOutbreakHistoryItem(match));
const deduplicatedMatches = deduplicateMatches(allMatches);
return deduplicatedMatches.map(match => createOutbreakHistoryItem(match));
};

export const doTimeWindowsOverlap = (window1: TimeWindow, window2: TimeWindow) => {
Expand Down Expand Up @@ -245,3 +254,35 @@ export const getNewOutbreakExposures = (
const newOutbreakExposures = detectedExposures.filter(item => newIds.indexOf(item.outbreakId) > -1);
return newOutbreakExposures;
};

const isKeyInObject = (key: string, object: object) => {
return Object.keys(object).indexOf(key) === -1;
};

const isSeverityHigher = (match: MatchData, deduplicationDict: MatchDeduplicationDict) => {
const timestampStr = match.timestamp.toString();
return deduplicationDict[timestampStr].maxSeverity < match.outbreakEvent.severity;
};

/**
* Look at all outbreak/checkin matches and remove duplicate matches
* so the same checkin never results in more than one exposure
*/
export const deduplicateMatches = (allMatches: MatchData[]) => {
const deduplicationDict: MatchDeduplicationDict = {};
for (const match of allMatches) {
const timestampStr = match.timestamp.toString();
if (isKeyInObject(timestampStr, deduplicationDict) || isSeverityHigher(match, deduplicationDict)) {
deduplicationDict[timestampStr] = {
maxOutbreakEndTimestamp: match.outbreakEvent.endTime,
maxSeverity: match.outbreakEvent.severity,
outbreakEventId: match.outbreakEvent.dedupeId,
};
}
}
const validOutbreakEventIds = Object.values(deduplicationDict).map(item => item.outbreakEventId);
const deduplicatedMatches = allMatches.filter(
match => validOutbreakEventIds.indexOf(match.outbreakEvent.dedupeId) > -1,
);
return deduplicatedMatches;
};

0 comments on commit 902fe19

Please sign in to comment.