schema.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. from datetime import datetime
  2. from typing import Any, Literal, Optional, Union
  3. from ninja import Field, ModelSchema, Schema
  4. from pydantic import computed_field
  5. from apps.event_ingest.schema import CSPReportSchema, EventException
  6. from glitchtip.api.schema import CamelWithLowerIdSchema, CamelSchema
  7. from sentry.interfaces.stacktrace import get_context
  8. from ..common_event_schema import (
  9. BaseIssueEvent,
  10. BaseRequest,
  11. EventBreadcrumb,
  12. ListKeyValue,
  13. )
  14. from .constants import IssueEventType
  15. from .models import Issue, IssueEvent
  16. class IssueSchema(CamelWithLowerIdSchema, ModelSchema):
  17. class Config:
  18. model = Issue
  19. model_fields = ["id", "title", "metadata"]
  20. populate_by_name = True
  21. class ExceptionEntryData(Schema):
  22. values: dict
  23. exc_omitted: None = None
  24. has_system_frames: bool
  25. class ExceptionEntry(Schema):
  26. type: Literal["exception"]
  27. data: dict
  28. class MessageEntry(Schema):
  29. type: Literal["message"]
  30. data: dict
  31. class APIEventBreadcrumb(EventBreadcrumb):
  32. """Slightly modified Breadcrumb for sentry api compatibility"""
  33. event_id: None = None
  34. class BreadcrumbsEntry(Schema):
  35. type: Literal["breadcrumbs"]
  36. data: dict[Literal["values"], list[APIEventBreadcrumb]]
  37. class Request(CamelSchema, BaseRequest):
  38. headers: Optional[ListKeyValue] = None
  39. query_string: Optional[ListKeyValue] = Field(
  40. default=None, serialization_alias="query"
  41. )
  42. @computed_field
  43. @property
  44. def inferred_content_type(self) -> Optional[str]:
  45. return next(
  46. (value for key, value in self.headers if key == "Content-Type"), None
  47. )
  48. class RequestEntry(Schema):
  49. type: Literal["request"]
  50. data: Request
  51. class IssueEventSchema(CamelSchema, ModelSchema, BaseIssueEvent):
  52. id: str = Field(validation_alias="id.hex")
  53. event_id: str
  54. project_id: int = Field(validation_alias="issue.project_id")
  55. group_id: int = Field(validation_alias="issue_id")
  56. date_created: datetime = Field(validation_alias="timestamp")
  57. date_received: datetime = Field(validation_alias="received")
  58. dist: Optional[str] = None
  59. culprit: Optional[str] = Field(validation_alias="transaction", default=None)
  60. packages: Optional[dict[str, Optional[str]]] = Field(
  61. validation_alias="data.modules", default=None
  62. )
  63. type: str = Field(validation_alias="get_type_display")
  64. message: str
  65. metadata: dict[str, str] = Field(default_factory=dict)
  66. tags: list[dict[str, Optional[str]]] = []
  67. entries: list[
  68. Union[BreadcrumbsEntry, ExceptionEntry, MessageEntry, RequestEntry]
  69. ] = Field(discriminator="type", default_factory=list)
  70. class Config:
  71. model = IssueEvent
  72. model_fields = ["id", "type", "title"]
  73. populate_by_name = True
  74. @staticmethod
  75. def resolve_tags(obj: IssueEvent):
  76. return [{"key": tag[0], "value": tag[1]} for tag in obj.tags.items()]
  77. @staticmethod
  78. def resolve_entries(obj: IssueEvent):
  79. entries = []
  80. data = obj.data
  81. if exception := data.get("exception"):
  82. exception = {"values": exception, "hasSystemFrames": False}
  83. # https://gitlab.com/glitchtip/sentry-open-source/sentry/-/blob/master/src/sentry/interfaces/stacktrace.py#L487
  84. # if any frame is "in_app" set this to True
  85. for value in exception["values"]:
  86. if (
  87. value.get("stacktrace", None) is not None
  88. and "frames" in value["stacktrace"]
  89. ):
  90. for frame in value["stacktrace"]["frames"]:
  91. if frame.get("in_app") is True:
  92. exception["hasSystemFrames"] = True
  93. if "in_app" in frame:
  94. frame["inApp"] = frame.pop("in_app")
  95. if "abs_path" in frame:
  96. frame["absPath"] = frame.pop("abs_path")
  97. if "colno" in frame:
  98. frame["colNo"] = frame.pop("colno")
  99. if "lineno" in frame:
  100. frame["lineNo"] = frame.pop("lineno")
  101. pre_context = frame.pop("pre_context", None)
  102. post_context = frame.pop("post_context", None)
  103. frame["context"] = get_context(
  104. frame["lineNo"],
  105. frame.get("context_line"),
  106. pre_context,
  107. post_context,
  108. )
  109. entries.append({"type": "exception", "data": exception})
  110. if breadcrumbs := data.get("breadcrumbs"):
  111. entries.append({"type": "breadcrumbs", "data": {"values": breadcrumbs}})
  112. if logentry := data.get("logentry"):
  113. entries.append({"type": "message", "data": logentry})
  114. elif message := data.get("message"):
  115. entries.append({"type": "message", "data": {"formatted": message}})
  116. if request := data.get("request"):
  117. entries.append({"type": "request", "data": request})
  118. if csp := data.get("csp"):
  119. entries.append({"type": IssueEventType.CSP.label, "data": csp})
  120. return entries
  121. class IssueEventDetailSchema(IssueEventSchema):
  122. user_report: list = [] # TODO
  123. next_event_id: Optional[str] = None
  124. previous_event_id: Optional[str] = None
  125. @staticmethod
  126. def resolve_previous_event_id(obj):
  127. if event_id := obj.previous:
  128. return event_id.hex
  129. @staticmethod
  130. def resolve_next_event_id(obj):
  131. if event_id := obj.next:
  132. return event_id.hex
  133. class IssueEventJsonSchema(ModelSchema, BaseIssueEvent):
  134. """
  135. Represents a more raw view of the event, built with open source (legacy) Sentry compatibility
  136. """
  137. event_id: str = Field(validation_alias="id.hex")
  138. timestamp: float = Field()
  139. x_datetime: datetime = Field(
  140. validation_alias="timestamp", serialization_alias="datetime"
  141. )
  142. breadcrumbs: Optional[Any] = Field(
  143. validation_alias="data.breadcrumbs", default=None
  144. )
  145. project: int = Field(validation_alias="issue.project_id")
  146. level: Optional[str] = Field(validation_alias="get_level_display")
  147. exception: Optional[Any] = Field(validation_alias="data.exception", default=None)
  148. modules: Optional[dict[str, str]] = Field(
  149. validation_alias="data.modules", default_factory=dict
  150. )
  151. sdk: Optional[dict] = Field(validation_alias="data.sdk", default_factory=dict)
  152. type: Optional[str] = Field(validation_alias="get_type_display")
  153. request: Optional[Any] = Field(validation_alias="data.request", default=None)
  154. environment: Optional[str] = Field(
  155. validation_alias="data.environment", default=None
  156. )
  157. class Config:
  158. model = IssueEvent
  159. model_fields = ["title", "transaction"]
  160. @staticmethod
  161. def resolve_timestamp(obj):
  162. return obj.timestamp.timestamp()
  163. class IssueEventDataSchema(Schema):
  164. """IssueEvent model data json schema"""
  165. metadata: Optional[dict[str, Any]] = None
  166. breadcrumbs: Optional[list[EventBreadcrumb]] = None
  167. exception: Optional[list[EventException]] = None
  168. class CSPIssueEventDataSchema(IssueEventDataSchema):
  169. csp: CSPReportSchema