diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts index 5457909c..f4ea7908 100644 --- a/lib/gui/app/app.ts +++ b/lib/gui/app/app.ts @@ -64,9 +64,6 @@ store.dispatch({ data: uuidV4(), }); -const applicationSessionUuid = store.getState().toJS().applicationSessionUuid; -const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid; - console.log(outdent` ${outdent} _____ _ _ @@ -82,13 +79,6 @@ console.log(outdent` Version = ${packageJSON.version}, Type = ${packageJSON.packageType} `); -const currentVersion = packageJSON.version; - -analytics.logEvent('Application start', { - packageType: packageJSON.packageType, - version: currentVersion, -}); - const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 }); function pluralize(word: string, quantity: number) { @@ -172,9 +162,6 @@ analytics.initAnalytics(); window.addEventListener('beforeunload', async (event) => { if (!flashState.isFlashing() || popupExists) { - analytics.logEvent('Close application', { - isFlashing: flashState.isFlashing(), - }); return; } @@ -184,8 +171,6 @@ window.addEventListener('beforeunload', async (event) => { // Don't open any more popups popupExists = true; - analytics.logEvent('Close attempt while flashing'); - try { const confirmed = await osDialog.showWarning({ confirmationLabel: i18next.t('yesExit'), @@ -194,19 +179,11 @@ window.addEventListener('beforeunload', async (event) => { description: messages.warning.exitWhileFlashing(), }); if (confirmed) { - analytics.logEvent('Close confirmed while flashing', { - flashInstanceUuid: flashState.getFlashUuid(), - }); - // This circumvents the 'beforeunload' event unlike // remote.app.quit() which does not. remote.process.exit(EXIT_CODES.SUCCESS); } - analytics.logEvent('Close rejected while flashing', { - applicationSessionUuid, - flashingWorkflowUuid, - }); popupExists = false; } catch (error: any) { exceptionReporter.report(error); diff --git a/lib/gui/app/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx index d3c2d389..bc65ebba 100644 --- a/lib/gui/app/components/drive-selector/drive-selector.tsx +++ b/lib/gui/app/components/drive-selector/drive-selector.tsx @@ -36,7 +36,7 @@ import prettyBytes from 'pretty-bytes'; import { getDrives, hasAvailableDrives } from '../../models/available-drives'; import { getImage, isDriveSelected } from '../../models/selection-state'; import { store } from '../../models/store'; -import { logEvent, logException } from '../../modules/analytics'; +import { logException } from '../../modules/analytics'; import { open as openExternal } from '../../os/open-external/services/open-external'; import type { GenericTableProps } from '../../styled-components'; import { Alert, Modal, Table } from '../../styled-components'; @@ -355,9 +355,6 @@ export class DriveSelector extends React.Component< private installMissingDrivers(drive: DriverlessDrive) { if (drive.link) { - logEvent('Open driver link modal', { - url: drive.link, - }); this.setState({ missingDriversModal: { drive } }); } } diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 223cb242..414da88a 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -22,7 +22,6 @@ import * as flashState from '../../models/flash-state'; import * as selectionState from '../../models/selection-state'; import * as settings from '../../models/settings'; import { Actions, store } from '../../models/store'; -import * as analytics from '../../modules/analytics'; import { FlashAnother } from '../flash-another/flash-another'; import type { FlashError } from '../flash-results/flash-results'; import { FlashResults } from '../flash-results/flash-results'; @@ -30,7 +29,6 @@ import { SafeWebview } from '../safe-webview/safe-webview'; function restart(goToMain: () => void) { selectionState.deselectAllDrives(); - analytics.logEvent('Restart'); // Reset the flashing workflow uuid store.dispatch({ diff --git a/lib/gui/app/components/safe-webview/safe-webview.tsx b/lib/gui/app/components/safe-webview/safe-webview.tsx index 62b08d78..efe8ae3d 100644 --- a/lib/gui/app/components/safe-webview/safe-webview.tsx +++ b/lib/gui/app/components/safe-webview/safe-webview.tsx @@ -21,7 +21,6 @@ import * as React from 'react'; import * as packageJSON from '../../../../../package.json'; import * as settings from '../../models/settings'; -import * as analytics from '../../modules/analytics'; /** * @summary Electron session identifier @@ -196,10 +195,6 @@ export class SafeWebview extends React.PureComponent< // only care about this event if it's a request for the main frame if (event.resourceType === 'mainFrame') { const HTTP_OK = 200; - const { webContents, ...webviewEvent } = event; - analytics.logEvent('SafeWebview loaded', { - ...webviewEvent, - }); this.setState({ shouldShow: event.statusCode === HTTP_OK, }); diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index 4c8129c8..50d73f84 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -21,7 +21,6 @@ import { Box, Checkbox, Flex, Txt } from 'rendition'; import { version, packageType } from '../../../../../package.json'; import * as settings from '../../models/settings'; -import * as analytics from '../../modules/analytics'; import { open as openExternal } from '../../os/open-external/services/open-external'; import { Modal } from '../../styled-components'; import * as i18next from 'i18next'; @@ -89,7 +88,6 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) { const toggleSetting = async (setting: string) => { const value = currentSettings[setting]; - analytics.logEvent('Toggle setting', { setting, value }); await settings.set(setting, !value); setCurrentSettings({ ...currentSettings, diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 82808f8d..00e7cf03 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -392,10 +392,6 @@ export class SourceSelector extends React.Component< } private reselectSource() { - analytics.logEvent('Reselect image', { - previousImage: selectionState.getImage(), - }); - selectionState.deselectImage(); this.props.hideAnalyticsAlert(); } @@ -426,7 +422,6 @@ export class SourceSelector extends React.Component< } if (supportedFormats.looksLikeWindowsImage(selected)) { - analytics.logEvent('Possibly Windows image', { image: selected }); this.setState({ warning: { message: messages.warning.looksLikeWindowsImage(), @@ -450,7 +445,6 @@ export class SourceSelector extends React.Component< metadata = await requestMetadata({ selected, SourceType, auth }); if (!metadata?.hasMBR && this.state.warning === null) { - analytics.logEvent('Missing partition table', { metadata }); this.setState({ warning: { message: messages.warning.missingPartitionTable(), @@ -468,7 +462,6 @@ export class SourceSelector extends React.Component< } } else { if (selected.partitionTableType === null) { - analytics.logEvent('Missing partition table', { selected }); this.setState({ warning: { message: messages.warning.driveMissingPartitionTable(), @@ -490,15 +483,6 @@ export class SourceSelector extends React.Component< metadata.auth = auth; metadata.SourceType = SourceType; selectionState.selectSource(metadata); - analytics.logEvent('Select image', { - // An easy way so we can quickly identify if we're making use of - // certain features without printing pages of text to DevTools. - image: { - ...metadata, - logo: Boolean(metadata.logo), - blockMap: Boolean(metadata.blockMap), - }, - }); } })(), }; @@ -519,11 +503,9 @@ export class SourceSelector extends React.Component< analytics.logException(error); return; } - analytics.logEvent(title, { path: sourcePath }); } private async openImageSelector() { - analytics.logEvent('Open image selector'); this.setState({ imageSelectorOpen: true }); try { @@ -531,7 +513,6 @@ export class SourceSelector extends React.Component< // Avoid analytics and selection state changes // if no file was resolved from the dialog. if (!imagePath) { - analytics.logEvent('Image selector closed'); return; } await this.selectSource(imagePath, 'File').promise; @@ -550,16 +531,12 @@ export class SourceSelector extends React.Component< } private openURLSelector() { - analytics.logEvent('Open image URL selector'); - this.setState({ showURLSelector: true, }); } private openDriveSelector() { - analytics.logEvent('Open drive selector'); - this.setState({ showDriveSelector: true, }); @@ -576,10 +553,6 @@ export class SourceSelector extends React.Component< } private showSelectedImageDetails() { - analytics.logEvent('Show selected image tooltip', { - imagePath: selectionState.getImage()?.path, - }); - this.setState({ showImageDetails: true, }); @@ -759,9 +732,7 @@ export class SourceSelector extends React.Component< done={async (imageURL: string, auth?: Authentication) => { // Avoid analytics and selection state changes // if no file was resolved from the dialog. - if (!imageURL) { - analytics.logEvent('URL selector closed'); - } else { + if (imageURL) { let promise; ({ promise, cancel: cancelURLSelection } = this.selectSource( imageURL, diff --git a/lib/gui/app/components/target-selector/target-selector.tsx b/lib/gui/app/components/target-selector/target-selector.tsx index a79daed9..8a7f867f 100644 --- a/lib/gui/app/components/target-selector/target-selector.tsx +++ b/lib/gui/app/components/target-selector/target-selector.tsx @@ -20,7 +20,6 @@ import { Flex, Txt } from 'rendition'; import type { DriveSelectorProps } from '../drive-selector/drive-selector'; import { DriveSelector } from '../drive-selector/drive-selector'; import { - isDriveSelected, getImage, getSelectedDrives, deselectDrive, @@ -28,7 +27,6 @@ import { deselectAllDrives, } from '../../models/selection-state'; import { observe } from '../../models/store'; -import * as analytics from '../../modules/analytics'; import { TargetSelectorButton } from './target-selector-button'; import TgtSvg from '../../../assets/tgt.svg'; @@ -77,21 +75,10 @@ export const selectAllTargets = (modalTargets: DrivelistDrive[]) => { ); // deselect drives deselected.forEach((drive) => { - analytics.logEvent('Toggle drive', { - drive, - previouslySelected: true, - }); deselectDrive(drive.device); }); // select drives modalTargets.forEach((drive) => { - // Don't send events for drives that were already selected - if (!isDriveSelected(drive.device)) { - analytics.logEvent('Toggle drive', { - drive, - previouslySelected: false, - }); - } selectDrive(drive.device); }); }; @@ -142,7 +129,6 @@ export const TargetSelector = ({ hideAnalyticsAlert(); }} reselectDrive={() => { - analytics.logEvent('Reselect drive'); setShowTargetSelectorModal(true); }} flashing={flashing} diff --git a/lib/gui/app/i18n/en.ts b/lib/gui/app/i18n/en.ts index 4d6fd34f..3e7c3fd8 100644 --- a/lib/gui/app/i18n/en.ts +++ b/lib/gui/app/i18n/en.ts @@ -133,8 +133,7 @@ const translation = { flashCompleted: 'Flash Completed!', }, settings: { - errorReporting: - 'Anonymously report errors and usage statistics to balena.io', + errorReporting: 'Anonymously report errors to balena.io', autoUpdate: 'Auto-updates enabled', settings: 'Settings', systemInformation: 'System Information', diff --git a/lib/gui/app/modules/analytics.ts b/lib/gui/app/modules/analytics.ts index 4ccc4d67..5452e192 100644 --- a/lib/gui/app/modules/analytics.ts +++ b/lib/gui/app/modules/analytics.ts @@ -15,12 +15,8 @@ */ import { findLastIndex, once } from 'lodash'; -import type { Client } from 'analytics-client'; -import { createClient, createNoopClient } from 'analytics-client'; import * as SentryRenderer from '@sentry/electron/renderer'; import * as settings from '../models/settings'; -import { store } from '../models/store'; -import { version } from '../../../../package.json'; type AnalyticsPayload = _.Dictionary; @@ -115,7 +111,6 @@ export const anonymizeAnalyticsPayload = ( return data; }; -let analyticsClient: Client; /** * @summary Init analytics configurations */ @@ -127,95 +122,8 @@ export const initAnalytics = once(() => { beforeSend: anonymizeSentryData, debug: process.env.ETCHER_SENTRY_DEBUG === 'true', }); - - const projectName = - settings.getSync('analyticsAmplitudeToken') || process.env.AMPLITUDE_TOKEN; - - const clientConfig = { - projectName, - endpoint: 'data.balena-cloud.com', - componentName: 'etcher', - componentVersion: version, - }; - analyticsClient = projectName - ? createClient(clientConfig) - : createNoopClient(); }); -const getCircularReplacer = () => { - const seen = new WeakSet(); - return (_key: any, value: any) => { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return; - } - seen.add(value); - } - return value; - }; -}; - -function flattenObject(obj: any) { - const toReturn: AnalyticsPayload = {}; - - for (const i in obj) { - if (!Object.prototype.hasOwnProperty.call(obj, i)) { - continue; - } - - if (Array.isArray(obj[i])) { - toReturn[i] = obj[i]; - continue; - } - - if (typeof obj[i] === 'object' && obj[i] !== null) { - const flatObject = flattenObject(obj[i]); - for (const x in flatObject) { - if (!Object.prototype.hasOwnProperty.call(flatObject, x)) { - continue; - } - - toReturn[i.toLowerCase() + '.' + x.toLowerCase()] = flatObject[x]; - } - } else { - toReturn[i] = obj[i]; - } - } - return toReturn; -} - -function formatEvent(data: any): AnalyticsPayload { - const event = JSON.parse(JSON.stringify(data, getCircularReplacer())); - return anonymizeAnalyticsPayload(flattenObject(event)); -} - -function reportAnalytics(message: string, data: AnalyticsPayload = {}) { - const { applicationSessionUuid, flashingWorkflowUuid } = store - .getState() - .toJS(); - - const event = formatEvent({ - ...data, - applicationSessionUuid, - flashingWorkflowUuid, - }); - analyticsClient.track(message, event); -} - -/** - * @summary Log an event - * - * @description - * This function sends the debug message to product analytics services. - */ -export async function logEvent(message: string, data: AnalyticsPayload = {}) { - const shouldReportAnalytics = await settings.get('errorReporting'); - if (shouldReportAnalytics) { - initAnalytics(); - reportAnalytics(message, data); - } -} - /** * @summary Log an exception * diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index d1701e75..8bcadc5f 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -20,44 +20,11 @@ import type { Dictionary } from 'lodash'; import * as errors from '../../../shared/errors'; import type { SourceMetadata } from '../../../shared/typings/source-selector'; import * as flashState from '../models/flash-state'; -import * as selectionState from '../models/selection-state'; import * as settings from '../models/settings'; -import * as analytics from '../modules/analytics'; import * as windowProgress from '../os/window-progress'; import { spawnChildAndConnect } from './api'; -/** - * @summary Handle a flash error and log it to analytics - */ -function handleErrorLogging( - error: Error & { code: string }, - analyticsData: any, -) { - const eventData = { - ...analyticsData, - flashInstanceUuid: flashState.getFlashUuid(), - }; - - if (error.code === 'EVALIDATION') { - analytics.logEvent('Validation error', eventData); - } else if (error.code === 'EUNPLUGGED') { - analytics.logEvent('Drive unplugged', eventData); - } else if (error.code === 'EIO') { - analytics.logEvent('Input/output error', eventData); - } else if (error.code === 'ENOSPC') { - analytics.logEvent('Out of space', eventData); - } else if (error.code === 'ECHILDDIED') { - analytics.logEvent('Child died unexpectedly', eventData); - } else { - analytics.logEvent('Flash error', { - ...eventData, - error: errors.toJSON(error), - }); - } -} - let cancelEmitter: (type: string) => void | undefined; - interface FlashResults { skip?: boolean; cancelled?: boolean; @@ -88,14 +55,6 @@ async function performWrite( const flashResults: FlashResults = {}; - const analyticsData = { - image, - drives, - driveCount: drives.length, - uuid: flashState.getFlashUuid(), - flashInstanceUuid: flashState.getFlashUuid(), - }; - const onFail = ({ device, error }: { device: any; error: any }) => { console.log('fail event'); console.log(device); @@ -103,7 +62,6 @@ async function performWrite( if (device.devicePath) { flashState.addFailedDeviceError({ device, error }); } - handleErrorLogging(error, analyticsData); finish(); }; @@ -195,17 +153,6 @@ export async function flash( drives.map((d) => d.devicePath).filter((p) => p != null) as string[], ); - const analyticsData = { - image, - drives, - driveCount: drives.length, - uuid: flashState.getFlashUuid(), - status: 'started', - flashInstanceUuid: flashState.getFlashUuid(), - }; - - analytics.logEvent('Flash', analyticsData); - // start api and call the flasher try { const result = await write(image, drives, flashState.setProgressState); @@ -220,39 +167,10 @@ export async function flash( windowProgress.clear(); - const { results = {} } = flashState.getFlashResults(); - - const eventData = { - ...analyticsData, - errors: results.errors, - devices: results.devices, - status: 'failed', - error, - }; - analytics.logEvent('Write failed', eventData); throw error; } windowProgress.clear(); - - if (flashState.wasLastFlashCancelled()) { - const eventData = { - ...analyticsData, - status: 'cancel', - }; - analytics.logEvent('Elevation cancelled', eventData); - } else { - const { results = {} } = flashState.getFlashResults(); - const eventData = { - ...analyticsData, - errors: results.errors, - devices: results.devices, - status: 'finished', - bytesWritten: results.bytesWritten, - sourceMetadata: results.sourceMetadata, - }; - analytics.logEvent('Done', eventData); - } } /** @@ -261,16 +179,6 @@ export async function flash( */ export async function cancel(type: string) { const status = type.toLowerCase(); - const drives = selectionState.getSelectedDevices(); - const analyticsData = { - image: selectionState.getImage()?.path, - drives, - driveCount: drives.length, - uuid: flashState.getFlashUuid(), - flashInstanceUuid: flashState.getFlashUuid(), - status, - }; - analytics.logEvent('Cancel', analyticsData); if (cancelEmitter) { cancelEmitter(status); diff --git a/lib/gui/app/os/open-external/services/open-external.ts b/lib/gui/app/os/open-external/services/open-external.ts index ef7c99c5..e9afb365 100644 --- a/lib/gui/app/os/open-external/services/open-external.ts +++ b/lib/gui/app/os/open-external/services/open-external.ts @@ -16,7 +16,6 @@ import * as electron from 'electron'; import * as settings from '../../../models/settings'; -import { logEvent } from '../../../modules/analytics'; /** * @summary Open an external resource @@ -27,8 +26,6 @@ export async function open(url: string) { return; } - logEvent('Open external link', { url }); - if (url) { electron.shell.openExternal(url); } diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 25aa4260..0042f623 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -198,9 +198,7 @@ export class FlashStep extends React.PureComponent< private handleFlashErrorResponse(shouldRetry: boolean) { this.setState({ errorMessage: '' }); flashState.resetState(); - if (shouldRetry) { - analytics.logEvent('Restart after failure'); - } else { + if (!shouldRetry) { selection.clear(); } }