Passed
Push — develop ( 1e512e...b44848 )
by Dean
02:45
created

ErrorReporterHandler.match_bundle()   A

Complexity

Conditions 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.3145

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
ccs 1
cts 6
cp 0.1666
rs 9.6666
cc 2
crap 4.3145
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
0 ignored issues
show
Unused Code introduced by
Unused iteritems imported from raven._compat
Loading history...
Unused Code introduced by
Unused string_types imported from raven._compat
Loading history...
11 1
from raven.handlers.logging import SentryHandler, RESERVED, extract_extra
0 ignored issues
show
Unused Code introduced by
Unused RESERVED imported from raven.handlers.logging
Loading history...
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:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
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:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
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
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are trying to unpack a non-sequence, which was defined at line 246.
Loading history...
Bug Best Practice introduced by
It seems like you are trying to unpack a non-sequence, which was defined at line 302.
Loading history...
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:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

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.

Loading history...
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