Compare commits

...

12 Commits

Author SHA1 Message Date
flowzone-app[bot]
391164bf15
v2.1.3 2025-05-15 18:09:58 +00:00
Anton Belodedenko
7c2c2bc3d6
Merge pull request #4411 from balena-io/ab77/operational
Remove stale secrets
2025-05-15 11:09:03 -07:00
flowzone-app[bot]
c2d160f5c7
v2.1.2 2025-05-08 08:51:47 +00:00
flowzone-app[bot]
385bf45883
Merge pull request #4435 from balena-io/aethernet/remove-analytics
patch: remove analytics code
2025-05-08 08:50:54 +00:00
Edwin Joassart
aa6d526fea
patch: remove analytics 2025-05-07 14:57:50 +02:00
flowzone-app[bot]
fdd082b9cd
v2.1.1 2025-05-05 17:19:52 +00:00
flowzone-app[bot]
624dc77969
Merge pull request #4454 from balena-io/aethernet/test-signing
patch: fix signin for windows
2025-05-05 17:19:08 +00:00
Edwin Joassart
a1e9be2f94 patch: fix signin windows artifacts 2025-05-05 17:39:06 +02:00
Anton Belodedenko
c2fc36971c Remove stale secrets
change-type: patch
2025-04-14 08:59:30 +00:00
flowzone-app[bot]
85b1e3c2c2
v2.1.0 2025-02-27 16:16:59 +00:00
Matthew Yarmolinsky
e5d1b4ce23
Merge pull request #4406 from balena-io/add-analytics-alert
Add informational notice about how to disable analytics collection
2025-02-27 11:16:08 -05:00
myarmolinsky
aac092fd4d Add informational notice about how to disable analytics collection
Change-type: minor
2025-02-20 09:51:30 -05:00
32 changed files with 262 additions and 578 deletions

4
.gitattributes vendored
View File

@ -62,7 +62,3 @@ CODEOWNERS text
*.ttf binary diff=hex
xz-without-extension binary diff=hex
wmic-output.txt binary diff=hex
# gitsecret
*.secret binary
.gitsecret/** binary

View File

@ -3,10 +3,10 @@ name: package and publish GitHub (draft) release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
description: 'JSON stringified object containing all the inputs from the calling workflow'
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
description: 'JSON stringified object containing all the secrets from the calling workflow'
required: true
# --- custom environment
@ -15,14 +15,14 @@ inputs:
# Beware that native modules will be built for this version,
# which might not be compatible with the one used by pkg (see forge.sidecar.ts)
# https://github.com/vercel/pkg-fetch/releases
default: "20.x"
default: '20.x'
VERBOSE:
type: string
default: "true"
default: 'true'
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
using: 'composite'
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v4
@ -146,8 +146,8 @@ runs:
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
smksp_registrar.exe list
smctl.exe keypair ls
smctl.exe windows certsync
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"

View File

@ -12,7 +12,7 @@ inputs:
# --- custom environment
NODE_VERSION:
type: string
default: '20.10'
default: '20.19'
VERBOSE:
type: string
default: 'true'

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a

View File

@ -1,3 +1,46 @@
- commits:
- subject: Remove stale secrets
hash: c2fc36971c9460eac6bd02cfc7bdcabec7b97a6d
body: ""
footer:
change-type: patch
author: Anton Belodedenko
nested: []
version: 2.1.3
title: ""
date: 2025-05-15T18:09:55.848Z
- commits:
- subject: "patch: remove analytics"
hash: aa6d526fea010d181f49dd81ae3bdaefb8d1938e
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 2.1.2
title: ""
date: 2025-05-08T08:51:44.810Z
- commits:
- subject: "patch: fix signin windows artifacts"
hash: a1e9be2f94629447e02994e52e12c67ec98de831
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 2.1.1
title: ""
date: 2025-05-05T17:19:50.443Z
- commits:
- subject: Add informational notice about how to disable analytics collection
hash: aac092fd4df8750024c082b25dcbd0ae6ee618fd
body: ""
footer:
Change-type: minor
change-type: minor
author: myarmolinsky
nested: []
version: 2.1.0
title: ""
date: 2025-02-27T16:16:57.036Z
- commits:
- subject: "major: build on ubuntu 22 and macos 13"
hash: 039a022353d1980ef9ddd19166515c531e48aba4

View File

@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
# v2.1.3
## (2025-05-15)
* Remove stale secrets [Anton Belodedenko]
# v2.1.2
## (2025-05-08)
* patch: remove analytics [Edwin Joassart]
# v2.1.1
## (2025-05-05)
* patch: fix signin windows artifacts [Edwin Joassart]
# v2.1.0
## (2025-02-27)
* Add informational notice about how to disable analytics collection [myarmolinsky]
# v2.0.0
## (2025-02-20)

View File

@ -1,10 +1,8 @@
Maintaining Etcher
==================
# Maintaining Etcher
This document is meant to serve as a guide for maintainers to perform common tasks.
Releasing
---------
## Releasing
### Release Types
@ -13,16 +11,15 @@ Releasing
- **release**: Full releases
Draft release is created from each PR, tagged with the branch name.
All merged PR will generate a new tag/version as a *pre-release*.
All merged PR will generate a new tag/version as a _pre-release_.
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
#### Preparation
- [Prepare the new version](#preparing-a-new-version)
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
- Upload build artifacts to GitHub release draft
- Upload build artifacts to GitHub release draft
#### Testing
@ -35,7 +32,7 @@ Mark the pre-release as final when it is necessary, then distribute the packages
- [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, Amplitude) to trickle in and check for elevated error rates, or regressions
- Wait 2-3 hours for analytics (Sentry) 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 Cloudfront](#uploading-packages-to-cloudfront)
- Post changelog with `#release-notes` tag on internal chat
@ -51,7 +48,6 @@ Make sure to set the analytics tokens when generating production release binarie
```bash
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
```
#### Linux
@ -71,7 +67,6 @@ npm run make
Our CI will appropriately sign artifacts for macOS and some Windows targets.
### Uploading packages to Cloudfront
Log in to cloudfront and upload the `rpm` and `deb` files.
@ -99,7 +94,6 @@ aws s3api delete-object --bucket <bucket name> --key <file name>
The Bintray dashboard provides an easy way to delete a version's files.
### Submitting binaries to Symantec
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)

View File

@ -1,22 +1,19 @@
Manual Testing
==============
# Manual Testing
This document describes a high-level script of manual tests to check for. We
should aim to replace items on this list with automated Spectron test cases.
Image Selection
---------------
## Image Selection
- [ ] Cancel image selection dialog
- [ ] Select an unbootable image (without a partition table), and expect a
sensible warning
sensible warning
- [ ] Attempt to select a ZIP archive with more than one image
- [ ] Attempt to select a tar archive (with any compression method)
- [ ] Change image selection
- [ ] Select a Windows image, and expect a sensible warning
Drive Selection
---------------
## Drive Selection
- [ ] Open the drive selection modal
- [ ] Switch drive selection
@ -25,16 +22,15 @@ Drive Selection
- [ ] Insert a locked SD Card and expect a warning
- [ ] Insert a too small drive and expect a warning
- [ ] Put an image into a drive and attempt to flash the image to the drive
that contains it
that contains it
- [ ] Attempt to flash a compressed image (for which we can get the
uncompressed size) into a drive that is big enough to hold the compressed
image, but not big enough to hold the uncompressed version
uncompressed size) into a drive that is big enough to hold the compressed
image, but not big enough to hold the uncompressed version
- [ ] Enable "Unsafe Mode" and attempt to select a system drive
- [ ] Enable "Unsafe Mode", and if there is only one system drive (and no
removable ones), don't expect autoselection
removable ones), don't expect autoselection
Image Support
-------------
## Image Support
Run the following tests with and without validation enabled:
@ -51,18 +47,17 @@ Run the following tests with and without validation enabled:
- [ ] Flash an archive image containing a blockmap file
- [ ] Flash an archive image containing a manifest metadata file
Flashing Process
----------------
## Flashing Process
- [ ] Unplug the drive during flash or validation
- [ ] Click "Flash", cancel elevation dialog, and click "Flash" again
- [ ] Start flashing an image, try to close Etcher, cancel the application
close warning dialog, and check that Etcher continues to flash the image
close warning dialog, and check that Etcher continues to flash the image
### Child Writer
- [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and
check that the UI reacts appropriately
check that the UI reacts appropriately
- [ ] Close the application while flashing using the window manager close icon
- [ ] Close the application while flashing using the OS keyboard shortcut
- [ ] Close the application from the terminal using Ctrl-C while flashing
@ -72,11 +67,10 @@ In all these cases, the child writer process should not remain alive. Note that
in some systems you need to open your process monitor tool of choice with extra
permissions to see the elevated child writer process.
GUI
----
## GUI
- [ ] Close application from the terminal using Ctrl-C while the application is
idle
idle
- [ ] Click footer links that take you to an external website
- [ ] Attempt to change image or drive selection while flashing
- [ ] Go to the settings page while flashing and come back
@ -85,31 +79,20 @@ GUI
- [ ] Minimize the application
- [ ] Start the application given no internet connection
Success Banner
--------------
## Success Banner
- [ ] Click an external link on the success banner (with and without internet
connection)
connection)
Elevation Prompt
----------------
## Elevation Prompt
- [ ] Flash an image as `root`/administrator
- [ ] Reject elevation prompt
- [ ] Put incorrect elevation prompt password
- [ ] Unplug the drive during elevation
Unmounting
----------
## Unmounting
- [ ] Disable unmounting and flash an image
- [ ] Flash an image with a file system that is readable by the host OS, and
check that is unmounted correctly
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 Amplitude**
check that is unmounted correctly

View File

@ -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);

View File

@ -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 } });
}
}

View File

@ -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({

View File

@ -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,
});

View File

@ -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,

View File

@ -308,6 +308,7 @@ const FlowSelector = styled(
interface SourceSelectorProps {
flashing: boolean;
hideAnalyticsAlert: () => void;
}
interface SourceSelectorState {
@ -359,6 +360,20 @@ export class SourceSelector extends React.Component<
ipcRenderer.removeListener('select-image', this.onSelectImage);
}
public componentDidUpdate(
_prevProps: Readonly<SourceSelectorProps>,
prevState: Readonly<SourceSelectorState>,
) {
if (
(!prevState.showDriveSelector && this.state.showDriveSelector) ||
(!prevState.showURLSelector && this.state.showURLSelector) ||
(!prevState.showImageDetails && this.state.showImageDetails) ||
(!prevState.imageSelectorOpen && this.state.imageSelectorOpen)
) {
this.props.hideAnalyticsAlert();
}
}
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
this.setState({ imageLoading: true });
await this.selectSource(
@ -377,11 +392,8 @@ export class SourceSelector extends React.Component<
}
private reselectSource() {
analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(),
});
selectionState.deselectImage();
this.props.hideAnalyticsAlert();
}
private selectSource(
@ -410,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(),
@ -434,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(),
@ -452,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(),
@ -474,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),
},
});
}
})(),
};
@ -503,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 {
@ -515,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;
@ -534,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,
});
@ -560,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,
});
@ -743,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,

View File

@ -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);
});
};
@ -100,12 +87,14 @@ interface TargetSelectorProps {
disabled: boolean;
hasDrive: boolean;
flashing: boolean;
hideAnalyticsAlert: () => void;
}
export const TargetSelector = ({
disabled,
hasDrive,
flashing,
hideAnalyticsAlert,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
@ -137,9 +126,9 @@ export const TargetSelector = ({
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
hideAnalyticsAlert();
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true);
}}
flashing={flashing}

View File

@ -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',

View File

@ -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<any>;
@ -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
*

View File

@ -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);

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -15,12 +15,13 @@
*/
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg';
import CloseSvg from '@fortawesome/fontawesome-free/svgs/solid/x.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg';
import * as path from 'path';
import prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { Flex } from 'rendition';
import { Alert, Flex, Link } from 'rendition';
import styled from 'styled-components';
import FinishPage from '../../components/finish/finish';
@ -35,6 +36,7 @@ import { observe } from '../../models/store';
import { open as openExternal } from '../../os/open-external/services/open-external';
import {
IconButton as BaseIcon,
IconButton,
ThemedProvider,
} from '../../styled-components';
@ -46,6 +48,7 @@ import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { theme } from '../../theme';
const Icon = styled(BaseIcon)`
margin-right: 20px;
@ -97,6 +100,8 @@ const StepBorder = styled.div<{
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
`;
const ANALYTICS_ALERT_VISIBILITY_KEY = 'analytics_alert_visible';
interface MainPageStateFromStore {
isFlashing: boolean;
hasImage: boolean;
@ -113,6 +118,7 @@ interface MainPageState {
isWebviewShowing: boolean;
hideSettings: boolean;
featuredProjectURL?: string;
analyticsAlertIsVisible: boolean;
}
export class MainPage extends React.Component<
@ -125,6 +131,8 @@ export class MainPage extends React.Component<
current: 'main',
isWebviewShowing: false,
hideSettings: true,
analyticsAlertIsVisible:
localStorage.getItem(ANALYTICS_ALERT_VISIBILITY_KEY) !== 'false',
...this.stateHelper(),
};
}
@ -153,6 +161,13 @@ export class MainPage extends React.Component<
return url.toString();
}
private hideAnalyticsAlert = () => {
if (this.state.analyticsAlertIsVisible) {
localStorage.setItem(ANALYTICS_ALERT_VISIBILITY_KEY, 'false');
this.setState({ analyticsAlertIsVisible: false });
}
};
public async componentDidMount() {
observe(() => {
this.setState(this.stateHelper());
@ -160,6 +175,17 @@ export class MainPage extends React.Component<
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
}
public componentDidUpdate(
_prevProps: object,
prevState: Readonly<MainPageState & MainPageStateFromStore>,
) {
if (this.state.analyticsAlertIsVisible) {
if (prevState.hideSettings !== this.state.hideSettings) {
this.setState({ analyticsAlertIsVisible: false });
}
}
}
private renderMain() {
const state = flashState.getFlashState();
const shouldDriveStepBeDisabled = !this.state.hasImage;
@ -169,86 +195,127 @@ export class MainPage extends React.Component<
!this.state.isFlashing || !this.state.isWebviewShowing;
return (
<Flex
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between"
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px 18px ${this.state.isWebviewShowing ? 35 : 55}px`}
flexDirection="column"
>
{notFlashingOrSplitView && (
<>
<SourceSelector flashing={this.state.isFlashing} />
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
<TargetSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
</>
)}
<Flex
justifyContent="space-between"
mb={this.state.analyticsAlertIsVisible ? '0px' : '92px'}
>
{notFlashingOrSplitView && (
<>
<SourceSelector
flashing={this.state.isFlashing}
hideAnalyticsAlert={this.hideAnalyticsAlert}
/>
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
<TargetSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
hideAnalyticsAlert={this.hideAnalyticsAlert}
/>
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
</>
)}
{this.state.isFlashing && this.state.isWebviewShowing && (
<Flex
style={{
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
{this.state.isFlashing && this.state.isWebviewShowing && (
<Flex
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
)}
<FlashStep
width={this.state.isWebviewShowing ? '220px' : '200px'}
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
<FlashStep
width={this.state.isWebviewShowing ? '220px' : '200px'}
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
{this.state.analyticsAlertIsVisible && (
<Alert mt="18px" style={{ boxShadow: 'none', fontSize: '12px' }}>
<Flex alignItems="center" justifyContent="space-between">
<Flex flexDirection="column">
<div>
Etcher collects a limited amount of anonymous data to help us
improve user experience. You can opt out in the{' '}
<Link onClick={() => this.setState({ hideSettings: false })}>
settings
</Link>
.
</div>
<div>
For more information about how we use this data, see our{' '}
<Link
onClick={(e) => {
e.stopPropagation();
openExternal('https://www.balena.io/privacy-policy');
}}
>
privacy policy
</Link>
.
</div>
</Flex>
{/* TODO: can we use onDismiss instead? */}
<IconButton onClick={this.hideAnalyticsAlert}>
<CloseSvg height="0.75rem" fill={theme.colors.text.main} />
</IconButton>
</Flex>
</Alert>
)}
</Flex>
);
}

165
npm-shrinkwrap.json generated
View File

@ -1,19 +1,18 @@
{
"name": "balena-etcher",
"version": "2.0.0",
"version": "2.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "balena-etcher",
"version": "2.0.0",
"version": "2.1.3",
"license": "Apache-2.0",
"dependencies": {
"@electron/remote": "^2.1.2",
"@fortawesome/fontawesome-free": "^6.5.2",
"@ronomon/direct-io": "^3.0.1",
"@sentry/electron": "^4.24.0",
"analytics-client": "^2.0.1",
"axios": "^1.6.8",
"debug": "4.3.4",
"drivelist": "^12.0.2",
@ -101,148 +100,6 @@
"node": ">=0.10.0"
}
},
"node_modules/@amplitude/analytics-browser": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-1.13.4.tgz",
"integrity": "sha512-FyNlrhLZUFI+lDHxbDGMoZED80iARS6VjTP+zZcfk0GWI7+lt0Meu5jD7G8xms21Ioxrg1+Lf1Q4v3CC6XN2Nw==",
"dependencies": {
"@amplitude/analytics-client-common": "^1.2.2",
"@amplitude/analytics-core": "^1.2.5",
"@amplitude/analytics-types": "^1.3.4",
"@amplitude/plugin-page-view-tracking-browser": "^1.0.12",
"@amplitude/plugin-web-attribution-browser": "^1.0.12",
"@amplitude/ua-parser-js": "^0.7.31",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/analytics-client-common": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-1.2.2.tgz",
"integrity": "sha512-vwGgVXl9FKEi99OzjqqhX8RrulQQ55aAllhgbdyxpyyAQ5NbbZOPdrxp1ow0oliCVvbSDgUYOAeAwTChIgnStA==",
"dependencies": {
"@amplitude/analytics-connector": "^1.5.0",
"@amplitude/analytics-core": "^1.2.5",
"@amplitude/analytics-types": "^1.3.4",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/analytics-connector": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz",
"integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g=="
},
"node_modules/@amplitude/analytics-core": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-1.2.5.tgz",
"integrity": "sha512-V7CVlHVN+1diKiOpdp2bCPZ0mbS4CmUYF+v+eXDwVfJL3M/t3sVcT1apXnmVYGYi14cGu9hQOD11rD6qKbUOsw==",
"dependencies": {
"@amplitude/analytics-types": "^1.3.4",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/analytics-types": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-1.3.4.tgz",
"integrity": "sha512-tR70gzqFkEzX9QpxvWYMfLCledT7vMhgd3d4/bkp3nnGXTOORaVUOCcSgOyxyuFdSx84T61aP/eZPKIcZcaP+A=="
},
"node_modules/@amplitude/marketing-analytics-browser": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@amplitude/marketing-analytics-browser/-/marketing-analytics-browser-0.2.9.tgz",
"integrity": "sha512-xOx5tCqV2A1r9+pYP7PPDfBqzQpKvhmIPR/CF4blpo7ZTYqCIWLg7QG1pN3uWFQuq5d4MWWz5QH+TUvKXifsHA==",
"dependencies": {
"@amplitude/analytics-browser": "^1.6.3",
"@amplitude/analytics-client-common": "^0.4.1",
"@amplitude/analytics-core": "^0.10.1",
"@amplitude/analytics-types": "^0.13.0",
"@amplitude/plugin-page-view-tracking-browser": "^0.4.9",
"@amplitude/plugin-web-attribution-browser": "^0.4.2",
"tslib": "^2.3.1"
}
},
"node_modules/@amplitude/marketing-analytics-browser/node_modules/@amplitude/analytics-client-common": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-0.4.1.tgz",
"integrity": "sha512-cwKHZVNfBt8kNmhXuSZ/BkEwdOSsCVQDXKgQysb4sp5AYkwqYV/bVd7yvWxffrrkK4N2PsYLnvODeTwANH/4UQ==",
"dependencies": {
"@amplitude/analytics-connector": "^1.4.5",
"@amplitude/analytics-core": "^0.10.1",
"@amplitude/analytics-types": "^0.13.0",
"tslib": "^2.3.1"
}
},
"node_modules/@amplitude/marketing-analytics-browser/node_modules/@amplitude/analytics-core": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-0.10.1.tgz",
"integrity": "sha512-XYJavGCnf0Y28chswEGNjSM2MqCMafsQvHpSgRD1JYTNrv+j/CTkj7P3TwyxriaXCTSfFcWTDPKRHd3SruU3Aw==",
"dependencies": {
"@amplitude/analytics-types": "^0.13.0",
"tslib": "^2.3.1"
}
},
"node_modules/@amplitude/marketing-analytics-browser/node_modules/@amplitude/analytics-types": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-0.13.0.tgz",
"integrity": "sha512-yti2SytTIh0R5QknuKO1RMgB+r8CGjauhPfFaaYiTm4keAvqYxDdG9ULarPDoOx2VPSfB5Za779Kt1Muc+34PA=="
},
"node_modules/@amplitude/marketing-analytics-browser/node_modules/@amplitude/plugin-page-view-tracking-browser": {
"version": "0.4.9",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-0.4.9.tgz",
"integrity": "sha512-dPeMativPA+UitDQbRv/FtBfAZtddbu9tgezbtSR90yubK+bS3oYWXETxR6X7P73qNBIZGUpH9i2nWVY6EXVQQ==",
"dependencies": {
"@amplitude/analytics-client-common": "^0.4.1",
"@amplitude/analytics-types": "^0.13.0",
"tslib": "^2.3.1"
}
},
"node_modules/@amplitude/marketing-analytics-browser/node_modules/@amplitude/plugin-web-attribution-browser": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-0.4.2.tgz",
"integrity": "sha512-9N1Qe4fTBmS7uDcgCA5PbIryJCf2V+BUhwP8n6BSwH1XOujx/sK0UQRgPlGRFSmmm2eH7QG9RKJkQj6VB3/zrQ==",
"dependencies": {
"@amplitude/analytics-client-common": "^0.4.1",
"@amplitude/analytics-types": "^0.13.0",
"tslib": "^2.3.1"
}
},
"node_modules/@amplitude/plugin-page-view-tracking-browser": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-1.0.12.tgz",
"integrity": "sha512-zbFbBi/+QrsWm1rPcFIAcxQ3t7uZwTuHCnHHzyZU/nQB/gyOgRh4U4uqt5DekLf4Tp3V2a+hmhmTE0KfRFuXLw==",
"dependencies": {
"@amplitude/analytics-client-common": "^1.2.2",
"@amplitude/analytics-types": "^1.3.4",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/plugin-web-attribution-browser": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@amplitude/plugin-web-attribution-browser/-/plugin-web-attribution-browser-1.0.12.tgz",
"integrity": "sha512-zoIqgIT34xbE3V2TyQYoRVCs7j3biY/AXkGzYphiriuUvKmQtRjBoP2o08nZHYFzVSOSq4Ixk7OlelilW10Krg==",
"dependencies": {
"@amplitude/analytics-client-common": "^1.2.2",
"@amplitude/analytics-core": "^1.2.5",
"@amplitude/analytics-types": "^1.3.4",
"tslib": "^2.4.1"
}
},
"node_modules/@amplitude/ua-parser-js": {
"version": "0.7.33",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
"integrity": "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@ -7026,16 +6883,6 @@
"ajv": "^6.9.1"
}
},
"node_modules/analytics-client": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/analytics-client/-/analytics-client-2.0.2.tgz",
"integrity": "sha512-03Qo4r86wzw7NV0voG7xNwZjbba7h0wC6A8Dd85Slgt1bMg0jWKBXS9DnWIMiUMT4vfm8HtLaBDJyJIKlu1glQ==",
"dependencies": {
"@amplitude/analytics-browser": "^1.5.4",
"@amplitude/marketing-analytics-browser": "^0.2.4",
"js-cookie": "^3.0.1"
}
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@ -16848,14 +16695,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -3,7 +3,7 @@
"private": true,
"displayName": "balenaEtcher",
"productName": "balenaEtcher",
"version": "2.0.0",
"version": "2.1.3",
"packageType": "local",
"main": ".webpack/main",
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
@ -34,7 +34,6 @@
"@fortawesome/fontawesome-free": "^6.5.2",
"@ronomon/direct-io": "^3.0.1",
"@sentry/electron": "^4.24.0",
"analytics-client": "^2.0.1",
"axios": "^1.6.8",
"debug": "4.3.4",
"drivelist": "^12.0.2",
@ -144,7 +143,7 @@
"node": ">=20 <21"
},
"versionist": {
"publishedAt": "2025-02-20T14:27:01.812Z"
"publishedAt": "2025-05-15T18:09:56.320Z"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",

Binary file not shown.

Binary file not shown.

View File

@ -63,9 +63,6 @@ const rules: Required<ModuleOptions>['rules'] = [
const injectAnalyticsToken = new DefinePlugin({
'process.env.SENTRY_TOKEN': JSON.stringify(process.env.SENTRY_TOKEN || ''),
'process.env.AMPLITUDE_TOKEN': JSON.stringify(
process.env.AMPLITUDE_TOKEN || '',
),
});
export const rendererConfig: Configuration = {