api.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. from anonymizeip import anonymize_ip
  2. from django.conf import settings
  3. from django.core.cache import cache
  4. from django.http import HttpResponse
  5. from ipware import get_client_ip
  6. from ninja import Router, Schema
  7. from ninja.errors import ValidationError
  8. from glitchtip.utils import async_call_celery_task
  9. from .authentication import EventAuthHttpRequest, event_auth
  10. from .schema import (
  11. CSPIssueEventSchema,
  12. EnvelopeSchema,
  13. ErrorIssueEventSchema,
  14. EventIngestSchema,
  15. EventUser,
  16. IngestIssueEvent,
  17. InterchangeIssueEvent,
  18. IssueEventSchema,
  19. SecuritySchema,
  20. TransactionEventSchema,
  21. )
  22. from .tasks import ingest_event, ingest_transaction
  23. router = Router(auth=event_auth)
  24. class EventIngestOut(Schema):
  25. event_id: str
  26. task_id: str | None = None # For debug purposes only
  27. class EnvelopeIngestOut(Schema):
  28. id: str | None = None
  29. def get_issue_event_class(event: IngestIssueEvent):
  30. return ErrorIssueEventSchema if event.exception else IssueEventSchema
  31. def get_ip_address(request: EventAuthHttpRequest) -> str | None:
  32. """
  33. Get IP address from request. Anonymize it based on project settings.
  34. Keep this logic in the api view, we aim to anonymize data before storing
  35. on redis/postgres.
  36. """
  37. project = request.auth
  38. client_ip, is_routable = get_client_ip(request)
  39. if is_routable:
  40. if project.should_scrub_ip_addresses:
  41. client_ip = anonymize_ip(client_ip)
  42. return client_ip
  43. return None
  44. @router.post("/{project_id}/store/", response=EventIngestOut)
  45. async def event_store(
  46. request: EventAuthHttpRequest,
  47. payload: EventIngestSchema,
  48. project_id: int,
  49. ):
  50. """
  51. Event store is the original event ingest API from OSS Sentry but is used less often
  52. Unlike Envelope, it accepts only one Issue event.
  53. """
  54. if cache.add("uuid" + payload.event_id.hex, True) is False:
  55. raise ValidationError([{"message": "Duplicate event id"}])
  56. if client_ip := get_ip_address(request):
  57. if payload.user:
  58. payload.user.ip_address = client_ip
  59. else:
  60. payload.user = EventUser(ip_address=client_ip)
  61. issue_event_class = get_issue_event_class(payload)
  62. issue_event = InterchangeIssueEvent(
  63. event_id=payload.event_id,
  64. project_id=project_id,
  65. organization_id=request.auth.organization_id,
  66. payload=issue_event_class(**payload.dict()),
  67. )
  68. task_result = await async_call_celery_task(ingest_event, issue_event.dict())
  69. result = {"event_id": payload.event_id.hex}
  70. if settings.IS_LOAD_TEST:
  71. result["task_id"] = task_result.task_id
  72. return result
  73. @router.post("/{project_id}/envelope/", response=EnvelopeIngestOut)
  74. async def event_envelope(
  75. request: EventAuthHttpRequest,
  76. payload: EnvelopeSchema,
  77. project_id: int,
  78. ):
  79. """
  80. Envelopes can contain various types of data.
  81. GlitchTip supports issue events and transaction events.
  82. Ignore other data types.
  83. Do support multiple valid events
  84. Make as few io calls as possible. Some language SDKs (PHP) cannot run async code
  85. and will block while waiting for GlitchTip to respond.
  86. """
  87. client_ip = get_ip_address(request)
  88. header = payload._header
  89. for item_header, item in payload._items:
  90. if item_header.type == "event" and isinstance(item, IngestIssueEvent):
  91. if item.user:
  92. item.user.ip_address = client_ip
  93. else:
  94. item.user = EventUser(ip_address=client_ip)
  95. issue_event_class = get_issue_event_class(item)
  96. interchange_event_kwargs = {
  97. "project_id": project_id,
  98. "organization_id": request.auth.organization_id,
  99. "payload": issue_event_class(**item.dict()),
  100. }
  101. if header.event_id:
  102. interchange_event_kwargs["event_id"] = header.event_id
  103. interchange_event = InterchangeIssueEvent(**interchange_event_kwargs)
  104. # Faux unique uuid as GlitchTip can accept duplicate UUIDs
  105. # The primary key of an event is uuid, received
  106. if cache.add("uuid" + interchange_event.event_id.hex, True) is True:
  107. await async_call_celery_task(ingest_event, interchange_event.dict())
  108. elif item_header.type == "transaction" and isinstance(
  109. item, TransactionEventSchema
  110. ):
  111. interchange_event_kwargs = {
  112. "project_id": project_id,
  113. "organization_id": request.auth.organization_id,
  114. "payload": TransactionEventSchema(**item.dict()),
  115. }
  116. interchange_event = InterchangeIssueEvent(**interchange_event_kwargs)
  117. if cache.add("uuid" + interchange_event.event_id.hex, True) is True:
  118. await async_call_celery_task(
  119. ingest_transaction, interchange_event.dict()
  120. )
  121. if header.event_id:
  122. return {"id": header.event_id.hex}
  123. return {}
  124. @router.post("/{project_id}/security/")
  125. async def event_security(
  126. request: EventAuthHttpRequest,
  127. payload: SecuritySchema,
  128. project_id: int,
  129. ):
  130. """
  131. Accept Security (and someday other) issue events.
  132. Reformats event to make CSP browser format match more standard
  133. event format.
  134. """
  135. event = CSPIssueEventSchema(csp=payload.csp_report.dict(by_alias=True))
  136. if client_ip := get_ip_address(request):
  137. if event.user:
  138. event.user.ip_address = client_ip
  139. else:
  140. event.user = EventUser(ip_address=client_ip)
  141. issue_event = InterchangeIssueEvent(
  142. project_id=project_id,
  143. organization_id=request.auth.organization_id,
  144. payload=event.dict(by_alias=True),
  145. )
  146. await async_call_celery_task(ingest_event, issue_event.dict(by_alias=True))
  147. return HttpResponse(status=201)