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

Unified Diff: lib/filterHits.js

Issue 6337686776315904: Issue 394 - hit statistics tool data collection (Closed)
Patch Set: Use Downloader to send the data to server Created April 6, 2016, 3 p.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « lib/contentPolicy.js ('k') | lib/prefs.json » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: lib/filterHits.js
===================================================================
new file mode 100644
--- /dev/null
+++ b/lib/filterHits.js
@@ -0,0 +1,298 @@
+/*
+ * 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, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", null);
+
+let {IO} = require("io");
+let {Prefs} = require("prefs");
+let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_MINUTE,
+ MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader");
+let {FilterNotifier} = require("filterNotifier");
+let {DownloadableSubscription} = require("subscriptionClasses");
+
+const CHECK_INTERVAL = MILLIS_IN_HOUR;
+const INITIAL_DELAY = MILLIS_IN_MINUTE * 2;
+const PUSH_INTERVAL = MILLIS_IN_DAY * 7;
+
+/**
+ * The value of storage statement normal execution constant
+ * @type Number
+ */
+const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASON_FINISHED;
+
+/**
+ * This class collects filter hits statistics in a SQLite database
+ * and sends them to the server when user opt-in for that
+ * @class
+ */
+let FilterHits = exports.FilterHits =
+{
+ /**
+ * Used to prevent timers running in parallel
+ * @type Number
+ */
+ _prefToggleTimeout: 0,
+
+ get connection()
+ {
+ if (!this.storageFile)
+ return null;
+
+ let connection = Services.storage.openDatabase(this.storageFile);
+ if (!connection.tableExists("filtersubscriptions"))
+ {
+ connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " +
+ "(id INTEGER, subscriptions TEXT, PRIMARY KEY" +
+ "(id), UNIQUE(subscriptions))");
+ }
+ if (!connection.tableExists("filterhits"))
+ {
+ connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " +
+ "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " +
+ "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " +
+ "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+
+ "filtersubscriptions(id))");
+ }
+ Object.defineProperty(this, "connection",
+ {
+ value: connection
+ });
+ return connection;
+ },
+
+ /**
+ * @return nsIFile SQLite database file with filter hits data
+ */
+ get storageFile()
+ {
+ let file = IO.resolveFilePath(Prefs.data_directory);
+ if (file)
+ file.append("adblockplus.sqlite");
+
+ Object.defineProperty(this, "storageFile",
+ {
+ value: file
+ });
+ return file;
+ },
+
+ /**
+ * Called on module startup.
+ */
+ init: function()
+ {
+ if (!Prefs.sendstats_status.lastPush)
+ {
+ Prefs.sendstats_status.lastPush = Date.now();
+ this._saveFilterHitPrefs();
+ }
+ Prefs.addListener(name =>
+ {
+ if (name == "sendstats")
+ {
+ if (this._prefToggleTimeout)
+ clearTimeout(this._prefToggleTimeout);
+ this._prefToggleTimeout = setTimeout(() =>
+ {
+ if (!Prefs.sendstats)
+ this.resetFilterHits();
+ }, 1000);
+ }
+ });
+
+ let downloader = new Downloader(this._getDownloadables.bind(this),
+ INITIAL_DELAY, CHECK_INTERVAL);
+ downloader.onExpirationChange = this._onExpirationChange.bind(this);
+ downloader.generateRequestData = this._generateFilterHitsData.bind(this);
+ downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this);
+ downloader.validResponses.push(204);
+ onShutdown.add(() => downloader.cancel());
+ },
+
+ /**
+ * Yields a Downloadable instance for the filter hits push.
+ */
+ _getDownloadables: function*()
+ {
+ let downloadable = new Downloadable(Prefs.sendstats_url);
+ if (Prefs.sendstats_status.lastError)
+ downloadable.lastError = Prefs.sendstats_status.lastError;
+ if (Prefs.sendstats_status.lastCheck)
+ downloadable.lastCheck = Prefs.sendstats_status.lastCheck;
+ if (Prefs.sendstats_status.lastPush)
+ {
+ downloadable.softExpiration = Prefs.sendstats_status.lastPush + PUSH_INTERVAL;
saroyanm 2016/04/06 15:15:45 I'm not sure regarding this, should we specify sof
+ downloadable.hardExpiration = Prefs.sendstats_status.lastPush + PUSH_INTERVAL;
+ }
+ yield downloadable;
+ },
+
+ _onExpirationChange: function(downloadable)
+ {
+ Prefs.sendstats_status.lastCheck = downloadable.lastCheck;
+ this._saveFilterHitPrefs();
+ },
+
+ _saveFilterHitPrefs: function()
+ {
+ Prefs.sendstats_status = JSON.parse(JSON.stringify(Prefs.sendstats_status));
+ },
+
+ _onDownloadSuccess: function(downloadable, responseText, errorCallback, redirectCallback)
+ {
+ Prefs.sendstats_status.lastError = 0;
+ Prefs.sendstats_status.lastPush = Date.now();
+ this._saveFilterHitPrefs();
+ },
+
+ /**
+ * Increases the filter hit count
+ * @param {Filter} filter
+ * @param {String} host
+ */
+ increaseFilterHits: function(filter, host, thirdParty)
+ {
+ 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;
+
+ if (!this.connection)
+ return;
+
+ let statements = [];
+ let filterSubscriptions = JSON.stringify(downloadableSubscriptions);
+ let subscriptionStatement = this.connection.createStatement("INSERT OR " +
+ "IGNORE INTO filtersubscriptions (subscriptions) VALUES " +
+ "(:subscriptions)");
+ subscriptionStatement.params.subscriptions = filterSubscriptions;
+ statements.push(subscriptionStatement);
+ let filterHitStatement = this.connection.createStatement("INSERT OR " +
+ "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " +
+ "subscriptions) VALUES (:filter, :host, :thirdParty, COALESCE ((SELECT " +
+ "hitCount FROM filterhits WHERE filter=:filter AND host=:host AND " +
+ "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " +
+ "FROM filtersubscriptions WHERE subscriptions=:subscriptions))");
+ filterHitStatement.params.filter = filter.text;
+ filterHitStatement.params.host = host;
+ filterHitStatement.params.lastHit = roundToHours(filter.lastHit);
+ filterHitStatement.params.thirdParty = thirdParty;
+ filterHitStatement.params.subscriptions = filterSubscriptions;
+ statements.push(filterHitStatement);
+
+ this.connection.executeAsync(statements, statements.length,
+ {
+ handleError: (error) =>
+ {
+ Cu.reportError("Error updating filter hits: " + error.message);
+ },
+ handleCompletion: (reason) =>
+ {
+ if (reason != REASON_FINISHED)
+ Cu.reportError("Updating filter hits canceled or aborted: " + reason);
+ }
+ });
+ },
+
+ /**
+ * Remove all local collected filter hits data
+ */
+ resetFilterHits: function()
+ {
+ if (!this.connection)
+ return;
+
+ this.connection.executeSimpleSQL("DELETE FROM filterhits");
+ this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions");
+ },
+
+ _generateFilterHitsData: function(downloadable)
+ {
+ return {
+ then: (resolve) =>
+ {
+ if (!this.connection)
+ return;
+
+ let statement = this.connection.createStatement("SELECT filterhits.*, " +
+ "filtersubscriptions.subscriptions FROM filterhits LEFT JOIN " +
+ "filtersubscriptions filtersubscriptions ON " +
+ "filterhits.subscriptions=filtersubscriptions.id");
+ statement.executeAsync(
+ {
+ handleResult: (result) =>
+ {
+ let filters = Object.create(null);
+ for (let row = result.getNextRow(); row;
+ row = result.getNextRow())
+ {
+ let filterText = row.getResultByName("filter");
+ let host = row.getResultByName("host");
+ let matchType = (row.getResultByName("thirdParty") ?
+ "thirdParty" : "firstParty");
+ 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];
+ filter.subscriptions = subscriptions;
+ filter[matchType] = Object.create(null);
+ filter[matchType][host] = {"hits": hitCount, latest: lastHit};
+ }
+ resolve({timeSincePush: roundToHours(Date.now() -
+ Prefs.sendstats_status.lastPush), "filters": filters});
+ },
+ handleError: (error) =>
+ {
+ Prefs.sendstats_status.lastError = Date.now();
+ this._saveFilterHitPrefs();
+ Cu.reportError("Error loading filter hits:" + error.message);
+ },
+ handleCompletion: (reason) =>
+ {
+ if (reason != REASON_FINISHED)
+ {
+ Prefs.sendstats_status.lastError = Date.now();
+ this._saveFilterHitPrefs();
+ Cu.reportError("Loading of filter hits canceled or aborted: " +
+ reason);
+ }
+ }
+ });
+ }
+ }
+ },
+};
+
+function roundToHours(timestamp)
+{
+ return Math.round(timestamp / MILLIS_IN_HOUR) *
+ (MILLIS_IN_HOUR / MILLIS_IN_SECOND);
+}
+
+FilterHits.init();
« no previous file with comments | « lib/contentPolicy.js ('k') | lib/prefs.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld