Passed
Push — beta ( 72a57d...7d0ef0 )
by Dean
03:02
created

ErrorReporterHandler   D

Complexity

Total Complexity 61

Size/Duplication

Total Lines 263
Duplicated Lines 0 %

Test Coverage

Coverage 5.41%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 61
c 3
b 0
f 0
dl 0
loc 263
ccs 8
cts 148
cp 0.0541
rs 4.054

7 Methods

Rating   Name   Duplication   Size   Complexity  
B _traceback_culprit() 0 38 6
D _generate_title() 0 37 8
A match_bundle() 0 9 2
B _module_name() 0 26 6
F _emit() 0 106 28
B _extract_exc_info() 0 27 6
B _exc_info() 0 12 5

How to fix   Complexity   

Complex Class

Complex classes like ErrorReporterHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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