Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/filterHits.js

Issue 6337686776315904: Issue 394 - hit statistics tool data collection (Closed)
Patch Set: Addressed Wladimir comments (bunch of changes) Created March 18, 2016, 5:32 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2016 Eyeo GmbH
4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation.
8 *
9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 let {Services} = Cu.import("resource://gre/modules/Services.jsm", null);
19 let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", null);
20 let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", n ull);
21
22 let {IO} = require("io");
23 let {Prefs} = require("prefs");
24 let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_DAY, MILLIS_IN_HOUR} = require("downloader");
25 let {FilterNotifier} = require("filterNotifier");
26 let {DownloadableSubscription} = require("subscriptionClasses");
27
28 const CHECK_INTERVAL = MILLIS_IN_DAY;
Wladimir Palant 2016/03/22 21:32:32 IMHO this should be one hour, like for subscriptio
saroyanm 2016/04/06 15:15:32 Done.
29 const INITIAL_DELAY = MILLIS_IN_SECOND * 5;
Wladimir Palant 2016/03/22 21:32:34 IMHO this is much too short. Given that this isn't
saroyanm 2016/04/06 15:15:38 Done.
30 const PUSH_INTERVAL = MILLIS_IN_DAY * 7;
31
32 /**
33 * The value of storage statement normal execution constant
34 * @type Number
35 */
36 const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASO N_FINISHED;
37
38 /**
39 * This class collects filter hits statistics in a SQLite database
40 * and sends them to the server when user opt-in for that
41 * @class
42 */
43 let FilterHits = exports.FilterHits =
44 {
45 /**
46 * Used to prevent timers running in parallel
47 * @type Number
48 */
49 _prefToggleTimeoutId: 0,
50
51 /**
52 * Indicates whether the data is being sent to the server
53 * @type Boolean
54 */
55 _sending: false,
56
57 get connection()
58 {
59 let connection = Services.storage.openDatabase(this.getStorageFile);
60 if (!connection.tableExists("filtersubscriptions"))
61 {
62 connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " +
63 "(id INTEGER, subscriptions TEXT, PRIMARY KEY" +
64 "(id), UNIQUE(subscriptions))");
65 }
66 if (!connection.tableExists("filterhits"))
67 {
68 connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " +
69 "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " +
70 "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " +
71 "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+
72 "filtersubscriptions(id))");
73 Prefs.sendstats_last_push = Date.now();
74 }
75 Object.defineProperty(this, "connection",
76 {
77 value: connection
78 });
79 return connection;
80 },
81
82 /**
83 * @return nsIFile SQLite database file with filter hits data
84 */
85 get getStorageFile()
Wladimir Palant 2016/03/22 21:32:27 This is a property, its name shouldn't start with
saroyanm 2016/04/06 15:15:26 Done.
86 {
87 let file = IO.resolveFilePath(Prefs.data_directory);
88 if (file)
89 file.append("adblockplus.sqlite");
90
91 Object.defineProperty(this, "getStorageFile",
92 {
93 value: file
94 });
95 return file;
96 },
97
98 /**
99 * Called on module startup.
100 */
101 init: function()
102 {
103 Prefs.addListener(name =>
104 {
105 if (name == "sendstats")
106 {
107 if (this._prefToggleTimeout)
108 clearTimeout(this._prefToggleTimeout);
109 this._prefToggleTimeout = setTimeout(() =>
110 {
111 if (!Prefs.sendstats)
112 this.resetFilterHits();
113 }, 1000);
114 }
115 });
116
117 let downloader = new Downloader(this._getDownloadables.bind(this),
118 INITIAL_DELAY, CHECK_INTERVAL, this.sendFilterHitsToServer);
Wladimir Palant 2016/03/22 21:32:28 Why isn't sendFilterHitsToServer bound? How does i
saroyanm 2016/04/06 15:15:31 My bad, changed the implementation already.
119 downloader.onExpirationChange = this._onExpirationChange.bind(this);
120 onShutdown.add(() => downloader.cancel());
121 },
122
123 /**
124 * Yields a Downloadable instances for the notifications download.
Wladimir Palant 2016/03/22 21:32:25 instances => instance notifications download => fi
saroyanm 2016/04/06 15:15:23 Done.
125 */
126 _getDownloadables: function*()
127 {
128 let downloadable = new Downloadable();
129 if (typeof Prefs.sendstats_status.lastError === "number")
130 downloadable.lastError = Prefs.sendstats_status.lastError;
131 if (typeof Prefs.sendstats_status.lastCheck === "number")
132 downloadable.lastCheck = Prefs.sendstats_status.lastCheck;
133 if (typeof Prefs.sendstats_status.softExpiration === "number")
134 downloadable.softExpiration = Prefs.sendstats_status.softExpiration;
135 if (typeof Prefs.sendstats_status.hardExpiration === "number")
136 downloadable.hardExpiration = Prefs.sendstats_status.hardExpiration;
137 yield downloadable;
138 },
139
140 _onExpirationChange: function(downloadable)
141 {
142 Prefs.sendstats_status.lastCheck = downloadable.lastCheck;
143 Prefs.sendstats_status.softExpiration = downloadable.softExpiration;
144 Prefs.sendstats_status.hardExpiration = downloadable.hardExpiration;
Wladimir Palant 2016/03/22 21:32:26 There is a limitation of Prefs module when it come
saroyanm 2016/04/06 15:15:42 Done.
145 },
146
147 /**
148 * Increases the filter hit count
149 * @param {Filter} filter
150 * @param {String} host
151 */
152 increaseFilterHits: function(filter, host, thirdParty)
153 {
154 let subscriptions = filter.subscriptions;
155 let downloadableSubscriptions = [];
156 for (let i = 0; i < subscriptions.length; i++)
157 {
158 if (subscriptions[i] instanceof DownloadableSubscription)
159 downloadableSubscriptions.push(subscriptions[i].url);
160 }
161
162 if (downloadableSubscriptions.length == 0)
163 return;
164
165 if (!this.getStorageFile)
Wladimir Palant 2016/03/22 21:32:35 How is the storage file related to the code here?
saroyanm 2016/04/06 15:15:41 Done.
166 return;
167
168 let statementsArray = [];
Wladimir Palant 2016/03/22 21:32:20 I don't think there is much point into adding data
saroyanm 2016/04/06 15:15:30 Done.
169 let filtersubscriptions = JSON.stringify(downloadableSubscriptions);
Wladimir Palant 2016/03/22 21:32:31 Nit: this should be camel case, filterSubscription
saroyanm 2016/04/06 15:15:24 Done.
170 let subscriptionStatement = this.connection.createStatement("INSERT OR " +
171 "IGNORE INTO filtersubscriptions (subscriptions) VALUES " +
172 "(:subscriptions)");
173 subscriptionStatement.params.subscriptions = filtersubscriptions;
174 statementsArray.push(subscriptionStatement);
175 let filterHitstatement = this.connection.createStatement("INSERT OR " +
Wladimir Palant 2016/03/22 21:32:30 Nit: proper camel case would be filterHitStatement
saroyanm 2016/04/06 15:15:39 Done.
176 "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " +
177 "subscriptions) VALUES (:filter, :host, :thirdParty, coalesce ((SELECT " +
Wladimir Palant 2016/03/22 21:32:18 Nit: COALESCE should be capitalized.
saroyanm 2016/04/06 15:15:40 Done.
178 "hitCount FROM filterhits WHERE filter=:filter AND host=:host AND " +
179 "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " +
180 "FROM filtersubscriptions WHERE subscriptions=:subscriptions))");
181 filterHitstatement.params.filter = filter.text;
182 filterHitstatement.params.host = host;
183 filterHitstatement.params.lastHit = roundToHours(filter.lastHit);
184 filterHitstatement.params.thirdParty = thirdParty;
185 filterHitstatement.params.subscriptions = filtersubscriptions;
186 statementsArray.push(filterHitstatement);
187
188 this.connection.executeAsync(statementsArray, statementsArray.length,
189 {
190 handleError: function(error)
191 {
192 Cu.reportError("Error during writing of filter hits: " + reason);
Wladimir Palant 2016/03/22 21:32:28 Variable reason is not defined. Also a nit: "Erro
saroyanm 2016/04/06 15:15:28 Done.
193 },
194 handleCompletion: function(reason)
195 {
196 if (reason != REASON_FINISHED)
197 Cu.reportError("Writing of filter hits canceled or aborted: " + reason );
Wladimir Palant 2016/03/22 21:32:32 Nit: "Updating filter hits canceled or aborted"?
saroyanm 2016/04/06 15:15:27 Done.
198 }
Wladimir Palant 2016/03/22 21:32:27 I actually meant getting rid of bind(this) by mean
saroyanm 2016/04/06 15:15:33 Done.
199 });
200 },
201
202 /**
203 * Remove all local collected filter hits data
204 */
205 resetFilterHits: function()
206 {
207 if (!this.getStorageFile)
Wladimir Palant 2016/03/22 21:32:34 As above, this should be checking this.connection
saroyanm 2016/04/06 15:15:22 Done.
208 return;
209
210 this.connection.executeSimpleSQL("DELETE FROM filterhits");
211 this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions");
212 },
213
214 /**
215 * Send local stored filter hit data to the
216 * service specified in Prefs.sendstats_url
217 */
218 sendFilterHitsToServer: function()
219 {
220 let now = Date.now();
221 if (Prefs.sendstats_last_push > now)
222 Prefs.sendstats_last_push = now;
Wladimir Palant 2016/03/22 21:32:25 I don't think you want to set sendstats_last_push
saroyanm 2016/04/06 15:15:30 I tried to fix the local time change issue here, b
223
224 if ((now - Prefs.sendstats_last_push < PUSH_INTERVAL) || !Prefs.sendstats
Wladimir Palant 2016/03/22 21:32:24 Why are you double-checking the push interval? Sup
saroyanm 2016/04/06 15:15:28 My bad, re-implemented this part.
225 || !this.getStorageFile)
226 return;
227
228 let statement = this.connection.createStatement("SELECT filterhits.*, " +
Wladimir Palant 2016/03/22 21:32:33 I guess you want to check this.connection here as
saroyanm 2016/04/06 15:15:25 Done.
229 "filtersubscriptions.subscriptions FROM filterhits LEFT JOIN " +
230 "filtersubscriptions filtersubscriptions ON " +
231 "filterhits.subscriptions=filtersubscriptions.id");
232 statement.executeAsync(
233 {
234 handleResult: (result) =>
235 {
236 let filters = Object.create(null);
237 for (let row = result.getNextRow(); row;
238 row = result.getNextRow())
239 {
240 let filterText = row.getResultByName("filter");
241 let host = row.getResultByName("host");
242 let matchType = (row.getResultByName("thirdParty") ?
243 "thirdParty" : "firstParty");
244 let hitCount = row.getResultByName("hitCount");
245 let lastHit = row.getResultByName("lastHit");
246 let subscriptions = row.getResultByName("subscriptions");
247
248 if (!(filterText in filters))
249 filters[filterText] = Object.create(null);
250
251 let filter = filters[filterText];
252 filter.subscriptions = subscriptions;
253 filter[matchType] = Object.create(null);
254 filter[matchType][host] = {"hits": hitCount, latest: lastHit};
255 }
256
257 let request = new XMLHttpRequest();
258 request.open("POST", Prefs.sendstats_url);
259 request.setRequestHeader("Content-Type", "application/json");
260 request.addEventListener("load", () =>
261 {
262 this._sending = false;
263 if (request.status >= 200 && request.status < 300)
264 {
265 Prefs.sendstats_status.lastError = 0;
266 Prefs.sendstats_last_push = Date.now();
267 FilterHits.resetFilterHits();
268 }
269 else
270 {
271 Prefs.sendstats_status.lastError = Date.now();
272 this._sending = false;
273 Cu.reportError("Couldn't send filter hit statistics to server. Statu s code: " + request.status);
274 }
275 }, false);
276 request.addEventListener("error", () =>
277 {
278 this._sending = false;
Wladimir Palant 2016/03/22 21:32:33 Set lastError here as well?
saroyanm 2016/04/06 15:15:41 Changed implementation, this should be done by Dow
279 Cu.reportError("Error occured while sending filter hit statistics to s erver.");
280 }, false);
281
282 let {addonName, addonVersion, application,
283 applicationVersion, platform, platformVersion} = require("info");
284 let data = {
285 version: 1,
286 timeSincePush: roundToHours(Date.now() - Prefs.sendstats_last_push),
287 addonName: addonName,
288 addonVersion: addonVersion,
289 application: application,
290 applicationVersion: applicationVersion,
291 platform: platform,
292 platformVersion: platformVersion,
293 filters: filters
294 };
295 if (!this._sending)
Wladimir Palant 2016/03/22 21:32:29 Shouldn't you check this *before* going through al
saroyanm 2016/04/06 15:15:21 Changed implementation, this should be done by Dow
296 {
297 this._sending = true;
298 request.send(JSON.stringify(data));
299 }
300 },
301 handleError: function(error)
302 {
303 Prefs.sendstats_status.lastError = Date.now();
304 Cu.reportError(error.message);
Wladimir Palant 2016/03/22 21:32:19 More informative message here as well? "Error load
saroyanm 2016/04/06 15:15:33 Done.
305 },
306 handleCompletion: function(reason)
307 {
308 if (reason != REASON_FINISHED)
309 {
310 Prefs.sendstats_status.lastError = Date.now();
311 Cu.reportError("Loading of filter hits canceled or aborted: " + reason );
312 }
313 }
314 });
315 },
316 };
317
318 function roundToHours(timestamp)
319 {
320 return Math.round(timestamp / MILLIS_IN_HOUR) * 3600;
Wladimir Palant 2016/03/22 21:32:30 3600 is actually (MILLIS_IN_HOUR / MILLIS_IN_SECON
saroyanm 2016/04/06 15:15:24 Done.
321 }
322
323 FilterHits.init();
OLDNEW

Powered by Google App Engine
This is Rietveld