123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- import uuid
- from django.conf import settings
- from django.contrib.postgres.indexes import GinIndex
- from django.contrib.postgres.search import SearchVectorField
- from django.db import models
- from django.utils import timezone
- from glitchtip.base_models import AggregationModel, CreatedModel, SoftDeleteModel
- from psqlextra.models import PostgresPartitionedModel
- from psqlextra.types import PostgresPartitioningMethod
- from sentry.constants import MAX_CULPRIT_LENGTH
- from .constants import EventStatus, IssueEventType, LogLevel
- from .utils import base32_encode
- class DeferedFieldManager(models.Manager):
- def __init__(self, defered_fields=[]):
- super().__init__()
- self.defered_fields = defered_fields
- def get_queryset(self, *args, **kwargs):
- return super().get_queryset(*args, **kwargs).defer(*self.defered_fields)
- class TagKey(models.Model):
- id = models.AutoField(primary_key=True)
- key = models.CharField(max_length=255, unique=True)
- class TagValue(models.Model):
- value = models.CharField(max_length=255, unique=True)
- class IssueTag(AggregationModel):
- """
- This model is a aggregate of event tags for an issue.
- It is denormalized data that powers fast search results.
- """
- issue = models.ForeignKey("Issue", on_delete=models.CASCADE)
- date = models.DateTimeField()
- tag_key = models.ForeignKey(TagKey, on_delete=models.CASCADE)
- tag_value = models.ForeignKey(TagValue, on_delete=models.CASCADE)
- count = models.PositiveIntegerField(default=1)
- class Meta:
- constraints = [
- models.UniqueConstraint(
- fields=["issue", "date", "tag_key", "tag_value"],
- name="issue_tag_key_value_unique",
- )
- ]
- class PartitioningMeta(AggregationModel.PartitioningMeta):
- pass
- class Issue(SoftDeleteModel):
- culprit = models.CharField(max_length=1024, blank=True, null=True)
- is_public = models.BooleanField(default=False)
- level = models.PositiveSmallIntegerField(
- choices=LogLevel.choices, default=LogLevel.ERROR
- )
- metadata = models.JSONField()
- project = models.ForeignKey(
- "projects.Project", on_delete=models.CASCADE, related_name="issues"
- )
- title = models.CharField(max_length=255)
- type = models.PositiveSmallIntegerField(
- choices=IssueEventType.choices, default=IssueEventType.DEFAULT
- )
- status = models.PositiveSmallIntegerField(
- choices=EventStatus.choices, default=EventStatus.UNRESOLVED
- )
- short_id = models.PositiveIntegerField(null=True)
- search_vector = SearchVectorField(editable=False, default="")
- count = models.PositiveIntegerField(default=1, editable=False)
- first_seen = models.DateTimeField(default=timezone.now, db_index=True)
- last_seen = models.DateTimeField(default=timezone.now, db_index=True)
- objects = DeferedFieldManager(["search_vector"])
- class Meta:
- base_manager_name = "objects"
- constraints = [
- models.UniqueConstraint(
- fields=["project", "short_id"],
- name="project_short_id_unique",
- )
- ]
- indexes = [
- GinIndex(fields=["search_vector"]),
- ]
- def __str__(self):
- return self.title
- def get_detail_url(self):
- return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/issues/{self.pk}"
- 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 ""
- class IssueHash(models.Model):
- issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="hashes")
- # Redundant project allows for unique constraint
- project = models.ForeignKey(
- "projects.Project", on_delete=models.CASCADE, related_name="+"
- )
- value = models.UUIDField(db_index=True)
- class Meta:
- constraints = [
- models.UniqueConstraint(
- fields=["project", "value"], name="issue hash project"
- )
- ]
- class Comment(models.Model):
- created = models.DateTimeField(auto_now_add=True)
- issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="comments")
- user = models.ForeignKey(
- "users.User", null=True, on_delete=models.SET_NULL, related_name="+"
- )
- text = models.TextField(blank=True, null=True)
- class Meta:
- ordering = ("-created",)
- class UserReport(CreatedModel):
- project = models.ForeignKey(
- "projects.Project", on_delete=models.CASCADE, related_name="+"
- )
- issue = models.ForeignKey(Issue, null=True, on_delete=models.CASCADE)
- event_id = models.UUIDField()
- name = models.CharField(max_length=128)
- email = models.EmailField()
- comments = models.TextField()
- class Meta:
- constraints = [
- models.UniqueConstraint(
- fields=["project", "event_id"],
- name="project_event_unique",
- )
- ]
- class IssueEvent(PostgresPartitionedModel, models.Model):
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
- type = models.PositiveSmallIntegerField(default=0, choices=IssueEventType.choices)
- timestamp = models.DateTimeField(help_text="Time at which event happened")
- received = models.DateTimeField(help_text="Time at which GlitchTip accepted event")
- title = models.CharField(max_length=255)
- transaction = models.CharField(max_length=MAX_CULPRIT_LENGTH)
- level = models.PositiveSmallIntegerField(
- choices=LogLevel.choices, default=LogLevel.ERROR
- )
- data = models.JSONField()
- # This could be HStore, but jsonb is just as good and removes need for
- # 'django.contrib.postgres' which makes several unnecessary SQL calls
- tags = models.JSONField()
- release = models.ForeignKey(
- "releases.Release", blank=True, null=True, on_delete=models.SET_NULL
- )
- class Meta:
- indexes = [models.Index(fields=["issue", "-received"])]
- class PartitioningMeta:
- method = PostgresPartitioningMethod.RANGE
- key = ["received"]
- def __str__(self):
- return self.eventID
- @property
- def eventID(self):
- return self.id.hex
- @property
- def message(self):
- """Often the title and message are the same. If message isn't stored, assume it's the title"""
- return self.data.get("message", self.title)
- @property
- def metadata(self):
- """Return metadata if exists, else return just the title as metadata"""
- return self.data.get("metadata", {"title": self.title})
- @property
- def platform(self):
- return self.data.get("platform")
|