Adds ability to track sent/failed PerUTCOffset in the PushWorker (#4158)

* Adds ability to track sent/failed PerUTCOffset in the PushWorker

- for scheduled push notifications at a certain time, it helps keep track of the state

* Makes sure we track it all correctly

* Adds to Postgres
This commit is contained in:
Florent Vilmart
2017-09-13 17:28:20 -04:00
committed by GitHub
parent be2760f9dd
commit d598d73f36
4 changed files with 160 additions and 22 deletions

View File

@@ -228,7 +228,7 @@ describe('PushWorker', () => {
}, },
response: { error: 'invalid error...' } response: { error: 'invalid error...' }
} }
], true); ], undefined, true);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
expect(spy.calls.count()).toBe(1); expect(spy.calls.count()).toBe(1);
const lastCall = spy.calls.mostRecent(); const lastCall = spy.calls.mostRecent();
@@ -241,5 +241,137 @@ describe('PushWorker', () => {
}); });
done(); done();
}); });
it('tracks push status per UTC offsets', (done) => {
const config = new Config('test');
const handler = pushStatusHandler(config, 'ABCDEF1234');
const spy = spyOn(config.database, "update").and.callThrough();
const UTCOffset = 1;
handler.setInitial().then(() => {
return handler.trackSent([
{
transmitted: false,
device: {
deviceToken: 1,
deviceType: 'ios',
},
},
{
transmitted: true,
device: {
deviceToken: 1,
deviceType: 'ios',
}
},
], UTCOffset)
}).then(() => {
expect(spy).toHaveBeenCalled();
expect(spy.calls.count()).toBe(1);
const lastCall = spy.calls.mostRecent();
expect(lastCall.args[0]).toBe('_PushStatus');
const updatePayload = lastCall.args[2];
expect(updatePayload.updatedAt instanceof Date).toBeTruthy();
// remove the updatedAt as not testable
delete updatePayload.updatedAt;
expect(lastCall.args[2]).toEqual({
numSent: { __op: 'Increment', amount: 1 },
numFailed: { __op: 'Increment', amount: 1 },
'sentPerType.ios': { __op: 'Increment', amount: 1 },
'failedPerType.ios': { __op: 'Increment', amount: 1 },
[`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
[`failedPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
count: { __op: 'Increment', amount: -2 },
});
const query = new Parse.Query('_PushStatus');
return query.get('ABCDEF1234', { useMasterKey: true });
}).then((pushStatus) => {
const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset');
expect(sentPerUTCOffset['1']).toBe(1);
const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset');
expect(failedPerUTCOffset['1']).toBe(1);
return handler.trackSent([
{
transmitted: false,
device: {
deviceToken: 1,
deviceType: 'ios',
},
},
{
transmitted: true,
device: {
deviceToken: 1,
deviceType: 'ios',
}
},
{
transmitted: true,
device: {
deviceToken: 1,
deviceType: 'ios',
}
},
], UTCOffset)
}).then(() => {
const query = new Parse.Query('_PushStatus');
return query.get('ABCDEF1234', { useMasterKey: true });
}).then((pushStatus) => {
const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset');
expect(sentPerUTCOffset['1']).toBe(3);
const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset');
expect(failedPerUTCOffset['1']).toBe(2);
}).then(done).catch(done.fail);
});
it('tracks push status per UTC offsets with negative offsets', (done) => {
const config = new Config('test');
const handler = pushStatusHandler(config);
spyOn(config.database, "create").and.callFake(() => {
return Promise.resolve();
});
const spy = spyOn(config.database, "update").and.callFake(() => {
return Promise.resolve();
});
const UTCOffset = -6;
handler.trackSent([
{
transmitted: false,
device: {
deviceToken: 1,
deviceType: 'ios',
},
response: { error: 'Unregistered' }
},
{
transmitted: true,
device: {
deviceToken: 1,
deviceType: 'ios',
},
response: { error: 'Unregistered' }
},
], UTCOffset).then(() => {
expect(spy).toHaveBeenCalled();
expect(spy.calls.count()).toBe(1);
const lastCall = spy.calls.mostRecent();
expect(lastCall.args[0]).toBe('_PushStatus');
const updatePayload = lastCall.args[2];
expect(updatePayload.updatedAt instanceof Date).toBeTruthy();
// remove the updatedAt as not testable
delete updatePayload.updatedAt;
expect(lastCall.args[2]).toEqual({
numSent: { __op: 'Increment', amount: 1 },
numFailed: { __op: 'Increment', amount: 1 },
'sentPerType.ios': { __op: 'Increment', amount: 1 },
'failedPerType.ios': { __op: 'Increment', amount: 1 },
[`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
[`failedPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 },
count: { __op: 'Increment', amount: -2 },
});
done();
});
});
}); });
}); });

View File

@@ -72,20 +72,22 @@ const defaultColumns = Object.freeze({
"subtitle": {type:'String'}, "subtitle": {type:'String'},
}, },
_PushStatus: { _PushStatus: {
"pushTime": {type:'String'}, "pushTime": {type:'String'},
"source": {type:'String'}, // rest or webui "source": {type:'String'}, // rest or webui
"query": {type:'String'}, // the stringified JSON query "query": {type:'String'}, // the stringified JSON query
"payload": {type:'String'}, // the stringified JSON payload, "payload": {type:'String'}, // the stringified JSON payload,
"title": {type:'String'}, "title": {type:'String'},
"expiry": {type:'Number'}, "expiry": {type:'Number'},
"status": {type:'String'}, "status": {type:'String'},
"numSent": {type:'Number'}, "numSent": {type:'Number'},
"numFailed": {type:'Number'}, "numFailed": {type:'Number'},
"pushHash": {type:'String'}, "pushHash": {type:'String'},
"errorMessage": {type:'Object'}, "errorMessage": {type:'Object'},
"sentPerType": {type:'Object'}, "sentPerType": {type:'Object'},
"failedPerType":{type:'Object'}, "failedPerType": {type:'Object'},
"count": {type:'Number'} "sentPerUTCOffset": {type:'Object'},
"failedPerUTCOffset": {type:'Object'},
"count": {type:'Number'}
}, },
_JobStatus: { _JobStatus: {
"jobName": {type: 'String'}, "jobName": {type: 'String'},

View File

@@ -47,7 +47,7 @@ export class PushWorker {
} }
} }
run({ body, query, pushStatus, applicationId }: any): Promise<*> { run({ body, query, pushStatus, applicationId, UTCOffset }: any): Promise<*> {
const config = new Config(applicationId); const config = new Config(applicationId);
const auth = master(config); const auth = master(config);
const where = utils.applyDeviceTokenExists(query.where); const where = utils.applyDeviceTokenExists(query.where);
@@ -56,13 +56,13 @@ export class PushWorker {
if (results.length == 0) { if (results.length == 0) {
return; return;
} }
return this.sendToAdapter(body, results, pushStatus, config); return this.sendToAdapter(body, results, pushStatus, config, UTCOffset);
}, err => { }, err => {
throw err; throw err;
}); });
} }
sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config): Promise<*> { sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config, UTCOffset: ?any): Promise<*> {
pushStatus = pushStatusHandler(config, pushStatus.objectId); pushStatus = pushStatusHandler(config, pushStatus.objectId);
// Check if we have locales in the push body // Check if we have locales in the push body
const locales = utils.getLocalesFromPush(body); const locales = utils.getLocalesFromPush(body);
@@ -75,7 +75,7 @@ export class PushWorker {
const promises = Object.keys(grouppedInstallations).map((locale) => { const promises = Object.keys(grouppedInstallations).map((locale) => {
const installations = grouppedInstallations[locale]; const installations = grouppedInstallations[locale];
const body = bodiesPerLocales[locale]; const body = bodiesPerLocales[locale];
return this.sendToAdapter(body, installations, pushStatus, config); return this.sendToAdapter(body, installations, pushStatus, config, UTCOffset);
}); });
return Promise.all(promises); return Promise.all(promises);
} }
@@ -83,7 +83,7 @@ export class PushWorker {
if (!utils.isPushIncrementing(body)) { if (!utils.isPushIncrementing(body)) {
logger.verbose(`Sending push to ${installations.length}`); logger.verbose(`Sending push to ${installations.length}`);
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => { return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
return pushStatus.trackSent(results); return pushStatus.trackSent(results, UTCOffset).then(() => results);
}); });
} }
@@ -95,7 +95,7 @@ export class PushWorker {
const payload = deepcopy(body); const payload = deepcopy(body);
payload.data.badge = parseInt(badge); payload.data.badge = parseInt(badge);
const installations = badgeInstallationsMap[badge]; const installations = badgeInstallationsMap[badge];
return this.sendToAdapter(payload, installations, pushStatus, config); return this.sendToAdapter(payload, installations, pushStatus, config, UTCOffset);
}); });
return Promise.all(promises); return Promise.all(promises);
} }

View File

@@ -162,7 +162,7 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
{status: "running", updatedAt: new Date(), count }); {status: "running", updatedAt: new Date(), count });
} }
const trackSent = function(results, cleanupInstallations = process.env.PARSE_SERVER_CLEANUP_INVALID_INSTALLATIONS) { const trackSent = function(results, UTCOffset, cleanupInstallations = process.env.PARSE_SERVER_CLEANUP_INVALID_INSTALLATIONS) {
const update = { const update = {
updatedAt: new Date(), updatedAt: new Date(),
numSent: 0, numSent: 0,
@@ -179,6 +179,10 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId
const deviceType = result.device.deviceType; const deviceType = result.device.deviceType;
const key = result.transmitted ? `sentPerType.${deviceType}` : `failedPerType.${deviceType}`; const key = result.transmitted ? `sentPerType.${deviceType}` : `failedPerType.${deviceType}`;
memo[key] = incrementOp(memo, key); memo[key] = incrementOp(memo, key);
if (typeof UTCOffset !== 'undefined') {
const offsetKey = result.transmitted ? `sentPerUTCOffset.${UTCOffset}` : `failedPerUTCOffset.${UTCOffset}`;
memo[offsetKey] = incrementOp(memo, offsetKey);
}
if (result.transmitted) { if (result.transmitted) {
memo.numSent++; memo.numSent++;
} else { } else {