Skip to content

Initial integration of Sentry (crash monitoring) #4172

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

Merged
merged 6 commits into from
Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
sudo apt install libopenshot-audio-dev libopenshot-dev python3-openshot
sudo apt install qttranslations5-l10n libssl-dev xvfb
sudo apt install python3-pyqt5 python3-pyqt5.qtsvg python3-pyqt5.qtwebengine python3-pyqt5.qtopengl python3-zmq python3-xdg
pip3 install setuptools wheel
pip3 install setuptools wheel sentry-sdk
pip3 install cx_Freeze==6.1 distro defusedxml requests certifi chardet urllib3

- name: Build Python package
Expand Down
9 changes: 5 additions & 4 deletions src/classes/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import traceback
import json

from classes import exceptions
from PyQt5.QtCore import PYQT_VERSION_STR
from PyQt5.QtCore import QT_VERSION_STR
from PyQt5.QtCore import pyqtSlot
Expand Down Expand Up @@ -136,6 +137,10 @@ def __init__(self, *args, mode=None):
# Set location of OpenShot program (for libopenshot)
openshot.Settings.Instance().PATH_OPENSHOT_INSTALL = info.PATH

# Check to disable sentry
if not self.settings.get('send_metrics'):
exceptions.disable_sentry_tracing()

def show_environment(self, info, openshot):
log = self.log
try:
Expand Down Expand Up @@ -163,10 +168,6 @@ def show_environment(self, info, openshot):
except Exception:
log.debug("Error displaying dependency/system details", exc_info=1)

# Init and attach exception handler
from classes import exceptions
sys.excepthook = exceptions.ExceptionHandler

def check_libopenshot_version(self, info, openshot):
"""Detect minimum libopenshot version"""
_ = self._tr
Expand Down
56 changes: 38 additions & 18 deletions src/classes/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,40 @@
import os
import traceback
import platform
import distro
import sentry_sdk

from classes import info
from classes.logger import log


def ExceptionHandler(exeception_type, exeception_value, exeception_traceback):
"""Callback for any unhandled exceptions"""
from classes.metrics import track_exception_stacktrace
def init_sentry_tracing():
"""Init all Sentry tracing"""

log.error(
'Unhandled Exception',
exc_info=(exeception_type, exeception_value, exeception_traceback))
# Determine sample rate for exceptions
traces_sample_rate = 0.1
environment = "production"
if "-dev" in info.VERSION:
# Dev mode, trace all exceptions
traces_sample_rate = 1.0
environment = "development"

# Build string of stack trace
stacktrace = "Python %s" % "".join(
traceback.format_exception(
exeception_type, exeception_value, exeception_traceback))
# Initialize sentry exception tracing
sentry_sdk.init(
"https://[email protected]/5795985",
traces_sample_rate=traces_sample_rate,
release=f"openshot@{info.VERSION}",
environment=environment
)
sentry_sdk.set_tag("system", platform.system())
sentry_sdk.set_tag("machine", platform.machine())
sentry_sdk.set_tag("processor", platform.processor())
sentry_sdk.set_tag("platform", platform.platform())
sentry_sdk.set_tag("distro", " ".join(distro.linux_distribution()))

# Report traceback to webservice (if enabled)
track_exception_stacktrace(stacktrace, "openshot-qt")

def disable_sentry_tracing():
"""Disable all Sentry tracing requests"""
sentry_sdk.init()


def tail_file(f, n, offset=None):
Expand All @@ -72,7 +86,7 @@ def tail_file(f, n, offset=None):

def libopenshot_crash_recovery():
"""Walk libopenshot.log for the last line before this launch"""
from classes.metrics import track_exception_stacktrace, track_metric_error
from classes.metrics import track_metric_error

log_path = os.path.join(info.USER_PATH, "libopenshot.log")
last_log_line = ""
Expand All @@ -84,7 +98,8 @@ def libopenshot_crash_recovery():
with open(log_path, "rb") as f:
# Read from bottom up
for raw_line in reversed(tail_file(f, 500)):
line = str(raw_line, 'utf-8')
# Format and remove extra spaces from line
line = " ".join(str(raw_line, 'utf-8').split()) + "\n"
# Detect stack trace
if "End of Stack Trace" in line:
found_stack = True
Expand Down Expand Up @@ -113,10 +128,13 @@ def libopenshot_crash_recovery():
# Split last stack trace (if any)
if last_stack_trace:
# Get top line of stack trace (for metrics)
last_log_line = last_stack_trace.split("\n")[0].strip()
exception_lines = last_stack_trace.split("\n")
last_log_line = exception_lines[0].strip()

# Send stacktrace for debugging (if send metrics is enabled)
track_exception_stacktrace(last_stack_trace, "libopenshot")
# Format and add exception log to Sentry context
# Split list of lines into smaller lists (so we don't exceed Sentry limits)
sentry_sdk.set_context("libopenshot", {"stack-trace": exception_lines})
sentry_sdk.set_tag("component", "libopenshot")

# Clear / normalize log line (so we can roll them up in the analytics)
if last_log_line:
Expand All @@ -142,3 +160,5 @@ def libopenshot_crash_recovery():

# Report exception (with last libopenshot line... if found)
track_metric_error("unhandled-crash%s" % last_log_line, True)

return last_log_line
2 changes: 1 addition & 1 deletion src/classes/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import os
from time import strftime

VERSION = "2.5.1-dev2"
VERSION = "2.5.1-dev3"
MINIMUM_LIBOPENSHOT_VERSION = "0.2.5"
DATE = "20200228000000"
NAME = "openshot-qt"
Expand Down
2 changes: 2 additions & 0 deletions src/classes/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import os
import locale
import sentry_sdk

from PyQt5.QtCore import QLocale, QLibraryInfo, QTranslator, QCoreApplication

Expand Down Expand Up @@ -119,6 +120,7 @@ def init_language():
if found_language:
log.debug("Exiting translation system (since we successfully loaded: {})".format(locale_name))
info.CURRENT_LANGUAGE = locale_name
sentry_sdk.set_tag("locale", locale_name)
break


Expand Down
27 changes: 0 additions & 27 deletions src/classes/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,6 @@ def track_metric_error(error_name, is_fatal=False):
t.start()


def track_exception_stacktrace(stacktrace, source):
"""Track an exception/stacktrace has occurred"""
t = threading.Thread(target=send_exception, args=[stacktrace, source])
t.daemon = True
t.start()


def track_metric_session(is_start=True):
"""Track a GUI screen being shown"""
metric_params = deepcopy(params)
Expand Down Expand Up @@ -183,23 +176,3 @@ def send_metric(params):
# All metrics have been sent (or attempted to send)
# Clear the queue
metric_queue.clear()


def send_exception(stacktrace, source):
"""Send exception stacktrace over HTTP for tracking"""
# Check if the user wants to send metrics and errors
if s.get("send_metrics"):

data = urllib.parse.urlencode({"stacktrace": stacktrace,
"platform": platform.system(),
"version": info.VERSION,
"source": source,
"unique_install_id": s.get("unique_install_id")})
url = "http://www.openshot.org/exception/json/"

# Send exception HTTP data
try:
r = requests.post(url, data=data, headers={"user-agent": user_agent, "content-type": "application/x-www-form-urlencoded"}, verify=False)
log.info("Track exception: [%s] %s | %s", r.status_code, r.url, r.text)
except Exception:
log.warning("Failed to track exception", exc_info=1)
7 changes: 5 additions & 2 deletions src/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,14 @@
pass # Quietly fail for older Qt5 versions

try:
from classes import info
from classes import info, exceptions
except ImportError:
import openshot_qt
sys.path.append(openshot_qt.OPENSHOT_PATH)
from classes import info
from classes import info, exceptions

# Initialize sentry exception tracing
exceptions.init_sentry_tracing()

# Global holder for QApplication instance
app = None
Expand Down
12 changes: 10 additions & 2 deletions src/windows/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import os
import shutil
import sentry_sdk
import webbrowser
from copy import deepcopy
from time import sleep
Expand Down Expand Up @@ -219,13 +220,17 @@ def create_lock_file(self):
lock_path = os.path.join(info.USER_PATH, ".lock")
# Check if it already exists
if os.path.exists(lock_path):
exceptions.libopenshot_crash_recovery()
log.error("Unhandled crash detected. Preserving cache.")
last_log_line = exceptions.libopenshot_crash_recovery() or "No Log Detected"
log.error(f"Unhandled crash detected: {last_log_line}")
self.destroy_lock_file()
else:
# Normal startup, clear thumbnails
self.clear_all_thumbnails()

# Reset Sentry component (it can be temporarily changed to libopenshot during
# the call to libopenshot_crash_recovery above)
sentry_sdk.set_tag("component", "openshot-qt")

# Write lock file (try a few times if failure)
lock_value = str(uuid4())
for attempt in range(5):
Expand Down Expand Up @@ -2804,6 +2809,9 @@ def __init__(self, *args, mode=None):
# Track 1st launch metric
track_metric_screen("initial-launch-screen")

# Set unique id for Sentry
sentry_sdk.set_user({"id": s.get("unique_install_id")})

# Track main screen
track_metric_screen("main-screen")

Expand Down
3 changes: 3 additions & 0 deletions src/windows/views/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from classes.logger import log
from classes.app import get_app
from classes.metrics import track_metric_screen
from classes.exceptions import init_sentry_tracing, disable_sentry_tracing


class TutorialDialog(QWidget):
Expand Down Expand Up @@ -70,12 +71,14 @@ def checkbox_metrics_callback(self, state):
if state == Qt.Checked:
# Enabling metrics sending
s.set("send_metrics", True)
init_sentry_tracing()

# Opt-in for metrics tracking
track_metric_screen("metrics-opt-in")
else:
# Opt-out for metrics tracking
track_metric_screen("metrics-opt-out")
disable_sentry_tracing()

# Disable metric sending
s.set("send_metrics", False)
Expand Down