diff --git a/app/browser/reducers/pageDataReducer.js b/app/browser/reducers/pageDataReducer.js new file mode 100644 index 00000000000..cb58f3145e9 --- /dev/null +++ b/app/browser/reducers/pageDataReducer.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const electron = require('electron') +const BrowserWindow = electron.BrowserWindow + +// Constants +const appConstants = require('../../../js/constants/appConstants') +const windowConstants = require('../../../js/constants/windowConstants') + +// State +const pageDataState = require('../../common/state/pageDataState') + +// Utils +const {makeImmutable} = require('../../common/state/immutableUtil') +const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const {responseHasContent} = require('../../common/lib/httpUtil') + +const pageDataReducer = (state, action, immutableAction) => { + action = immutableAction || makeImmutable(action) + switch (action.get('actionType')) { + case windowConstants.WINDOW_SET_FOCUSED_FRAME: + { + if (action.get('location')) { + state = pageDataState.addView(state, action.get('location'), action.get('tabId')) + } + break + } + case appConstants.APP_WINDOW_BLURRED: + { + let windowCount = BrowserWindow.getAllWindows().filter((win) => win.isFocused()).length + if (windowCount === 0) { + state = pageDataState.addView(state) + } + break + } + case appConstants.APP_IDLE_STATE_CHANGED: + { + if (action.get('idleState') !== 'active') { + state = pageDataState.addView(state) + } + break + } + case appConstants.APP_WINDOW_CLOSED: + { + state = pageDataState.addView(state) + break + } + case 'event-set-page-info': + { + // retains all past pages, not really sure that's needed... [MTR] + state = pageDataState.addInfo(state, action.get('pageInfo')) + break + } + case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: + { + // Only capture response for the page (not subresources, like images, JavaScript, etc) + if (action.getIn(['details', 'resourceType']) === 'mainFrame') { + const pageUrl = action.getIn(['details', 'newURL']) + + // create a page view event if this is a page load on the active tabId + const lastActiveTabId = pageDataState.getLastActiveTabId(state) + const tabId = action.get('tabId') + if (!lastActiveTabId || tabId === lastActiveTabId) { + state = pageDataState.addView(state, pageUrl, tabId) + } + + const responseCode = action.getIn(['details', 'httpResponseCode']) + if (isSourceAboutUrl(pageUrl) || !responseHasContent(responseCode)) { + break + } + + const pageLoadEvent = makeImmutable({ + timestamp: new Date().getTime(), + url: pageUrl, + tabId: tabId, + details: action.get('details') + }) + state = pageDataState.addLoad(state, pageLoadEvent) + } + break + } + } + + return state +} + +module.exports = pageDataReducer diff --git a/app/browser/tabs.js b/app/browser/tabs.js index 441ad89f701..966ba25a60f 100644 --- a/app/browser/tabs.js +++ b/app/browser/tabs.js @@ -511,7 +511,7 @@ const api = { tab.on('did-get-response-details', (evt, status, newURL, originalURL, httpResponseCode, requestMethod, referrer, headers, resourceType) => { if (resourceType === 'mainFrame') { - windowActions.gotResponseDetails(tabId, {status, newURL, originalURL, httpResponseCode, requestMethod, referrer, headers, resourceType}) + windowActions.gotResponseDetails(tabId, {status, newURL, originalURL, httpResponseCode, requestMethod, referrer, resourceType}) } }) }) diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index e788259bef2..7c80c7a088f 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -4,44 +4,56 @@ 'use strict' -const {responseHasContent} = require('./httpUtil') const moment = require('moment') +// Utils +const {responseHasContent} = require('./httpUtil') +const {makeImmutable} = require('../../common/state/immutableUtil') + /** * Is page an actual page being viewed by the user? (not an error page, etc) * If the page is invalid, we don't want to collect usage info. - * @param {Object} view - an entry from page_view (from EventStore) - * @param {Object} responseList - full page_response array (from EventStore) + * @param {Map} view - an entry from ['pageData', 'view'] + * @param {List} responseList - full ['pageData', 'load'] List * @return {boolean} true if page should have usage collected, false if not */ -module.exports.shouldTrackView = (view, responseList) => { - if (!view || !view.url || !view.tabId) { +const shouldTrackView = (view, responseList) => { + view = makeImmutable(view) + + if (view == null) { return false } - if (!responseList || !Array.isArray(responseList) || !responseList.length) { + + const tabId = view.get('tabId') + const url = view.get('url') + + if (!url || !tabId) { return false } - const tabId = view.tabId - const url = view.url + responseList = makeImmutable(responseList) + if (!responseList || responseList.size === 0) { + return false + } - for (let i = responseList.length; i > -1; i--) { - const response = responseList[i] + for (let i = (responseList.size - 1); i > -1; i--) { + const response = responseList.get(i) - if (!response) continue + if (!response) { + continue + } - const responseUrl = response && response.details - ? response.details.newURL - : null + const responseUrl = response.getIn(['details', 'newURL'], null) - if (url === responseUrl && response.tabId === tabId) { - return responseHasContent(response.details.httpResponseCode) + if (url === responseUrl && response.get('tabId') === tabId) { + return responseHasContent(response.getIn(['details', 'httpResponseCode'])) } } + return false } -module.exports.btcToCurrencyString = (btc, ledgerData) => { +const btcToCurrencyString = (btc, ledgerData) => { const balance = Number(btc || 0) const currency = ledgerData.get('currency') || 'USD' @@ -69,17 +81,17 @@ module.exports.btcToCurrencyString = (btc, ledgerData) => { return `${balance} BTC` } -module.exports.formattedTimeFromNow = (timestamp) => { +const formattedTimeFromNow = (timestamp) => { moment.locale(navigator.language) return moment(new Date(timestamp)).fromNow() } -module.exports.formattedDateFromTimestamp = (timestamp, format) => { +const formattedDateFromTimestamp = (timestamp, format) => { moment.locale(navigator.language) return moment(new Date(timestamp)).format(format) } -module.exports.walletStatus = (ledgerData) => { +const walletStatus = (ledgerData) => { let status = {} if (ledgerData.get('error')) { @@ -93,7 +105,7 @@ module.exports.walletStatus = (ledgerData) => { status.id = 'insufficientFundsStatus' } else if (pendingFunds > 0) { status.id = 'pendingFundsStatus' - status.args = {funds: module.exports.btcToCurrencyString(pendingFunds, ledgerData)} + status.args = {funds: btcToCurrencyString(pendingFunds, ledgerData)} } else if (transactions && transactions.size > 0) { status.id = 'defaultWalletStatus' } else { @@ -106,3 +118,11 @@ module.exports.walletStatus = (ledgerData) => { } return status } + +module.exports = { + shouldTrackView, + btcToCurrencyString, + formattedTimeFromNow, + formattedDateFromTimestamp, + walletStatus +} diff --git a/app/common/lib/pageDataUtil.js b/app/common/lib/pageDataUtil.js new file mode 100644 index 00000000000..c9ed1131916 --- /dev/null +++ b/app/common/lib/pageDataUtil.js @@ -0,0 +1,17 @@ + +const urlFormat = require('url').format +const _ = require('underscore') + +const urlParse = require('../../common/urlParse') + +const getInfoKey = (url) => { + if (typeof url !== 'string') { + return null + } + + return urlFormat(_.pick(urlParse(url), [ 'protocol', 'host', 'hostname', 'port', 'pathname' ])) +} + +module.exports = { + getInfoKey +} diff --git a/app/common/state/pageDataState.js b/app/common/state/pageDataState.js new file mode 100644 index 00000000000..4896bc34122 --- /dev/null +++ b/app/common/state/pageDataState.js @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Immutable = require('immutable') + +// State +const tabState = require('./tabState') + +// Utils +const pageDataUtil = require('../lib/pageDataUtil') +const {getWebContents} = require('../../browser/webContentsCache') +const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const {makeImmutable} = require('./immutableUtil') + +const pageDataState = { + addView: (state, url = null, tabId = null) => { + const tab = getWebContents(tabId) + const isPrivate = !tab || + tab.isDestroyed() || + !tab.session.partition.startsWith('persist:') + + state = pageDataState.setLastActiveTabId(state, tabId) + + if ((url && isSourceAboutUrl(url)) || isPrivate) { + url = null + } + + const lastView = pageDataState.getView(state) + if (lastView.get('url') === url) { + return state + } + + let pageViewEvent = makeImmutable({ + timestamp: new Date().getTime(), + url, + tabId + }) + return state.setIn(['pageData', 'view'], pageViewEvent) + }, + + addInfo: (state, data) => { + data = makeImmutable(data) + + if (data == null) { + return state + } + + const key = pageDataUtil.getInfoKey(data.get('url')) + + data = data.set('key', key) + state = state.setIn(['pageData', 'last', 'info'], key) + return state.setIn(['pageData', 'info', key], data) + }, + + addLoad: (state, data) => { + if (data == null) { + return state + } + + // select only last 100 loads + const newLoad = state.getIn(['pageData', 'load'], Immutable.List()).slice(-100).push(data) + return state.setIn(['pageData', 'load'], newLoad) + }, + + getView: (state) => { + return state.getIn(['pageData', 'view']) || Immutable.Map() + }, + + getLastInfo: (state) => { + const key = state.getIn(['pageData', 'last', 'info']) + + if (key == null) { + Immutable.Map() + } + + return state.getIn(['pageData', 'info', key], Immutable.Map()) + }, + + getLoad: (state) => { + return state.getIn(['pageData', 'load'], Immutable.List()) + }, + + getLastActiveTabId: (state) => { + return state.getIn(['pageData', 'last', 'tabId']) || tabState.TAB_ID_NONE + }, + + setLastActiveTabId: (state, tabId) => { + return state.setIn(['pageData', 'last', 'tabId'], tabId) + }, + + setPublisher: (state, key, publisher) => { + if (key == null) { + return state + } + + return state.setIn(['pageData', 'info', key, 'publisher'], publisher) + } +} + +module.exports = pageDataState diff --git a/app/ledger.js b/app/ledger.js index 95f3bd4ec9c..93cf99a4491 100644 --- a/app/ledger.js +++ b/app/ledger.js @@ -36,7 +36,6 @@ const os = require('os') const path = require('path') const urlParse = require('./common/urlParse') const urlFormat = require('url').format -const util = require('util') const Immutable = require('immutable') const electron = require('electron') @@ -57,16 +56,17 @@ const uuid = require('uuid') const appActions = require('../js/actions/appActions') const appConfig = require('../js/constants/appConfig') const appConstants = require('../js/constants/appConstants') +const windowConstants = require('../js/constants/windowConstants') const messages = require('../js/constants/messages') const settings = require('../js/constants/settings') const request = require('../js/lib/request') const getSetting = require('../js/settings').getSetting const locale = require('./locale') const appStore = require('../js/stores/appStore') -const eventStore = require('../js/stores/eventStore') const rulesolver = require('./extensions/brave/content/scripts/pageInformation') const ledgerUtil = require('./common/lib/ledgerUtil') const tabs = require('./browser/tabs') +const pageDataState = require('./common/state/pageDataState') const {fileUrl} = require('../js/lib/appUrlUtil') // "only-when-needed" loading... @@ -191,6 +191,7 @@ const doAction = (state, action) => { } case appConstants.APP_IDLE_STATE_CHANGED: + state = pageDataChanged(state) visit('NOOP', underscore.now(), null) break @@ -280,8 +281,15 @@ const doAction = (state, action) => { appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) break - default: - break + case 'event-set-page-info': + case appConstants.APP_WINDOW_BLURRED: + case appConstants.APP_CLOSE_WINDOW: + case windowConstants.WINDOW_SET_FOCUSED_FRAME: + case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: + { + state = pageDataChanged(state) + break + } } return state @@ -624,68 +632,79 @@ underscore.keys(fileTypes).forEach((fileType) => { }) signatureMax = Math.ceil(signatureMax * 1.5) -eventStore.addChangeListener(() => { - var initP - const eventState = eventStore.getState().toJS() - var view = eventState.page_view - var info = eventState.page_info - var pageLoad = eventState.page_load +const pageDataChanged = (state) => { + // NB: in theory we have already seen every element in info except for (perhaps) the last one... + const info = pageDataState.getLastInfo(state) - if ((!synopsis) || (!util.isArray(info))) return + if (!synopsis || info.isEmpty()) { + return + } -// NB: in theory we have already seen every element in info except for (perhaps) the last one... - underscore.rest(info, info.length - 1).forEach((page) => { - let pattern, publisher - let location = page.url + if (info.get('url', '').match(/^about/)) { + return + } - if (location.match(/^about/)) return + let publisher = info.get('publisher') + const location = info.get('key') + if (publisher) { + // TODO refactor + if (synopsis.publishers[publisher] && + (typeof synopsis.publishers[publisher].faviconURL === 'undefined' || synopsis.publishers[publisher].faviconURL === null)) { + getFavIcon(synopsis.publishers[publisher], info, location) + } - location = urlFormat(underscore.pick(urlParse(location), [ 'protocol', 'host', 'hostname', 'port', 'pathname' ])) - publisher = locations[location] && locations[location].publisher - if (publisher) { - if (synopsis.publishers[publisher] && - (typeof synopsis.publishers[publisher].faviconURL === 'undefined' || synopsis.publishers[publisher].faviconURL === null)) { - getFavIcon(synopsis.publishers[publisher], page, location) + // TODO refactor + return updateLocation(location, publisher) + } else { + try { + publisher = ledgerPublisher.getPublisher(location, publisherInfo._internal.ruleset.raw) + // TODO refactor + if (publisher && !blockedP(publisher)) { + state = pageDataState.setPublisher(state, location, publisher) + } else { + publisher = null } - return updateLocation(location, publisher) + } catch (ex) { + console.error('getPublisher error for ' + location + ': ' + ex.toString()) } + } - if (!page.publisher) { - try { - publisher = ledgerPublisher.getPublisher(location, publisherInfo._internal.ruleset.raw) - if ((publisher) && (blockedP(publisher))) publisher = null - if (publisher) page.publisher = publisher - } catch (ex) { - console.error('getPublisher error for ' + location + ': ' + ex.toString()) + if (!publisher) { + return + } + + const pattern = `https?://${publisher}` + const initP = !synopsis.publishers[publisher] + // TODO refactor + synopsis.initPublisher(publisher) + + if (initP) { + // TODO refactor + excludeP(publisher, (unused, exclude) => { + if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { + exclude = false + } else { + exclude = !exclude } - } - locations[location] = underscore.omit(page, [ 'url', 'protocol', 'faviconURL' ]) - if (!page.publisher) return - - publisher = page.publisher - pattern = `https?://${publisher}` - initP = !synopsis.publishers[publisher] - synopsis.initPublisher(publisher) - if (initP) { - excludeP(publisher, (unused, exclude) => { - if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { - exclude = false - } else { - exclude = !exclude - } - appActions.changeSiteSetting(pattern, 'ledgerPayments', exclude) - updatePublisherInfo() - }) - } - updateLocation(location, publisher) - getFavIcon(synopsis.publishers[publisher], page, location) - }) + appActions.changeSiteSetting(pattern, 'ledgerPayments', exclude) + updatePublisherInfo() + }) + } + // TODO refactor + updateLocation(location, publisher) + // TODO refactor + getFavIcon(synopsis.publishers[publisher], info, location) + + const pageLoad = pageDataState.getLoad(state) + const view = pageDataState.getView(state) - view = underscore.last(view) || {} if (ledgerUtil.shouldTrackView(view, pageLoad)) { - visit(view.url || 'NOOP', view.timestamp || underscore.now(), view.tabId) + // TODO refactor + visit(view.get('url', 'NOOP'), view.get('timestamp', underscore.now()), view.get('tabId')) } -}) + + return state +} /* * module initialization diff --git a/app/sessionStore.js b/app/sessionStore.js index 5b8568883db..506daab1967 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -393,6 +393,7 @@ module.exports.cleanAppData = (immutableData, isShutdown) => { } immutableData = immutableData.delete('menu') + immutableData = immutableData.delete('pageData') try { immutableData = tabState.getPersistentState(immutableData) @@ -956,7 +957,17 @@ module.exports.defaultAppState = () => { count: 0 }, defaultWindowParams: {}, - searchDetail: null + searchDetail: null, + pageData: { + info: {}, + last: { + info: '', + url: '', + tabId: -1 + }, + load: [], + view: {} + } } } diff --git a/docs/state.md b/docs/state.md index c8d807a0bd8..7fe72d38293 100644 --- a/docs/state.md +++ b/docs/state.md @@ -172,6 +172,85 @@ AppStore themeColor: string } }, + ledger: { + isBooting: boolean, // flag which telll us if wallet is still creating or not + isQuiting: boolan, // flag which tell us if we are closing ledger (because of browser close) + synopsis: { + options: { + emptyScores: { + concave: number, + visits: number + }, + frameSize: number, + minPublisherDuration: number, + minPublisherVisits: number, + numFrames: number, + scorekeeper: string, // concave or visits + scorekeepers: Array, // concave and visits + showOnlyVerified: boolean + }, + publishers: { + [publisherId]: { + duration: number, + faviconUrl: string, + options: { + exclude: boolean, + verified: boolean, + stickyP: boolean + }, + pinPercentage: number, + protocol: string, + scores: { + concave: number, + visits: number + }, + visits: number, + weight: number + } + } + }, + creating: boolean, + created: boolan, + reconcileFrequency: number, + reconcileStamp: number, + address: string, // Bitcoin wallet address + + // Bitcoin wallet balance (truncated BTC and satoshis) + balance: undefined, + unconfirmed: undefined, + satoshis: undefined, + + // the desired contribution (the btc value approximates the amount/currency designation) + btc: undefined, + amount: undefined, + currency: undefined, + + paymentURL: undefined, + buyURL: undefined, + bravery: undefined, + + // wallet credentials + paymentId: undefined, + passphrase: undefined, + + // advanced ledger settings + minPublisherDuration: undefined, + minPublisherVisits: undefined, + showOnlyVerified: undefined, + + hasBitcoinHandler: false, + + // geoIP/exchange information + countryCode: undefined, + exchangeInfo: undefined, + + _internal: { + exchangeExpiry: 0, + exchanges: {}, + geoipExpiry: 0 + }, + error: null // TODO we don't need it anymore + }, menu: { template: object // used on Windows and by our tests: template object with Menubar control }, @@ -192,6 +271,39 @@ AppStore noScript: { enabled: boolean // enable noscript }, + pageData: { + info: [{ + faviconURL: string, + protocol: string, + publisher: string, + timestamp: number, + url: string, + }], + last: { + info: string, // last added info + tabId: number, // last active tabId + url: string // last active URL + }, + load: [{ + timestamp: number, + url: string, + tabId: number, + details: { + status: boolean, + newURL: string, + originalURL: string, + httpResponseCode: number, + requestMethod: string, + referrer: string, + resourceType: string + } + }], + view: { + timestamp: number, + url: string, + tabId: number + } // we save only the last view + }, pinnedSites: { [siteKey]: { location: string, @@ -283,9 +395,6 @@ AppStore 'advanced.minimum-visits': number, 'advanced.auto-suggest-sites': boolean // show auto suggestion }, - locationSiteKeyCache: { - [location]: Array. // location -> site keys - }, siteSettings: { [hostPattern]: { adControl: string, // (showBraveAds | blockAds | allowAdsAndTracking) @@ -647,29 +756,6 @@ WindowStore timestamp: number // timestamp in milliseconds } }, - publisherInfo: { - synopsis: [{ - daysSpent: number, // e.g., 1 - duration: number, // total millisecond-views, e.g., 93784000 = 1 day, 2 hours, 3 minutes, 4 seconds - faviconURL: string, // i.e., "data:image/...;base64,..." - hoursSpent: number, // e.g., 2 - minutesSpent: number, // e.g., 3 - percentage: number, // i.e., 0, 1, ... 100 - pinPercentage: number, // i.e., 0, 1, ... 100 - publisherURL: string, // publisher site, e.g., "https://wikipedia.org/" - rank: number, // i.e., 1, 2, 3, ... - score: number, // float indicating the current score - secondsSpent: number, // e.g., 4 - site: string, // publisher name, e.g., "wikipedia.org" - verified: boolean, // there is a verified wallet for this publisher - views: number, // total page-views, - weight: number // float indication of the ration - }], // one entry for each publisher having a non-zero `score` - synopsisOptions: { - minPublisherDuration: number, // e.g., 8000 for 8 seconds - minPublisherVisits: number // e.g., 0 - } - }, searchResults: array, // autocomplete server results if enabled ui: { bookmarksToolbar: { diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 91626bdd9a9..c840693f4ae 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -196,6 +196,7 @@ const handleAppAction = (action) => { require('../../app/browser/reducers/updatesReducer'), require('../../app/browser/reducers/topSitesReducer'), require('../../app/browser/reducers/braverySettingsReducer'), + require('../../app/browser/reducers/pageDataReducer'), require('../../app/ledger').doAction, require('../../app/browser/menu') ] diff --git a/js/stores/eventStore.js b/js/stores/eventStore.js deleted file mode 100644 index c33068cff38..00000000000 --- a/js/stores/eventStore.js +++ /dev/null @@ -1,156 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const appConstants = require('../constants/appConstants') -const appDispatcher = require('../dispatcher/appDispatcher') -const AppStore = require('./appStore') -const EventEmitter = require('events').EventEmitter -const Immutable = require('immutable') -const windowConstants = require('../constants/windowConstants') -const debounce = require('../lib/debounce') -const {getWebContents} = require('../../app/browser/webContentsCache') -const {isSourceAboutUrl} = require('../lib/appUrlUtil') -const {responseHasContent} = require('../../app/common/lib/httpUtil') - -const electron = require('electron') -const BrowserWindow = electron.BrowserWindow - -let eventState = Immutable.fromJS({ - page_load: [], - page_view: [], - page_info: [] -}) - -const CHANGE_EVENT = 'change' - -class EventStore extends EventEmitter { - getState () { - return eventState - } - - emitChanges () { - this.emit(CHANGE_EVENT) - } - - addChangeListener (callback) { - this.on(CHANGE_EVENT, callback) - } - - removeChangeListener (callback) { - this.removeListener(CHANGE_EVENT, callback) - } -} - -const eventStore = new EventStore() -const emitChanges = debounce(eventStore.emitChanges.bind(eventStore), 5) - -let lastActivePageUrl = null -let lastActiveTabId = null - -const addPageView = (url, tabId) => { - const tab = getWebContents(tabId) - const isPrivate = !tab || - tab.isDestroyed() || - !tab.session.partition.startsWith('persist:') - - if ((url && isSourceAboutUrl(url)) || isPrivate) { - url = null - } - - if (lastActivePageUrl === url) { - return - } - - let pageViewEvent = Immutable.fromJS({ - timestamp: new Date().getTime(), - url, - tabId - }) - eventState = eventState.set('page_view', eventState.get('page_view').slice(-100).push(pageViewEvent)) - lastActivePageUrl = url -} - -const windowBlurred = (windowId) => { - let windowCount = BrowserWindow.getAllWindows().filter((win) => win.isFocused()).length - if (windowCount === 0) { - addPageView(null, null) - } -} - -const windowClosed = (windowId) => { - let windowCount = BrowserWindow.getAllWindows().length - let win = BrowserWindow.getFocusedWindow() - // window may not be closed yet - if (windowCount > 0 && win && win.id === windowId) { - win.once('closed', () => { - windowClosed(windowId) - }) - } - - if (!win || windowCount === 0) { - addPageView(null, null) - } -} - -// Register callback to handle all updates -const doAction = (action) => { - switch (action.actionType) { - case windowConstants.WINDOW_SET_FOCUSED_FRAME: - if (action.location) { - addPageView(action.location, action.tabId) - } - break - case appConstants.APP_WINDOW_BLURRED: - windowBlurred(action.windowId) - break - case appConstants.APP_IDLE_STATE_CHANGED: - if (action.idleState !== 'active') { - addPageView(null, null) - } else { - addPageView(lastActivePageUrl, lastActiveTabId) - } - break - case appConstants.APP_CLOSE_WINDOW: - appDispatcher.waitFor([AppStore.dispatchToken], () => { - windowClosed(action.windowId) - }) - break - case 'event-set-page-info': - // retains all past pages, not really sure that's needed... [MTR] - eventState = eventState.set('page_info', eventState.get('page_info').slice(-100).push(action.pageInfo)) - break - case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: - // Only capture response for the page (not subresources, like images, JavaScript, etc) - if (action.details && action.details.resourceType === 'mainFrame') { - const pageUrl = action.details.newURL - - // create a page view event if this is a page load on the active tabId - if (!lastActiveTabId || action.tabId === lastActiveTabId) { - addPageView(pageUrl, action.tabId) - } - - const responseCode = action.details.httpResponseCode - if (isSourceAboutUrl(pageUrl) || !responseHasContent(responseCode)) { - break - } - - const pageLoadEvent = Immutable.fromJS({ - timestamp: new Date().getTime(), - url: pageUrl, - tabId: action.tabId, - details: action.details - }) - eventState = eventState.set('page_load', eventState.get('page_load').slice(-100).push(pageLoadEvent)) - } - break - default: - return - } - - emitChanges() -} - -appDispatcher.register(doAction) - -module.exports = eventStore diff --git a/test/unit/app/browser/reducers/pageDataReducerTest.js b/test/unit/app/browser/reducers/pageDataReducerTest.js new file mode 100644 index 00000000000..7e0029dfbe1 --- /dev/null +++ b/test/unit/app/browser/reducers/pageDataReducerTest.js @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* global describe, it, before, after, afterEach */ +const mockery = require('mockery') +const Immutable = require('immutable') +const assert = require('assert') +const sinon = require('sinon') + +const fakeElectron = require('../../../lib/fakeElectron') +const appConstants = require('../../../../../js/constants/appConstants') +const windowConstants = require('../../../../../js/constants/windowConstants') + +describe('pageDataReducer unit tests', function () { + let pageDataReducer, pageDataState + + const state = Immutable.fromJS({ + pageData: { + view: {}, + load: [], + info: {}, + last: { + info: '', + tabId: null + } + } + }) + + before(function () { + this.clock = sinon.useFakeTimers() + this.clock.tick(0) + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('../../browser/webContentsCache', { + getWebContents: (tabId) => { + if (tabId == null) return null + + return { + isDestroyed: () => false, + session: { + partition: 'persist:0' + } + } + } + }) + pageDataState = require('../../../../../app/common/state/pageDataState') + pageDataReducer = require('../../../../../app/browser/reducers/pageDataReducer') + }) + + after(function () { + mockery.disable() + this.clock.restore() + }) + + describe('WINDOW_SET_FOCUSED_FRAME', function () { + let spy + + afterEach(function () { + spy.restore() + }) + + it('null case', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: windowConstants.WINDOW_SET_FOCUSED_FRAME + }) + + assert.equal(spy.notCalled, true) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: windowConstants.WINDOW_SET_FOCUSED_FRAME, + location: 'https://brave.com', + tabId: 1 + }) + + const expectedState = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + })) + + assert.equal(spy.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('APP_WINDOW_BLURRED', function () { + + }) + + describe('APP_IDLE_STATE_CHANGED', function () { + + }) + + describe('event-set-page-info', function () { + + }) + + describe('APP_WINDOW_CLOSED', function () { + let spy + + afterEach(function () { + spy.restore() + }) + + it('data is ok', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: appConstants.APP_WINDOW_CLOSED + }) + + const expectedState = state + .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: null, + tabId: null + })) + + assert.equal(spy.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('WINDOW_GOT_RESPONSE_DETAILS', function () { + + }) +}) diff --git a/test/unit/app/common/lib/ledgerUtilTest.js b/test/unit/app/common/lib/ledgerUtilTest.js index df643fb74ba..9210b698665 100644 --- a/test/unit/app/common/lib/ledgerUtilTest.js +++ b/test/unit/app/common/lib/ledgerUtilTest.js @@ -1,15 +1,42 @@ /* global describe, it */ const ledgerUtil = require('../../../../../app/common/lib/ledgerUtil') const assert = require('assert') +const Immutable = require('immutable') require('../../../braveUnit') describe('ledgerUtil test', function () { describe('shouldTrackView', function () { - const validView = { tabId: 1, url: 'https://brave.com/' } - const validResponseList = [{ tabId: validView.tabId, details: { newURL: validView.url, httpResponseCode: 200 } }] - const noMatchResponseList = [{ tabId: 3, details: { newURL: 'https://not-brave.com' } }] - const matchButErrored = [{ tabId: validView.tabId, details: { newURL: validView.url, httpResponseCode: 404 } }] + const validView = Immutable.fromJS({ + tabId: 1, + url: 'https://brave.com/' + }) + const validResponseList = Immutable.fromJS([ + { + tabId: validView.get('tabId'), + details: { + newURL: validView.get('url'), + httpResponseCode: 200 + } + } + ]) + const noMatchResponseList = Immutable.fromJS([ + { + tabId: 3, + details: { + newURL: 'https://not-brave.com' + } + } + ]) + const matchButErrored = Immutable.fromJS([ + { + tabId: validView.get('tabId'), + details: { + newURL: validView.get('url'), + httpResponseCode: 404 + } + } + ]) describe('input validation', function () { it('returns false if view is falsey', function () { diff --git a/test/unit/app/common/lib/pageDataUtilTest.js b/test/unit/app/common/lib/pageDataUtilTest.js new file mode 100644 index 00000000000..70744ec882d --- /dev/null +++ b/test/unit/app/common/lib/pageDataUtilTest.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global describe, it */ +const pageDataUtil = require('../../../../../app/common/lib/pageDataUtil') +const assert = require('assert') + +require('../../../braveUnit') + +describe('pageDataUtil unit tests', () => { + describe('getInfoKey', () => { + it('null case', () => { + const result = pageDataUtil.getInfoKey() + assert.equal(result, null) + }) + + it('url is converted to location', () => { + const result = pageDataUtil.getInfoKey('https://brave.com') + assert.equal(result, 'https://brave.com/') + }) + }) +}) diff --git a/test/unit/app/common/state/pageDataStateTest.js b/test/unit/app/common/state/pageDataStateTest.js new file mode 100644 index 00000000000..bf99e051a82 --- /dev/null +++ b/test/unit/app/common/state/pageDataStateTest.js @@ -0,0 +1,304 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global describe, it, before, beforeEach, after */ +const Immutable = require('immutable') +const assert = require('assert') +const sinon = require('sinon') +const mockery = require('mockery') + +describe('pageDataState unit tests', function () { + let pageDataState, isPrivate, clock, now + + const state = Immutable.fromJS({ + pageData: { + view: {}, + load: [], + info: {}, + last: { + info: '', + tabId: null + } + } + }) + + const stateWithData = Immutable.fromJS({ + pageData: { + view: { + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + }, + load: [ + { + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + } + ], + info: { + 'https://brave.com/': { + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + } + }, + last: { + info: '', + tabId: 1 + } + } + }) + + before(function () { + clock = sinon.useFakeTimers() + now = new Date(0) + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('../../browser/webContentsCache', { + getWebContents: (tabId) => { + if (tabId == null) return null + + return { + isDestroyed: () => false, + session: { + partition: isPrivate ? '' : 'persist:0' + } + } + } + }) + pageDataState = require('../../../../../app/common/state/pageDataState') + }) + + beforeEach(function () { + isPrivate = false + }) + + after(function () { + mockery.disable() + clock.restore() + }) + + describe('addView', function () { + it('null case', function () { + const result = pageDataState.addView(state) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: null, + tabId: null + })) + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + + it('url is the same as last one', function () { + const newState = state + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + })) + const result = pageDataState.addView(state, 'https://brave.com', 1) + const expectedResult = newState + .setIn(['pageData', 'last', 'tabId'], 1) + + assert.deepEqual(result, expectedResult) + }) + + it('url is private', function () { + isPrivate = true + + const result = pageDataState.addView(state, 'https://brave.com', 1) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: null, + tabId: 1 + })) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + + it('url is about page', function () { + const result = pageDataState.addView(state, 'about:history', 1) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: null, + tabId: 1 + })) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + + it('url is ok', function () { + const result = pageDataState.addView(state, 'https://brave.com', 1) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + })) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + }) + + describe('addInfo', function () { + it('null case', function () { + const result = pageDataState.addInfo(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + const data = Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com' + }) + + const result = pageDataState.addInfo(state, data) + const expectedResult = state + .setIn(['pageData', 'info', 'https://brave.com/'], data.set('key', 'https://brave.com/')) + .setIn(['pageData', 'last', 'info'], 'https://brave.com/') + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + }) + + describe('addLoad', function () { + it('null case', function () { + const result = pageDataState.addLoad(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + const result = pageDataState.addLoad(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('we only take last 100 views', function () { + let newState = state + + for (let i = 0; i < 100; i++) { + const data = Immutable.fromJS([{ + timestamp: now.getTime(), + url: `https://page${i}.com`, + tabId: 1 + }]) + newState = newState.setIn(['pageData', 'load'], newState.getIn(['pageData', 'load']).push(data)) + } + + const newLoad = Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + }) + + const result = pageDataState.addLoad(newState, newLoad) + const expectedResult = newState + .setIn(['pageData', 'load'], newState.getIn(['pageData', 'load']).shift()) + .setIn(['pageData', 'load'], newState.getIn(['pageData', 'load']).push(newLoad)) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + }) + + describe('getView', function () { + it('null case', function () { + const result = pageDataState.getView(state) + assert.deepEqual(result, Immutable.Map()) + }) + + it('data is ok', function () { + const result = pageDataState.getView(stateWithData) + assert.deepEqual(result.toJS(), stateWithData.getIn(['pageData', 'view']).toJS()) + }) + }) + + describe('getLastInfo', function () { + it('null case', function () { + const result = pageDataState.getLastInfo(state) + assert.deepEqual(result, Immutable.Map()) + }) + + it('key is provided, but data is not there', function () { + const newState = state + .setIn(['pageData', 'last', 'info'], 'https://brave.com/') + .setIn(['pageData', 'info', 'https://test.com/'], Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://test.com', + tabId: 1 + })) + + const result = pageDataState.getLastInfo(newState) + assert.deepEqual(result, Immutable.Map()) + }) + + it('key is provided and data is there', function () { + const info = Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + }) + + const newState = state + .setIn(['pageData', 'last', 'info'], 'https://brave.com/') + .setIn(['pageData', 'info', 'https://brave.com/'], info) + + const result = pageDataState.getLastInfo(newState) + assert.deepEqual(result.toJS(), info.toJS()) + }) + }) + + describe('getLoad', function () { + it('null case', function () { + const result = pageDataState.getLoad(state) + assert.deepEqual(result, Immutable.List()) + }) + + it('data is there', function () { + const result = pageDataState.getLoad(stateWithData) + assert.deepEqual(result.toJS(), stateWithData.getIn(['pageData', 'load']).toJS()) + }) + }) + + describe('getLastActiveTabId', function () { + it('null case', function () { + const result = pageDataState.getLastActiveTabId(state) + assert.deepEqual(result, -1) + }) + + it('data is there', function () { + const result = pageDataState.getLastActiveTabId(stateWithData) + assert.deepEqual(result, 1) + }) + }) + + describe('setLastActiveTabId', function () { + it('id is saved', function () { + const result = pageDataState.setLastActiveTabId(state, 10) + const expectedState = state.setIn(['pageData', 'last', 'tabId'], 10) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('setPublisher', function () { + it('null case', function () { + const result = pageDataState.setPublisher(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + const result = pageDataState.setPublisher(stateWithData, 'https://brave.com/', 'https://brave.com') + const expectedState = stateWithData.setIn(['pageData', 'info', 'https://brave.com/', 'publisher'], 'https://brave.com') + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) +})