Passed
Push — master ( ce647f...f4ead8 )
by Dean
02:57
created

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