1
|
1 |
|
from plugin.core.constants import 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 |
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, iteritems, text_type |
|
|
|
|
11
|
1 |
|
from raven.handlers.logging import SentryHandler, RESERVED, 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 |
|
PARAMS = { |
29
|
|
|
# Message processors + filters |
30
|
|
|
'processors': [ |
31
|
|
|
'raven.processors.RemoveStackLocalsProcessor', |
32
|
|
|
'plugin.raven.processors.RelativePathProcessor' |
33
|
|
|
], |
34
|
|
|
|
35
|
|
|
'exclude_paths': [ |
36
|
|
|
'Framework.api', |
37
|
|
|
'Framework.code', |
38
|
|
|
'Framework.components', |
39
|
|
|
'Framework.core', |
40
|
|
|
'urllib2' |
41
|
|
|
], |
42
|
|
|
|
43
|
|
|
# Plugin + System details |
44
|
|
|
'release': VERSION, |
45
|
|
|
'tags': merge(SystemHelper.attributes(), { |
46
|
|
|
'plugin.version': VERSION, |
47
|
|
|
'plugin.branch': PLUGIN_VERSION_BRANCH |
48
|
|
|
}) |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
# Configure raven breadcrumbs |
52
|
1 |
|
breadcrumbs.ignore_logger('plugin.core.logger.handlers.error_reporter.ErrorReporter') |
53
|
1 |
|
breadcrumbs.ignore_logger('peewee') |
54
|
|
|
|
55
|
|
|
|
56
|
1 |
|
class ErrorReporter(Client): |
57
|
1 |
|
server = 'sentry.skipthe.net' |
58
|
1 |
|
key = '240c00f6a02542f8900d8a6a1aba365a:7432061e2ac54ed0aabe4ec3fe3ea0d9' |
59
|
1 |
|
project = 1 |
60
|
|
|
|
61
|
1 |
|
def __init__(self, dsn=None, raise_send_errors=False, **options): |
62
|
|
|
# Build URI |
63
|
1 |
|
if dsn is None: |
64
|
1 |
|
dsn = self.build_dsn() |
65
|
|
|
|
66
|
|
|
# Construct raven client |
67
|
1 |
|
super(ErrorReporter, self).__init__(dsn, raise_send_errors, **options) |
68
|
|
|
|
69
|
1 |
|
def build_dsn(self, protocol='threaded+requests+http'): |
70
|
1 |
|
return '%s://%s@%s/%s' % ( |
71
|
|
|
protocol, |
72
|
|
|
self.key, |
73
|
|
|
self.server, |
74
|
|
|
self.project |
75
|
|
|
) |
76
|
|
|
|
77
|
1 |
|
def set_protocol(self, protocol): |
78
|
|
|
# Build new DSN URI |
79
|
1 |
|
dsn = self.build_dsn(protocol) |
80
|
|
|
|
81
|
|
|
# Update client DSN |
82
|
1 |
|
self.set_dsn(dsn) |
83
|
|
|
|
84
|
1 |
|
def send_remote(self, url, data, headers=None): |
85
|
|
|
if headers is None: |
86
|
|
|
headers = {} |
87
|
|
|
|
88
|
|
|
# Update user agent |
89
|
|
|
headers['User-Agent'] = 'raven-python/%s tfp/%s-%s' % ( |
90
|
|
|
# Raven |
91
|
|
|
raven.VERSION, |
92
|
|
|
|
93
|
|
|
# Trakt.tv (for Plex) |
94
|
|
|
VERSION, |
95
|
|
|
PLUGIN_VERSION_BRANCH |
96
|
|
|
) |
97
|
|
|
|
98
|
|
|
# Send event |
99
|
|
|
super(ErrorReporter, self).send_remote(url, data, headers) |
100
|
|
|
|
101
|
|
|
|
102
|
1 |
|
class ErrorReporterHandler(SentryHandler): |
103
|
1 |
|
def _emit(self, record, **kwargs): |
104
|
|
|
data, extra = extract_extra(record) |
105
|
|
|
|
106
|
|
|
# Use client name as default user id |
107
|
|
|
data.setdefault('user', {'id': self.client.name}) |
108
|
|
|
|
109
|
|
|
# Retrieve stack |
110
|
|
|
stack = getattr(record, 'stack', None) |
111
|
|
|
if stack is True: |
112
|
|
|
stack = iter_stack_frames() |
113
|
|
|
|
114
|
|
|
if stack: |
115
|
|
|
stack = self._get_targetted_stack(stack, record) |
116
|
|
|
|
117
|
|
|
# Build message |
118
|
|
|
date = datetime.datetime.utcfromtimestamp(record.created) |
119
|
|
|
event_type = 'raven.events.Message' |
120
|
|
|
handler_kwargs = { |
121
|
|
|
'params': record.args, |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
try: |
125
|
|
|
handler_kwargs['message'] = text_type(record.msg) |
126
|
|
|
except UnicodeDecodeError: |
127
|
|
|
# Handle binary strings where it should be unicode... |
128
|
|
|
handler_kwargs['message'] = repr(record.msg)[1:-1] |
129
|
|
|
|
130
|
|
|
try: |
131
|
|
|
handler_kwargs['formatted'] = text_type(record.message) |
132
|
|
|
except UnicodeDecodeError: |
133
|
|
|
# Handle binary strings where it should be unicode... |
134
|
|
|
handler_kwargs['formatted'] = repr(record.message)[1:-1] |
135
|
|
|
|
136
|
|
|
# Retrieve exception information from record |
137
|
|
|
try: |
138
|
|
|
exc_info = self._exc_info(record) |
139
|
|
|
except Exception as ex: |
|
|
|
|
140
|
|
|
log.info('Unable to retrieve exception info - %s', ex, exc_info=True) |
141
|
|
|
exc_info = None |
142
|
|
|
|
143
|
|
|
# Parse exception information |
144
|
|
|
exception_hash = None |
145
|
|
|
|
146
|
|
|
# If there's no exception being processed, exc_info may be a 3-tuple of None |
147
|
|
|
# http://docs.python.org/library/sys.html#sys.exc_info |
148
|
|
|
if exc_info and len(exc_info) == 3 and all(exc_info): |
149
|
|
|
message = handler_kwargs.get('formatted') |
150
|
|
|
|
151
|
|
|
# Replace exception messages with more helpful details |
152
|
|
|
if not record.exc_info and message and RE_TRACEBACK_HEADER.match(message): |
153
|
|
|
# Generate new record title |
154
|
|
|
handler_kwargs['formatted'] = '%s\n\n%s' % ( |
155
|
|
|
self._generate_title(record, exc_info), |
156
|
|
|
message |
157
|
|
|
) |
158
|
|
|
elif not record.exc_info: |
159
|
|
|
log.debug("Message %r doesn't match traceback header", message) |
160
|
|
|
|
161
|
|
|
# capture the standard message first so that we ensure |
162
|
|
|
# the event is recorded as an exception, in addition to having our |
163
|
|
|
# message interface attached |
164
|
|
|
handler = self.client.get_handler(event_type) |
165
|
|
|
data.update(handler.capture(**handler_kwargs)) |
166
|
|
|
|
167
|
|
|
event_type = 'raven.events.Exception' |
168
|
|
|
handler_kwargs = {'exc_info': exc_info} |
169
|
|
|
|
170
|
|
|
# Calculate exception hash |
171
|
|
|
exception_hash = ErrorHasher.hash(exc_info=exc_info) |
172
|
|
|
|
173
|
|
|
# HACK: discover a culprit when we normally couldn't |
174
|
|
|
elif not (data.get('stacktrace') or data.get('culprit')) and (record.name or record.funcName): |
175
|
|
|
culprit = self._label_from_frame({'module': record.name, 'function': record.funcName}) |
176
|
|
|
|
177
|
|
|
if culprit: |
178
|
|
|
data['culprit'] = culprit |
179
|
|
|
|
180
|
|
|
data['level'] = record.levelno |
181
|
|
|
data['logger'] = record.name |
182
|
|
|
|
183
|
|
|
# Store record `tags` in message |
184
|
|
|
if hasattr(record, 'tags'): |
185
|
|
|
kwargs['tags'] = record.tags |
186
|
|
|
elif self.tags: |
187
|
|
|
kwargs['tags'] = self.tags |
188
|
|
|
|
189
|
|
|
# Store `exception_hash` in message (if defined) |
190
|
|
|
if exception_hash: |
191
|
|
|
if 'tags' not in kwargs: |
192
|
|
|
kwargs['tags'] = {} |
193
|
|
|
|
194
|
|
|
kwargs['tags']['exception.hash'] = exception_hash |
195
|
|
|
|
196
|
|
|
kwargs.update(handler_kwargs) |
197
|
|
|
|
198
|
|
|
return self.client.capture( |
199
|
|
|
event_type, stack=stack, data=data, |
200
|
|
|
extra=extra, date=date, **kwargs |
201
|
|
|
) |
202
|
|
|
|
203
|
1 |
|
@classmethod |
204
|
|
|
def _exc_info(cls, record): |
205
|
|
|
if record.exc_info and all(record.exc_info): |
206
|
|
|
return record.exc_info |
207
|
|
|
|
208
|
|
|
# Determine if record is a formatted exception |
209
|
|
|
message = record.getMessage() |
210
|
|
|
|
211
|
|
|
if message and message.lower().startswith('exception'): |
212
|
|
|
return cls._extract_exc_info(record) |
213
|
|
|
|
214
|
|
|
return None |
215
|
|
|
|
216
|
1 |
|
@staticmethod |
217
|
|
|
def _extract_exc_info(record): |
218
|
|
|
# Retrieve last exception information |
219
|
|
|
exc_info = sys.exc_info() |
220
|
|
|
|
221
|
|
|
# Ensure exception information is valid |
222
|
|
|
if not exc_info or len(exc_info) != 3 or not all(exc_info): |
223
|
|
|
return None |
224
|
|
|
|
225
|
|
|
# Retrieve exception |
226
|
|
|
_, ex, _ = exc_info |
227
|
|
|
|
228
|
|
|
if not hasattr(ex, 'message'): |
229
|
|
|
return None |
230
|
|
|
|
231
|
|
|
# Retrieve last line of log record |
232
|
|
|
lines = record.message.strip().split('\n') |
233
|
|
|
last_line = lines[-1].lower() |
234
|
|
|
|
235
|
|
|
# Ensure exception message matches last line of record message |
236
|
|
|
message = ex.message.lower() |
237
|
|
|
|
238
|
|
|
if message not in last_line: |
239
|
|
|
log.debug("Ignored exception with message %r, doesn't match line: %r", message, last_line) |
240
|
|
|
return None |
241
|
|
|
|
242
|
|
|
return exc_info |
243
|
|
|
|
244
|
1 |
|
@classmethod |
245
|
|
|
def _traceback_culprit(cls, tb): |
246
|
|
|
result = None |
247
|
|
|
|
248
|
|
|
found_bundle = False |
249
|
|
|
|
250
|
|
|
while tb is not None: |
251
|
|
|
frame = tb.tb_frame |
252
|
|
|
line_num = tb.tb_lineno |
253
|
|
|
|
254
|
|
|
code = frame.f_code |
255
|
|
|
path = code.co_filename |
256
|
|
|
function_name = code.co_name |
257
|
|
|
|
258
|
|
|
# Retrieve bundle path |
259
|
|
|
bundle_path = cls.match_bundle(path) |
260
|
|
|
|
261
|
|
|
if bundle_path and bundle_path.startswith('trakttv.bundle'): |
262
|
|
|
# Found trace matching the current bundle |
263
|
|
|
found_bundle = True |
264
|
|
|
elif found_bundle: |
265
|
|
|
# Use previous trace matching current bundle |
266
|
|
|
break |
267
|
|
|
|
268
|
|
|
# Check if there is another trace available |
269
|
|
|
if tb.tb_next is None: |
270
|
|
|
# No traces left, use current trace |
271
|
|
|
result = (path, line_num, function_name) |
272
|
|
|
break |
273
|
|
|
|
274
|
|
|
# Store current culprit |
275
|
|
|
result = (path, line_num, function_name) |
276
|
|
|
|
277
|
|
|
# Move to next trace |
278
|
|
|
tb = tb.tb_next |
279
|
|
|
|
280
|
|
|
# Return "best" culprit match |
281
|
|
|
return result |
282
|
|
|
|
283
|
1 |
|
@staticmethod |
284
|
|
|
def match_bundle(path): |
285
|
|
|
path = path.lower().replace('\\', '/') |
286
|
|
|
match = RE_BUNDLE_PATH.match(path) |
287
|
|
|
|
288
|
|
|
if match: |
289
|
|
|
return match.group('path') |
290
|
|
|
|
291
|
|
|
return None |
292
|
|
|
|
293
|
1 |
|
@classmethod |
294
|
|
|
def _generate_title(cls, record, exc_info): |
295
|
|
|
_, ex, tb = exc_info |
296
|
|
|
|
297
|
|
|
# Try retrieve culprit from traceback |
298
|
|
|
try: |
299
|
|
|
culprit = cls._traceback_culprit(tb) |
300
|
|
|
except Exception as ex: |
|
|
|
|
301
|
|
|
log.info('Unable to retrieve traceback culprit - %s', ex, exc_info=True) |
302
|
|
|
culprit = None |
303
|
|
|
|
304
|
|
|
if culprit and len(culprit) == 3: |
305
|
|
|
file_path, _, function_name = culprit |
|
|
|
|
306
|
|
|
|
307
|
|
|
if function_name != '<module>': |
308
|
|
|
return 'Exception raised in %s(): %s' % (function_name, ex) |
309
|
|
|
|
310
|
|
|
# Build module name from path |
311
|
|
|
try: |
312
|
|
|
module = cls._module_name(file_path) |
313
|
|
|
except Exception as ex: |
|
|
|
|
314
|
|
|
log.info('Unable to retrieve module name - %s', ex, exc_info=True) |
315
|
|
|
module = None |
316
|
|
|
|
317
|
|
|
if not module: |
318
|
|
|
return 'Exception raised in <unknown>' |
319
|
|
|
|
320
|
|
|
return 'Exception raised in %s: %s' % (module, ex) |
321
|
|
|
|
322
|
|
|
# Try retrieve culprit from log record |
323
|
|
|
if record.funcName: |
324
|
|
|
return 'Exception raised in %s()' % ( |
325
|
|
|
record.funcName |
326
|
|
|
) |
327
|
|
|
|
328
|
|
|
log.debug('Unable to generate title for record %r, exc_info: %r', record, exc_info) |
329
|
|
|
return 'Exception raised in <unknown>' |
330
|
|
|
|
331
|
1 |
|
@staticmethod |
332
|
|
|
def _module_name(file_path): |
333
|
|
|
# Convert to relative path |
334
|
|
|
path = file_path.lower() |
335
|
|
|
path = path[path.index('trakttv.bundle'):] |
336
|
|
|
path = os.path.splitext(path)[0] |
337
|
|
|
|
338
|
|
|
# Split path into fragments |
339
|
|
|
fragments = path.split(os.sep)[2:] |
340
|
|
|
|
341
|
|
|
if not fragments or fragments[0] not in ['code', 'libraries']: |
342
|
|
|
return None |
343
|
|
|
|
344
|
|
|
# Build module name |
345
|
|
|
module = None |
346
|
|
|
|
347
|
|
|
if fragments[0] == 'code': |
348
|
|
|
module = '.'.join(fragments) |
349
|
|
|
elif fragments[0] == 'libraries': |
350
|
|
|
module = '.'.join(fragments[2:]) |
351
|
|
|
|
352
|
|
|
# Verify module name was built |
353
|
|
|
if not module: |
354
|
|
|
return None |
355
|
|
|
|
356
|
|
|
return module |
357
|
|
|
|
358
|
1 |
|
@staticmethod |
359
|
|
|
def _label_from_frame(frame): |
360
|
|
|
module = frame.get('module') or '?' |
361
|
|
|
function = frame.get('function') or '?' |
362
|
|
|
|
363
|
|
|
if module == function == '?': |
364
|
|
|
return '' |
365
|
|
|
|
366
|
|
|
return '%s in %s' % (module, function) |
367
|
|
|
|
368
|
|
|
|
369
|
|
|
# Build client |
370
|
1 |
|
RAVEN = ErrorReporter(**PARAMS) |
371
|
|
|
|
372
|
|
|
# Construct logging handler |
373
|
1 |
|
ERROR_REPORTER_HANDLER = ErrorReporterHandler(RAVEN, level=logging.WARNING) |
374
|
|
|
|
375
|
1 |
|
ERROR_REPORTER_HANDLER.filters = [ |
376
|
|
|
FrameworkFilter('filter'), |
377
|
|
|
|
378
|
|
|
DuplicateReportFilter(), |
379
|
|
|
EventsReportFilter(), |
380
|
|
|
ExceptionReportFilter(), |
381
|
|
|
RequestsReportFilter(), |
382
|
|
|
TraktReportFilter() |
383
|
|
|
] |
384
|
|
|
|