serializers.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. import uuid
  2. from typing import Dict, List, Tuple, Union
  3. from urllib.parse import urlparse
  4. from anonymizeip import anonymize_ip
  5. from django.db import transaction
  6. from django.db.utils import IntegrityError
  7. from ipware import get_client_ip
  8. from rest_framework import serializers
  9. from rest_framework.exceptions import PermissionDenied
  10. from environments.models import Environment
  11. from glitchtip.serializers import FlexibleDateTimeField
  12. from issues.models import EventType, Issue
  13. from issues.serializers import BaseBreadcrumbsSerializer
  14. from issues.tasks import update_search_index_issue
  15. from releases.models import Release
  16. from sentry.eventtypes.base import DefaultEvent
  17. from sentry.eventtypes.error import ErrorEvent
  18. from .event_context_processors import EVENT_CONTEXT_PROCESSORS
  19. from .event_processors import EVENT_PROCESSORS
  20. from .event_tag_processors import TAG_PROCESSORS
  21. from .fields import (
  22. ForgivingDisallowRegexField,
  23. ForgivingHStoreField,
  24. GenericField,
  25. QueryStringField,
  26. )
  27. from .models import Event, LogLevel
  28. def replace(data: Union[str, dict, list], match: str, repl: str):
  29. """A recursive replace function"""
  30. if isinstance(data, dict):
  31. return {k: replace(v, match, repl) for k, v in data.items()}
  32. elif isinstance(data, list):
  33. return [replace(i, match, repl) for i in data]
  34. elif isinstance(data, str):
  35. return data.replace(match, repl)
  36. return data
  37. def sanitize_bad_postgres_chars(data: Union[str, dict, list]):
  38. """
  39. Remove values which are not supported by the postgres string data types
  40. """
  41. known_bads = ["\x00"]
  42. for known_bad in known_bads:
  43. data = data.replace(known_bad, " ")
  44. return data
  45. def sanitize_bad_postgres_json(data: Union[str, dict, list]):
  46. """
  47. Remove values which are not supported by the postgres JSONB data type
  48. """
  49. known_bads = ["\u0000"]
  50. for known_bad in known_bads:
  51. data = replace(data, known_bad, " ")
  52. return data
  53. class RequestSerializer(serializers.Serializer):
  54. env = serializers.DictField(
  55. child=serializers.CharField(allow_blank=True, allow_null=True), required=False
  56. )
  57. # Dict values can be both str and List[str]
  58. headers = serializers.DictField(required=False)
  59. url = serializers.CharField(required=False, allow_blank=True)
  60. method = serializers.CharField(required=False, allow_blank=True)
  61. query_string = QueryStringField(required=False, allow_null=True)
  62. class BreadcrumbsSerializer(BaseBreadcrumbsSerializer):
  63. timestamp = GenericField(required=False)
  64. def validate_level(self, value):
  65. if value == "log":
  66. return "info"
  67. return value
  68. class BaseSerializer(serializers.Serializer):
  69. def process_user(self, should_scrub_ip_addresses, data):
  70. """Fetch user data from SDK event and request"""
  71. user = data.get("user", {})
  72. if self.context and self.context.get("request"):
  73. client_ip, is_routable = get_client_ip(self.context["request"])
  74. if user or is_routable:
  75. if is_routable:
  76. if should_scrub_ip_addresses:
  77. client_ip = anonymize_ip(client_ip)
  78. user["ip_address"] = client_ip
  79. return user
  80. class SentrySDKEventSerializer(BaseSerializer):
  81. """Represents events coming from a OSS sentry SDK client"""
  82. breadcrumbs = serializers.JSONField(required=False)
  83. tags = ForgivingHStoreField(required=False)
  84. event_id = serializers.UUIDField(required=False, default=uuid.uuid4)
  85. extra = serializers.JSONField(required=False)
  86. request = RequestSerializer(required=False)
  87. server_name = serializers.CharField(required=False)
  88. sdk = serializers.JSONField(required=False)
  89. platform = serializers.CharField(required=False)
  90. release = serializers.CharField(required=False, allow_null=True)
  91. environment = ForgivingDisallowRegexField(
  92. required=False, allow_null=True, disallow_regex=r"^[^\n\r\f\/]*$"
  93. )
  94. _meta = serializers.JSONField(required=False)
  95. class FormattedMessageSerializer(serializers.Serializer):
  96. formatted = serializers.CharField(
  97. required=False
  98. ) # Documented as required, but some Sentry SDKs don't send it
  99. message = serializers.CharField(required=False)
  100. params = serializers.JSONField(required=False)
  101. def validate(self, attrs):
  102. data = super().validate(attrs)
  103. if not data.get("formatted") and data.get("params"):
  104. params = data["params"]
  105. if isinstance(params, list):
  106. data["formatted"] = data["message"] % tuple(params)
  107. elif isinstance(params, dict):
  108. data["formatted"] = data["message"].format(**params)
  109. return data
  110. # OSS Sentry only keeps unformatted "message" when it creates a formatted message
  111. return {key: data[key] for key in data if key != "message"}
  112. class MessageField(serializers.CharField):
  113. def to_internal_value(self, data):
  114. if isinstance(data, dict):
  115. serializer = FormattedMessageSerializer(data=data)
  116. serializer.is_valid(raise_exception=True)
  117. return serializer.validated_data
  118. return super().to_internal_value(data)
  119. class LogEntrySerializer(serializers.Serializer):
  120. formatted = serializers.CharField(required=False)
  121. message = serializers.CharField(required=False)
  122. params = serializers.JSONField(required=False)
  123. def validate(self, attrs):
  124. data = super().validate(attrs)
  125. if not data.get("formatted") and data.get("params"):
  126. params = data["params"]
  127. if isinstance(params, list):
  128. data["formatted"] = data["message"] % tuple(data["params"])
  129. elif isinstance(params, dict):
  130. data["formatted"] = data["message"].format(**params)
  131. return data
  132. class StoreDefaultSerializer(SentrySDKEventSerializer):
  133. """
  134. Default serializer. Used as both a base class and for default error types
  135. """
  136. type = EventType.DEFAULT
  137. contexts = serializers.JSONField(required=False)
  138. level = serializers.CharField(required=False)
  139. logentry = LogEntrySerializer(required=False)
  140. message = MessageField(required=False, allow_blank=True, allow_null=True)
  141. timestamp = FlexibleDateTimeField(required=False)
  142. transaction = serializers.CharField(
  143. required=False, allow_null=True, allow_blank=True
  144. )
  145. user = serializers.JSONField(required=False)
  146. modules = serializers.JSONField(required=False)
  147. def validate_breadcrumbs(self, value):
  148. """
  149. Normalize breadcrumbs, which may come in as dict or list
  150. """
  151. if isinstance(value, list):
  152. value = {"values": value}
  153. if value.get("values") == []:
  154. return None
  155. serializer = BreadcrumbsSerializer(data=value.get("values"), many=True)
  156. if serializer.is_valid():
  157. return {"values": serializer.validated_data}
  158. return value
  159. def get_eventtype(self):
  160. """Get event type class from self.type"""
  161. if self.type is EventType.DEFAULT:
  162. return DefaultEvent()
  163. if self.type is EventType.ERROR:
  164. return ErrorEvent()
  165. def modify_exception(self, exception):
  166. """OSS Sentry does this, I have no idea why"""
  167. if exception:
  168. for value in exception.get("values", []):
  169. value.pop("module", None)
  170. if value.get("stacktrace") and value["stacktrace"].get("frames"):
  171. frames = value["stacktrace"]["frames"]
  172. # If in_app is always true, make it false ¯\_(ツ)_/¯
  173. if all(x.get("in_app") for x in frames):
  174. for frame in frames:
  175. frame["in_app"] = False
  176. return exception
  177. def generate_tags(self, data: Dict, tags: List[Tuple[str, str]] = None):
  178. """
  179. Determine tag relational data
  180. Optionally pass tags array for existing known tags to generate
  181. """
  182. if tags is None:
  183. tags = []
  184. for Processor in TAG_PROCESSORS:
  185. processor = Processor()
  186. value = processor.get_tag_values(data)
  187. if value:
  188. tags.append((processor.tag, value))
  189. if data.get("tags"):
  190. tags += [(k, v) for k, v in data["tags"].items()]
  191. return tags
  192. def annotate_contexts(self, event):
  193. """
  194. SDK events may contain contexts. This function adds additional contexts data
  195. """
  196. contexts = event.get("contexts")
  197. for Processor in EVENT_CONTEXT_PROCESSORS:
  198. processor = Processor()
  199. if contexts is None or not contexts.get(processor.name):
  200. processor_contexts = processor.get_context(event)
  201. if processor_contexts:
  202. if contexts is None:
  203. contexts = {}
  204. contexts[processor.name] = processor_contexts
  205. return contexts
  206. def get_message(self, data):
  207. """Prefer message over logentry"""
  208. if "message" in data:
  209. if isinstance(data["message"], dict):
  210. return data["message"].get("formatted") or data["message"].get(
  211. "message", ""
  212. )
  213. return data["message"]
  214. return data.get("logentry", {}).get("message", "")
  215. def get_logentry(self, data):
  216. if "logentry" in data:
  217. return data.get("logentry")
  218. elif "message" in data:
  219. message = data["message"]
  220. if isinstance(message, dict):
  221. return message
  222. return {"formatted": message}
  223. def is_url(self, filename: str) -> bool:
  224. return filename.startswith(("file:", "http:", "https:", "applewebdata:"))
  225. def normalize_stacktrace(self, stacktrace):
  226. """
  227. Port of semaphore store/normalize/stacktrace.rs
  228. """
  229. if not stacktrace:
  230. return
  231. for frame in stacktrace.get("frames", []):
  232. if not frame.get("abs_path") and frame.get("filename"):
  233. frame["abs_path"] = frame["filename"]
  234. if frame.get("filename") and self.is_url(frame["filename"]):
  235. frame["filename"] = urlparse(frame["filename"]).path
  236. def create(self, validated_data):
  237. data = validated_data
  238. eventtype = self.get_eventtype()
  239. metadata = eventtype.get_metadata(data)
  240. exception = data.get("exception")
  241. if (
  242. data.get("stacktrace")
  243. and exception
  244. and len(exception.get("values", 0)) > 0
  245. and not exception["values"][0].get("stacktrace")
  246. ):
  247. # stacktrace is deprecated, but supported at this time
  248. # Assume it's for the first exception value
  249. exception["values"][0]["stacktrace"] = data.get("stacktrace")
  250. exception = self.modify_exception(exception)
  251. if isinstance(exception, dict):
  252. for value in exception.get("values", []):
  253. self.normalize_stacktrace(value.get("stacktrace"))
  254. for Processor in EVENT_PROCESSORS:
  255. Processor(self.context.get("release_id"), data).run()
  256. title = eventtype.get_title(metadata)
  257. culprit = eventtype.get_location(data)
  258. request = data.get("request")
  259. breadcrumbs = data.get("breadcrumbs")
  260. level = None
  261. if data.get("level"):
  262. level = LogLevel.from_string(data["level"])
  263. if request:
  264. headers = request.get("headers")
  265. if headers:
  266. request["inferred_content_type"] = headers.get("Content-Type")
  267. sorted_headers = sorted([pair for pair in headers.items()])
  268. for idx, header in enumerate(sorted_headers):
  269. if isinstance(header[1], list):
  270. sorted_headers[idx] = (header[0], header[1][0])
  271. request["headers"] = sorted_headers
  272. contexts = self.annotate_contexts(data)
  273. data["contexts"] = contexts
  274. with transaction.atomic():
  275. defaults = {
  276. "metadata": sanitize_bad_postgres_json(metadata),
  277. }
  278. if level:
  279. defaults["level"] = level
  280. tags = []
  281. if self.context["environment_id"]:
  282. tags.append(("environment", data.get("environment")))
  283. if self.context["release_id"]:
  284. tags.append(("release", data.get("release")))
  285. tags = self.generate_tags(data, tags)
  286. defaults["tags"] = {tag[0]: [tag[1]] for tag in tags}
  287. issue, _ = Issue.objects.get_or_create(
  288. title=sanitize_bad_postgres_chars(title),
  289. culprit=sanitize_bad_postgres_chars(culprit),
  290. project_id=self.context["project_id"],
  291. type=self.type,
  292. defaults=defaults,
  293. )
  294. json_data = {
  295. "breadcrumbs": breadcrumbs,
  296. "contexts": contexts,
  297. "culprit": culprit,
  298. "exception": exception,
  299. "logentry": self.get_logentry(data),
  300. "metadata": metadata,
  301. "message": self.get_message(data),
  302. "modules": data.get("modules"),
  303. "platform": data.get("platform", "other"),
  304. "request": request,
  305. "sdk": data.get("sdk"),
  306. "title": title,
  307. "type": self.type.label,
  308. }
  309. # if environment:
  310. # json_data["environment"] = environment.name
  311. if data.get("logentry"):
  312. json_data["logentry"] = data.get("logentry")
  313. extra = data.get("extra")
  314. if extra:
  315. json_data["extra"] = extra
  316. user = self.process_user(self.context["should_scrub_ip_addresses"], data)
  317. if user:
  318. json_data["user"] = user
  319. errors = None
  320. handled_errors = self.context.get("handled_errors")
  321. if handled_errors:
  322. errors = []
  323. for field_name, field_errors in handled_errors.items():
  324. for error in field_errors:
  325. errors.append(
  326. {
  327. "reason": str(error),
  328. "type": error.code,
  329. "name": field_name,
  330. "value": error.value,
  331. }
  332. )
  333. params = {
  334. "event_id": data["event_id"],
  335. "issue": issue,
  336. "tags": {tag[0]: tag[1] for tag in tags},
  337. "errors": errors,
  338. "timestamp": data.get("timestamp"),
  339. "data": sanitize_bad_postgres_json(json_data),
  340. "release_id": self.context["release_id"],
  341. }
  342. if level:
  343. params["level"] = level
  344. try:
  345. event = Event.objects.create(**params)
  346. except IntegrityError as err:
  347. # This except is more efficient than a query for exists().
  348. if err.args and "event_id" in err.args[0]:
  349. raise PermissionDenied(
  350. "An event with the same ID already exists (%s)"
  351. % params["event_id"]
  352. ) from err
  353. raise err
  354. # issue.check_for_status_update()
  355. # Expire after 1 hour - in case of major backup
  356. # update_search_index_issue(args=[issue.pk], countdown=10, expires=3600)
  357. return event
  358. class StoreErrorSerializer(StoreDefaultSerializer):
  359. """Primary difference is the presense of exception attribute"""
  360. type = EventType.ERROR
  361. exception = serializers.JSONField(required=False)
  362. stacktrace = serializers.JSONField(
  363. required=False, help_text="Deprecated but supported at this time"
  364. )
  365. class StoreCSPReportSerializer(BaseSerializer):
  366. """
  367. CSP Report Serializer
  368. Very different format from others Store serializers.
  369. Does not extend base class due to differences.
  370. """
  371. type = EventType.CSP
  372. def __init__(self, *args, **kwargs):
  373. super().__init__(*args, **kwargs)
  374. # This is done to support the hyphen
  375. self.fields.update({"csp-report": serializers.JSONField()})
  376. def create(self, validated_data):
  377. project = self.context.get("project")
  378. csp = validated_data["csp-report"]
  379. title = self.get_title(csp)
  380. culprit = self.get_culprit(csp)
  381. uri = self.get_uri(csp)
  382. directive = self.get_effective_directive(csp)
  383. metadata = {
  384. "message": title,
  385. "uri": uri,
  386. "directive": directive,
  387. }
  388. issue, _ = Issue.objects.get_or_create(
  389. title=title,
  390. culprit=culprit,
  391. project_id=project.id,
  392. type=EventType.CSP,
  393. defaults={"metadata": metadata},
  394. )
  395. # Convert - to _
  396. normalized_csp = dict((k.replace("-", "_"), v) for k, v in csp.items())
  397. if "effective_directive" not in normalized_csp:
  398. normalized_csp["effective_directive"] = directive
  399. json_data = {
  400. "culprit": culprit,
  401. "csp": normalized_csp,
  402. "title": title,
  403. "metadata": metadata,
  404. "message": title,
  405. "type": EventType.CSP.label,
  406. }
  407. user = self.process_user(project, validated_data)
  408. if user:
  409. json_data["user"] = user
  410. params = {
  411. "issue": issue,
  412. "data": json_data,
  413. }
  414. return Event.objects.create(**params)
  415. def get_effective_directive(self, data):
  416. """
  417. Some browers return effective-directive and others don't.
  418. Infer missing ones from violated directive
  419. """
  420. if "effective-directive" in data:
  421. return data["effective-directive"]
  422. first_violation = data["violated-directive"].split()[0]
  423. return first_violation
  424. def get_uri(self, data):
  425. url = data["blocked-uri"]
  426. return urlparse(url).netloc
  427. def get_title(self, data):
  428. effective_directive = self.get_effective_directive(data)
  429. humanized_directive = effective_directive.replace("-src", "")
  430. uri = self.get_uri(data)
  431. return f"Blocked '{humanized_directive}' from '{uri}'"
  432. def get_culprit(self, data):
  433. # "style-src cdn.example.com"
  434. return data.get("violated-directive")
  435. class EnvelopeHeaderSerializer(serializers.Serializer):
  436. event_id = serializers.UUIDField(required=False)
  437. sent_at = FlexibleDateTimeField(required=False)