Skip to content

Commit ccd6155

Browse files
authored
Merge branch 'master' into push-readme
2 parents e817b6f + fbe64b6 commit ccd6155

File tree

9 files changed

+159
-43
lines changed

9 files changed

+159
-43
lines changed

plugins/push/api/api.js

+6
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ plugins.register('/master', function() {
8383
common.dbUniqueMap.users.push(common.dbMap['messaging-enabled'] = DBMAP.MESSAGING_ENABLED);
8484
fields(platforms, true).forEach(f => common.dbUserMap[f] = f);
8585
PUSH.cache = common.cache.cls(PUSH_CACHE_GROUP);
86+
setTimeout(() => {
87+
const jobManager = require('../../../api/parts/jobs');
88+
jobManager.job("push:clear-stats").replace().schedule("at 3:00 am every 7 days");
89+
}, 10000);
8690
});
8791

8892
plugins.register('/master/runners', runners => {
@@ -361,6 +365,7 @@ plugins.register('/i/app_users/export', ({app_id, uids, export_commands, dbargs,
361365
* @apiDefine PushMessageBody
362366
*
363367
* @apiBody {ObjectID} app Application ID
368+
* @apiBody {Boolean} saveStats Store each individual push records into push_stats for debugging
364369
* @apiBody {String[]} platforms Array of platforms to send to
365370
* @apiBody {String="draft"} [status] Message status, only set to draft when creating or editing a draft message, don't set otherwise
366371
* @apiBody {Object} filter={} User profile filter to limit recipients of this message
@@ -410,6 +415,7 @@ plugins.register('/i/app_users/export', ({app_id, uids, export_commands, dbargs,
410415
*
411416
* @apiSuccess {ObjectID} _id Message ID
412417
* @apiSuccess {ObjectID} app Application ID
418+
* @apiSuccess {Boolean} saveStats Store each individual push records into push_stats for debugging
413419
* @apiSuccess {String[]} platforms Array of platforms to send to
414420
* @apiSuccess {Number} state Message state, for internal use
415421
* @apiSuccess {String="created", "inactive", "draft", "scheduled", "sending", "sent", "stopped", "failed"} [status] Message status: "created" is for messages yet to be scheduled (put into queue), "inactive" - cannot be scheduled (approval required for push approver plugin), "draft", "scheduled", "sending", "sent", "stopped" - automated message has been stopped, "failed" - failed to send all notifications

plugins/push/api/jobs/clear-stats.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
/**
3+
* @typedef {import("mongodb").Db} MongoDb
4+
*/
5+
6+
const { Job } = require('../../../../api/parts/jobs/job.js');
7+
const log = require('../../../../api/utils/log.js')('job:push:clear-stats');
8+
9+
const EXPIRY = 30 * 24 * 60 * 60 * 1000; // 30 days
10+
11+
/**
12+
* Clears push_stats collection
13+
*/
14+
class ClearStatsJob extends Job {
15+
/**
16+
* Clears push_stats based on EXPIRY and MAX_RECORDS
17+
* @param {MongoDb} db - db connection
18+
*/
19+
async run(db) {
20+
log.d('Clearing push_stats');
21+
await db.collection("push_stats").deleteMany({
22+
d: { $lte: new Date(Date.now() - EXPIRY) }
23+
});
24+
}
25+
}
26+
27+
module.exports = ClearStatsJob;

plugins/push/api/jobs/util/resultor.js

+69-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
const { FRAME, FRAME_NAME } = require('../../send/proto'),
22
{ DoFinish } = require('./do_finish'),
33
{ ERROR, TriggerKind, State, Status, PushError, Result } = require('../../send/data');
4+
/**
5+
* @typedef {import("mongodb").ObjectId} ObjectId
6+
*/
7+
8+
/**
9+
* PushStat object (collection: push_stats)
10+
* @typedef {Object} PushStat
11+
* @property {ObjectId} a - application id
12+
* @property {ObjectId} m - message id from "messages" collection
13+
* @property {string} u - uid from app_users{appId}
14+
* @property {string} t - token from "push_{appId}" collection
15+
* @property {string=} r - id returned from provider
16+
* @property {Date} d - date this message sent to this user
17+
* @property {string=} e - error message
18+
* @property {string} p - platform: "a" for android, "i" for ios and "h" for huawei
19+
* @property {string} f - token type: "p" for production
20+
*/
421

522
/**
623
* Stream responsible for handling sending results:
@@ -34,7 +51,10 @@ class Resultor extends DoFinish {
3451
this.fatalErrors = {}; // {mid: []}
3552
this.toDelete = []; // [push id, push id, ...]
3653
this.count = 0; // number of results cached
37-
this.last = null; // time of last data from
54+
this.last = null; // time of last data from
55+
56+
/** @type {PushStat[]} */
57+
this.pushStats = [];
3858

3959
this.data.on('app', app => {
4060
this.changed[app._id] = {};
@@ -118,11 +138,17 @@ class Resultor extends DoFinish {
118138
if (id < 0) {
119139
return;
120140
}
121-
let {p, m, pr} = this.data.pushes[id],
141+
const p = this.data.pushes[id];
142+
let {p: platform, m, pr} = p,
122143
msg = this.data.message(m),
123144
result,
124145
rp, rl;
125146

147+
// additional fields to keep this in push_stats
148+
if (msg && msg.saveStats) {
149+
this.pushStats.push({ a: p.a, m: p.m, p: p.p, f: p.f, u: p.u, t: p.t, d: new Date, r: null, e: results.toString() });
150+
}
151+
126152
if (msg) {
127153
result = msg.result;
128154
result.lastRun.processed++;
@@ -131,7 +157,7 @@ class Resultor extends DoFinish {
131157
else {
132158
result = this.noMessage[m] || (this.noMessage[m] = new Result());
133159
}
134-
rp = result.sub(p, undefined, PLATFORM[p].parent);
160+
rp = result.sub(platform, undefined, PLATFORM[platform].parent);
135161
rl = rp.sub(pr.la || 'default');
136162

137163
result.processed++;
@@ -141,8 +167,8 @@ class Resultor extends DoFinish {
141167
rl.recordError(results.message, 1);
142168
rl.processed++;
143169

144-
if (PLATFORM[p].parent) {
145-
rp = result.sub(PLATFORM[p].parent),
170+
if (PLATFORM[platform].parent) {
171+
rp = result.sub(PLATFORM[platform].parent),
146172
rl = rp.sub(pr.la || 'default');
147173
rp.recordError(results.message, 1);
148174
rp.processed++;
@@ -159,29 +185,39 @@ class Resultor extends DoFinish {
159185
}
160186
else {
161187
results.forEach(res => {
162-
let id, token;
163-
if (typeof res === 'string') {
164-
this.log.d('Ok for %s', id);
165-
id = res;
166-
}
167-
else {
188+
let id, resultId, token;
189+
190+
if (Array.isArray(res)) {
168191
this.log.d('New token for %s', id);
169192
id = res[0];
170193
token = res[1];
171194
}
195+
else {
196+
id = res;
197+
}
198+
199+
if (typeof id !== "string") {
200+
resultId = id.r;
201+
id = id.p;
202+
}
172203

173204
let p = this.data.pushes[id];
174205
if (!p) { // 2 or more resultors on one pool
175206
return;
176207
}
177208

178-
this.data.decSending(p.m);
179-
180-
let m = this.data.message(p.m),
209+
let msg = this.data.message(p.m),
181210
result, rp, rl;
182211

183-
if (m) {
184-
result = m.result;
212+
// additional fields to keep this in push_stats
213+
if (msg && msg.saveStats) {
214+
this.pushStats.push({ a: p.a, m: p.m, p: p.p, f: p.f, u: p.u, t: p.t, d: new Date, r: resultId, e: null });
215+
}
216+
217+
this.data.decSending(p.m);
218+
219+
if (msg) {
220+
result = msg.result;
185221
result.lastRun.processed++;
186222
}
187223
else {
@@ -220,14 +256,6 @@ class Resultor extends DoFinish {
220256
});
221257
this.log.d('Added %d results', results.length);
222258
}
223-
224-
// // in case no more data is expected, we can safely close the stream
225-
// if (this.check()) {
226-
// for (let _ in this.state.pushes) {
227-
// return;
228-
// }
229-
// this.do_flush(() => this.end());
230-
// }
231259
}
232260
else if (frame & FRAME.ERROR) {
233261
let error = results.messageError(),
@@ -241,28 +269,35 @@ class Resultor extends DoFinish {
241269
return;
242270
}
243271
this.log.d('Error %d %s for %s', results.type, results.name, id);
244-
let {m, p, pr} = this.data.pushes[id],
272+
const p = this.data.pushes[id];
273+
let {m, p: platform, pr} = p,
245274
result, rp, rl;
275+
let msg = this.data.message(m);
276+
277+
// additional fields to keep this in push_stats
278+
if (msg && msg.saveStats) {
279+
this.pushStats.push({ a: p.a, m: p.m, p: p.p, f: p.f, u: p.u, t: p.t, d: new Date, r: null, e: results.toString() });
280+
}
281+
246282
mids[m] = (mids[m] || 0) + 1;
247283
delete this.data.pushes[id];
248284
this.toDelete.push(id);
249285

250-
let msg = this.data.message(m);
251286
if (msg) {
252287
result = msg.result;
253288
}
254289
else {
255290
result = this.noMessage[m] || (this.noMessage[m] = new Result());
256291
}
257292

258-
rp = result.sub(p, undefined, PLATFORM[p].parent);
293+
rp = result.sub(platform, undefined, PLATFORM[platform].parent);
259294
rl = rp.sub(pr.la || 'default');
260295

261296
rp.processed++;
262297
rl.processed++;
263298

264-
if (PLATFORM[p].parent) {
265-
rp = result.sub(PLATFORM[p].parent),
299+
if (PLATFORM[platform].parent) {
300+
rp = result.sub(PLATFORM[platform].parent),
266301
rl = rp.sub(pr.la || 'default');
267302
rp.processed++;
268303
rl.processed++;
@@ -514,6 +549,11 @@ class Resultor extends DoFinish {
514549
}
515550
}
516551

552+
if (this.pushStats.length) {
553+
promises.push(this.db.collection("push_stats").insertMany(this.pushStats));
554+
this.pushStats = [];
555+
}
556+
517557
Promise.all(promises).then(() => {
518558
this.log.d('do_flush done');
519559
callback();

plugins/push/api/send/data/message.js

+27-3
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@ class Message extends Mongoable {
3333

3434
/**
3535
* Validation scheme of this class
36-
*
36+
*
3737
* @returns {object} validateArgs scheme
3838
*/
3939
static get scheme() {
4040
return {
4141
_id: { required: false, type: 'ObjectID' },
4242
app: { required: true, type: 'ObjectID' },
43+
saveStats: { required: false, type: 'Boolean' },
4344
platforms: { required: true, type: 'String[]', in: () => require('../platforms').platforms },
4445
state: { type: 'Number' },
4546
status: { type: 'String', in: Object.values(Status) },
@@ -148,9 +149,32 @@ class Message extends Mongoable {
148149
}
149150
}
150151

152+
/**
153+
* Getter for message.saveStats
154+
*
155+
* @returns {boolean} saveStats
156+
*/
157+
get saveStats() {
158+
return this._data.saveStats;
159+
}
160+
161+
/**
162+
* Setter for message.saveStats
163+
*
164+
* @param {boolean} value value to set
165+
*/
166+
set saveStats(value) {
167+
if (typeof value !== "boolean") {
168+
this._data.saveStats = false;
169+
}
170+
else {
171+
this._data.saveStats = value;
172+
}
173+
}
174+
151175
/**
152176
* Getter for platforms
153-
*
177+
*
154178
* @returns {string[]|undefined} platforms array
155179
*/
156180
get platforms() {
@@ -159,7 +183,7 @@ class Message extends Mongoable {
159183

160184
/**
161185
* Setter for platforms
162-
*
186+
*
163187
* @param {string[]|undefined} arr platforms array
164188
*/
165189
set platforms(arr) {

plugins/push/api/send/platforms/a.js

+3-9
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class FCM extends Splitter {
155155
return errors[err];
156156
};
157157

158-
const messages = pushes.map(p => p.t).map((token) => ({
158+
const messages = pushes.map(({ t: token }) => ({
159159
token,
160160
...content,
161161
}));
@@ -178,20 +178,14 @@ class FCM extends Splitter {
178178
.sendEach(messages)
179179
.then(async result => {
180180
const allPushIds = pushes.map(p => p._id);
181-
182-
if (!result.failureCount) {
183-
this.send_results(allPushIds, bytes);
184-
return;
185-
}
186-
187181
// array of successfully sent push._id:
188182
const sentSuccessfully = [];
189183

190184
// check for each message
191185
for (let i = 0; i < result.responses.length; i++) {
192-
const { success, error } = result.responses[i];
186+
const { success, error, messageId } = result.responses[i];
193187
if (success) {
194-
sentSuccessfully.push(allPushIds[i]);
188+
sentSuccessfully.push({ p: allPushIds[i], r: messageId });
195189
}
196190
else {
197191
const sdkError = FCM_SDK_ERRORS[error.code];

plugins/push/api/send/platforms/i.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,6 @@ class APN extends Base {
679679
}
680680
}
681681
// =======0========000=================0========000=================0========0
682-
console.log(JSON.stringify(reqHeaders, null, 2), JSON.stringify(content, null, 2));
683682
let stream = this.session.request(reqHeaders),
684683
status,
685684
data = '';
@@ -708,7 +707,8 @@ class APN extends Base {
708707
status = headers[':status'];
709708
// self.log.d('%d: status %d: %j', i, status, self.session.state);
710709
if (status === 200) {
711-
oks.push(p._id);
710+
const apnsUniqueId = headers["apns-unique-id"];
711+
oks.push({ p: p._id, r: apnsUniqueId });
712712
stream.destroy();
713713
streamDone();
714714
}

plugins/push/frontend/public/javascripts/countly.models.js

+3
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@
224224
isCohorts: typeof countlyCohorts !== 'undefined',
225225
_id: null,
226226
demo: false,
227+
saveStats: false,
227228
name: "",
228229
platforms: [],
229230
message: {
@@ -1413,6 +1414,7 @@
14131414
return {
14141415
_id: dto._id || null,
14151416
demo: this.mapDemo(dto),
1417+
saveStats: dto.saveStats || false,
14161418
status: this.mapStatus(dto),
14171419
createdAt: dto.info && dto.info.created ? moment(dto.info.created).format("dddd, Do MMMM YYYY h:mm") : null,
14181420
name: dto.info && dto.info.title,
@@ -1995,6 +1997,7 @@
19951997
mapModelToBaseDto: function(pushNotificationModel, options) {
19961998
var resultDto = {
19971999
app: countlyCommon.ACTIVE_APP_ID,
2000+
saveStats: pushNotificationModel.saveStats || false,
19982001
platforms: this.mapPlatforms(pushNotificationModel.platforms),
19992002
};
20002003
if (pushNotificationModel._id) {

plugins/push/frontend/public/localization/push.properties

+3
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ push-notification.testing-tooltip = Sends the push notification to applications'
246246
push-notification.send-to-test-users = Send to test users
247247
push-notification.confirmation-uppercase = CONFIRMATION
248248
push-notification.confirmation-uppercase-description = CONFIRMATION description
249+
push-notification.save-push-stats = Keep individual push records for each user
250+
push-notification.debugging = DEBUGGING
251+
push-notification.push-stats-warning = This options enables the storage of each push record inside "push_stats" collection for every message per device for debug purposes. Please enable this option with caution since it can fill up the database quickly.
249252
push-notification.i-am-ready-to-send = I am ready to send this message to real-users
250253
push-notification.was-successfully-saved = Push notification message was successfully saved
251254
push-notification.was-successfully-sent-to-test-users = Push notification message was successfully sent to test users

0 commit comments

Comments
 (0)