Removes corvus in favor of sentry and analytics client

Change-type: patch
Signed-off-by: Otavio Jacobi
This commit is contained in:
Otávio Jacobi 2022-11-29 10:44:35 -03:00
parent 8cd6da1260
commit 3513a83b90
9 changed files with 494 additions and 574 deletions

View File

@ -172,7 +172,7 @@ runs:
for target in ${TARGETS}; do
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
--c.extraMetadata.analytics.sentry.token='${{ steps.sentry.outputs.dsn }}' \
--c.extraMetadata.analytics.mixpanel.token='balena-etcher' \
--c.extraMetadata.analytics.amplitude.token='balena-etcher' \
--c.extraMetadata.packageType="${target}"
find dist -type f -maxdepth 1

View File

@ -31,7 +31,7 @@ Releasing
- [Post release note to forums](https://forums.balena.io/c/etcher)
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
- [Update the website](https://github.com/balena-io/etcher-homepage)
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions
- If regressions arise; pull the release, and release a patched version, else:
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
- [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3)
@ -48,7 +48,7 @@ Make sure to set the analytics tokens when generating production release binarie
```bash
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
```
#### Linux

View File

@ -112,4 +112,4 @@ Analytics
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
check that no request is sent
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
F5), and check that initial events are not sent to Mixpanel**
F5), and check that initial events are not sent to Amplitude**

View File

@ -296,6 +296,8 @@ driveScanner.start();
let popupExists = false;
analytics.initAnalytics();
window.addEventListener('beforeunload', async (event) => {
if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', {

View File

@ -15,84 +15,47 @@
*/
import * as _ from 'lodash';
import * as resinCorvus from 'resin-corvus/browser';
import * as packageJSON from '../../../../package.json';
import { getConfig } from '../../../shared/utils';
import { Client, createClient, createNoopClient } from 'analytics-client';
import * as SentryRenderer from '@sentry/electron/renderer';
import * as settings from '../models/settings';
import { store } from '../models/store';
import * as packageJSON from '../../../../package.json';
const DEFAULT_PROBABILITY = 0.1;
async function installCorvus(): Promise<void> {
const sentryToken =
(await settings.get('analyticsSentryToken')) ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
const mixpanelToken =
(await settings.get('analyticsMixpanelToken')) ||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
resinCorvus.install({
services: {
sentry: sentryToken,
mixpanel: mixpanelToken,
},
options: {
release: packageJSON.version,
shouldReport: () => {
return settings.getSync('errorReporting');
},
mixpanelDeferred: true,
},
});
}
let mixpanelSample = DEFAULT_PROBABILITY;
let analyticsClient: Client;
/**
* @summary Init analytics configurations
*/
async function initConfig() {
await installCorvus();
let validatedConfig = null;
try {
const configUrl = await settings.get('configUrl');
const config = await getConfig(configUrl);
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
if (isClientEligible(mixpanelSample)) {
validatedConfig = validateMixpanelConfig(mixpanel);
}
} catch (err) {
resinCorvus.logException(err);
}
resinCorvus.setConfigs({
mixpanel: validatedConfig,
});
}
export const initAnalytics = _.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryRenderer.init({ dsn });
initConfig();
const projectName =
settings.getSync('analyticsAmplitudeToken') ||
_.get(packageJSON, ['analytics', 'amplitude', 'token']);
/**
* @summary Check that the client is eligible for analytics
*/
function isClientEligible(probability: number) {
return Math.random() < probability;
}
/**
* @summary Check that config has at least HTTP_PROTOCOL and api_host
*/
function validateMixpanelConfig(config: {
api_host?: string;
HTTP_PROTOCOL?: string;
}) {
const mixpanelConfig = {
api_host: 'https://api.mixpanel.com',
const clientConfig = {
projectName,
endpoint: 'data.balena-staging.com',
componentName: 'etcher',
componentVersion: packageJSON.version,
};
if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
}
return mixpanelConfig;
analyticsClient = projectName
? createClient(clientConfig)
: createNoopClient();
});
function reportAnalytics(message: string, data: _.Dictionary<any> = {}) {
const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState()
.toJS();
analyticsClient.track(message, {
...data,
applicationSessionUuid,
flashingWorkflowUuid,
});
}
/**
@ -101,16 +64,12 @@ function validateMixpanelConfig(config: {
* @description
* This function sends the debug message to product analytics services.
*/
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState()
.toJS();
resinCorvus.logEvent(message, {
...data,
sample: mixpanelSample,
applicationSessionUuid,
flashingWorkflowUuid,
});
export async function logEvent(message: string, data: _.Dictionary<any> = {}) {
const shouldReportAnalytics = await settings.get('errorReporting');
if (shouldReportAnalytics) {
initAnalytics();
reportAnalytics(message, data);
}
}
/**
@ -119,4 +78,11 @@ export function logEvent(message: string, data: _.Dictionary<any> = {}) {
* @description
* This function logs an exception to error reporting services.
*/
export const logException = resinCorvus.logException;
export function logException(error: any) {
const shouldReportErrors = settings.getSync('errorReporting');
if (shouldReportErrors) {
initAnalytics();
console.error(error);
SentryRenderer.captureException(error);
}
}

View File

@ -20,6 +20,7 @@ import { promises as fs } from 'fs';
import { platform } from 'os';
import * as path from 'path';
import * as semver from 'semver';
import * as _ from 'lodash';
import './app/i18n';
@ -27,9 +28,10 @@ import { packageType, version } from '../../package.json';
import * as EXIT_CODES from '../shared/exit-codes';
import { delay, getConfig } from '../shared/utils';
import * as settings from './app/models/settings';
import { logException } from './app/modules/analytics';
import { buildWindowMenu } from './menu';
import * as i18n from 'i18next';
import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json';
const customProtocol = 'etcher';
const scheme = `${customProtocol}://`;
@ -53,13 +55,21 @@ async function checkForUpdates(interval: number) {
packageUpdated = true;
}
} catch (err) {
logException(err);
logMainProcessException(err);
}
}
await delay(interval);
}
}
function logMainProcessException(error: any) {
const shouldReportErrors = settings.getSync('errorReporting');
if (shouldReportErrors) {
console.error(error);
SentryMain.captureException(error);
}
}
async function isFile(filePath: string): Promise<boolean> {
try {
const stat = await fs.stat(filePath);
@ -94,6 +104,14 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
}
}
const initSentryMain = _.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryMain.init({ dsn });
});
const sourceSelectorReady = new Promise((resolve) => {
electron.ipcMain.on('source-selector-ready', resolve);
});
@ -190,8 +208,9 @@ async function createMainWindow() {
const page = mainWindow.webContents;
page.once('did-frame-finish-load', async () => {
console.log('packageUpdatable', packageUpdatable);
autoUpdater.on('error', (err) => {
logException(err);
logMainProcessException(err);
});
if (packageUpdatable) {
try {
@ -208,7 +227,7 @@ async function createMainWindow() {
onlineConfig?.autoUpdates?.checkForUpdatesTimer ?? 300000;
checkForUpdates(checkForUpdatesTimer);
} catch (err) {
logException(err);
logMainProcessException(err);
}
}
});
@ -233,6 +252,7 @@ async function main(): Promise<void> {
if (!electron.app.requestSingleInstanceLock()) {
electron.app.quit();
} else {
initSentryMain();
await electron.app.whenReady();
const window = await createMainWindow();
electron.app.on('second-instance', async (_event, argv) => {
@ -256,7 +276,6 @@ async function main(): Promise<void> {
});
}
}
main();
console.time('ready-to-show');

898
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,7 @@
"@balena/lint": "5.4.2",
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
"@fortawesome/fontawesome-free": "5.15.4",
"@sentry/electron": "^4.1.2",
"@svgr/webpack": "5.5.0",
"@types/chai": "4.3.4",
"@types/copy-webpack-plugin": "6.4.3",
@ -68,6 +69,7 @@
"@types/terser-webpack-plugin": "5.0.4",
"@types/tmp": "0.2.3",
"@types/webpack-node-externals": "2.5.3",
"analytics-client": "^2.0.1",
"aws4-axios": "2.4.9",
"chai": "4.3.7",
"copy-webpack-plugin": "7.0.0",
@ -102,7 +104,6 @@
"react-i18next": "11.18.6",
"redux": "4.2.0",
"rendition": "19.3.2",
"resin-corvus": "2.0.5",
"semver": "7.3.8",
"simple-progress-webpack-plugin": "1.1.2",
"sinon": "9.2.4",

View File

@ -29,7 +29,7 @@ import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
import * as tsconfigRaw from './tsconfig.webpack.json';
/**
* Don't webpack package.json as mixpanel & sentry tokens
* Don't webpack package.json as sentry tokens
* will be inserted in it after webpacking
*/
function externalPackageJson(packageJsonPath: string) {