Passed
Push — master ( aeb165...d9ae97 )
by Dean
03:04
created

ErrorReporterHandler._module_name()   B

Complexity

Conditions 6

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

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