 Issue 6337686776315904:
  Issue 394 - hit statistics tool data collection  (Closed)
    
  
    Issue 6337686776315904:
  Issue 394 - hit statistics tool data collection  (Closed) 
  | Index: lib/filterHits.js | 
| =================================================================== | 
| new file mode 100644 | 
| --- /dev/null | 
| +++ b/lib/filterHits.js | 
| @@ -0,0 +1,263 @@ | 
| +/* | 
| + * This file is part of Adblock Plus <https://adblockplus.org/>, | 
| + * Copyright (C) 2006-2016 Eyeo GmbH | 
| + * | 
| + * Adblock Plus is free software: you can redistribute it and/or modify | 
| + * it under the terms of the GNU General Public License version 3 as | 
| + * published by the Free Software Foundation. | 
| + * | 
| + * Adblock Plus is distributed in the hope that it will be useful, | 
| + * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
| + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 
| + * GNU General Public License for more details. | 
| + * | 
| + * You should have received a copy of the GNU General Public License | 
| + * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 
| + */ | 
| + | 
| +let {Services} = Cu.import("resource://gre/modules/Services.jsm", null); | 
| +let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", null); | 
| +let {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", null); | 
| + | 
| +let {Prefs} = require("prefs"); | 
| +let {Utils} = require("utils"); | 
| +let {MILLIS_IN_DAY} = require("downloader"); | 
| +let {FilterNotifier} = require("filterNotifier"); | 
| +let {DownloadableSubscription} = require("subscriptionClasses"); | 
| + | 
| +/** | 
| + * This class collect filter hits statistics in SQLite database | 
| + * and send them to the server when user opt-in for that | 
| 
Wladimir Palant
2016/02/29 14:35:55
Nit: collect => collects, in SQLite => in an SQLit
 
saroyanm
2016/03/18 18:23:13
Done.
 | 
| + * @class | 
| + */ | 
| +let FilterHits = exports.FilterHits = | 
| +{ | 
| + /** | 
| + * Indicates the timeframe between pushes | 
| + * @type Number | 
| + */ | 
| + _pushInterval: MILLIS_IN_DAY * 7, | 
| 
Wladimir Palant
2016/02/29 14:35:50
That's a constant, it really shouldn't be declared
 
saroyanm
2016/03/18 18:23:30
Done.
 | 
| + | 
| + /** | 
| + * The value of storage statement normal execution constant | 
| + * @type Number | 
| + */ | 
| + _finished: Components.interfaces.mozIStorageStatementCallback.REASON_FINISHED, | 
| 
Wladimir Palant
2016/02/29 14:35:48
Same here, constants shouldn't be properties.
 
saroyanm
2016/03/18 18:23:23
Done.
 | 
| + | 
| + /** | 
| + * Indicates whether the data is being sent to the server | 
| + * @type Boolean | 
| + */ | 
| + _sending: false, | 
| + | 
| + /** | 
| + * Called on module startup. | 
| + */ | 
| + init: function() | 
| + { | 
| + Prefs.addListener(function(name) | 
| + { | 
| + if (name == "sendstats") | 
| + { | 
| + setTimeout(function() | 
| 
Wladimir Palant
2016/02/29 14:35:57
Nit: Creating a number of timers running in parall
 
saroyanm
2016/03/18 18:23:12
Done.
 | 
| + { | 
| + if (!Prefs[name]) | 
| 
Wladimir Palant
2016/02/29 14:35:56
Here as well, just use Prefs.sendstats please - it
 
saroyanm
2016/03/18 18:23:21
Done.
 | 
| + this.resetFilterHits(); | 
| + }.bind(this), 1000); | 
| + } | 
| + }.bind(this)); | 
| 
Wladimir Palant
2016/02/29 14:35:53
bind(this) shouldn't be necessary in most cases, j
 
saroyanm
2016/03/18 18:23:31
Done.
 | 
| + | 
| + let now = new Date().getTime(); | 
| + if (now && (now - Prefs.sendstats_last_push > this._pushInterval)) | 
| + this.sendFilterHitsToServer(); | 
| 
Wladimir Palant
2016/02/29 14:35:55
Please use Date.now(). Also, it's a safe bet that
 
saroyanm
2016/03/18 18:23:16
I'm not sure if it's a good idea to change the Dow
 | 
| + }, | 
| + | 
| + /** | 
| + * Increases the filter hit count | 
| + * @param {Filter} filter | 
| + * @param {String} host | 
| + */ | 
| + increaseFilterHits: function(filter, host) | 
| + { | 
| + let subscriptions = filter.subscriptions; | 
| + let downloadableSubscriptions = []; | 
| + for (let i = 0; i < subscriptions.length; i++) | 
| + { | 
| + if (subscriptions[i] instanceof DownloadableSubscription) | 
| + downloadableSubscriptions.push(subscriptions[i].url); | 
| + } | 
| + | 
| + if (downloadableSubscriptions.length == 0) | 
| + return; | 
| + | 
| + let storageFile = this.getStorageFile(); | 
| + if (!storageFile) | 
| + return; | 
| + | 
| + let connection = Services.storage.openDatabase(storageFile); | 
| + this.checkCreateTable(connection); | 
| 
Wladimir Palant
2016/02/29 14:35:51
Should we be doing this on every filter hit? Initi
 
saroyanm
2016/03/18 18:23:24
Good point, I've cached connection opening, but st
 | 
| + | 
| + let statementText = "INSERT OR REPLACE INTO filterhits " + | 
| + "(filter, host, firstParty, hitCount, lastHit, subscriptions) VALUES " + | 
| + "(:filter, :host, :firstParty, coalesce ((SELECT hitCount " + | 
| + "FROM filterhits WHERE filter=:filter AND host=:host AND " + | 
| + "firstParty=:firstParty), 0) + 1, :lastHit, :subscriptions)"; | 
| + | 
| + let statement = connection.createStatement(statementText); | 
| + statement.params.filter = filter.text; | 
| + statement.params.host = host; | 
| + statement.params.lastHit = filter.lastHit; | 
| + statement.params.firstParty = filter.thirdParty != "thirdParty"; | 
| 
Wladimir Palant
2016/02/29 14:35:52
This is a filter flag you are checking here, it on
 
saroyanm
2016/03/18 18:23:27
Done.
 | 
| + statement.params.subscriptions = JSON.stringify(downloadableSubscriptions); | 
| + | 
| + statement.executeAsync( | 
| + { | 
| + handleError: function(aError) | 
| 
Wladimir Palant
2016/02/29 14:35:45
Nit: we don't use Hungarian notation for parameter
 
saroyanm
2016/03/18 18:23:14
Done.
 | 
| + { | 
| + Cu.reportError(aError.message); | 
| + }, | 
| + | 
| + handleCompletion: function(aReason) | 
| + { | 
| + if (aReason != this._finished) | 
| + Cu.reportError("Writing of filter hits canceled or aborted."); | 
| 
Wladimir Palant
2016/02/29 14:35:58
Maybe add aReason to the text, so that we can look
 
saroyanm
2016/03/18 18:23:28
Done.
 | 
| + }.bind(this) | 
| 
Wladimir Palant
2016/02/29 14:35:49
Please get rid of bind(this) here as well.
 
saroyanm
2016/03/18 18:23:22
Done.
 | 
| + }); | 
| + | 
| + connection.asyncClose(); | 
| + }, | 
| + | 
| + /** | 
| + * Remove all local collected filter hits data | 
| + */ | 
| + resetFilterHits: function() | 
| + { | 
| + let storageFile = this.getStorageFile(); | 
| + if (!storageFile) | 
| + return; | 
| + | 
| + let connection = Services.storage.openDatabase(storageFile); | 
| + connection.executeSimpleSQL("DROP TABLE IF EXISTS filterhits"); | 
| 
Wladimir Palant
2016/02/29 14:35:56
Probably better to leave the database initialized
 
saroyanm
2016/03/18 18:23:29
Done.
 | 
| + }, | 
| + | 
| + /** | 
| + * Send local stored filter hit data to the | 
| + * service specified in Prefs.sendstats_url | 
| + */ | 
| + sendFilterHitsToServer: function() | 
| + { | 
| + if (!Prefs.sendstats) | 
| + return; | 
| + | 
| + let storageFile = this.getStorageFile(); | 
| + if (!storageFile) | 
| + return; | 
| + | 
| + let connection = Services.storage.openDatabase(storageFile); | 
| + this.checkCreateTable(connection); | 
| + | 
| + let statement = connection.createStatement("SELECT * FROM filterhits"); | 
| + statement.executeAsync( | 
| + { | 
| + handleResult: function(aResultSet) | 
| + { | 
| + let filters = Object.create(null); | 
| + for (let row = aResultSet.getNextRow(); row; | 
| + row = aResultSet.getNextRow()) | 
| + { | 
| + | 
| + let filterText = row.getResultByName("filter"); | 
| + let host = row.getResultByName("host"); | 
| + let filterType = (row.getResultByName("firstParty") == 1 ? | 
| 
Wladimir Palant
2016/02/29 14:35:52
a) Not filterType, matchType.
b) Don't compare to
 
saroyanm
2016/03/18 18:23:22
Done.
 | 
| + "firstParty" : "thirdParty"); | 
| + let hitCount = row.getResultByName("hitCount"); | 
| + let lastHit = row.getResultByName("lastHit"); | 
| + let subscriptions = row.getResultByName("subscriptions"); | 
| + | 
| + if (!(filterText in filters)) | 
| + filters[filterText] = Object.create(null); | 
| + | 
| + let filter = filters[filterText]; | 
| + | 
| + if (!filter[subscriptions]) | 
| + filter[subscriptions] = subscriptions; | 
| 
Wladimir Palant
2016/02/29 14:35:53
You probably meant filter.subscriptions here.
In
 
saroyanm
2016/03/18 18:23:26
Done, but not sure if current implementation is wh
 | 
| + | 
| + if (!(filterType in filter)) | 
| + filter[filterType] = Object.create(null); | 
| + | 
| + if (!(host in filter[filterType])) | 
| 
Wladimir Palant
2016/02/29 14:35:50
This check is pointless, filter/type/host combinat
 
saroyanm
2016/03/18 18:23:16
Done.
 | 
| + filter[filterType][host] = {"hits": hitCount, latest: lastHit}; | 
| + } | 
| + | 
| + let request = new XMLHttpRequest(); | 
| + request.open("POST", Prefs.sendstats_url); | 
| + request.setRequestHeader("Content-Type", "application/json"); | 
| + request.addEventListener("load", function(event) | 
| 
Wladimir Palant
2016/02/29 14:35:58
You should handle "error" event as well, and be it
 
saroyanm
2016/03/18 18:23:27
Done.
 | 
| + { | 
| + FilterHits._sending = false; | 
| 
Wladimir Palant
2016/02/29 14:35:57
Why are you setting this member? It isn't being us
 
saroyanm
2016/03/18 18:23:15
Done.
 | 
| + if (request.status >= 200 && request.status < 300) | 
| + { | 
| + Prefs.sendstats_last_push = new Date().getTime(); | 
| + FilterHits.resetFilterHits(); | 
| + } | 
| + else | 
| + { | 
| + Cu.reportError("Couldn't send filter hit statistics to server."); | 
| 
Wladimir Palant
2016/02/29 14:35:54
I think that this should be more precise - server
 
saroyanm
2016/03/18 18:23:24
Done.
 | 
| + } | 
| + }, false); | 
| + | 
| + let {addonName, addonVersion, application, | 
| + applicationVersion, platform, platformVersion} = require("info"); | 
| + let data = { | 
| + version: 1, | 
| + timeSincePush: Prefs.sendstats_last_push, | 
| 
Wladimir Palant
2016/02/29 14:35:54
That's the time of last push, not the time *since*
 
saroyanm
2016/03/18 18:23:30
Done, but I can't understand why we need to use th
 | 
| + addonName: addonName, | 
| + addonVersion: addonVersion, | 
| + application: application, | 
| + applicationVersion: applicationVersion, | 
| + platform: platform, | 
| + platformVersion: platformVersion, | 
| + filters: filters | 
| + }; | 
| + | 
| + this._sending = true; | 
| + request.send(JSON.stringify(data)); | 
| + }.bind(this), | 
| + handleError: function(aError) | 
| + { | 
| + Cu.reportError(aError.message); | 
| + }, | 
| + handleCompletion: function(aReason) | 
| + { | 
| + if (aReason != this._finished) | 
| + Cu.reportError("Loading of filter hits canceled or aborted."); | 
| + }.bind(this) | 
| + }); | 
| + connection.asyncClose(); | 
| + }, | 
| + | 
| + /** | 
| + * @return nsIFile SQLite database file with filter hits data | 
| + */ | 
| + getStorageFile: function() | 
| + { | 
| + return FileUtils.getFile("ProfD", ["adblockplus.sqlite"]); | 
| 
Wladimir Palant
2016/02/29 14:35:51
Please don't create files all over the user's prof
 
saroyanm
2016/03/18 18:23:14
Done.
 | 
| + }, | 
| + | 
| + /** | 
| + * Creates table in local SQLite database for storing | 
| + * filter hit data if the does not exists yet | 
| + */ | 
| + checkCreateTable: function(connection) | 
| + { | 
| + if (!connection.tableExists("filterhits")) | 
| + { | 
| + connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " + | 
| + "host TEXT, firstParty INTEGER, hitCount INTEGER, lastHit INTEGER, " + | 
| 
Wladimir Palant
2016/02/29 14:35:44
We always have thirdParty as a flag, please use it
 
saroyanm
2016/03/18 18:23:25
Done.
 | 
| + "subscriptions TEXT, PRIMARY KEY(filter, host, firstParty))"); | 
| + Prefs.sendstats_last_push = new Date().getTime(); | 
| + } | 
| + }, | 
| +}; | 
| + | 
| +FilterHits.init(); |