diff --git a/package-lock.json b/package-lock.json index 87e9fb269..858409fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ipfs-webui", - "version": "2.3.3", + "version": "2.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4880,9 +4880,9 @@ } }, "countly-sdk-web": { - "version": "18.11.0", - "resolved": "https://registry.npmjs.org/countly-sdk-web/-/countly-sdk-web-18.11.0.tgz", - "integrity": "sha512-qSa0kUxAqLTIhSFRvk6bLgiEWaS6/UcPzPen/F9MF0PkVoww7z1F692m0zDgq37uHf9knih2SvJOx89f3auvCA==" + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/countly-sdk-web/-/countly-sdk-web-19.2.1.tgz", + "integrity": "sha512-EcVxRjwIREaMaGy9Th4cIbB4RZ9PVFT3AdD/2vbSgMWRvbEGDJMtP0H5cBiS1zIExBiOEAuu77BPSiU2sO/8Og==" }, "create-ecdh": { "version": "4.0.3", @@ -15520,14 +15520,25 @@ } }, "react": { - "version": "16.6.3", - "resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz", - "integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==", + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.4.tgz", + "integrity": "sha512-0GQ6gFXfUH7aZcjGVymlPOASTuSjlQL4ZtVC5YKH+3JL6bBLCVO21DknzmaPlI90LN253ojj02nsapy+j7wIjg==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.11.2" + "scheduler": "^0.13.4" + }, + "dependencies": { + "scheduler": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.4.tgz", + "integrity": "sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } } }, "react-ace": { @@ -15701,14 +15712,25 @@ } }, "react-dom": { - "version": "16.6.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.3.tgz", - "integrity": "sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==", + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.4.tgz", + "integrity": "sha512-Ob2wK7XG2tUDt7ps7LtLzGYYB6DXMCLj0G5fO6WeEICtT4/HdpOi7W/xLzZnR6RCG1tYza60nMdqtxzA8FaPJQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.11.2" + "scheduler": "^0.13.4" + }, + "dependencies": { + "scheduler": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.4.tgz", + "integrity": "sha512-cvSOlRPxOHs5dAhP9yiS/6IDmVAVxmk33f0CtTJRkmUWcb1Us+t7b1wqdzoC0REw2muC9V5f1L/w5R5uKGaepA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } } }, "react-error-overlay": { diff --git a/package.json b/package.json index b5714ae5c..4b5fe153f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@tableflip/react-inspector": "^2.3.0", "brace": "^0.11.1", "chart.js": "^2.7.2", - "countly-sdk-web": "^18.11.0", + "countly-sdk-web": "^19.2.1", "d3": "^5.7.0", "details-polyfill": "^1.1.0", "file-extension": "^4.0.5", @@ -52,7 +52,7 @@ "multiaddr-to-uri": "^4.0.1", "prop-types": "^15.6.2", "pull-file-reader": "^1.0.2", - "react": "^16.4.2", + "react": "^16.8.4", "react-ace": "^6.1.4", "react-chartjs-2": "^2.7.4", "react-copy-to-clipboard": "^5.0.1", @@ -60,7 +60,7 @@ "react-debounce-render": "^4.0.3", "react-dnd": "^5.0.0", "react-dnd-html5-backend": "^5.0.1", - "react-dom": "^16.4.2", + "react-dom": "^16.8.4", "react-faux-dom": "^4.2.0", "react-helmet": "^5.2.0", "react-i18next": "^7.11.0", diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index efaef58b0..253772006 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -5,6 +5,7 @@ "reset": "Reset", "language": "Language", "config": "IPFS Config", + "analytics": "Analytics", "languageModal": { "title": "Change Language", "description": "Pick your preferred language" @@ -30,18 +31,35 @@ "downloadCopiedHashDescription": "Use <0>{ctrlKey}0> + <2>{altKey}2> + <3>D3> to download the last copied hash.", "AnalyticsToggle": { "label": "Help improve this app by sending anonymous usage data", - "summary": "What data is collected?", - "paragraph1": "Protocol Labs hosts a <1>Countly1> instance to record anonymous usage data for this app.", - "paragraph2": "The information collected includes:", - "item0": "A random, generated device ID", - "item1": "Session duration", - "item2": "Country code & city from IP address. IP address is discarded", - "item3": "Operating system and version", - "item4": "Display resolution and density", - "item5": "Locale (browser language, e.g German)", - "item6": "Browser information", - "item7": "Which app sections are visited", - "item8": "App errors", - "paragraph3": "No CIDs, filenames, or other personal information are collected. We want metrics to show us which features are useful to help us prioritise what to work on next, and system configuration information to guide our testing." + "summary": "Configure what is collected", + "paragraph1": "No CIDs, filenames, or other personal information are collected. We want metrics to show us which features are useful to help us prioritise what to work on next, and system configuration information to guide our testing.", + "paragraph2": "Protocol Labs hosts a <1>Countly1> instance to record anonymous usage data for this app.", + "basicInfo": "The following additional information is sent to countly.ipfs.io", + "optionalInfo": "You can opt-in to send the following information:", + "sessions": { + "label": "Sessions", + "summary": "when and how long you use the app, and browser metrics", + "details": "<0>The following browser metrics are sent:0><1><0>A random, generated device ID0><1>Timestamp when the session starts1><2>Periodic timestamps to track duration2><3>App version e.g. 2.4.43><4>Locale e.g. en-GB4><5>User Agent e.g. Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) ...5><6>Screen resolution e.g. 800x6006><7>Screen pixel density e.g. 17>1>" + }, + "events": { + "label": "Events", + "summary": "actions like adding or deleting files, and how long it took", + "details": "App specific actions. We record only that the action happened, how long it took from start to finish, and a count if the event involved multiple items." + }, + "views": { + "label": "Page views", + "summary": "which sections of the app you visit", + "details": "Records which sections of the apps are visited. The paths recorded only include the pattern used to match the route rather than the full url. " + }, + "location": { + "label": "Location", + "summary": "Country code from IP address. IP address is discarded", + "details": "Your IP address is used to calculate a Country code for your location, like \"FR\" for France. Where consent is not given to track location, an empty location property is sent with all requests to signal to the server to disable the location look up. This information helps guide our translation effort and figure out where we should put on events." + }, + "crashes": { + "label": "App errors", + "summary": "JavaScript error messages and stack traces", + "details": "Records JavaScript error messages and stack traces that occur while using the app, where possible. It is very helpful to know when the app is not working for you, but <1>error messages may include identifiable information1> like CIDs or file paths, so only enable this if you are comfortable sharing that information with us." + } } } diff --git a/public/locales/en/status.json b/public/locales/en/status.json index 469e9c760..98915af4d 100644 --- a/public/locales/en/status.json +++ b/public/locales/en/status.json @@ -39,8 +39,9 @@ "paragraph2": "<0>For more info on how to get started with IPFS you can <1>read the guide1>.0>" }, "AskToEnable": { - "label": "Help improve this app by sending anonymous usage data", - "yesLabel": "Enable", - "noLabel": "No thanks" + "label": "Help improve this app by sending anonymous usage data.", + "yesLabel": "OK", + "noLabel": "No thanks", + "detailsLabel": "More info" } } diff --git a/src/bundles/analytics.js b/src/bundles/analytics.js index a67e45736..489a2f0ff 100644 --- a/src/bundles/analytics.js +++ b/src/bundles/analytics.js @@ -1,76 +1,108 @@ import root from 'window-or-global' -const IGNORE_ACTIONS = /^(FILES_FETCH_|FILES_WRITE_UPDATED)/ -const USER_ACTIONS = /^(CONFIG_SAVE_|FILES_|DESKTOP_)/ -const ASYNC_ACTIONS = /^(.+)_(STARTED|FINISHED|FAILED)$/ +// Only record specific actions listed here. +const ASYNC_ACTIONS_TO_RECORD = [ + 'IPFS_INIT', + 'CONFIG_SAVE', + 'FILES_MAKEDIR', + 'FILES_WRITE', + 'FILES_ADDBYPATH', + 'FILES_MOVE', + 'FILES_DELETE', + 'FILES_DOWNLOADLINK' +] + +const ASYNC_ACTION_RE = new RegExp(`^${ASYNC_ACTIONS_TO_RECORD.join('_|')}`) +const ASYNC_ACTION_STATE_RE = /^(.+)_(STARTED|FINISHED|FAILED)$/ + +const COUNTLY_KEY_WEBUI = '8fa213e6049bff23b08e5f5fbac89e7c27397612' +const COUNTLY_KEY_WEBUI_TEST = '700fd825c3b257e021bd9dbc6cbf044d33477531' +const COUNTLY_KEY_DESKTOP = '47fbb3db3426d2ae32b3b65fe40c564063d8b55d' +const COUNTLY_KEY_DESKTOP_TEST = '6b00e04fa5370b1ce361d2f24a09c74254eee382' + +function pickAppKey () { + const isProd = process.env.NODE_ENV === 'production' + const isDesktop = !!root.ipfsDesktop + if (isDesktop) { + return isProd ? COUNTLY_KEY_DESKTOP : COUNTLY_KEY_DESKTOP_TEST + } else { + return isProd ? COUNTLY_KEY_WEBUI : COUNTLY_KEY_WEBUI_TEST + } +} + +const consentGroups = { + all: ['sessions', 'events', 'views', 'location', 'crashes'], + safe: ['sessions', 'events', 'views', 'location'] +} const createAnalyticsBundle = ({ countlyUrl = 'https://countly.ipfs.io', - countlyAppKey, - appVersion, - appGitRevision, - debug = false + countlyAppKey = pickAppKey(), + appVersion = process.env.REACT_APP_VERSION, + appGitRevision = process.env.REACT_APP_GIT_REV, + debug = true }) => { return { name: 'analytics', - persistActions: ['ANALYTICS_ENABLED', 'ANALYTICS_DISABLED'], + persistActions: ['ANALYTICS_ENABLED', 'ANALYTICS_DISABLED', 'ANALYTICS_ADD_CONSENT', 'ANALYTICS_REMOVE_CONSENT'], init: async (store) => { + // test code sets a mock Counly instance on the global. if (!root.Countly) { - // lazy-load to simplify testing. - root.Countly = await import('countly-sdk-web') + root.Countly = {} + root.Countly.q = [] + await import('countly-sdk-web') } const Countly = root.Countly - Countly.q = Countly.q || [] + Countly.require_consent = true Countly.url = countlyUrl Countly.app_key = countlyAppKey Countly.app_version = appVersion Countly.debug = debug - // Don't track clicks as it can include full url. - // Countly.q.push(['track_clicks']); - // Countly.q.push(['track_links']) + // Configure what to track. Nothing is sent without user consent. Countly.q.push(['track_sessions']) - Countly.q.push(['track_pageview']) Countly.q.push(['track_errors']) - if (!store.selectAnalyticsEnabled()) { - Countly.q.push(['opt_out']) - Countly.ignore_visitor = true - } + // Don't track clicks or links as it can include full url. + // Countly.q.push(['track_clicks']) + // Countly.q.push(['track_links']) - Countly.init() + if (store.selectAnalyticsEnabled()) { + const consent = store.selectAnalyticsConsent() + Countly.q.push(['add_consent', consent]) + } store.subscribeToSelectors(['selectRouteInfo'], ({ routeInfo }) => { + // skip routes with no hash, as we'll be immediately redirected to `/#` + if (!root.location || !root.location.hash) return /* By tracking the pattern rather than the window.location, we limit the info we collect to just the app sections that are viewed, and avoid recording specific CIDs or local repo paths that would contain personal information. */ - if (root.Countly) { - root.Countly.q.push(['track_pageview', routeInfo.pattern]) - } + root.Countly.q.push(['track_pageview', routeInfo.pattern]) }) + + Countly.init() }, - // Record durations for user actions + // Listen to redux actions getMiddleware: () => (store) => { const EventMap = new Map() return next => action => { - const res = next(action) - if (store.selectAnalyticsEnabled() && !IGNORE_ACTIONS.test(action.type) && USER_ACTIONS.test(action.type)) { - if (ASYNC_ACTIONS.test(action.type)) { - const [_, name, state] = ASYNC_ACTIONS.exec(action.type) // eslint-disable-line no-unused-vars - if (state === 'STARTED') { - EventMap.set(name, root.performance.now()) + // Record durations for async actions + if (ASYNC_ACTION_RE.test(action.type)) { + const [_, name, state] = ASYNC_ACTION_STATE_RE.exec(action.type) // eslint-disable-line no-unused-vars + if (state === 'STARTED') { + EventMap.set(name, root.performance.now()) + } else { + const start = EventMap.get(name) + if (!start) { + EventMap.delete(name) } else { - const start = EventMap.get(name) - if (!start) { - EventMap.delete(name) - return - } const durationInSeconds = (root.performance.now() - start) / 1000 root.Countly.q.push(['add_event', { key: state === 'FAILED' ? action.type : name, @@ -78,49 +110,66 @@ const createAnalyticsBundle = ({ dur: durationInSeconds }]) } - } else { - root.Countly.q.push(['add_event', { - key: action.type, - count: 1 - }]) + } + + // Record errors. Only from explicitly selected actions. + const error = action.error || (action.payload && action.payload.error) + if (error) { + root.Countly.q.push(['add_log', action.type]) + root.Countly.q.push(['log_error', error]) } } - return res + // We're middleware. Don't forget to pass control back to the next. + return next(action) } }, - reducer: (state = { lastEnabledAt: 0, lastDisabledAt: 0 }, action) => { + reducer: (state = { + lastEnabledAt: 0, + lastDisabledAt: 0, + consent: [] + }, action) => { if (action.type === 'ANALYTICS_ENABLED') { - return { ...state, lastEnabledAt: Date.now() } + return { ...state, lastEnabledAt: Date.now(), consent: action.payload.consent } } if (action.type === 'ANALYTICS_DISABLED') { - return { ...state, lastDisabledAt: Date.now() } + return { ...state, lastDisabledAt: Date.now(), consent: action.payload.consent } + } + if (action.type === 'ANALYTICS_ADD_CONSENT') { + const consent = state.consent.filter(item => item !== action.payload.name).concat(action.payload.name) + return { ...state, lastEnabledAt: Date.now(), consent } } + if (action.type === 'ANALYTICS_REMOVE_CONSENT') { + const consent = state.consent.filter(item => item !== action.payload.name) + const lastDisabledAt = (consent.length === 0) ? Date.now() : state.lastDisabledAt + return { ...state, lastDisabledAt, consent } + } + + // deal with missing consent state from 2.4.0 release. + if (!state.consent) { + if (state.lastEnabledAt > state.lastDisabledAt) { + return { ...state, consent: consentGroups.safe } + } else { + return { ...state, consent: [] } + } + } + return state }, selectAnalytics: (state) => state.analytics, - /* - Use the users preference. - */ - selectAnalyticsEnabled: (state) => { - const { lastEnabledAt, lastDisabledAt } = state.analytics - // where never opted in or out, analytics are disabled by default - if (!lastEnabledAt && !lastDisabledAt) { - return false - } - // otherwise return their most recent choice. - return lastEnabledAt > lastDisabledAt - }, + selectAnalyticsConsent: (state) => state.analytics.consent, + + selectAnalyticsEnabled: (state) => state.analytics.consent.length > 0, /* Ask the user if we may enable analytics. */ selectAnalyticsAskToEnable: (state) => { - const { lastEnabledAt, lastDisabledAt } = state.analytics + const { lastEnabledAt, lastDisabledAt, consent } = state.analytics // user has not explicitly chosen - if (!lastEnabledAt && !lastDisabledAt) { + if (!lastEnabledAt && !lastDisabledAt && consent.length === 0) { // ask to enable. return true } @@ -128,7 +177,11 @@ const createAnalyticsBundle = ({ return false }, - doToggleAnalytics: () => async ({ dispatch, store }) => { + selectAnalyticsActionsToRecord: () => { + return Array.from(ASYNC_ACTIONS_TO_RECORD) + }, + + doToggleAnalytics: () => ({ dispatch, store }) => { const enable = !store.selectAnalyticsEnabled() if (enable) { store.doEnableAnalytics() @@ -137,14 +190,34 @@ const createAnalyticsBundle = ({ } }, - doDisableAnalytics: () => async ({ dispatch, store }) => { - root.Countly.opt_out() - dispatch({ type: 'ANALYTICS_DISABLED' }) + doDisableAnalytics: () => ({ dispatch, store }) => { + root.Countly.q.push(['remove_consent', consentGroups.all]) + dispatch({ type: 'ANALYTICS_DISABLED', payload: { consent: [] } }) + }, + + doEnableAnalytics: () => ({ dispatch, store }) => { + root.Countly.q.push(['remove_consent', consentGroups.all]) + root.Countly.q.push(['add_consent', consentGroups.safe]) + dispatch({ type: 'ANALYTICS_ENABLED', payload: { consent: consentGroups.safe } }) + }, + + doToggleConsent: (name) => ({ dispatch, store }) => { + const isEnabled = store.selectAnalyticsConsent().includes(name) + if (isEnabled) { + store.doRemoveConsent(name) + } else { + store.doAddConsent(name) + } + }, + + doRemoveConsent: (name) => ({ dispatch, store }) => { + root.Countly.q.push(['remove_consent', name]) + dispatch({ type: 'ANALYTICS_REMOVE_CONSENT', payload: { name } }) }, - doEnableAnalytics: () => async ({ dispatch, store }) => { - root.Countly.opt_in() - dispatch({ type: 'ANALYTICS_ENABLED' }) + doAddConsent: (name) => ({ dispatch, store }) => { + root.Countly.q.push(['add_consent', name]) + dispatch({ type: 'ANALYTICS_ADD_CONSENT', payload: { name } }) } } } diff --git a/src/bundles/analytics.test.js b/src/bundles/analytics.test.js index 3e5566651..1c5a0c16e 100644 --- a/src/bundles/analytics.test.js +++ b/src/bundles/analytics.test.js @@ -1,10 +1,10 @@ /* global it, expect, beforeEach, afterEach, jest */ import { composeBundlesRaw } from 'redux-bundler' import createAnalyticsBundle from './analytics' -import sleep from '../../test/helpers/sleep' beforeEach(() => { global.Countly = { + q: [], opt_out: jest.fn(), opt_in: jest.fn(), init: jest.fn() @@ -25,16 +25,24 @@ function createStore (analyticsOpts = {}) { )() } +it('should disable analytics by default', () => { + const store = createStore() + expect(store.selectAnalyticsEnabled()).toBe(false) + expect(store.selectAnalyticsConsent()).toEqual([]) +}) + it('should enable analytics if user has explicitly enabled it', () => { const store = createStore() store.doEnableAnalytics() expect(store.selectAnalyticsEnabled()).toBe(true) + expect(store.selectAnalyticsConsent()).toEqual(['sessions', 'events', 'views', 'location']) }) it('should disable analytics if user has explicitly disabled it', () => { const store = createStore() store.doDisableAnalytics() expect(store.selectAnalyticsEnabled()).toBe(false) + expect(store.selectAnalyticsConsent()).toEqual([]) }) it('should enable selectAnalyticsAskToEnable if user has not explicity enabled or disabled it', () => { @@ -60,18 +68,47 @@ it('should disable selectAnalyticsAskToEnable if analytics are enabled', () => { expect(store.selectAnalyticsAskToEnable()).toBe(false) }) -it('should toggle analytics', async (done) => { +it('should toggle analytics', () => { const store = createStore() - expect(store.selectAnalyticsEnabled()).toBe(false) store.doToggleAnalytics() expect(store.selectAnalyticsEnabled()).toBe(true) + expect(store.selectAnalyticsConsent()).toEqual(['sessions', 'events', 'views', 'location']) + + store.doToggleAnalytics() + expect(store.selectAnalyticsEnabled()).toBe(false) + expect(store.selectAnalyticsConsent()).toEqual([]) +}) + +it('should toggle consent', () => { + const store = createStore() - // we calc enabled state from time diff between lastEnabledAt and lastDisabledAt, so need a pause - await sleep() + store.doToggleConsent('crashes') + expect(store.selectAnalyticsEnabled()).toBe(true) + expect(store.selectAnalyticsConsent()).toEqual(['crashes']) store.doToggleAnalytics() expect(store.selectAnalyticsEnabled()).toBe(false) + expect(store.selectAnalyticsConsent()).toEqual([]) + + store.doToggleAnalytics() + expect(store.selectAnalyticsEnabled()).toBe(true) + expect(store.selectAnalyticsConsent()).toEqual(['sessions', 'events', 'views', 'location']) + + store.doToggleConsent('sessions') + expect(store.selectAnalyticsEnabled()).toBe(true) + expect(store.selectAnalyticsConsent()).toEqual(['events', 'views', 'location']) - done() + store.doToggleConsent('location') + expect(store.selectAnalyticsEnabled()).toBe(true) + expect(store.selectAnalyticsConsent()).toEqual(['events', 'views']) + + store.doToggleConsent('views') + expect(store.selectAnalyticsEnabled()).toBe(true) + expect(store.selectAnalyticsConsent()).toEqual(['events']) + + // Removing all consent is equivalent to disabling the analytics. + store.doToggleConsent('events') + expect(store.selectAnalyticsEnabled()).toBe(false) + expect(store.selectAnalyticsConsent()).toEqual([]) }) diff --git a/src/bundles/index.js b/src/bundles/index.js index 36c1052be..d3da9a20f 100644 --- a/src/bundles/index.js +++ b/src/bundles/index.js @@ -22,9 +22,6 @@ import ipfsDesktop from './ipfs-desktop' import repoStats from './repo-stats' import createAnalyticsBundle from './analytics' -const COUNTLY_KEY_WEBUI = '8fa213e6049bff23b08e5f5fbac89e7c27397612' -const COUNTLY_KEY_DESKTOP = '47fbb3db3426d2ae32b3b65fe40c564063d8b55d' - export default composeBundles( createCacheBundle(bundleCache.set), appIdle({ idleTimeout: 5000 }), @@ -79,9 +76,5 @@ export default composeBundles( retryInitBundle, ipfsDesktop, repoStats, - createAnalyticsBundle({ - countlyAppKey: window.ipfsDesktop ? COUNTLY_KEY_DESKTOP : COUNTLY_KEY_WEBUI, - appVersion: process.env.REACT_APP_VERSION, - appGitRevision: process.env.REACT_APP_GIT_REV - }) + createAnalyticsBundle({}) ) diff --git a/src/bundles/routes.js b/src/bundles/routes.js index 75f54c5cc..bd0391c80 100644 --- a/src/bundles/routes.js +++ b/src/bundles/routes.js @@ -4,6 +4,7 @@ import FilesPage from '../files/LoadableFilesPage' import { ExplorePage, StartExploringPage } from 'ipld-explorer-components' import PeersPage from '../peers/LoadablePeersPage' import SettingsPage from '../settings/LoadableSettingsPage' +import AnalyticsPage from '../settings/AnalyticsPage' import WelcomePage from '../welcome/LoadableWelcomePage' export default createRouteBundle({ @@ -12,6 +13,7 @@ export default createRouteBundle({ '/files*': FilesPage, '/peers': PeersPage, '/settings': SettingsPage, + '/settings/analytics': AnalyticsPage, '/welcome': WelcomePage, '/': StatusPage, '': StatusPage diff --git a/src/components/analytics-toggle/AnalyticsToggle.js b/src/components/analytics-toggle/AnalyticsToggle.js index f2bfa4665..9c0bd802c 100644 --- a/src/components/analytics-toggle/AnalyticsToggle.js +++ b/src/components/analytics-toggle/AnalyticsToggle.js @@ -1,38 +1,174 @@ -import React from 'react' +import React, { useState } from 'react' import { Trans } from 'react-i18next' +import { connect } from 'redux-bundler-react' import Checkbox from '../checkbox/Checkbox' import Details from '../details/Details' -const AnalyticsToggle = ({ doToggleAnalytics, analyticsEnabled, t }) => { - // Simplify fetching a list of i18n keys. - const items = Array(9).fill(1) +const ExampleRequest = ({ url, method = 'GET' }) => { + return ( +
+ {method}
{url}
+
+ )
+}
+
+const QueryParams = ({ url }) => {
+ if (!url) return null
+ const params = (new URL(url)).searchParams
+ const entries = [...params]
+ return (
+ + View source +
+ )} + {exampleRequest && ( +{t(`AnalyticsToggle.paragraph1`)}
+
-
{t('AnalyticsToggle.paragraph2')}
-{t(`AnalyticsToggle.paragraph3`)}
+{t('AnalyticsToggle.optionalInfo')}
+The following browser metrics are sent
+
+
The recorded actions are:
+{name}
+ {t('AnalyticsToggle.views.details')}
+{t('AnalyticsToggle.location.details')}
+
+