/
exception.py
483 lines (405 loc) · 15.2 KB
/
exception.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
from __future__ import absolute_import
__all__ = ("Exception", "Mechanism", "upgrade_legacy_mechanism")
import re
import six
from django.conf import settings
from sentry.interfaces.base import Interface
from sentry.utils.json import prune_empty_keys
from sentry.interfaces.stacktrace import Stacktrace, slim_frame_data
from sentry.utils.safe import get_path
_type_value_re = re.compile("^(\w+):(.*)$")
def upgrade_legacy_mechanism(data):
"""
Conversion from mechanism objects sent by old sentry-cocoa SDKs. It assumes
"type": "generic" and moves "posix_signal", "mach_exception" into "meta".
All other keys are moved into "data".
Example old payload:
>>> {
>>> "posix_signal": {
>>> "name": "SIGSEGV",
>>> "code_name": "SEGV_NOOP",
>>> "signal": 11,
>>> "code": 0
>>> },
>>> "relevant_address": "0x1",
>>> "mach_exception": {
>>> "exception": 1,
>>> "exception_name": "EXC_BAD_ACCESS",
>>> "subcode": 8,
>>> "code": 1
>>> }
>>> }
Example normalization:
>>> {
>>> "type": "generic",
>>> "data": {
>>> "relevant_address": "0x1"
>>> },
>>> "meta": {
>>> "mach_exception": {
>>> "exception": 1,
>>> "subcode": 8,
>>> "code": 1,
>>> "name": "EXC_BAD_ACCESS"
>>> },
>>> "signal": {
>>> "number": 11,
>>> "code": 0,
>>> "name": "SIGSEGV",
>>> "code_name": "SEGV_NOOP"
>>> }
>>> }
>>> }
"""
# Early exit for current protocol. We assume that when someone sends a
# "type", we do not need to preprocess and can immediately validate
if data is None or data.get("type") is not None:
return data
result = {"type": "generic"}
# "posix_signal" and "mach_exception" were optional root-level objects,
# which have now moved to special keys inside "meta". We only create "meta"
# if there is actual data to add.
posix_signal = data.pop("posix_signal", None)
if posix_signal and posix_signal.get("signal"):
result.setdefault("meta", {})["signal"] = prune_empty_keys(
{
"number": posix_signal.get("signal"),
"code": posix_signal.get("code"),
"name": posix_signal.get("name"),
"code_name": posix_signal.get("code_name"),
}
)
mach_exception = data.pop("mach_exception", None)
if mach_exception:
result.setdefault("meta", {})["mach_exception"] = prune_empty_keys(
{
"exception": mach_exception.get("exception"),
"code": mach_exception.get("code"),
"subcode": mach_exception.get("subcode"),
"name": mach_exception.get("exception_name"),
}
)
# All remaining data has to be moved to the "data" key. We assume that even
# if someone accidentally sent a corret top-level key (such as "handled"),
# it will not pass our interface validation and should be moved to "data"
# instead.
result.setdefault("data", {}).update(data)
return result
class Mechanism(Interface):
"""
an optional field residing in the exception interface. It carries additional
information about the way the exception was created on the target system.
This includes general exception values obtained from operating system or
runtime APIs, as well as mechanism-specific values.
>>> {
>>> "type": "mach",
>>> "description": "EXC_BAD_ACCESS",
>>> "data": {
>>> "relevant_address": "0x1"
>>> },
>>> "handled": false,
>>> "synthetic": false,
>>> "help_link": "https://developer.apple.com/library/content/qa/qa1367/_index.html",
>>> "meta": {
>>> "mach_exception": {
>>> "exception": 1,
>>> "subcode": 8,
>>> "code": 1
>>> },
>>> "signal": {
>>> "number": 11
>>> }
>>> }
>>> }
"""
@classmethod
def to_python(cls, data):
for key in ("type", "synthetic", "description", "help_link", "handled", "data", "meta"):
data.setdefault(key, None)
return cls(**data)
def to_json(self):
return prune_empty_keys(
{
"type": self.type,
"synthetic": self.synthetic,
"description": self.description,
"help_link": self.help_link,
"handled": self.handled,
"data": self.data or None,
"meta": prune_empty_keys(self.meta) or None,
}
)
def iter_tags(self):
yield (self.path, self.type)
if self.handled is not None:
yield ("handled", self.handled and "yes" or "no")
def uncontribute_non_stacktrace_variants(variants):
"""If we have multiple variants and at least one has a stacktrace, we
want to mark all non stacktrace variants non contributing. The reason
for this is that otherwise we end up in very generic grouping which has
some negative consequences for the quality of the groups.
"""
if len(variants) <= 1:
return variants
any_stacktrace_contributes = False
non_contributing_components = []
stacktrace_variants = set()
# In case any of the variants has a contributing stacktrace, we want
# to make all other variants non contributing. Thr e
for (key, component) in six.iteritems(variants):
if any(
s.contributes for s in component.iter_subcomponents(id="stacktrace", recursive=True)
):
any_stacktrace_contributes = True
stacktrace_variants.add(key)
else:
non_contributing_components.append(component)
if any_stacktrace_contributes:
if len(stacktrace_variants) == 1:
hint_suffix = "but the %s variant does" % next(iter(stacktrace_variants))
else:
# this branch is basically dead because we only have two
# variants right now, but this is so this does not break in
# the future.
hint_suffix = "others do"
for component in non_contributing_components:
component.update(
contributes=False,
hint="ignored because this variant does not contain a "
"stacktrace, but %s" % hint_suffix,
)
return variants
class SingleException(Interface):
"""
A standard exception with a ``type`` and value argument, and an optional
``module`` argument describing the exception class type and
module namespace. Either ``type`` or ``value`` must be present.
You can also optionally bind a stacktrace interface to an exception. The
spec is identical to ``stacktrace``.
>>> {
>>> "type": "ValueError",
>>> "value": "My exception value",
>>> "module": "__builtins__",
>>> "mechanism": {},
>>> "stacktrace": {
>>> # see stacktrace
>>> }
>>> }
"""
grouping_variants = ["system", "app"]
@classmethod
def to_python(cls, data, slim_frames=True):
if get_path(data, "stacktrace", "frames", filter=True):
stacktrace = Stacktrace.to_python(data["stacktrace"], slim_frames=slim_frames)
else:
stacktrace = None
if get_path(data, "raw_stacktrace", "frames", filter=True):
raw_stacktrace = Stacktrace.to_python(
data["raw_stacktrace"], slim_frames=slim_frames, raw=True
)
else:
raw_stacktrace = None
type = data.get("type")
value = data.get("value")
if data.get("mechanism"):
mechanism = Mechanism.to_python(data["mechanism"])
else:
mechanism = None
kwargs = {
"type": type,
"value": value,
"module": data.get("module"),
"mechanism": mechanism,
"stacktrace": stacktrace,
"thread_id": data.get("thread_id"),
"raw_stacktrace": raw_stacktrace,
}
return cls(**kwargs)
def to_json(self):
mechanism = (
isinstance(self.mechanism, Mechanism)
and self.mechanism.to_json()
or self.mechanism
or None
)
if self.stacktrace:
stacktrace = self.stacktrace.to_json()
else:
stacktrace = None
if self.raw_stacktrace:
raw_stacktrace = self.raw_stacktrace.to_json()
else:
raw_stacktrace = None
return prune_empty_keys(
{
"type": self.type,
"value": self.value,
"mechanism": mechanism,
"module": self.module,
"stacktrace": stacktrace,
"thread_id": self.thread_id,
"raw_stacktrace": raw_stacktrace,
}
)
def get_api_context(self, is_public=False, platform=None):
mechanism = (
isinstance(self.mechanism, Mechanism)
and self.mechanism.get_api_context(is_public=is_public, platform=platform)
or self.mechanism
or None
)
if self.stacktrace:
stacktrace = self.stacktrace.get_api_context(is_public=is_public, platform=platform)
else:
stacktrace = None
if self.raw_stacktrace:
raw_stacktrace = self.raw_stacktrace.get_api_context(
is_public=is_public, platform=platform
)
else:
raw_stacktrace = None
return {
"type": self.type,
"value": six.text_type(self.value) if self.value else None,
"mechanism": mechanism,
"threadId": self.thread_id,
"module": self.module,
"stacktrace": stacktrace,
"rawStacktrace": raw_stacktrace,
}
def get_api_meta(self, meta, is_public=False, platform=None):
mechanism_meta = (
self.mechanism.get_api_meta(meta["mechanism"], is_public=is_public, platform=platform)
if isinstance(self.mechanism, Mechanism) and meta.get("mechanism")
else None
)
stacktrace_meta = (
self.stacktrace.get_api_meta(meta, is_public=is_public, platform=platform)
if self.stacktrace and meta.get("stacktrace")
else None
)
return {
"": meta.get(""),
"type": meta.get("type"),
"value": meta.get("value"),
"mechanism": mechanism_meta,
"threadId": meta.get("thread_id"),
"module": meta.get("module"),
"stacktrace": stacktrace_meta,
}
class Exception(Interface):
"""
An exception consists of a list of values. In most cases, this list
contains a single exception, with an optional stacktrace interface.
Each exception has a mandatory ``value`` argument and optional ``type`` and
``module`` arguments describing the exception class type and module
namespace.
You can also optionally bind a stacktrace interface to an exception. The
spec is identical to ``stacktrace``.
>>> {
>>> "values": [{
>>> "type": "ValueError",
>>> "value": "My exception value",
>>> "module": "__builtins__",
>>> "mechanism": {
>>> # see sentry.interfaces.Mechanism
>>> },
>>> "stacktrace": {
>>> # see stacktrace
>>> }
>>> }]
>>> }
Values should be sent oldest to newest, this includes both the stacktrace
and the exception itself.
.. note:: This interface can be passed as the 'exception' key in addition
to the full interface path.
"""
score = 2000
grouping_variants = ["system", "app"]
def exceptions(self):
return get_path(self.values, filter=True)
def __getitem__(self, key):
return self.exceptions()[key]
def __iter__(self):
return iter(self.exceptions())
def __len__(self):
return len(self.exceptions())
@classmethod
def to_python(cls, data):
return cls(
values=[
v and SingleException.to_python(v, slim_frames=False)
for v in get_path(data, "values", default=[])
],
exc_omitted=data.get("exc_omitted"),
)
# TODO(ja): Fix all following methods when to_python is refactored. All
# methods below might throw if None exceptions are in ``values``.
def to_json(self):
return prune_empty_keys(
{
"values": [v and v.to_json() for v in self.values] or None,
"exc_omitted": self.exc_omitted,
}
)
def get_api_context(self, is_public=False, platform=None):
return {
"values": [
v.get_api_context(is_public=is_public, platform=platform) for v in self.values if v
],
"hasSystemFrames": any(
v.stacktrace.get_has_system_frames() for v in self.values if v and v.stacktrace
),
"excOmitted": self.exc_omitted,
}
def get_api_meta(self, meta, is_public=False, platform=None):
if not meta:
return meta
result = {}
values = meta.get("values", meta)
for index, value in six.iteritems(values):
exc = self.values[int(index)]
if exc is not None:
result[index] = exc.get_api_meta(value, is_public=is_public, platform=platform)
return {"values": result}
def to_string(self, event, is_public=False, **kwargs):
if not self.values:
return ""
output = []
for exc in self.values:
if not exc:
continue
output.append(u"{0}: {1}\n".format(exc.type, exc.value))
if exc.stacktrace:
output.append(
exc.stacktrace.get_stacktrace(
event, system_frames=False, max_frames=5, header=False
)
+ "\n\n"
)
return ("".join(output)).strip()
def get_stacktrace(self, *args, **kwargs):
exc = self.values[0]
if exc.stacktrace:
return exc.stacktrace.get_stacktrace(*args, **kwargs)
return ""
def iter_tags(self):
if not self.values or not self.values[0]:
return
mechanism = self.values[0].mechanism
if mechanism:
for tag in mechanism.iter_tags():
yield tag
def slim_exception_data(instance, frame_allowance=settings.SENTRY_MAX_STACKTRACE_FRAMES):
"""
Removes various excess metadata from middle frames which go beyond
``frame_allowance``.
"""
# TODO(dcramer): it probably makes sense to prioritize a certain exception
# rather than distributing allowance among all exceptions
frames = []
for exception in instance.values:
if exception is None or not exception.stacktrace:
continue
frames.extend(exception.stacktrace.frames)
slim_frame_data(frames, frame_allowance)