diff --git a/lib/ActivityStreams.js b/lib/ActivityStreams.js index 549238c54e..249cfbc710 100644 --- a/lib/ActivityStreams.js +++ b/lib/ActivityStreams.js @@ -62,7 +62,7 @@ function ActivityStreams(options = {}) { this._telemetrySender = new TelemetrySender(); this._tabTracker = new TabTracker(this.appURLs, options.clientID, this._memoized); - this._previewProvider = new PreviewProvider(); + this._previewProvider = new PreviewProvider(this._tabTracker); this._populatingCache = { places: false, preview: false, @@ -152,9 +152,11 @@ ActivityStreams.prototype = { * Process the passed in links, save them, get from cache and response to content. */ _processAndSendLinks(links, responseType, append, worker, previewsOnly = false) { + const event = this._tabTracker.generateEvent({source: responseType}); let processedLinks = this._previewProvider.processLinks(links); - this._previewProvider.asyncSaveLinks(processedLinks); + this._previewProvider.asyncSaveLinks(processedLinks, event); const cachedLinks = this._previewProvider.getEnhancedLinks(processedLinks, previewsOnly); + this._handlePerformanceEvent(event, processedLinks, cachedLinks); this.send(am.actions.Response(responseType, cachedLinks, {append}), worker); }, @@ -211,6 +213,12 @@ ActivityStreams.prototype = { this._tabTracker.handleUserEvent(data.msg.data); }, + _handlePerformanceEvent(event, processedLinks, cachedLinks) { + this._tabTracker.handlePerformanceEvent(event, "previewCacheRequest", processedLinks.length); + this._tabTracker.handlePerformanceEvent(event, "previewCacheHits", cachedLinks.length); + this._tabTracker.handlePerformanceEvent(event, "previewCacheMisses", processedLinks.length - cachedLinks.length); + }, + _onRouteChange(eventName, data) { if (data) { this._tabTracker.handleRouteChange(tabs.activeTab, data.msg.data); @@ -354,8 +362,9 @@ ActivityStreams.prototype = { })); yield Promise.all(promises); + const event = this._tabTracker.generateEvent({source: "BUILD_PREVIEW_CACHE"}); linksToSend = this._previewProvider.processLinks(linksToSend); - yield this._previewProvider.asyncSaveLinks(linksToSend); + yield this._previewProvider.asyncSaveLinks(linksToSend, event); this._populatingCache.preview = false; Services.obs.notifyObservers(null, "activity-streams-previews-cache-complete", null); } diff --git a/lib/PreviewProvider.js b/lib/PreviewProvider.js index 5b373756f5..4522c5f5fd 100644 --- a/lib/PreviewProvider.js +++ b/lib/PreviewProvider.js @@ -37,10 +37,11 @@ const DEFAULT_OPTIONS = { initFresh: false, }; -function PreviewProvider(options = {}) { +function PreviewProvider(tabTracker, options = {}) { this.options = Object.assign({}, DEFAULT_OPTIONS, options); this._onPrefChange = this._onPrefChange.bind(this); this._tippyTopProvider = new TippyTopProvider(); + this._tabTracker = tabTracker; this.init(); this._runPeriodicUpdate(); } @@ -225,7 +226,7 @@ PreviewProvider.prototype = { /** * Request links from embedly, optionally filtering out known links */ - asyncSaveLinks: Task.async(function*(links, newOnly = true, updateAccessTime = true) { + asyncSaveLinks: Task.async(function*(links, event, newOnly = true, updateAccessTime = true) { // optionally filter out known links, and links which already have a request in process let linksList = this._uniqueLinks(links).filter(link => link && (!newOnly || !ss.storage.embedlyData[link.cacheKey]) && !this._alreadyRequested.has(link.cacheKey)); @@ -239,7 +240,7 @@ PreviewProvider.prototype = { } // for each bundle of 25 links, create a new request to embedly requestQueue.forEach(requestBundle => { - promises.push(this._asyncFetchAndCache(requestBundle, updateAccessTime)); + promises.push(this._asyncFetchAndCache(requestBundle, event, updateAccessTime)); }); yield Promise.all(promises); }), @@ -256,8 +257,9 @@ PreviewProvider.prototype = { links.push(link); } } + const event = this._tabTracker.generateEvent({source: "UPDATE_LINKS"}); let linksToUpdate = this.processLinks(links); - yield this.asyncSaveLinks(linksToUpdate, false, false); + yield this.asyncSaveLinks(linksToUpdate, event, false, false); Services.obs.notifyObservers(null, "activity-streams-preview-cache-update", null); }), @@ -292,17 +294,24 @@ PreviewProvider.prototype = { /** * Extracts data from embedly and caches it */ - _asyncFetchAndCache: Task.async(function*(newLinks, updateAccessTime = true) { + _asyncFetchAndCache: Task.async(function*(newLinks, event, updateAccessTime = true) { if (!this.enabled) { return; } // extract only the sanitized link urls to send to embedly let linkURLs = newLinks.map(link => link.sanitizedURL); + this._tabTracker.handlePerformanceEvent(event, "embedlyProxyRequestSentCount", newLinks.length); try { - // Make network call when enabled + // Make network call when enabled and record how long the network call took + const startNetworkCall = Date.now(); let response = yield this._asyncGetLinkData(linkURLs); + const endNetworkCall = Date.now(); + this._tabTracker.handlePerformanceEvent(event, "embedlyProxyRequestTime", endNetworkCall - startNetworkCall); + if (response.ok) { let responseJson = yield response.json(); + this._tabTracker.handlePerformanceEvent(event, "embedlyProxyRequestReceivedCount", responseJson.urls.length); + this._tabTracker.handlePerformanceEvent(event, "embedlyProxyRequestSucess", 1); let currentTime = Date.now(); newLinks.forEach(link => { let data = responseJson.urls[link.sanitizedURL]; @@ -315,6 +324,8 @@ PreviewProvider.prototype = { } ss.storage.embedlyData[link.cacheKey].refreshTime = currentTime; }); + } else { + this._tabTracker.handlePerformanceEvent(event, "embedlyProxyFailure", 1); } } catch (err) { Cu.reportError(err); diff --git a/lib/TabTracker.js b/lib/TabTracker.js index 959ecc76aa..293b3b8135 100644 --- a/lib/TabTracker.js +++ b/lib/TabTracker.js @@ -3,6 +3,7 @@ const tabs = require("sdk/tabs"); const {Cu} = require("chrome"); const self = require("sdk/self"); +const {uuid} = require("sdk/util/uuid"); const simplePrefs = require("sdk/simple-prefs"); const eventConstants = require("../common/event-constants"); @@ -12,6 +13,7 @@ Cu.import("resource://gre/modules/Locale.jsm"); const TELEMETRY_PREF = "telemetry"; const COMPLETE_NOTIF = "tab-session-complete"; const ACTION_NOTIF = "user-action-event"; +const PERFORMANCE_NOTIF = "performance-event"; const PERF_LOG_COMPLETE_NOTIF = "performance-log-complete"; function TabTracker(trackableURLs, clientID, placesQueries) { @@ -106,6 +108,16 @@ TabTracker.prototype = { } }, + handlePerformanceEvent(eventData, eventName, value) { + let payload = Object.assign({}, eventData); + payload.action = "activity_stream_performance"; + payload.tab_id = tabs.activeTab.id; + payload.event = eventName; + payload.value = value; + this._setCommonProperties(payload, tabs.activeTab.url); + Services.obs.notifyObservers(null, PERFORMANCE_NOTIF, JSON.stringify(payload)); + }, + handleRouteChange(tab, route) { if (!route.isFirstLoad) { this.navigateAwayFromPage(tab, "route_change"); @@ -113,6 +125,10 @@ TabTracker.prototype = { } }, + generateEvent(eventData) { + return Object.assign({}, eventData, {event_id: String(uuid())}); + }, + navigateAwayFromPage(tab, reason) { // we can't use tab.url, because it's pointing to a new url of the page // we have to use the URL stored in this._openTabs object diff --git a/lib/TelemetrySender.js b/lib/TelemetrySender.js index bbeea0d6ac..45bd07cdd7 100644 --- a/lib/TelemetrySender.js +++ b/lib/TelemetrySender.js @@ -8,6 +8,7 @@ Cu.importGlobalProperties(["fetch"]); const ENDPOINT_PREF = "telemetry.ping.endpoint"; const TELEMETRY_PREF = "telemetry"; const ACTION_NOTIF = "user-action-event"; +const PERFORMANCE_NOTIF = "performance-event"; const COMPLETE_NOTIF = "tab-session-complete"; const LOGGING_PREF = "performance.log"; @@ -22,12 +23,13 @@ function TelemetrySender() { if (this.enabled) { Services.obs.addObserver(this, COMPLETE_NOTIF); Services.obs.addObserver(this, ACTION_NOTIF); + Services.obs.addObserver(this, PERFORMANCE_NOTIF); } } TelemetrySender.prototype = { observe(subject, topic, data) { - if (topic === COMPLETE_NOTIF || topic === ACTION_NOTIF) { + if (topic === COMPLETE_NOTIF || topic === ACTION_NOTIF || topic === PERFORMANCE_NOTIF) { this._sendPing(data); } }, @@ -44,6 +46,7 @@ TelemetrySender.prototype = { if (this.enabled && !newValue) { Services.obs.removeObserver(this, COMPLETE_NOTIF); Services.obs.removeObserver(this, ACTION_NOTIF); + Services.obs.removeObserver(this, PERFORMANCE_NOTIF); } else if (!this.enabled && newValue) { Services.obs.addObserver(this, COMPLETE_NOTIF); Services.obs.addObserver(this, ACTION_NOTIF); diff --git a/test/test-PreviewProvider.js b/test/test-PreviewProvider.js index e04ba703e9..17d0cc151b 100644 --- a/test/test-PreviewProvider.js +++ b/test/test-PreviewProvider.js @@ -434,7 +434,8 @@ exports.test_get_enhanced_previews_only = function*(assert) { before(exports, function*() { simplePrefs.prefs["embedly.endpoint"] = `http://localhost:${gPort}/embedlyLinkData`; simplePrefs.prefs["previews.enabled"] = true; - gPreviewProvider = new PreviewProvider({initFresh: true}); + let mockTabTracker = {handlePerformanceEvent: function() {}, generateEvent: function() {}}; + gPreviewProvider = new PreviewProvider(mockTabTracker, {initFresh: true}); }); after(exports, function*() { diff --git a/test/test-TabTracker.js b/test/test-TabTracker.js index 3b8289694b..85827bbfff 100644 --- a/test/test-TabTracker.js +++ b/test/test-TabTracker.js @@ -399,6 +399,41 @@ exports.test_TabTracker_unload_reason_with_user_action = function*(assert) { } }; +exports.test_TabTracker_performance_action_pings = function*(assert) { + let performanceEventPromise = new Promise(resolve => { + function observe(subject, topic, data) { + if (topic === "performance-event") { + Services.obs.removeObserver(observe, "performance-event"); + resolve(JSON.parse(data)); + } + } + Services.obs.addObserver(observe, "performance-event"); + }); + + let eventData1 = { + msg: { + data: { + source: "TOP_FRECENT_SITES_REQUEST", + event_id: "{c4f7e4a0-947b-7343-8a56-934c724492cc}", + event: "previewCacheHit", + value: 1 + } + } + }; + const event1 = app._tabTracker.generateEvent({source: "TOP_FRECENT_SITES_REQUEST"}); + app._tabTracker.handlePerformanceEvent(event1, "previewCacheHit", 1); + + let pingData = yield performanceEventPromise; + let additionalKeys = ["client_id", "addon_version", "locale", "action", "tab_id", "page"]; + for (let key of additionalKeys) { + assert.ok(pingData[key], `The ping has the additional key ${key}`); + } + assert.ok(/{[0-9a-f\-]+}/.test(eventData1.msg.data.event_id), "ping has a UUID as an event ID"); + assert.deepEqual(eventData1.msg.data.source, pingData.source, "the ping has the correct source"); + assert.deepEqual(eventData1.msg.data.event, pingData.event, "the ping has the correct event"); + assert.deepEqual(eventData1.msg.data.value, pingData.value, "the ping has the correct value"); +}; + exports.test_TabTracker_handleRouteChange_FirstLoad = function(assert) { assert.deepEqual(app.tabData, {}, "tabData starts out empty"); app._tabTracker.handleRouteChange({}, {isFirstLoad: true});