chore: add Sentry error reporting to project

This commit is contained in:
Jelle Glebbeek
2021-06-01 03:42:33 +02:00
parent 8c928dcfe5
commit 4c59c26f5d
7 changed files with 102 additions and 53 deletions

10
main.js
View File

@@ -8,14 +8,19 @@ const AppUpdater = require("./modules/AppUpdater");
const TaskList = require("./modules/persistence/TaskList");
const DoneAction = require("./modules/DoneAction");
const ClipboardWatcher = require("./modules/ClipboardWatcher");
const Analytics = require("./modules/Analytics");
let win
let env
let queryManager
let clipboardWatcher
let taskList
let analytics;
let appStarting = true;
analytics = new Analytics(app);
analytics.initSentry().then(() => console.log("Sentry initialized"));
function sendLogToRenderer(log, isErr) {
if(win == null) return;
win.webContents.send("log", {log: log, isErr: isErr});
@@ -196,12 +201,9 @@ function createWindow(env) {
}
app.on('ready', async () => {
env = new Environment(app);
env = new Environment(app, analytics);
await env.initialize();
createWindow(env);
if(app.isPackaged && process.argv[2] !== '--dev') {
env.analytics.sendDownload();
}
globalShortcut.register('Control+Shift+I', () => { win.webContents.openDevTools(); })
})

View File

@@ -1,25 +1,27 @@
const axios = require("axios");
const querystring = require("querystring");
const Utils = require("./Utils");
const Sentry = require("@sentry/electron");
const path = require("path");
class Analytics {
constructor(version, paths, settings) {
this.paths = paths;
this.version = version;
this.settings = settings;
constructor(app) {
this.app = app;
}
async sendDownload() {
if(!this.settings.statSend) {
await axios.post('http://backend.jelleglebbeek.com/youtubedl/download.php/', querystring.stringify({ version: this.version }));
this.settings.statSend = true;
this.settings.save();
}
initSentry() {
return new Promise(resolve => {
require('dotenv').config({path: this.app.isPackaged ? path.join(process.cwd(), "/resources/app.asar/.env") : path.resolve(process.cwd(), '.env')});
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: "youtube-dl-gui@" + this.app.getVersion(),
sendDefaultPii: true,
environment: process.argv[2] === '--dev' ? "development" : "production"
});
resolve();
});
}
async sendReport(err) {
const id = Utils.getRandomID(8);
await axios.post('http://backend.jelleglebbeek.com/youtubedl/errorreport.php/', querystring.stringify({ id: id, version: this.version, code: err.error.code, description: err.error.description, platform: process.platform, url: err.url, type: err.type, quality: err.quality}));
async sendReport(id) {
//Legacy code, no longer used.
//Await axios.post('http://backend.jelleglebbeek.com/youtubedl/errorreport.php/', querystring.stringify({ id: id, version: this.version, code: err.error.code, description: err.error.description, platform: process.platform, url: err.url, type: err.type, quality: err.quality}));
return id;
}
}

View File

@@ -1,13 +1,13 @@
const Bottleneck = require("bottleneck");
const Filepaths = require("./Filepaths");
const Settings = require("./persistence/Settings");
const Analytics = require("./Analytics");
const DetectPython = require("./DetectPython");
const fs = require("fs").promises;
class Environment {
constructor(app) {
constructor(app, analytics) {
this.app = app;
this.analytics = analytics;
this.version = app.getVersion();
this.cookiePath = null;
this.mainAudioOnly = false;
@@ -45,7 +45,6 @@ class Environment {
} else {
this.pythonCommand = "python";
}
this.analytics = new Analytics(this.app.getVersion(), this.paths, this.settings);
}
changeMaxConcurrent(max) {

View File

@@ -1,3 +1,6 @@
const Sentry = require("@sentry/electron");
const Utils = require("./Utils");
class ErrorHandler {
constructor(win, queryManager, env) {
this.env = env;
@@ -188,12 +191,25 @@ class ErrorHandler {
if(video.type === "playlist") return;
let errorDef = {
identifier: identifier,
error_id: Utils.getRandomID(8),
unexpected: true,
error: {
code: "Unhandled exception",
description: error,
}
};
Sentry.captureMessage(error, scope => {
scope.setLevel(Sentry.Severity.Error);
scope.setTag("url", video.url);
scope.setTag("error_id", errorDef.error_id);
if(video.formats != null) {
scope.setData("formats", video.formats);
}
if(video.selected_format_index != null) {
scope.setData("selected_format", video.formats[video.selected_format_index].serialize())
}
scope.setData("settings", this.env.settings);
});
this.win.webContents.send("error", errorDef);
this.unhandledErrors.push(errorDef);
this.queryManager.onError(identifier);
@@ -206,6 +222,7 @@ class ErrorHandler {
console.error(errorDef.code + " - " + errorDef.description);
this.win.webContents.send("error", { error: errorDef, identifier: identifier, unexpected: false, url: video.url });
this.queryManager.onError(identifier);
Sentry.captureMessage(errorDef.code, Sentry.Severity.Warning);
}
async reportError(args) {
@@ -215,7 +232,7 @@ class ErrorHandler {
err.url = video.url;
err.type = args.type;
err.quality = args.quality;
return await this.env.analytics.sendReport(err);
return await this.env.analytics.sendReport(err.error_id);
}
}
}

View File

@@ -1,5 +1,14 @@
const Sentry = require("@sentry/electron");
const version = require('./package.json').version;
const { contextBridge, ipcRenderer } = require('electron')
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: "youtube-dl-gui@" + version,
sendDefaultPii: true,
environment: process.argv[2] === '--dev' ? "development" : "production"
});
contextBridge.exposeInMainWorld(
"main",
{

View File

@@ -1,50 +1,66 @@
const Analytics = require('../modules/Analytics');
const axios = require("axios");
const Utils = require("../modules/Utils");
const dotenv = require("dotenv");
const Sentry = require("@sentry/electron");
const path = require("path");
jest.mock('axios');
jest.mock('dotenv');
jest.mock('@sentry/electron', () => ({
init: jest.fn()
}));
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(axios, 'post').mockResolvedValue("");
});
describe('sendDownload', () => {
it('posts data to the backend when not done already', () => {
const instance = new Analytics("v2.0.0-test1", null, {statSend: false, save: jest.fn()});
instance.sendDownload().then(() => {
expect(axios.post).toBeCalledTimes(1);
});
describe('init sentry', () => {
it("loads dotenv with packaged path", () => {
const dotenvMock = jest.spyOn(dotenv, 'config').mockImplementation(() => {});
const instance = instanceBuiler(true);
instance.initSentry();
expect(dotenvMock).toBeCalledTimes(1);
expect(dotenvMock).toBeCalledWith({path: path.join(process.cwd(), "/resources/app.asar/.env")});
});
it("loads dotenv with test path", () => {
const dotenvMock = jest.spyOn(dotenv, 'config').mockImplementation(() => {});
const instance = instanceBuiler(false);
instance.initSentry();
expect(dotenvMock).toBeCalledTimes(1);
expect(dotenvMock).toBeCalledWith({path: path.resolve(process.cwd(), '.env')})
});
it('does not post data to backend when done already', () => {
const instance = new Analytics("v2.0.0-test1", null, {statSend: true, save: jest.fn()});
instance.sendDownload().then(() => {
expect(axios.post).not.toBeCalled();
});
});
it('updates the statSend setting after sending data', () => {
const instance = new Analytics("v2.0.0-test1", null, {statSend: false, save: jest.fn()});
instance.sendDownload().then(() => {
expect(instance.settings.statSend).toBe(true);
expect(instance.settings.save).toBeCalledTimes(1);
});
it("inits sentry in dev mode", () => {
process.argv = ["", "", "--dev"];
jest.spyOn(dotenv, 'config').mockImplementation(() => {});
const instance = instanceBuiler();
instance.initSentry();
expect(Sentry.init).toBeCalledTimes(1);
expect(Sentry.init.mock.calls[0][0].environment).toBe("development");
});
it("inits sentry in prod mode", () => {
process.argv = ["", ""];
jest.spyOn(dotenv, 'config').mockImplementation(() => {});
const instance = instanceBuiler();
instance.initSentry();
expect(Sentry.init).toBeCalledTimes(1);
expect(Sentry.init.mock.calls[0][0].environment).toBe("production");
});
});
describe('sendReport', () => {
it('posts data to the backend', () => {
const instance = new Analytics("v2.0.0-test1", null, {statSend: true, save: jest.fn()});
instance.sendReport({error: {}}).then(() => {
expect(axios.post).toBeCalledTimes(1);
});
});
it('returns the report id', () => {
const testID = "test__id";
const randomIDSpy = jest.spyOn(Utils, 'getRandomID').mockReturnValueOnce(testID)
const instance = new Analytics("v2.0.0-test1", null, {statSend: true, save: jest.fn()});
instance.sendReport({error: {}}).then((data) => {
const instance = new Analytics();
instance.sendReport(testID).then((data) => {
expect(data).toBe(testID);
});
randomIDSpy.mockRestore();
});
});
function instanceBuiler(packaged) {
const app = { isPackaged: packaged, getVersion: jest.fn()}
return new Analytics(app);
}

View File

@@ -1,4 +1,5 @@
const ErrorHandler = require("../modules/ErrorHandler");
const Utils = require("../modules/Utils");
beforeEach(() => {
jest.clearAllMocks();
@@ -54,17 +55,20 @@ describe('raiseUnhandledError', () => {
expect(instance.queryManager.onError).not.toBeCalled();
});
it('adds the error to the unhandled error list', () => {
const randomIDSpy = jest.spyOn(Utils, 'getRandomID').mockReturnValueOnce("12345678");
const instance = instanceBuilder();
instance.queryManager.getVideo.mockReturnValue({type: "single", identifier: "test__identifier"});
instance.raiseUnhandledError("test__unhandled", "test__identifier");
expect(instance.unhandledErrors).toContainEqual({
identifier: "test__identifier",
unexpected: true,
error_id: "12345678",
error: {
code: "Unhandled exception",
description: "test__unhandled",
}
});
randomIDSpy.mockRestore();
});
it('sends the error to the renderer process', () => {
const instance = instanceBuilder();