Store calculated type and value on issue and event and use these values in the templates

This commit is contained in:
Klaas van Schelven
2024-04-08 15:30:41 +02:00
parent 729a4c7ea1
commit 652823f8c3
10 changed files with 119 additions and 37 deletions
+4
View File
@@ -41,6 +41,8 @@ class EventAdmin(admin.ModelAdmin):
'event_id',
'ingested_event',
'server_side_timestamp',
'calculated_type',
'calculated_value',
'issue',
'project',
'timestamp',
@@ -65,6 +67,8 @@ class EventAdmin(admin.ModelAdmin):
'event_id',
'ingested_event',
'server_side_timestamp',
'calculated_type',
'calculated_value',
'issue',
'timestamp',
'project',
@@ -0,0 +1,23 @@
# Generated by Django 4.2.11 on 2024-04-08 13:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0010_alter_event_event_id_alter_event_id'),
]
operations = [
migrations.AddField(
model_name='event',
name='calculated_type',
field=models.CharField(blank=True, default='', max_length=255),
),
migrations.AddField(
model_name='event',
name='calculated_value',
field=models.CharField(blank=True, default='', max_length=255),
),
]
@@ -0,0 +1,35 @@
import json
from django.db import migrations
from issues.utils import get_type_and_value_for_data
def fill_calculated_data(apps, schema_editor):
# same caveats on lack of code copy-pasting, as well as the reasons these caveats probably don't matter, apply as in
# the previous data-migration.
Event = apps.get_model('events', 'Event')
Issue = apps.get_model('issues', 'Issue')
for event in Event.objects.all():
event_data = json.loads(event.data)
event.calculated_type, event.calculated_value = get_type_and_value_for_data(event_data)
event.save()
# this is for Issues, which is not in the same app, but who cares
for issue in Issue.objects.all():
event_data = json.loads(issue.event_set.first().data)
issue.calculated_type, issue.calculated_value = get_type_and_value_for_data(event_data)
issue.save()
class Migration(migrations.Migration):
dependencies = [
('events', '0011_event_calculated_type_event_calculated_value'),
]
operations = [
migrations.RunPython(fill_calculated_data, migrations.RunPython.noop),
]
+8 -1
View File
@@ -134,6 +134,10 @@ class Event(models.Model):
# this is a temporary(?), bugsink-specific value;
debug_info = models.CharField(max_length=255, blank=True, null=False, default="")
# denormalized/cached fields:
calculated_type = models.CharField(max_length=255, blank=True, null=False, default="")
calculated_value = models.CharField(max_length=255, blank=True, null=False, default="")
class Meta:
unique_together = (("project", "event_id"),)
# index_together = (("group_id", "datetime"),) TODO seriously think about indexes
@@ -150,7 +154,7 @@ class Event(models.Model):
return "/events/event/%s/download/" % self.id
@classmethod
def from_ingested(cls, ingested_event, issue, parsed_data):
def from_ingested(cls, ingested_event, issue, parsed_data, calculated_type, calculated_value):
# 'from_ingested' may be a bit of a misnomer... the full 'from_ingested' is done in 'digest_event' in the views.
# below at least puts the parsed_data in the right place, and does some of the basic object set up (FKs to other
# objects etc).
@@ -184,6 +188,9 @@ class Event(models.Model):
'has_logentry': "logentry" in parsed_data,
'debug_info': ingested_event.debug_info,
'calculated_type': calculated_type,
'calculated_value': calculated_value,
}
)
return event, created
+6 -3
View File
@@ -14,7 +14,7 @@ from compat.auth import parse_auth_header_value
from projects.models import Project
from issues.models import Issue, IssueStateManager, Grouping
from issues.utils import get_issue_grouper_for_data
from issues.utils import get_type_and_value_for_data, get_issue_grouper_for_data
from issues.regressions import issue_is_regression
import sentry_sdk_extensions
@@ -103,7 +103,8 @@ class BaseIngestAPIView(APIView):
# leave this at the top -- it may involve reading from the DB which should come before any DB writing
pc_registry = get_pc_registry()
grouping_key = get_issue_grouper_for_data(event_data)
calculated_type, calculated_value = get_type_and_value_for_data(event_data)
grouping_key = get_issue_grouper_for_data(event_data, calculated_type, calculated_value)
if not Grouping.objects.filter(project=ingested_event.project, grouping_key=grouping_key).exists():
issue = Issue.objects.create(
@@ -111,6 +112,8 @@ class BaseIngestAPIView(APIView):
first_seen=ingested_event.timestamp,
last_seen=ingested_event.timestamp,
event_count=1,
calculated_type=calculated_type,
calculated_value=calculated_value,
)
# even though in our data-model a given grouping does not imply a single Issue (in fact, that's the whole
# point of groupings as a data-model), at-creation such implication does exist, because manual information
@@ -128,7 +131,7 @@ class BaseIngestAPIView(APIView):
issue = grouping.issue
issue_created = False
event, event_created = Event.from_ingested(ingested_event, issue, event_data)
event, event_created = Event.from_ingested(ingested_event, issue, event_data, calculated_type, calculated_value)
if not event_created:
# note: previously we created the event before the issue, which allowed for one less query. I don't see
# straight away how we can reproduce that now that we create issue-before-event (since creating the issue
+4
View File
@@ -16,6 +16,8 @@ class GroupingInline(admin.TabularInline):
class IssueAdmin(admin.ModelAdmin):
fields = [
'project',
'calculated_type',
'calculated_value',
'last_seen',
'first_seen',
'is_resolved',
@@ -44,6 +46,8 @@ class IssueAdmin(admin.ModelAdmin):
readonly_fields = [
'project',
'calculated_type',
'calculated_value',
'event_count',
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2.11 on 2024-04-08 13:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('issues', '0016_create_grouping_objects'),
]
operations = [
migrations.AddField(
model_name='issue',
name='calculated_type',
field=models.CharField(blank=True, default='', max_length=255),
),
migrations.AddField(
model_name='issue',
name='calculated_value',
field=models.CharField(blank=True, default='', max_length=255),
),
]
+7 -17
View File
@@ -9,7 +9,9 @@ from django.db.models import F, Value
from bugsink.volume_based_condition import VolumeBasedCondition
from alerts.tasks import send_unmute_alert
from .utils import parse_lines, serialize_lines, filter_qs_for_fixed_at, exclude_qs_for_fixed_at
from .utils import (
parse_lines, serialize_lines, filter_qs_for_fixed_at, exclude_qs_for_fixed_at,
get_title_for_exception_type_and_value)
class IncongruentStateException(Exception):
@@ -27,10 +29,12 @@ class Issue(models.Model):
project = models.ForeignKey(
"projects.Project", blank=False, null=True, on_delete=models.SET_NULL) # SET_NULL: cleanup 'later'
# denormalized fields:
# denormalized/cached fields:
last_seen = models.DateTimeField(blank=False, null=False) # based on event.server_side_timestamp
first_seen = models.DateTimeField(blank=False, null=False) # based on event.server_side_timestamp
event_count = models.IntegerField(blank=False, null=False)
calculated_type = models.CharField(max_length=255, blank=True, null=False, default="")
calculated_value = models.CharField(max_length=255, blank=True, null=False, default="")
# fields related to resolution:
# what does this mean for the release-based use cases? it means what you filter on.
@@ -75,21 +79,7 @@ class Issue(models.Model):
return values[-1] if values else {}
def title(self):
# TODO: refactor to a (filled-on-create) field
first_event = self.event_set.first()
if first_event.has_logentry:
parsed_data = json.loads(first_event.data)
logentry = parsed_data.get("logentry", {})
formatted = logentry.get("formatted", logentry.get("message", ""))
result = "Log Message" + \
(" (" + parsed_data["level"].upper() + ")" if parsed_data.get("level") else "") + \
(": " + formatted if formatted else "")
return result
main_exception = self.get_main_exception()
return main_exception.get("type", "none") + ": " + main_exception.get("value", "none")
return get_title_for_exception_type_and_value(self.calculated_type, self.calculated_value)
def get_fixed_at(self):
return parse_lines(self.fixed_at)
+2 -12
View File
@@ -90,20 +90,10 @@
</div> {# top, RHS (buttons) #}
<div class="overflow-hidden"><!-- top, LHS (various texts) -->
{% if event.has_exception %}
<h1 class="text-4xl font-bold text-ellipsis whitespace-nowrap overflow-hidden pb-1 {# needed for descenders of 'g' #}">{{ issue.get_main_exception.type }}</h1>
<div class="text-xl text-ellipsis whitespace-nowrap overflow-hidden">{{ issue.get_main_exception.value }}</div>
<h1 class="text-4xl font-bold text-ellipsis whitespace-nowrap overflow-hidden pb-1 {# needed for descenders of 'g' #}">{{ issue.calculated_type }}</h1>
<div class="text-xl text-ellipsis whitespace-nowrap overflow-hidden">{{ issue.calculated_value }}</div>
{% if parsed_data.request %}<div class="italic mt-4">{{ parsed_data.request.method }} {{ parsed_data.request.url }}</div>{% endif %}
<div class="text-ellipsis whitespace-nowrap overflow-hidden">{% with issue.get_main_exception.stacktrace.frames|last as last_frame %}<span class="font-bold">{% if last_frame.module %}{{ last_frame.module}}{% else %}{{ last_frame.filename }}{% endif %}</span>{% if last_frame.function %} in <span class="font-bold">{{ last_frame.function }}</span>{% endif %}{% endwith %}</div>
{% elif event.has_logentry %}
<h1 class="text-4xl font-bold text-ellipsis whitespace-nowrap overflow-hidden pb-1 {# needed for descenders of 'g' #}">Log Message {% if parsed_data.level %}({{ parsed_data.level|upper }}){% endif %}</h1>
<div class="text-xl text-ellipsis whitespace-nowrap overflow-hidden">{% if parsed_data.logentry.formatted %}{{ parsed_data.logentry.formatted }}{% else %}{{ parsed_data.logentry.message }}{% endif %} </div> {# what if no message at all? #}
{% if parsed_data.request %}<div class="italic mt-4">{{ parsed_data.request.method }} {{ parsed_data.request.url }}</div>{% endif %}
<div class="text-ellipsis whitespace-nowrap overflow-hidden"><span class="font-bold">{{ parsed_data.logger }}</span></div>
{% else %}
<h1 class="text-4xl font-bold text-ellipsis whitespace-nowrap overflow-hidden">??? TOOD</h1>
{% endif %}
</div> {# top, LHS (various texts) #}
</div>
+7 -4
View File
@@ -8,7 +8,7 @@ from sentry.utils.safe import get_path, trim
from sentry.utils.strings import strip
def get_type_and_value(data):
def get_type_and_value_for_data(data):
if "exception" in data and data["exception"]:
return get_exception_type_and_value_for_exception(data)
return get_exception_type_and_value_for_logmessage(data)
@@ -67,9 +67,12 @@ def default_issue_grouper(title: str, transaction: str) -> str:
return title + "" + transaction
def get_issue_grouper_for_data(data):
type_, value = get_type_and_value(data)
title = get_title_for_exception_type_and_value(type_, value)
def get_issue_grouper_for_data(data, calculated_type=None, calculated_value=None):
if calculated_type is None and calculated_value is None:
# convenience for calling code from tests, when digesting we don't do this because we already have this info
calculated_type, calculated_value = get_type_and_value_for_data(data)
title = get_title_for_exception_type_and_value(calculated_type, calculated_value)
transaction = force_str(data.get("transaction") or "<no transaction>")
fingerprint = data.get("fingerprint")