1
|
1 |
|
from plugin.core.constants import PLUGIN_VERSION, PLUGIN_VERSION_BASE, PLUGIN_VERSION_BRANCH |
2
|
1 |
|
from plugin.core.helpers.error import ErrorHasher |
3
|
1 |
|
from plugin.core.helpers.variable import merge |
4
|
1 |
|
from plugin.core.libraries.helpers.system import SystemHelper |
5
|
1 |
|
from plugin.core.logger.filters import DuplicateReportFilter, ExceptionReportFilter, FrameworkFilter,\ |
6
|
|
|
RequestsReportFilter, TraktReportFilter, TraktNetworkFilter |
7
|
1 |
|
from plugin.core.logger.filters.events import EventsReportFilter |
8
|
|
|
|
9
|
1 |
|
from raven import Client, breadcrumbs |
10
|
1 |
|
from raven._compat import string_types, text_type |
11
|
1 |
|
from raven.handlers.logging import SentryHandler, extract_extra |
12
|
1 |
|
from raven.utils.stacks import iter_stack_frames |
13
|
1 |
|
import datetime |
14
|
1 |
|
import logging |
15
|
1 |
|
import os |
16
|
1 |
|
import raven |
17
|
1 |
|
import re |
18
|
1 |
|
import sys |
19
|
|
|
|
20
|
1 |
|
log = logging.getLogger(__name__) |
21
|
|
|
|
22
|
|
|
|
23
|
1 |
|
VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_BASE]) |
24
|
|
|
|
25
|
1 |
|
RE_BUNDLE_PATH = re.compile(r"^.*?(?P<path>\w+\.bundle(?:\\|\/).*?)$", re.IGNORECASE) |
26
|
1 |
|
RE_TRACEBACK_HEADER = re.compile(r"^Exception (.*?)\(most recent call last\)(.*?)$", re.IGNORECASE | re.DOTALL) |
27
|
|
|
|
28
|
1 |
|
ENVIRONMENTS = { |
29
|
|
|
'master': 'production', |
30
|
|
|
'beta': 'beta' |
31
|
|
|
} |
32
|
|
|
|
33
|
1 |
|
PARAMS = { |
34
|
|
|
# Message processors + filters |
35
|
|
|
'processors': [ |
36
|
|
|
'raven.processors.RemoveStackLocalsProcessor', |
37
|
|
|
'plugin.raven.processors.RelativePathProcessor' |
38
|
|
|
], |
39
|
|
|
|
40
|
|
|
'exclude_paths': [ |
41
|
|
|
'Framework.api', |
42
|
|
|
'Framework.code', |
43
|
|
|
'Framework.components', |
44
|
|
|
'Framework.core', |
45
|
|
|
'urllib2' |
46
|
|
|
], |
47
|
|
|
|
48
|
|
|
# Release details |
49
|
|
|
'release': PLUGIN_VERSION, |
50
|
|
|
'environment': ENVIRONMENTS.get(PLUGIN_VERSION_BRANCH, 'development'), |
51
|
|
|
|
52
|
|
|
# Tags |
53
|
|
|
'tags': merge(SystemHelper.attributes(), { |
54
|
|
|
'plugin.version': VERSION, |
55
|
|
|
'plugin.branch': PLUGIN_VERSION_BRANCH |
56
|
|
|
}) |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
# Configure raven breadcrumbs |
60
|
1 |
|
breadcrumbs.ignore_logger('plugin.core.logger.handlers.error_reporter.ErrorReporter') |
61
|
1 |
|
breadcrumbs.ignore_logger('peewee') |
62
|
|
|
|
63
|
|
|
|
64
|
1 |
|
class ErrorReporterClient(Client): |
65
|
1 |
|
server = 'sentry.skipthe.net' |
66
|
|
|
|
67
|
1 |
|
def __init__(self, project, key, raise_send_errors=False, **kwargs): |
68
|
1 |
|
self.project = project |
69
|
1 |
|
self.key = key |
70
|
|
|
|
71
|
|
|
# Construct raven client |
72
|
1 |
|
super(ErrorReporterClient, self).__init__(self.build_dsn(), raise_send_errors, **kwargs) |
73
|
|
|
|
74
|
1 |
|
def build_dsn(self, protocol='threaded+requests+http'): |
75
|
1 |
|
return '%s://%s@%s/%s' % ( |
76
|
|
|
protocol, |
77
|
|
|
self.key, |
78
|
|
|
self.server, |
79
|
|
|
self.project |
80
|
|
|
) |
81
|
|
|
|
82
|
1 |
|
def set_protocol(self, protocol): |
83
|
|
|
# Build new DSN URI |
84
|
1 |
|
dsn = self.build_dsn(protocol) |
85
|
|
|
|
86
|
|
|
# Update client DSN |
87
|
1 |
|
self.set_dsn(dsn) |
88
|
|
|
|
89
|
1 |
|
def send_remote(self, url, data, headers=None): |
90
|
|
|
if headers is None: |
91
|
|
|
headers = {} |
92
|
|
|
|
93
|
|
|
# Update user agent |
94
|
|
|
headers['User-Agent'] = 'raven-python/%s tfp/%s-%s' % ( |
95
|
|
|
# Raven |
96
|
|
|
raven.VERSION, |
97
|
|
|
|
98
|
|
|
# Trakt.tv (for Plex) |
99
|
|
|
VERSION, |
100
|
|
|
PLUGIN_VERSION_BRANCH |
101
|
|
|
) |
102
|
|
|
|
103
|
|
|
# Send event |
104
|
|
|
super(ErrorReporterClient, self).send_remote(url, data, headers) |
105
|
|
|
|
106
|
|
|
|
107
|
1 |
|
class ErrorReporterHandler(SentryHandler): |
108
|
1 |
|
def _emit(self, record, **kwargs): |
109
|
|
|
data, extra = extract_extra(record) |
110
|
|
|
|
111
|
|
|
# Use client name as default user id |
112
|
|
|
data.setdefault('user', {'id': self.client.name}) |
113
|
|
|
|
114
|
|
|
# Retrieve stack |
115
|
|
|
stack = getattr(record, 'stack', None) |
116
|
|
|
if stack is True: |
117
|
|
|
stack = iter_stack_frames() |
118
|
|
|
|
119
|
|
|
if stack: |
120
|
|
|
stack = self._get_targetted_stack(stack, record) |
121
|
|
|
|
122
|
|
|
# Build message |
123
|
|
|
date = datetime.datetime.utcfromtimestamp(record.created) |
124
|
|
|
event_type = 'raven.events.Message' |
125
|
|
|
handler_kwargs = { |
126
|
|
|
'params': record.args, |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
try: |
130
|
|
|
handler_kwargs['message'] = text_type(record.msg) |
131
|
|
|
except UnicodeDecodeError: |
132
|
|
|
# Handle binary strings where it should be unicode... |
133
|
|
|
handler_kwargs['message'] = repr(record.msg)[1:-1] |
134
|
|
|
|
135
|
|
|
try: |
136
|
|
|
handler_kwargs['formatted'] = text_type(record.message) |
137
|
|
|
except UnicodeDecodeError: |
138
|
|
|
# Handle binary strings where it should be unicode... |
139
|
|
|
handler_kwargs['formatted'] = repr(record.message)[1:-1] |
140
|
|
|
|
141
|
|
|
# Retrieve exception information from record |
142
|
|
|
try: |
143
|
|
|
exc_info = self._exc_info(record) |
144
|
|
|
except Exception as ex: |
|
|
|
|
145
|
|
|
log.info('Unable to retrieve exception info - %s', ex, exc_info=True) |
146
|
|
|
exc_info = None |
147
|
|
|
|
148
|
|
|
# Parse exception information |
149
|
|
|
exception_hash = None |
150
|
|
|
|
151
|
|
|
# If there's no exception being processed, exc_info may be a 3-tuple of None |
152
|
|
|
# http://docs.python.org/library/sys.html#sys.exc_info |
153
|
|
|
if exc_info and len(exc_info) == 3 and all(exc_info): |
154
|
|
|
message = handler_kwargs.get('formatted') |
155
|
|
|
|
156
|
|
|
# Replace exception messages with more helpful details |
157
|
|
|
if not record.exc_info and message and RE_TRACEBACK_HEADER.match(message): |
158
|
|
|
# Generate new record title |
159
|
|
|
handler_kwargs['formatted'] = '%s\n\n%s' % ( |
160
|
|
|
self._generate_title(record, exc_info), |
161
|
|
|
message |
162
|
|
|
) |
163
|
|
|
elif not record.exc_info: |
164
|
|
|
log.debug("Message %r doesn't match traceback header", message) |
165
|
|
|
|
166
|
|
|
# capture the standard message first so that we ensure |
167
|
|
|
# the event is recorded as an exception, in addition to having our |
168
|
|
|
# message interface attached |
169
|
|
|
handler = self.client.get_handler(event_type) |
170
|
|
|
data.update(handler.capture(**handler_kwargs)) |
171
|
|
|
|
172
|
|
|
event_type = 'raven.events.Exception' |
173
|
|
|
handler_kwargs = {'exc_info': exc_info} |
174
|
|
|
|
175
|
|
|
# Calculate exception hash |
176
|
|
|
exception_hash = ErrorHasher.hash(exc_info=exc_info) |
177
|
|
|
|
178
|
|
|
# HACK: discover a culprit when we normally couldn't |
179
|
|
|
elif not (data.get('stacktrace') or data.get('culprit')) and (record.name or record.funcName): |
180
|
|
|
culprit = self._label_from_frame({'module': record.name, 'function': record.funcName}) |
181
|
|
|
|
182
|
|
|
if culprit: |
183
|
|
|
data['culprit'] = culprit |
184
|
|
|
|
185
|
|
|
data['level'] = record.levelno |
186
|
|
|
data['logger'] = record.name |
187
|
|
|
|
188
|
|
|
# Store record `tags` in message |
189
|
|
|
if hasattr(record, 'tags'): |
190
|
|
|
kwargs['tags'] = record.tags |
191
|
|
|
elif self.tags: |
192
|
|
|
kwargs['tags'] = self.tags |
193
|
|
|
|
194
|
|
|
# Store `exception_hash` in message (if defined) |
195
|
|
|
if exception_hash: |
196
|
|
|
if 'tags' not in kwargs: |
197
|
|
|
kwargs['tags'] = {} |
198
|
|
|
|
199
|
|
|
kwargs['tags']['exception.hash'] = exception_hash |
200
|
|
|
|
201
|
|
|
kwargs.update(handler_kwargs) |
202
|
|
|
|
203
|
|
|
return self.client.capture( |
204
|
|
|
event_type, stack=stack, data=data, |
205
|
|
|
extra=extra, date=date, **kwargs |
206
|
|
|
) |
207
|
|
|
|
208
|
1 |
|
@classmethod |
209
|
|
|
def _exc_info(cls, record): |
210
|
|
|
if record.exc_info and all(record.exc_info): |
211
|
|
|
return record.exc_info |
212
|
|
|
|
213
|
|
|
# Determine if record is a formatted exception |
214
|
|
|
message = record.getMessage() |
215
|
|
|
|
216
|
|
|
if message and message.lower().startswith('exception'): |
217
|
|
|
return cls._extract_exc_info(record) |
218
|
|
|
|
219
|
|
|
return None |
220
|
|
|
|
221
|
1 |
|
@staticmethod |
222
|
|
|
def _extract_exc_info(record): |
223
|
|
|
# Retrieve last exception information |
224
|
|
|
exc_info = sys.exc_info() |
225
|
|
|
|
226
|
|
|
# Ensure exception information is valid |
227
|
|
|
if not exc_info or len(exc_info) != 3 or not all(exc_info): |
228
|
|
|
return None |
229
|
|
|
|
230
|
|
|
# Retrieve exception |
231
|
|
|
_, ex, _ = exc_info |
232
|
|
|
|
233
|
|
|
if not hasattr(ex, 'message') or not isinstance(ex.message, string_types): |
234
|
|
|
return None |
235
|
|
|
|
236
|
|
|
# Retrieve last line of log record |
237
|
|
|
lines = record.message.strip().split('\n') |
238
|
|
|
last_line = lines[-1].lower() |
239
|
|
|
|
240
|
|
|
# Ensure exception message matches last line of record message |
241
|
|
|
message = ex.message.lower() |
242
|
|
|
|
243
|
|
|
if message not in last_line: |
244
|
|
|
log.debug("Ignored exception with message %r, doesn't match line: %r", message, last_line) |
245
|
|
|
return None |
246
|
|
|
|
247
|
|
|
return exc_info |
248
|
|
|
|
249
|
1 |
|
@classmethod |
250
|
|
|
def _traceback_culprit(cls, tb): |
251
|
|
|
result = None |
252
|
|
|
|
253
|
|
|
found_bundle = False |
254
|
|
|
|
255
|
|
|
while tb is not None: |
256
|
|
|
frame = tb.tb_frame |
257
|
|
|
line_num = tb.tb_lineno |
258
|
|
|
|
259
|
|
|
code = frame.f_code |
260
|
|
|
path = code.co_filename |
261
|
|
|
function_name = code.co_name |
262
|
|
|
|
263
|
|
|
# Retrieve bundle path |
264
|
|
|
bundle_path = cls.match_bundle(path) |
265
|
|
|
|
266
|
|
|
if bundle_path and bundle_path.startswith('trakttv.bundle'): |
267
|
|
|
# Found trace matching the current bundle |
268
|
|
|
found_bundle = True |
269
|
|
|
elif found_bundle: |
270
|
|
|
# Use previous trace matching current bundle |
271
|
|
|
break |
272
|
|
|
|
273
|
|
|
# Check if there is another trace available |
274
|
|
|
if tb.tb_next is None: |
275
|
|
|
# No traces left, use current trace |
276
|
|
|
result = (path, line_num, function_name) |
277
|
|
|
break |
278
|
|
|
|
279
|
|
|
# Store current culprit |
280
|
|
|
result = (path, line_num, function_name) |
281
|
|
|
|
282
|
|
|
# Move to next trace |
283
|
|
|
tb = tb.tb_next |
284
|
|
|
|
285
|
|
|
# Return "best" culprit match |
286
|
|
|
return result |
287
|
|
|
|
288
|
1 |
|
@staticmethod |
289
|
|
|
def match_bundle(path): |
290
|
|
|
path = path.lower().replace('\\', '/') |
291
|
|
|
match = RE_BUNDLE_PATH.match(path) |
292
|
|
|
|
293
|
|
|
if match: |
294
|
|
|
return match.group('path') |
295
|
|
|
|
296
|
|
|
return None |
297
|
|
|
|
298
|
1 |
|
@classmethod |
299
|
|
|
def _generate_title(cls, record, exc_info): |
300
|
|
|
_, ex, tb = exc_info |
301
|
|
|
|
302
|
|
|
# Try retrieve culprit from traceback |
303
|
|
|
try: |
304
|
|
|
culprit = cls._traceback_culprit(tb) |
305
|
|
|
except Exception as ex: |
|
|
|
|
306
|
|
|
log.info('Unable to retrieve traceback culprit - %s', ex, exc_info=True) |
307
|
|
|
culprit = None |
308
|
|
|
|
309
|
|
|
if culprit and len(culprit) == 3: |
310
|
|
|
file_path, _, function_name = culprit |
|
|
|
|
311
|
|
|
|
312
|
|
|
if function_name != '<module>': |
313
|
|
|
return 'Exception raised in %s(): %s' % (function_name, ex) |
314
|
|
|
|
315
|
|
|
# Build module name from path |
316
|
|
|
try: |
317
|
|
|
module = cls._module_name(file_path) |
318
|
|
|
except Exception as ex: |
|
|
|
|
319
|
|
|
log.info('Unable to retrieve module name - %s', ex, exc_info=True) |
320
|
|
|
module = None |
321
|
|
|
|
322
|
|
|
if not module: |
323
|
|
|
return 'Exception raised in <unknown>' |
324
|
|
|
|
325
|
|
|
return 'Exception raised in %s: %s' % (module, ex) |
326
|
|
|
|
327
|
|
|
# Try retrieve culprit from log record |
328
|
|
|
if record.funcName: |
329
|
|
|
return 'Exception raised in %s()' % ( |
330
|
|
|
record.funcName |
331
|
|
|
) |
332
|
|
|
|
333
|
|
|
log.debug('Unable to generate title for record %r, exc_info: %r', record, exc_info) |
334
|
|
|
return 'Exception raised in <unknown>' |
335
|
|
|
|
336
|
1 |
|
@staticmethod |
337
|
|
|
def _module_name(file_path): |
338
|
|
|
# Convert to relative path |
339
|
|
|
path = file_path.lower() |
340
|
|
|
path = path[path.index('trakttv.bundle'):] |
341
|
|
|
path = os.path.splitext(path)[0] |
342
|
|
|
|
343
|
|
|
# Split path into fragments |
344
|
|
|
fragments = path.split(os.sep)[2:] |
345
|
|
|
|
346
|
|
|
if not fragments or fragments[0] not in ['code', 'libraries']: |
347
|
|
|
return None |
348
|
|
|
|
349
|
|
|
# Build module name |
350
|
|
|
module = None |
351
|
|
|
|
352
|
|
|
if fragments[0] == 'code': |
353
|
|
|
module = '.'.join(fragments) |
354
|
|
|
elif fragments[0] == 'libraries': |
355
|
|
|
module = '.'.join(fragments[2:]) |
356
|
|
|
|
357
|
|
|
# Verify module name was built |
358
|
|
|
if not module: |
359
|
|
|
return None |
360
|
|
|
|
361
|
|
|
return module |
362
|
|
|
|
363
|
1 |
|
@staticmethod |
364
|
|
|
def _label_from_frame(frame): |
365
|
|
|
module = frame.get('module') or '?' |
366
|
|
|
function = frame.get('function') or '?' |
367
|
|
|
|
368
|
|
|
if module == function == '?': |
369
|
|
|
return '' |
370
|
|
|
|
371
|
|
|
return '%s in %s' % (module, function) |
372
|
|
|
|
373
|
|
|
|
374
|
1 |
|
class ErrorReporter(object): |
375
|
1 |
|
plugin = ErrorReporterClient( |
376
|
|
|
project=1, |
377
|
|
|
key='9297cd482d974cc983eaa11665662082:1452d96d3b794ef0914e2b20d7a590b5', |
378
|
|
|
**PARAMS |
379
|
|
|
) |
380
|
|
|
|
381
|
1 |
|
trakt = ErrorReporterClient( |
382
|
|
|
project=8, |
383
|
|
|
key='904ebc3f0c2642aea78c341c7cefbbb6:3fac822481004f8e8e41b56261056a31', |
384
|
|
|
enable_breadcrumbs=False, |
385
|
|
|
**PARAMS |
386
|
|
|
) |
387
|
|
|
|
388
|
1 |
|
@classmethod |
389
|
1 |
|
def construct_handler(cls, client, filters, level=logging.WARNING): |
390
|
1 |
|
handler = ErrorReporterHandler(client, level=level) |
391
|
1 |
|
handler.filters = filters |
392
|
1 |
|
return handler |
393
|
|
|
|
394
|
1 |
|
@classmethod |
395
|
|
|
def set_name(cls, name): |
396
|
|
|
cls.plugin.name = name |
397
|
|
|
cls.trakt.name = name |
398
|
|
|
|
399
|
1 |
|
@classmethod |
400
|
|
|
def set_protocol(cls, protocol): |
401
|
1 |
|
cls.plugin.set_protocol(protocol) |
402
|
1 |
|
cls.trakt.set_protocol(protocol) |
403
|
|
|
|
404
|
1 |
|
@classmethod |
405
|
|
|
def set_tags(cls, *args, **kwargs): |
406
|
|
|
# Update clients with dictionary arguments |
407
|
1 |
|
for value in args: |
408
|
1 |
|
if type(value) is dict: |
409
|
1 |
|
cls.plugin.tags.update(value) |
410
|
1 |
|
cls.trakt.tags.update(value) |
411
|
|
|
else: |
412
|
|
|
raise ValueError('Only dictionaries can be provided as arguments, found: %s' % type(value)) |
413
|
|
|
|
414
|
|
|
# Update clients with `kwargs` tags |
415
|
1 |
|
cls.plugin.tags.update(kwargs) |
416
|
1 |
|
cls.trakt.tags.update(kwargs) |
417
|
|
|
|
418
|
|
|
|
419
|
|
|
# Construct logging handlers |
420
|
1 |
|
PLUGIN_REPORTER_HANDLER = ErrorReporter.construct_handler(ErrorReporter.plugin, [ |
421
|
|
|
FrameworkFilter('filter'), |
422
|
|
|
TraktNetworkFilter(), |
423
|
|
|
|
424
|
|
|
DuplicateReportFilter(), |
425
|
|
|
EventsReportFilter(), |
426
|
|
|
ExceptionReportFilter(), |
427
|
|
|
RequestsReportFilter(), |
428
|
|
|
TraktReportFilter() |
429
|
|
|
]) |
430
|
|
|
|
431
|
1 |
|
TRAKT_REPORTER_HANDLER = ErrorReporter.construct_handler(ErrorReporter.trakt, [ |
432
|
|
|
TraktNetworkFilter(mode='include') |
433
|
|
|
]) |
434
|
|
|
|
Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.
So, unless you specifically plan to handle any error, consider adding a more specific exception.