mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-28 17:27:17 +00:00
233 lines
5.8 KiB
TypeScript
233 lines
5.8 KiB
TypeScript
/*
|
|
* Copyright 2016 balena.io
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
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>;
|
|
|
|
const clearUserPath = (filename: string): string => {
|
|
const generatedFile = filename.split('generated').reverse()[0];
|
|
return generatedFile !== filename ? `generated${generatedFile}` : filename;
|
|
};
|
|
|
|
export const anonymizeSentryData = (
|
|
event: SentryRenderer.Event,
|
|
): SentryRenderer.Event => {
|
|
event.exception?.values?.forEach((exception) => {
|
|
exception.stacktrace?.frames?.forEach((frame) => {
|
|
if (frame.filename) {
|
|
frame.filename = clearUserPath(frame.filename);
|
|
}
|
|
});
|
|
});
|
|
|
|
event.breadcrumbs?.forEach((breadcrumb) => {
|
|
if (breadcrumb.data?.url) {
|
|
breadcrumb.data.url = clearUserPath(breadcrumb.data.url);
|
|
}
|
|
});
|
|
|
|
if (event.request?.url) {
|
|
event.request.url = clearUserPath(event.request.url);
|
|
}
|
|
|
|
return event;
|
|
};
|
|
|
|
const extractPathRegex = /(.*)(^|\s)(file:\/\/)?(\w:)?([\\/].+)/;
|
|
const etcherSegmentMarkers = ['app.asar', 'Resources'];
|
|
|
|
export const anonymizePath = (input: string) => {
|
|
// First, extract a part of the value that matches a path pattern.
|
|
const match = extractPathRegex.exec(input);
|
|
if (match === null) {
|
|
return input;
|
|
}
|
|
const mainPart = match[5];
|
|
const space = match[2];
|
|
const beginning = match[1];
|
|
const uriPrefix = match[3] || '';
|
|
|
|
// We have to deal with both Windows and POSIX here.
|
|
// The path starts with its separator (we work with absolute paths).
|
|
const sep = mainPart[0];
|
|
const segments = mainPart.split(sep);
|
|
|
|
// Moving from the end, find the first marker and cut the path from there.
|
|
const startCutIndex = findLastIndex(segments, (segment) =>
|
|
etcherSegmentMarkers.includes(segment),
|
|
);
|
|
return (
|
|
beginning +
|
|
space +
|
|
uriPrefix +
|
|
'[PERSONAL PATH]' +
|
|
sep +
|
|
segments.splice(startCutIndex).join(sep)
|
|
);
|
|
};
|
|
|
|
const safeAnonymizePath = (input: string) => {
|
|
try {
|
|
return anonymizePath(input);
|
|
} catch (e) {
|
|
return '[ANONYMIZE PATH FAILED]';
|
|
}
|
|
};
|
|
|
|
const sensitiveEtcherProperties = [
|
|
'error.description',
|
|
'error.message',
|
|
'error.stack',
|
|
'image',
|
|
'image.path',
|
|
'path',
|
|
];
|
|
|
|
export const anonymizeAnalyticsPayload = (
|
|
data: AnalyticsPayload,
|
|
): AnalyticsPayload => {
|
|
for (const prop of sensitiveEtcherProperties) {
|
|
const value = data[prop];
|
|
if (value != null) {
|
|
data[prop] = safeAnonymizePath(value.toString());
|
|
}
|
|
}
|
|
return data;
|
|
};
|
|
|
|
let analyticsClient: Client;
|
|
/**
|
|
* @summary Init analytics configurations
|
|
*/
|
|
export const initAnalytics = once(() => {
|
|
const dsn =
|
|
settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
|
|
SentryRenderer.init({
|
|
dsn,
|
|
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
|
|
*
|
|
* @description
|
|
* This function logs an exception to error reporting services.
|
|
*/
|
|
export function logException(error: any) {
|
|
const shouldReportErrors = settings.getSync('errorReporting');
|
|
console.error(error);
|
|
if (shouldReportErrors) {
|
|
initAnalytics();
|
|
SentryRenderer.captureException(error);
|
|
}
|
|
}
|