schema.py 9.6 KB

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