api.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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.http import HttpResponse
  6. from ipware import get_client_ip
  7. from ninja import Router, Schema
  8. from .authentication import EventAuthHttpRequest, event_auth
  9. from .schema import (
  10. CSPIssueEventSchema,
  11. EnvelopeSchema,
  12. ErrorIssueEventSchema,
  13. EventIngestSchema,
  14. EventUser,
  15. IngestIssueEvent,
  16. InterchangeIssueEvent,
  17. IssueEventSchema,
  18. SecuritySchema,
  19. )
  20. from .tasks import ingest_event
  21. router = Router(auth=event_auth)
  22. class EventIngestOut(Schema):
  23. event_id: str
  24. class EnvelopeIngestOut(Schema):
  25. id: str
  26. async def async_call_celery_task(task, *args):
  27. """
  28. Either dispatch the real celery task or run it with sync_to_async
  29. This can be used for testing or a celery-less operation.
  30. """
  31. if settings.CELERY_TASK_ALWAYS_EAGER:
  32. return await sync_to_async(task.delay)(*args)
  33. else:
  34. return task.delay(*args)
  35. def get_issue_event_class(event: IngestIssueEvent):
  36. return ErrorIssueEventSchema if event.exception else IssueEventSchema
  37. def get_ip_address(request: EventAuthHttpRequest) -> Optional[str]:
  38. """
  39. Get IP address from request. Anonymize it based on project settings.
  40. Keep this logic in the api view, we aim to anonymize data before storing
  41. on redis/postgres.
  42. """
  43. project = request.auth
  44. client_ip, is_routable = get_client_ip(request)
  45. if is_routable:
  46. if project.should_scrub_ip_addresses:
  47. client_ip = anonymize_ip(client_ip)
  48. return client_ip
  49. return None
  50. @router.post("/{project_id}/store/", response=EventIngestOut)
  51. async def event_store(
  52. request: EventAuthHttpRequest,
  53. payload: EventIngestSchema,
  54. project_id: int,
  55. ):
  56. """
  57. Event store is the original event ingest API from OSS Sentry but is used less often
  58. Unlike Envelope, it accepts only one Issue event.
  59. """
  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. payload=issue_event_class(**payload.dict()),
  70. )
  71. await async_call_celery_task(ingest_event, issue_event.dict())
  72. return {"event_id": payload.event_id.hex}
  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":
  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. issue_event = InterchangeIssueEvent(
  97. event_id=header.event_id,
  98. project_id=project_id,
  99. payload=issue_event_class(**item.dict()),
  100. )
  101. await async_call_celery_task(ingest_event, issue_event.dict())
  102. elif item_header.type == "transaction":
  103. pass
  104. # ingest_transaction.delay(project_id, {})
  105. return {"id": header.event_id.hex}
  106. @router.post("/{project_id}/security/")
  107. async def event_security(
  108. request: EventAuthHttpRequest,
  109. payload: SecuritySchema,
  110. project_id: int,
  111. ):
  112. """
  113. Accept Security (and someday other) issue events.
  114. Reformats event to make CSP browser format match more standard
  115. event format.
  116. """
  117. event = CSPIssueEventSchema(csp=payload.csp_report.dict(by_alias=True))
  118. if client_ip := get_ip_address(request):
  119. if event.user:
  120. event.user.ip_address = client_ip
  121. else:
  122. event.user = EventUser(ip_address=client_ip)
  123. issue_event = InterchangeIssueEvent(
  124. project_id=project_id,
  125. payload=event.dict(by_alias=True),
  126. )
  127. await async_call_celery_task(ingest_event, issue_event.dict(by_alias=True))
  128. return HttpResponse(status=201)