models.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import collections
  2. from django.contrib.postgres.search import SearchVectorField
  3. from django.contrib.postgres.indexes import GinIndex
  4. from django.conf import settings
  5. from django.db import models
  6. from django.db.models import Max, Count
  7. from django.db.utils import InternalError
  8. from events.models import LogLevel
  9. from glitchtip.model_utils import FromStringIntegerChoices
  10. from glitchtip.base_models import CreatedModel
  11. from .utils import base32_encode
  12. class EventType(models.IntegerChoices):
  13. DEFAULT = 0, "default"
  14. ERROR = 1, "error"
  15. CSP = 2, "csp"
  16. TRANSACTION = 3, "transaction"
  17. class EventStatus(FromStringIntegerChoices):
  18. UNRESOLVED = 0, "unresolved"
  19. RESOLVED = 1, "resolved"
  20. IGNORED = 2, "ignored"
  21. class Issue(CreatedModel):
  22. """
  23. Sentry called this a "group". A issue is a collection of events with meta data
  24. such as resolved status.
  25. """
  26. # annotations Not implemented
  27. # assigned_to Not implemented
  28. culprit = models.CharField(max_length=1024, blank=True, null=True)
  29. has_seen = models.BooleanField(default=False)
  30. # is_bookmarked Not implement - is per user
  31. is_public = models.BooleanField(default=False)
  32. level = models.PositiveSmallIntegerField(
  33. choices=LogLevel.choices, default=LogLevel.ERROR
  34. )
  35. metadata = models.JSONField()
  36. tags = models.JSONField(default=dict)
  37. project = models.ForeignKey("projects.Project", on_delete=models.CASCADE)
  38. title = models.CharField(max_length=255)
  39. type = models.PositiveSmallIntegerField(
  40. choices=EventType.choices, default=EventType.DEFAULT
  41. )
  42. status = models.PositiveSmallIntegerField(
  43. choices=EventStatus.choices, default=EventStatus.UNRESOLVED
  44. )
  45. # See migration 0004 for trigger that sets search_vector, count, last_seen
  46. short_id = models.PositiveIntegerField(null=True)
  47. search_vector = SearchVectorField(null=True, editable=False)
  48. count = models.PositiveIntegerField(default=1, editable=False)
  49. last_seen = models.DateTimeField(auto_now_add=True, db_index=True)
  50. class Meta:
  51. unique_together = (
  52. ("title", "culprit", "project", "type"),
  53. ("project", "short_id"),
  54. )
  55. indexes = [GinIndex(fields=["search_vector"], name="search_vector_idx")]
  56. def event(self):
  57. return self.event_set.first()
  58. def __str__(self):
  59. return self.title
  60. def check_for_status_update(self):
  61. """
  62. Determine if issue should regress back to unresolved
  63. Typically run when processing a new event related to the issue
  64. """
  65. if self.status == EventStatus.RESOLVED:
  66. self.status = EventStatus.UNRESOLVED
  67. self.save()
  68. # Delete notifications so that new alerts are sent for regressions
  69. self.notification_set.all().delete()
  70. def get_hex_color(self):
  71. if self.level == LogLevel.INFO:
  72. return "#4b60b4"
  73. elif self.level is LogLevel.WARNING:
  74. return "#e9b949"
  75. elif self.level in [LogLevel.ERROR, LogLevel.FATAL]:
  76. return "#e52b50"
  77. @property
  78. def short_id_display(self):
  79. """
  80. Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32
  81. The intention is to be human readable identifiers that can reference an issue.
  82. """
  83. if self.short_id is not None:
  84. return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}"
  85. return ""
  86. def get_detail_url(self):
  87. return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}"
  88. @classmethod
  89. def update_index(cls, issue_id: int, skip_tags=False):
  90. """
  91. Update search index/tag aggregations
  92. """
  93. vector_query = """SELECT generate_issue_tsvector(jsonb_agg(data)) from (SELECT events_event.data from events_event where issue_id = %s limit 200) as data"""
  94. issue = (
  95. cls.objects.extra(
  96. select={"new_vector": vector_query}, select_params=(issue_id,)
  97. )
  98. .annotate(
  99. new_count=Count("event"),
  100. new_last_seen=Max("event__created"),
  101. new_level=Max("event__level"),
  102. )
  103. .defer("search_vector")
  104. .get(pk=issue_id)
  105. )
  106. update_fields = ["last_seen", "count", "level"]
  107. if (
  108. issue.new_vector
  109. ): # This check is important, because generate_issue_tsvector returns null on size limit
  110. update_fields.append("search_vector")
  111. issue.search_vector = issue.new_vector
  112. if issue.new_last_seen:
  113. issue.last_seen = issue.new_last_seen
  114. if issue.new_count:
  115. issue.count = issue.new_count
  116. if issue.new_level:
  117. issue.level = issue.new_level
  118. if not skip_tags:
  119. update_fields.append("tags")
  120. tags = (
  121. issue.event_set.all()
  122. .order_by("tags")
  123. .values_list("tags", flat=True)
  124. .distinct()
  125. )
  126. super_dict = collections.defaultdict(set)
  127. for tag in tags:
  128. for key, value in tag.items():
  129. super_dict[key].add(value)
  130. issue.tags = {k: list(v) for k, v in super_dict.items()}
  131. issue.save(update_fields=update_fields)