api.py 6.6 KB

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