schema.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  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 apps.projects.models import Project
  7. from apps.users.models import User
  8. from glitchtip.schema import CamelSchema
  9. from sentry.interfaces.stacktrace import get_context
  10. from ..shared.schema.contexts import Contexts
  11. from ..shared.schema.event import (
  12. BaseIssueEvent,
  13. BaseRequest,
  14. EventBreadcrumb,
  15. ListKeyValue,
  16. )
  17. from ..shared.schema.user import EventUser
  18. from .constants import IssueEventType
  19. from .models import Comment, Issue, IssueEvent, UserReport
  20. class ProjectReference(CamelSchema, ModelSchema):
  21. id: str
  22. class Config:
  23. model = Project
  24. model_fields = ["platform", "slug", "name"]
  25. populate_by_name = True
  26. @staticmethod
  27. def resolve_id(obj: Project):
  28. return str(obj.id)
  29. # For Sentry compatibility
  30. def to_camel_with_lower_id(string: str) -> str:
  31. return "".join(
  32. word if i == 0 else "Id" if word == "id" else word.capitalize()
  33. for i, word in enumerate(string.split("_"))
  34. )
  35. class IssueSchema(ModelSchema):
  36. id: str
  37. count: str
  38. type: str = Field(validation_alias="get_type_display")
  39. level: str = Field(validation_alias="get_level_display")
  40. status: str = Field(validation_alias="get_status_display")
  41. project: ProjectReference = Field(validation_alias="project")
  42. short_id: str = Field(validation_alias="short_id_display")
  43. num_comments: int
  44. stats: Optional[dict[str, list[list[float]]]] = {"24h": []}
  45. share_id: Optional[int] = None
  46. logger: Optional[str] = None
  47. permalink: Optional[str] = "Not implemented"
  48. status_details: Optional[dict[str, str]] = {}
  49. subscription_details: Optional[str] = None
  50. user_count: Optional[int] = 0
  51. matching_event_id: Optional[str] = Field(
  52. default=None, serialization_alias="matchingEventId"
  53. )
  54. @staticmethod
  55. def resolve_culprit(obj: Issue):
  56. return obj.culprit or ""
  57. @staticmethod
  58. def resolve_matching_event_id(obj: Issue, context):
  59. if event_id := context["request"].matching_event_id:
  60. return event_id.hex
  61. class Config:
  62. model = Issue
  63. model_fields = [
  64. "title",
  65. "metadata",
  66. "culprit",
  67. "first_seen",
  68. "last_seen",
  69. ]
  70. alias_generator = to_camel_with_lower_id
  71. coerce_numbers_to_str = True
  72. populate_by_name = True
  73. class IssueDetailSchema(IssueSchema):
  74. user_report_count: int
  75. class ExceptionEntryData(Schema):
  76. values: dict
  77. exc_omitted: None = None
  78. has_system_frames: bool
  79. class ExceptionEntry(Schema):
  80. type: Literal["exception"]
  81. data: dict
  82. class MessageEntry(Schema):
  83. type: Literal["message"]
  84. data: dict
  85. class CSPEntry(Schema):
  86. type: Literal["csp"]
  87. data: dict
  88. class APIEventBreadcrumb(EventBreadcrumb):
  89. """Slightly modified Breadcrumb for sentry api compatibility"""
  90. event_id: Optional[str] = None
  91. class BreadcrumbsEntry(Schema):
  92. type: Literal["breadcrumbs"]
  93. data: dict[Literal["values"], list[APIEventBreadcrumb]]
  94. class Request(CamelSchema, BaseRequest):
  95. headers: Optional[ListKeyValue] = None
  96. query_string: Optional[ListKeyValue] = Field(
  97. default=None, serialization_alias="query"
  98. )
  99. @computed_field
  100. @property
  101. def inferred_content_type(self) -> Optional[str]:
  102. if self.headers:
  103. return next(
  104. (value for key, value in self.headers if key == "Content-Type"), None
  105. )
  106. return None
  107. class Config(CamelSchema.Config, BaseRequest.Config):
  108. pass
  109. class RequestEntry(Schema):
  110. type: Literal["request"]
  111. data: Request
  112. class IssueEventSchema(CamelSchema, ModelSchema, BaseIssueEvent):
  113. id: str = Field(validation_alias="id.hex")
  114. event_id: str
  115. project_id: int = Field(validation_alias="issue.project_id")
  116. group_id: str
  117. date_created: datetime = Field(validation_alias="timestamp")
  118. date_received: datetime = Field(validation_alias="received")
  119. dist: Optional[str] = None
  120. culprit: Optional[str] = Field(validation_alias="transaction", default=None)
  121. packages: Optional[dict[str, Optional[str]]] = Field(
  122. validation_alias="data.modules", default=None
  123. )
  124. type: str = Field(validation_alias="get_type_display")
  125. message: str
  126. metadata: dict[str, str] = Field(default_factory=dict)
  127. tags: list[dict[str, Optional[str]]] = []
  128. entries: list[
  129. Union[BreadcrumbsEntry, CSPEntry, ExceptionEntry, MessageEntry, RequestEntry]
  130. ] = Field(discriminator="type", default_factory=list)
  131. contexts: Optional[Contexts] = Field(validation_alias="data.contexts", default=None)
  132. context: Optional[dict[str, Any]] = Field(
  133. validation_alias="data.extra", default=None
  134. )
  135. user: Optional[Any] = Field(validation_alias="data.user", default=None)
  136. class Config:
  137. model = IssueEvent
  138. model_fields = ["id", "type", "title"]
  139. populate_by_name = True
  140. @staticmethod
  141. def resolve_group_id(obj: IssueEvent):
  142. return str(obj.issue_id)
  143. @staticmethod
  144. def resolve_tags(obj: IssueEvent):
  145. return [{"key": tag[0], "value": tag[1]} for tag in obj.tags.items()]
  146. @staticmethod
  147. def resolve_entries(obj: IssueEvent):
  148. entries = []
  149. data = obj.data
  150. if exception := data.get("exception"):
  151. exception = {"values": exception, "hasSystemFrames": False}
  152. # https://gitlab.com/glitchtip/sentry-open-source/sentry/-/blob/master/src/sentry/interfaces/stacktrace.py#L487
  153. # if any frame is "in_app" set this to True
  154. for value in exception["values"]:
  155. if (
  156. value.get("stacktrace", None) is not None
  157. and "frames" in value["stacktrace"]
  158. ):
  159. for frame in value["stacktrace"]["frames"]:
  160. if frame.get("in_app") is True:
  161. exception["hasSystemFrames"] = True
  162. if "in_app" in frame:
  163. frame["inApp"] = frame.pop("in_app")
  164. if "abs_path" in frame:
  165. frame["absPath"] = frame.pop("abs_path")
  166. if "colno" in frame:
  167. frame["colNo"] = frame.pop("colno")
  168. if "lineno" in frame:
  169. frame["lineNo"] = frame.pop("lineno")
  170. pre_context = frame.pop("pre_context", None)
  171. post_context = frame.pop("post_context", None)
  172. if "context" not in frame:
  173. frame["context"] = get_context(
  174. frame["lineNo"],
  175. frame.get("context_line"),
  176. pre_context,
  177. post_context,
  178. )
  179. entries.append({"type": "exception", "data": exception})
  180. if breadcrumbs := data.get("breadcrumbs"):
  181. entries.append({"type": "breadcrumbs", "data": {"values": breadcrumbs}})
  182. if logentry := data.get("logentry"):
  183. entries.append({"type": "message", "data": logentry})
  184. elif message := data.get("message"):
  185. entries.append({"type": "message", "data": {"formatted": message}})
  186. if request := data.get("request"):
  187. entries.append({"type": "request", "data": request})
  188. if csp := data.get("csp"):
  189. entries.append({"type": IssueEventType.CSP.label, "data": csp})
  190. return entries
  191. class UserReportSchema(CamelSchema, ModelSchema):
  192. event_id: str = Field(validation_alias="event_id.hex")
  193. event: dict[str, str]
  194. date_created: datetime = Field(validation_alias="created")
  195. user: Optional[str] = None
  196. class Config:
  197. model = UserReport
  198. model_fields = ["id", "name", "email", "comments"]
  199. populate_by_name = True
  200. @staticmethod
  201. def resolve_event(obj):
  202. return {
  203. "eventId": obj.event_id.hex,
  204. }
  205. # TODO: Sentry includes a full user object with its nested comments,
  206. # so we should drop this schema once we create a full user schema
  207. class CommentUserSchema(CamelSchema, ModelSchema):
  208. id: str
  209. class Config:
  210. model = User
  211. model_fields = [
  212. "email",
  213. ]
  214. populate_by_name = True
  215. @staticmethod
  216. def resolve_id(obj: User):
  217. return str(obj.id)
  218. class CommentSchema(CamelSchema, ModelSchema):
  219. data: dict[str, str]
  220. type: Optional[str] = "note"
  221. date_created: datetime = Field(validation_alias="created")
  222. user: CommentUserSchema
  223. class Config:
  224. model = Comment
  225. model_fields = ["id"]
  226. @staticmethod
  227. def resolve_data(obj: Comment):
  228. return {
  229. "text": obj.text,
  230. }
  231. class IssueEventDetailSchema(IssueEventSchema):
  232. user_report: Optional[UserReportSchema]
  233. next_event_id: Optional[str] = None
  234. previous_event_id: Optional[str] = None
  235. @staticmethod
  236. def resolve_previous_event_id(obj):
  237. if event_id := obj.previous:
  238. return event_id.hex
  239. @staticmethod
  240. def resolve_next_event_id(obj):
  241. if event_id := obj.next:
  242. return event_id.hex
  243. class IssueEventJsonSchema(ModelSchema, BaseIssueEvent):
  244. """
  245. Represents a more raw view of the event, built with open source (legacy) Sentry compatibility
  246. """
  247. event_id: str = Field(validation_alias="id.hex")
  248. timestamp: float = Field()
  249. x_datetime: datetime = Field(
  250. validation_alias="timestamp", serialization_alias="datetime"
  251. )
  252. breadcrumbs: Optional[Any] = Field(
  253. validation_alias="data.breadcrumbs", default=None
  254. )
  255. project: int = Field(validation_alias="issue.project_id")
  256. level: Optional[str] = Field(validation_alias="get_level_display")
  257. exception: Optional[Any] = Field(validation_alias="data.exception", default=None)
  258. modules: Optional[dict[str, str]] = Field(
  259. validation_alias="data.modules", default_factory=dict
  260. )
  261. contexts: Optional[dict] = Field(validation_alias="data.contexts", default=None)
  262. sdk: Optional[dict] = Field(validation_alias="data.sdk", default_factory=dict)
  263. type: Optional[str] = Field(validation_alias="get_type_display")
  264. request: Optional[Any] = Field(validation_alias="data.request", default=None)
  265. environment: Optional[str] = Field(
  266. validation_alias="data.environment", default=None
  267. )
  268. extra: Optional[dict[str, Any]] = Field(validation_alias="data.extra", default=None)
  269. user: Optional[EventUser] = Field(validation_alias="data.user", default=None)
  270. class Config:
  271. model = IssueEvent
  272. model_fields = ["title", "transaction", "tags"]
  273. @staticmethod
  274. def resolve_timestamp(obj):
  275. return obj.timestamp.timestamp()
  276. class IssueEventDataSchema(Schema):
  277. """IssueEvent model data json schema"""
  278. metadata: Optional[dict[str, Any]] = None
  279. breadcrumbs: Optional[list[EventBreadcrumb]] = None
  280. exception: Optional[list[EventException]] = None
  281. class CSPIssueEventDataSchema(IssueEventDataSchema):
  282. csp: CSPReportSchema
  283. class IssueTagTopValue(CamelSchema):
  284. name: str
  285. value: str
  286. count: int
  287. key: str
  288. class IssueTagSchema(CamelSchema):
  289. top_values: list[IssueTagTopValue]
  290. unique_values: int
  291. key: str
  292. name: str
  293. total_values: int