Passed
Push — master ( 6e8656...9792e6 )
by Dean
05:08 queued 02:39
created

ErrorReporterClient.send_remote()   A

Complexity

Conditions 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.048

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
ccs 1
cts 5
cp 0.2
rs 9.4285
cc 2
crap 4.048
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:
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...
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:
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...
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
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 251.
Loading history...
Bug Best Practice introduced by
It seems like you are trying to unpack a non-sequence, which was defined at line 307.
Loading history...
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:
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...
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