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

Delta Between Two Patch Sets: lib/filterHits.js

Issue 6337686776315904: Issue 394 - hit statistics tool data collection (Closed)
Left Patch Set: Rebase to changeset 4157 Created April 6, 2016, 2:56 p.m.
Right Patch Set: Use Downloader to send the data to server Created April 6, 2016, 3 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « lib/contentPolicy.js ('k') | lib/prefs.json » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2016 Eyeo GmbH 3 * Copyright (C) 2006-2016 Eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 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 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 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/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 let {Services} = Cu.import("resource://gre/modules/Services.jsm", null); 18 let {Services} = Cu.import("resource://gre/modules/Services.jsm", null);
19 let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.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); 20 let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", n ull);
21 21
22 let {IO} = require("io"); 22 let {IO} = require("io");
23 let {Prefs} = require("prefs"); 23 let {Prefs} = require("prefs");
24 let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_DAY, MILLIS_IN_HOUR} = require("downloader"); 24 let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_MINUTE,
25 MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader");
25 let {FilterNotifier} = require("filterNotifier"); 26 let {FilterNotifier} = require("filterNotifier");
26 let {DownloadableSubscription} = require("subscriptionClasses"); 27 let {DownloadableSubscription} = require("subscriptionClasses");
27 28
28 const CHECK_INTERVAL = MILLIS_IN_DAY; 29 const CHECK_INTERVAL = MILLIS_IN_HOUR;
29 const INITIAL_DELAY = MILLIS_IN_SECOND * 5; 30 const INITIAL_DELAY = MILLIS_IN_MINUTE * 2;
30 const PUSH_INTERVAL = MILLIS_IN_DAY * 7; 31 const PUSH_INTERVAL = MILLIS_IN_DAY * 7;
31 32
32 /** 33 /**
33 * The value of storage statement normal execution constant 34 * The value of storage statement normal execution constant
34 * @type Number 35 * @type Number
35 */ 36 */
36 const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASO N_FINISHED; 37 const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASO N_FINISHED;
37 38
38 /** 39 /**
39 * This class collects filter hits statistics in a SQLite database 40 * This class collects filter hits statistics in a SQLite database
40 * and sends them to the server when user opt-in for that 41 * and sends them to the server when user opt-in for that
41 * @class 42 * @class
42 */ 43 */
43 let FilterHits = exports.FilterHits = 44 let FilterHits = exports.FilterHits =
44 { 45 {
45 /** 46 /**
46 * Used to prevent timers running in parallel 47 * Used to prevent timers running in parallel
47 * @type Number 48 * @type Number
48 */ 49 */
49 _prefToggleTimeoutId: 0, 50 _prefToggleTimeout: 0,
50
51 /**
52 * Indicates whether the data is being sent to the server
53 * @type Boolean
54 */
55 _sending: false,
56 51
57 get connection() 52 get connection()
58 { 53 {
59 let connection = Services.storage.openDatabase(this.getStorageFile); 54 if (!this.storageFile)
55 return null;
56
57 let connection = Services.storage.openDatabase(this.storageFile);
60 if (!connection.tableExists("filtersubscriptions")) 58 if (!connection.tableExists("filtersubscriptions"))
61 { 59 {
62 connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " + 60 connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " +
63 "(id INTEGER, subscriptions TEXT, PRIMARY KEY" + 61 "(id INTEGER, subscriptions TEXT, PRIMARY KEY" +
64 "(id), UNIQUE(subscriptions))"); 62 "(id), UNIQUE(subscriptions))");
65 } 63 }
66 if (!connection.tableExists("filterhits")) 64 if (!connection.tableExists("filterhits"))
67 { 65 {
68 connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " + 66 connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " +
69 "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " + 67 "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " +
70 "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " + 68 "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " +
71 "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+ 69 "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+
72 "filtersubscriptions(id))"); 70 "filtersubscriptions(id))");
73 Prefs.sendstats_last_push = Date.now();
74 } 71 }
75 Object.defineProperty(this, "connection", 72 Object.defineProperty(this, "connection",
76 { 73 {
77 value: connection 74 value: connection
78 }); 75 });
79 return connection; 76 return connection;
80 }, 77 },
81 78
82 /** 79 /**
83 * @return nsIFile SQLite database file with filter hits data 80 * @return nsIFile SQLite database file with filter hits data
84 */ 81 */
85 get getStorageFile() 82 get storageFile()
86 { 83 {
87 let file = IO.resolveFilePath(Prefs.data_directory); 84 let file = IO.resolveFilePath(Prefs.data_directory);
88 if (file) 85 if (file)
89 file.append("adblockplus.sqlite"); 86 file.append("adblockplus.sqlite");
90 87
91 Object.defineProperty(this, "getStorageFile", 88 Object.defineProperty(this, "storageFile",
92 { 89 {
93 value: file 90 value: file
94 }); 91 });
95 return file; 92 return file;
96 }, 93 },
97 94
98 /** 95 /**
99 * Called on module startup. 96 * Called on module startup.
100 */ 97 */
101 init: function() 98 init: function()
102 { 99 {
100 if (!Prefs.sendstats_status.lastPush)
101 {
102 Prefs.sendstats_status.lastPush = Date.now();
103 this._saveFilterHitPrefs();
104 }
103 Prefs.addListener(name => 105 Prefs.addListener(name =>
104 { 106 {
105 if (name == "sendstats") 107 if (name == "sendstats")
106 { 108 {
107 if (this._prefToggleTimeout) 109 if (this._prefToggleTimeout)
108 clearTimeout(this._prefToggleTimeout); 110 clearTimeout(this._prefToggleTimeout);
109 this._prefToggleTimeout = setTimeout(() => 111 this._prefToggleTimeout = setTimeout(() =>
110 { 112 {
111 if (!Prefs.sendstats) 113 if (!Prefs.sendstats)
112 this.resetFilterHits(); 114 this.resetFilterHits();
113 }, 1000); 115 }, 1000);
114 } 116 }
115 }); 117 });
116 118
117 let downloader = new Downloader(this._getDownloadables.bind(this), 119 let downloader = new Downloader(this._getDownloadables.bind(this),
118 INITIAL_DELAY, CHECK_INTERVAL, this.sendFilterHitsToServer); 120 INITIAL_DELAY, CHECK_INTERVAL);
119 downloader.onExpirationChange = this._onExpirationChange.bind(this); 121 downloader.onExpirationChange = this._onExpirationChange.bind(this);
122 downloader.generateRequestData = this._generateFilterHitsData.bind(this);
123 downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this);
124 downloader.validResponses.push(204);
120 onShutdown.add(() => downloader.cancel()); 125 onShutdown.add(() => downloader.cancel());
121 }, 126 },
122 127
123 /** 128 /**
124 * Yields a Downloadable instances for the notifications download. 129 * Yields a Downloadable instance for the filter hits push.
125 */ 130 */
126 _getDownloadables: function*() 131 _getDownloadables: function*()
127 { 132 {
128 let downloadable = new Downloadable(); 133 let downloadable = new Downloadable(Prefs.sendstats_url);
129 if (typeof Prefs.sendstats_status.lastError === "number") 134 if (Prefs.sendstats_status.lastError)
130 downloadable.lastError = Prefs.sendstats_status.lastError; 135 downloadable.lastError = Prefs.sendstats_status.lastError;
131 if (typeof Prefs.sendstats_status.lastCheck === "number") 136 if (Prefs.sendstats_status.lastCheck)
132 downloadable.lastCheck = Prefs.sendstats_status.lastCheck; 137 downloadable.lastCheck = Prefs.sendstats_status.lastCheck;
133 if (typeof Prefs.sendstats_status.softExpiration === "number") 138 if (Prefs.sendstats_status.lastPush)
134 downloadable.softExpiration = Prefs.sendstats_status.softExpiration; 139 {
135 if (typeof Prefs.sendstats_status.hardExpiration === "number") 140 downloadable.softExpiration = Prefs.sendstats_status.lastPush + PUSH_INTER VAL;
saroyanm 2016/04/06 15:15:45 I'm not sure regarding this, should we specify sof
136 downloadable.hardExpiration = Prefs.sendstats_status.hardExpiration; 141 downloadable.hardExpiration = Prefs.sendstats_status.lastPush + PUSH_INTER VAL;
142 }
137 yield downloadable; 143 yield downloadable;
138 }, 144 },
139 145
140 _onExpirationChange: function(downloadable) 146 _onExpirationChange: function(downloadable)
141 { 147 {
142 Prefs.sendstats_status.lastCheck = downloadable.lastCheck; 148 Prefs.sendstats_status.lastCheck = downloadable.lastCheck;
143 Prefs.sendstats_status.softExpiration = downloadable.softExpiration; 149 this._saveFilterHitPrefs();
144 Prefs.sendstats_status.hardExpiration = downloadable.hardExpiration; 150 },
151
152 _saveFilterHitPrefs: function()
153 {
154 Prefs.sendstats_status = JSON.parse(JSON.stringify(Prefs.sendstats_status));
155 },
156
157 _onDownloadSuccess: function(downloadable, responseText, errorCallback, redire ctCallback)
158 {
159 Prefs.sendstats_status.lastError = 0;
160 Prefs.sendstats_status.lastPush = Date.now();
161 this._saveFilterHitPrefs();
145 }, 162 },
146 163
147 /** 164 /**
148 * Increases the filter hit count 165 * Increases the filter hit count
149 * @param {Filter} filter 166 * @param {Filter} filter
150 * @param {String} host 167 * @param {String} host
151 */ 168 */
152 increaseFilterHits: function(filter, host, thirdParty) 169 increaseFilterHits: function(filter, host, thirdParty)
153 { 170 {
154 let subscriptions = filter.subscriptions; 171 let subscriptions = filter.subscriptions;
155 let downloadableSubscriptions = []; 172 let downloadableSubscriptions = [];
156 for (let i = 0; i < subscriptions.length; i++) 173 for (let i = 0; i < subscriptions.length; i++)
157 { 174 {
158 if (subscriptions[i] instanceof DownloadableSubscription) 175 if (subscriptions[i] instanceof DownloadableSubscription)
159 downloadableSubscriptions.push(subscriptions[i].url); 176 downloadableSubscriptions.push(subscriptions[i].url);
160 } 177 }
161 178
162 if (downloadableSubscriptions.length == 0) 179 if (downloadableSubscriptions.length == 0)
163 return; 180 return;
164 181
165 if (!this.getStorageFile) 182 if (!this.connection)
166 return; 183 return;
167 184
168 let statementsArray = []; 185 let statements = [];
169 let filtersubscriptions = JSON.stringify(downloadableSubscriptions); 186 let filterSubscriptions = JSON.stringify(downloadableSubscriptions);
170 let subscriptionStatement = this.connection.createStatement("INSERT OR " + 187 let subscriptionStatement = this.connection.createStatement("INSERT OR " +
171 "IGNORE INTO filtersubscriptions (subscriptions) VALUES " + 188 "IGNORE INTO filtersubscriptions (subscriptions) VALUES " +
172 "(:subscriptions)"); 189 "(:subscriptions)");
173 subscriptionStatement.params.subscriptions = filtersubscriptions; 190 subscriptionStatement.params.subscriptions = filterSubscriptions;
174 statementsArray.push(subscriptionStatement); 191 statements.push(subscriptionStatement);
175 let filterHitstatement = this.connection.createStatement("INSERT OR " + 192 let filterHitStatement = this.connection.createStatement("INSERT OR " +
176 "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " + 193 "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " +
177 "subscriptions) VALUES (:filter, :host, :thirdParty, coalesce ((SELECT " + 194 "subscriptions) VALUES (:filter, :host, :thirdParty, COALESCE ((SELECT " +
178 "hitCount FROM filterhits WHERE filter=:filter AND host=:host AND " + 195 "hitCount FROM filterhits WHERE filter=:filter AND host=:host AND " +
179 "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " + 196 "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " +
180 "FROM filtersubscriptions WHERE subscriptions=:subscriptions))"); 197 "FROM filtersubscriptions WHERE subscriptions=:subscriptions))");
181 filterHitstatement.params.filter = filter.text; 198 filterHitStatement.params.filter = filter.text;
182 filterHitstatement.params.host = host; 199 filterHitStatement.params.host = host;
183 filterHitstatement.params.lastHit = roundToHours(filter.lastHit); 200 filterHitStatement.params.lastHit = roundToHours(filter.lastHit);
184 filterHitstatement.params.thirdParty = thirdParty; 201 filterHitStatement.params.thirdParty = thirdParty;
185 filterHitstatement.params.subscriptions = filtersubscriptions; 202 filterHitStatement.params.subscriptions = filterSubscriptions;
186 statementsArray.push(filterHitstatement); 203 statements.push(filterHitStatement);
187 204
188 this.connection.executeAsync(statementsArray, statementsArray.length, 205 this.connection.executeAsync(statements, statements.length,
189 { 206 {
190 handleError: function(error) 207 handleError: (error) =>
191 { 208 {
192 Cu.reportError("Error during writing of filter hits: " + reason); 209 Cu.reportError("Error updating filter hits: " + error.message);
193 }, 210 },
194 handleCompletion: function(reason) 211 handleCompletion: (reason) =>
195 { 212 {
196 if (reason != REASON_FINISHED) 213 if (reason != REASON_FINISHED)
197 Cu.reportError("Writing of filter hits canceled or aborted: " + reason ); 214 Cu.reportError("Updating filter hits canceled or aborted: " + reason);
198 } 215 }
199 }); 216 });
200 }, 217 },
201 218
202 /** 219 /**
203 * Remove all local collected filter hits data 220 * Remove all local collected filter hits data
204 */ 221 */
205 resetFilterHits: function() 222 resetFilterHits: function()
206 { 223 {
207 if (!this.getStorageFile) 224 if (!this.connection)
208 return; 225 return;
209 226
210 this.connection.executeSimpleSQL("DELETE FROM filterhits"); 227 this.connection.executeSimpleSQL("DELETE FROM filterhits");
211 this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions"); 228 this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions");
212 }, 229 },
213 230
214 /** 231 _generateFilterHitsData: function(downloadable)
215 * Send local stored filter hit data to the 232 {
216 * service specified in Prefs.sendstats_url 233 return {
217 */ 234 then: (resolve) =>
218 sendFilterHitsToServer: function() 235 {
219 { 236 if (!this.connection)
220 let now = Date.now(); 237 return;
221 if (Prefs.sendstats_last_push > now) 238
222 Prefs.sendstats_last_push = now; 239 let statement = this.connection.createStatement("SELECT filterhits.*, " +
223 240 "filtersubscriptions.subscriptions FROM filterhits LEFT JOIN " +
224 if ((now - Prefs.sendstats_last_push < PUSH_INTERVAL) || !Prefs.sendstats 241 "filtersubscriptions filtersubscriptions ON " +
225 || !this.getStorageFile) 242 "filterhits.subscriptions=filtersubscriptions.id");
226 return; 243 statement.executeAsync(
227
228 let statement = this.connection.createStatement("SELECT filterhits.*, " +
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 { 244 {
240 let filterText = row.getResultByName("filter"); 245 handleResult: (result) =>
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 { 246 {
265 Prefs.sendstats_status.lastError = 0; 247 let filters = Object.create(null);
266 Prefs.sendstats_last_push = Date.now(); 248 for (let row = result.getNextRow(); row;
267 FilterHits.resetFilterHits(); 249 row = result.getNextRow())
268 } 250 {
269 else 251 let filterText = row.getResultByName("filter");
252 let host = row.getResultByName("host");
253 let matchType = (row.getResultByName("thirdParty") ?
254 "thirdParty" : "firstParty");
255 let hitCount = row.getResultByName("hitCount");
256 let lastHit = row.getResultByName("lastHit");
257 let subscriptions = row.getResultByName("subscriptions");
258
259 if (!(filterText in filters))
260 filters[filterText] = Object.create(null);
261
262 let filter = filters[filterText];
263 filter.subscriptions = subscriptions;
264 filter[matchType] = Object.create(null);
265 filter[matchType][host] = {"hits": hitCount, latest: lastHit};
266 }
267 resolve({timeSincePush: roundToHours(Date.now() -
268 Prefs.sendstats_status.lastPush), "filters": filters});
269 },
270 handleError: (error) =>
270 { 271 {
271 Prefs.sendstats_status.lastError = Date.now(); 272 Prefs.sendstats_status.lastError = Date.now();
272 this._sending = false; 273 this._saveFilterHitPrefs();
273 Cu.reportError("Couldn't send filter hit statistics to server. Statu s code: " + request.status); 274 Cu.reportError("Error loading filter hits:" + error.message);
275 },
276 handleCompletion: (reason) =>
277 {
278 if (reason != REASON_FINISHED)
279 {
280 Prefs.sendstats_status.lastError = Date.now();
281 this._saveFilterHitPrefs();
282 Cu.reportError("Loading of filter hits canceled or aborted: " +
283 reason);
284 }
274 } 285 }
275 }, false); 286 });
276 request.addEventListener("error", () =>
277 {
278 this._sending = false;
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)
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);
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 } 287 }
314 }); 288 }
315 }, 289 },
316 }; 290 };
317 291
318 function roundToHours(timestamp) 292 function roundToHours(timestamp)
319 { 293 {
320 return Math.round(timestamp / MILLIS_IN_HOUR) * 3600; 294 return Math.round(timestamp / MILLIS_IN_HOUR) *
295 (MILLIS_IN_HOUR / MILLIS_IN_SECOND);
321 } 296 }
322 297
323 FilterHits.init(); 298 FilterHits.init();
LEFTRIGHT

Powered by Google App Engine
This is Rietveld