Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removes corvus in favor of sentry and analytics client #3891

Merged
merged 6 commits into from Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Prev Previous commit
Anonymizes all paths before sending
Change-type: patch
  • Loading branch information
otaviojacobi committed Jan 12, 2023
commit 86d43a536f7c9aa6b450a9f2f90341e07364208e
7 changes: 6 additions & 1 deletion lib/gui/app/components/safe-webview/safe-webview.tsx
Expand Up @@ -183,7 +183,12 @@ 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;
analytics.logEvent('SafeWebview loaded', { event });
const { webContents, ...webviewEvent } = event;
analytics.logEvent('SafeWebview loaded', {
...webviewEvent,
screen_height: webContents?.hostWebContents.browserWindowOptions.height,
screen_width: webContents?.hostWebContents.browserWindowOptions.width,
});
this.setState({
shouldShow: event.statusCode === HTTP_OK,
});
Expand Down
122 changes: 117 additions & 5 deletions lib/gui/app/modules/analytics.ts
Expand Up @@ -21,12 +21,14 @@ import * as settings from '../models/settings';
import { store } from '../models/store';
import * as packageJSON 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 anonymizeData = (
export const anonymizeSentryData = (
event: SentryRenderer.Event,
): SentryRenderer.Event => {
event.exception?.values?.forEach((exception) => {
Expand All @@ -50,6 +52,68 @@ export const anonymizeData = (
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
Expand All @@ -58,7 +122,7 @@ export const initAnalytics = _.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryRenderer.init({ dsn, beforeSend: anonymizeData });
SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData });

const projectName =
settings.getSync('analyticsAmplitudeToken') ||
Expand All @@ -75,16 +139,64 @@ export const initAnalytics = _.once(() => {
: createNoopClient();
});

function reportAnalytics(message: string, data: _.Dictionary<any> = {}) {
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 (!obj.hasOwnProperty(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 (!flatObject.hasOwnProperty(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();

analyticsClient.track(message, {
const event = formatEvent({
...data,
applicationSessionUuid,
flashingWorkflowUuid,
});
analyticsClient.track(message, event);
}

/**
Expand All @@ -93,7 +205,7 @@ function reportAnalytics(message: string, data: _.Dictionary<any> = {}) {
* @description
* This function sends the debug message to product analytics services.
*/
export async function logEvent(message: string, data: _.Dictionary<any> = {}) {
export async function logEvent(message: string, data: AnalyticsPayload = {}) {
const shouldReportAnalytics = await settings.get('errorReporting');
if (shouldReportAnalytics) {
initAnalytics();
Expand Down
4 changes: 2 additions & 2 deletions lib/gui/etcher.ts
Expand Up @@ -32,7 +32,7 @@ import { buildWindowMenu } from './menu';
import * as i18n from 'i18next';
import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json';
import { anonymizeData } from './app/modules/analytics';
import { anonymizeSentryData } from './app/modules/analytics';

const customProtocol = 'etcher';
const scheme = `${customProtocol}://`;
Expand Down Expand Up @@ -110,7 +110,7 @@ const initSentryMain = _.once(() => {
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);

SentryMain.init({ dsn, beforeSend: anonymizeData });
SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
});

const sourceSelectorReady = new Promise((resolve) => {
Expand Down