123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- import collections
- from django.contrib.postgres.search import SearchVectorField
- from django.contrib.postgres.indexes import GinIndex
- from django.conf import settings
- from django.db import models
- from django.db.models import Max, Count
- from django.db.utils import InternalError
- from events.models import LogLevel
- from glitchtip.model_utils import FromStringIntegerChoices
- from glitchtip.base_models import CreatedModel
- from .utils import base32_encode
- class EventType(models.IntegerChoices):
- DEFAULT = 0, "default"
- ERROR = 1, "error"
- CSP = 2, "csp"
- TRANSACTION = 3, "transaction"
- class EventStatus(FromStringIntegerChoices):
- UNRESOLVED = 0, "unresolved"
- RESOLVED = 1, "resolved"
- IGNORED = 2, "ignored"
- class Issue(CreatedModel):
- """
- Sentry called this a "group". A issue is a collection of events with meta data
- such as resolved status.
- """
- # annotations Not implemented
- # assigned_to Not implemented
- culprit = models.CharField(max_length=1024, blank=True, null=True)
- has_seen = models.BooleanField(default=False)
- # is_bookmarked Not implement - is per user
- is_public = models.BooleanField(default=False)
- level = models.PositiveSmallIntegerField(
- choices=LogLevel.choices, default=LogLevel.ERROR
- )
- metadata = models.JSONField()
- tags = models.JSONField(default=dict)
- project = models.ForeignKey("projects.Project", on_delete=models.CASCADE)
- title = models.CharField(max_length=255)
- type = models.PositiveSmallIntegerField(
- choices=EventType.choices, default=EventType.DEFAULT
- )
- status = models.PositiveSmallIntegerField(
- choices=EventStatus.choices, default=EventStatus.UNRESOLVED
- )
- # See migration 0004 for trigger that sets search_vector, count, last_seen
- short_id = models.PositiveIntegerField(null=True)
- search_vector = SearchVectorField(null=True, editable=False)
- count = models.PositiveIntegerField(default=1, editable=False)
- last_seen = models.DateTimeField(auto_now_add=True, db_index=True)
- class Meta:
- unique_together = (
- ("title", "culprit", "project", "type"),
- ("project", "short_id"),
- )
- indexes = [GinIndex(fields=["search_vector"], name="search_vector_idx")]
- def event(self):
- return self.event_set.first()
- def __str__(self):
- return self.title
- def check_for_status_update(self):
- """
- Determine if issue should regress back to unresolved
- Typically run when processing a new event related to the issue
- """
- if self.status == EventStatus.RESOLVED:
- self.status = EventStatus.UNRESOLVED
- self.save()
- # Delete notifications so that new alerts are sent for regressions
- self.notification_set.all().delete()
- def get_hex_color(self):
- if self.level == LogLevel.INFO:
- return "#4b60b4"
- elif self.level is LogLevel.WARNING:
- return "#e9b949"
- elif self.level in [LogLevel.ERROR, LogLevel.FATAL]:
- return "#e52b50"
- @property
- def short_id_display(self):
- """
- Short IDs are per project issue counters. They show as PROJECT_SLUG-ID_BASE32
- The intention is to be human readable identifiers that can reference an issue.
- """
- if self.short_id is not None:
- return f"{self.project.slug.upper()}-{base32_encode(self.short_id)}"
- return ""
- def get_detail_url(self):
- return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}"
- @classmethod
- def update_index(cls, issue_id: int, skip_tags=False):
- """
- Update search index/tag aggregations
- """
- 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"""
- issue = (
- cls.objects.extra(
- select={"new_vector": vector_query}, select_params=(issue_id,)
- )
- .annotate(
- new_count=Count("event"),
- new_last_seen=Max("event__created"),
- new_level=Max("event__level"),
- )
- .defer("search_vector")
- .get(pk=issue_id)
- )
- update_fields = ["last_seen", "count", "level"]
- if (
- issue.new_vector
- ): # This check is important, because generate_issue_tsvector returns null on size limit
- update_fields.append("search_vector")
- issue.search_vector = issue.new_vector
- if issue.new_last_seen:
- issue.last_seen = issue.new_last_seen
- if issue.new_count:
- issue.count = issue.new_count
- if issue.new_level:
- issue.level = issue.new_level
- if not skip_tags:
- update_fields.append("tags")
- tags = (
- issue.event_set.all()
- .order_by("tags")
- .values_list("tags", flat=True)
- .distinct()
- )
- super_dict = collections.defaultdict(set)
- for tag in tags:
- for key, value in tag.items():
- super_dict[key].add(value)
- issue.tags = {k: list(v) for k, v in super_dict.items()}
- issue.save(update_fields=update_fields)
|