mirror of
https://github.com/bugsink/bugsink.git
synced 2026-05-07 23:39:59 -05:00
Store calculated type and value on issue and event and use these values in the templates
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user