-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Expand file tree
/
Copy patherrormapping.py
More file actions
146 lines (117 loc) · 4.45 KB
/
errormapping.py
File metadata and controls
146 lines (117 loc) · 4.45 KB
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
from __future__ import annotations
import logging
import random
import re
import time
from urllib.parse import parse_qsl
import orjson
from django.conf import settings
from django.core.cache import cache
from sentry import http
from sentry.utils.meta import Meta
from sentry.utils.safe import get_path
from sentry.utils.strings import count_sprintf_parameters
logger = logging.getLogger(__name__)
SOFT_TIMEOUT = 600
SOFT_TIMEOUT_FUZZINESS = 10
HARD_TIMEOUT = 3 * 24 * 60 * 60 # 3 days = 259200 seconds
REACT_MAPPING_URL = (
"https://raw.githubusercontent.com/facebook/react/master/scripts/error-codes/codes.json"
)
# Regex for React error messages.
# * The `(\d+)` group matches the error code
# * The `(?:\?(\S+))?` group optionally matches a query (non-capturing),
# and `(\S+)` matches the query parameters.
REACT_ERROR_REGEX = r"Minified React error #(\d+); visit https?://[^?]+(?:\?(\S+))?"
error_processors: dict[str, Processor] = {}
def is_expired(ts):
return ts > (time.time() - SOFT_TIMEOUT - random.random() * SOFT_TIMEOUT_FUZZINESS)
class Processor:
def __init__(self, vendor: str, mapping_url, regex, func):
self.vendor: str = vendor
self.mapping_url = mapping_url
self.regex = re.compile(regex)
self.func = func
def load_mapping(self):
key = f"javascript.errormapping:{self.vendor}"
mapping = cache.get(key)
cached_rv = None
if mapping is not None:
ts, cached_rv = orjson.loads(mapping)
if not is_expired(ts):
return cached_rv
try:
with http.build_session() as session:
response = session.get(
self.mapping_url,
allow_redirects=True,
timeout=settings.SENTRY_SOURCE_FETCH_TIMEOUT,
)
# Make sure we only get a 2xx to prevent caching bad data
response.raise_for_status()
data = response.json()
cache.set(key, orjson.dumps([time.time(), data]).decode(), HARD_TIMEOUT)
except Exception:
if cached_rv is None:
raise
return cached_rv
return data
def try_process(self, exc):
if not exc.get("value"):
return False
match = self.regex.search(exc["value"])
if match is None:
return False
mapping = self.load_mapping()
return self.func(exc, match, mapping)
def minified_error(vendor, mapping_url, regex):
def decorator(f):
error_processors[vendor] = Processor(vendor, mapping_url, regex, f)
return decorator
@minified_error(
vendor="react",
mapping_url=REACT_MAPPING_URL,
regex=REACT_ERROR_REGEX,
)
def process_react_exception(exc, match, mapping):
error_id, qs = match.groups()
msg_format = mapping.get(error_id)
if msg_format is None:
return False
arg_count = count_sprintf_parameters(msg_format)
args = []
for k, v in parse_qsl(qs, keep_blank_values=True):
if k == "args[]":
args.append(v)
# Due to truncated error messages we sometimes might not be able to
# get all arguments. In that case we fill up missing parameters for
# the format string with <redacted>.
args_t = tuple(args + ["<redacted>"] * (arg_count - len(args)))[:arg_count]
exc["value"] = msg_format % args_t
return True
def rewrite_exception(data):
"""Rewrite an exception in an event if needed. Updates the exception
in place and returns `True` if a modification was performed or `False`
otherwise.
"""
meta = Meta(data.get("_meta"))
rv = False
values_meta = meta.enter("exception", "values")
for index, exc in enumerate(get_path(data, "exception", "values", default=())):
if exc is None:
continue
for processor in error_processors.values():
try:
original_value = exc.get("value")
if processor.try_process(exc):
values_meta.enter(index, "value").add_remark(
{"rule_id": f"@processing:{processor.vendor}", "type": "s"}, original_value
)
rv = True
break
except Exception as e:
logger.exception('Failed to run processor "%s": %s', processor.vendor, e)
data.setdefault("_metrics", {})["flag.processing.error"] = True
if meta.raw():
data["_meta"] = meta.raw()
return rv