Event search: first version

This commit is contained in:
Klaas van Schelven
2025-03-04 13:51:56 +01:00
parent 0cbdae9411
commit 4cde74d7cb
11 changed files with 240 additions and 87 deletions

View File

@@ -4,7 +4,6 @@ import uuid
from django.db import models
from django.db.utils import IntegrityError
from django.db.models import Min, Max
from django.utils.functional import cached_property
from projects.models import Project
@@ -276,18 +275,6 @@ class Event(models.Model):
return None, False
def get_digest_order_bounds(self):
if not hasattr(self, "_digest_order_bounds"):
d = Event.objects.filter(issue_id=self.issue.id).aggregate(lo=Min("digest_order"), hi=Max("digest_order"))
self._digest_order_bounds = d["lo"], d["hi"]
return self._digest_order_bounds
def has_prev(self):
return self.digest_order > self.get_digest_order_bounds()[0]
def has_next(self):
return self.digest_order < self.get_digest_order_bounds()[1]
@cached_property
def get_tags(self):
return list(

View File

@@ -1,5 +1,11 @@
{% if event.has_prev %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}
<a href="{% url this_view issue_pk=issue.pk nav="first" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="First event">
{% load add_to_qs %}
<form action="{% url this_view issue_pk=issue.pk nav="last" %}" method="get">{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #}
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 focus:ring-cyan-200 rounded-md mr-2"/>
</form>
{% if has_prev %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}
<a href="{% url this_view issue_pk=issue.pk nav="first" %}{% current_qs %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="First event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -8,8 +14,8 @@
</div>
{% endif %}
{% if event.has_prev %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="prev" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Previous event">
{% if has_prev %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="prev" %}{% current_qs %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Previous event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -18,8 +24,8 @@
</div>
{% endif %}
{% if event.has_next %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="next" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Next event">
{% if has_next %}
<a href="{% url this_view issue_pk=issue.pk digest_order=event.digest_order nav="next" %}{% current_qs %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Next event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -28,8 +34,8 @@
</div>
{% endif %}
{% if event.has_next %}
<a href="{% url this_view issue_pk=issue.pk nav="last" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Last event">
{% if has_next %}
<a href="{% url this_view issue_pk=issue.pk nav="last" %}{% current_qs %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Last event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}

View File

@@ -3,6 +3,7 @@
{% load issues %}
{% load humanize %}
{% load stricter_templates %}
{% load add_to_qs %}
{% block title %}{{ issue.title }} · {{ block.super }}{% endblock %}
{% block content %}
@@ -95,7 +96,7 @@
<div class="overflow-hidden"><!-- top, LHS (various texts) -->
<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 %}
{% if request_repr %}<div class="italic mt-4">{{ request_repr }}</div>{% endif %}
<div class="text-ellipsis whitespace-nowrap overflow-hidden"><span class="font-bold">{% if issue.last_frame_module %}{{ issue.last_frame_module}}{% else %}{{ issue.last_frame_filename }}{% endif %}</span>{% if issue.last_frame_function %} in <span class="font-bold">{{ issue.last_frame_function }}</span>{% endif %}</div>
</div> {# top, LHS (various texts) #}
@@ -106,13 +107,13 @@
{# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}
<div class="ml-4 mb-4 mr-4 border-2 overflow-x-auto flex-[2_1_96rem]"><!-- the whole of the big tabbed view--> {# 96rem is 1536px, which matches the 2xl class; this is no "must" but eyeballing revealed: good result #}
<div class="flex bg-slate-50 border-b-2"><!-- container for the actual tab buttons -->
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "stacktrace" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Stacktrace</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/details/"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "event-details" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event&nbsp;Details</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/breadcrumbs/"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "breadcrumbs" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Breadcrumbs</div></a>
<a href="/issues/issue/{{ issue.id }}/tags/"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "tags" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Tags</div></a>
<a href="/issues/issue/{{ issue.id }}/events/"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "event-list" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event&nbsp;List</div></a>
<a href="/issues/issue/{{ issue.id }}/grouping/"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "grouping" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Grouping</div></a>
<a href="/issues/issue/{{ issue.id }}/history/"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "history" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">History</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}none{% endif %}/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "stacktrace" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Stacktrace</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}none{% endif %}/details/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "event-details" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event&nbsp;Details</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}none{% endif %}/breadcrumbs/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "breadcrumbs" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Breadcrumbs</div></a>
<a href="/issues/issue/{{ issue.id }}/events/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "event-list" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event&nbsp;List</div></a>
<a href="/issues/issue/{{ issue.id }}/tags/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "tags" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Tags</div></a>
<a href="/issues/issue/{{ issue.id }}/grouping/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "grouping" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Grouping</div></a>
<a href="/issues/issue/{{ issue.id }}/history/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 {% if tab == "history" %}text-cyan-500 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">History</div></a>
</div>
<div class="m-4"><!-- div for tab_content -->

View File

@@ -8,7 +8,7 @@
<div class="flex">
<div class="overflow-hidden">
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})</div>
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})</div>
</div>
<div class="ml-auto flex-none">

View File

@@ -1,24 +1,34 @@
{% extends "issues/base.html" %}
{% load static %}
{% load stricter_templates %}
{% load add_to_qs %}
{% load humanize %}
{% block tab_content %}
<div class="flex">
<div class="overflow-hidden">
<div class="italic">xxxx xx xx xx:xx (Event xxx of {{ issue.digested_event_count }})</div>
<div class="italic">{{ issue.digested_event_count|intcomma }} events in total{% if q %} — {{ event_qs_count }} found by search{% endif %}.</div>
</div>
<div class="ml-auto flex-none">
<div class="flex place-content-end">
{# copy/paste of _event_nav, but not based on any event (we have none), prev/next are meaningless also #}
{# so we have first/last enabled, and the middle ones disabled #}
{# copy/paste of _event_nav, but not based on any event (we have none), prev/next are meaningless also; first/last only when we have an event_qs to navigate through #}
<form action="{% url this_view issue_pk=issue.pk nav="last" %}" method="get">{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #}
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 focus:ring-cyan-200 rounded-md mr-2"/>
</form>
<a href="{% url this_view issue_pk=issue.pk nav="first" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="First event">
{% if event_qs_count %}
<a href="{% url this_view issue_pk=issue.pk nav="first" %}{% current_qs %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="First event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" />
</svg>
</a>
{% else %}
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center" title="First event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" /></svg>
</div>
{% endif %}
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center" title="Previous event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
@@ -28,19 +38,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</div>
<a href="{% url this_view issue_pk=issue.pk nav="last" %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Last event">
{% if event_qs_count %}
<a href="{% url this_view issue_pk=issue.pk nav="last" %}{% current_qs %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Last event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center" title="Last event">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</div>
{% endif %}
</div>
</div>
</div>
<h1 class="text-2xl font-bold mt-4">404: Event missing from Bugsink</h1>
<h1 class="text-2xl font-bold mt-4">{% if event_qs_count %}404: Event missing from Bugsink{% else %}No Events{% endif %}</h1>
<div class="mb-6">
{# We apply the heuristic (textually) that if you have some events in your event_qs, but no current event, you're in a "404-like" ("This event not found") state #}
{# and if there's really no events in the qs, that that fact is what you should focus on. #}
{# This works well in practice (better than trying to match these texts to "where in _get_event() did we go wrong?" #}
{% if event_qs_count %}
This event cannot be found. It could have been removed manually or as part of the eviction process.
{% elif q %}
No events found for this search.
{% else %}
No events found. They could have been removed manually or as part of the eviction process.
{% endif %}
</div>
{% endblock %}

View File

@@ -7,7 +7,7 @@
<div class="flex">
<div class="overflow-hidden">
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})</div>
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})</div>
</div>
<div class="ml-auto flex-none">

View File

@@ -1,17 +1,23 @@
{% extends "issues/base.html" %}
{% load issues %}
{% load add_to_qs %}
{% load humanize %}
{% load add_to_qs %}
{% block tab_content %}
<div class="flex">
<div class="overflow-hidden">
<div class="italic">
Showing {{ page_obj.start_index|intcomma }} - {{ page_obj.end_index|intcomma }} of {{ page_obj.paginator.count|intcomma }}
{% if issue.digested_event_count != issue.stored_event_count %}
available events ({{ issue.digested_event_count|intcomma }} total observed).
{% else %}
total events.
Showing {{ page_obj.start_index|intcomma }} - {{ page_obj.end_index|intcomma }} of
{% if page_obj.paginator.count == issue.stored_event_count and issue.stored_event_count == issue.digested_event_count %} {# all equal #}
{{ page_obj.paginator.count|intcomma }} total events.
{% elif page_obj.paginator.count == issue.stored_event_count and issue.stored_event_count != issue.digested_event_count %} {# evictions applied #}
{{ page_obj.paginator.count|intcomma }} available events ({{ issue.digested_event_count|intcomma }} total observed).
{% elif page_obj.paginator.count != issue.stored_event_count and issue.stored_event_count == issue.digested_event_count %} {# search filters #}
{{ page_obj.paginator.count|intcomma }} events found ({{ issue.digested_event_count|intcomma }} total observed).
{% else %} {# everything unequal #}
{{ page_obj.paginator.count|intcomma }} events found ({{ issue.digested_event_count|intcomma }} available, {{ issue.digested_event_count|intcomma }} total observed).
{% endif %}
</div>
</div>
@@ -20,8 +26,12 @@
{# UI / UX question: is it a good idea to reuse-with-different-meaning (pages, not events) for this? #}
{# adapted copy/pasta from _event_nav #}
<div class="flex place-content-end">
<form action="." method="get">
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 focus:ring-cyan-200 rounded-md mr-2"/>
</form>
{% if page_obj.has_previous %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}
<a href="?page=1" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="First page">
<a href="?{% add_to_qs page=1 %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="First page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M3.22 7.595a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 0 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06l-3.25 3.25Zm8.25-3.25-3.25 3.25a.75.75 0 0 0 0 1.06l3.25 3.25a.75.75 0 1 0 1.06-1.06l-2.72-2.72 2.72-2.72a.75.75 0 0 0-1.06-1.06Z" clip-rule="evenodd" /></svg></a>
{% else %}
<div class="font-bold text-slate-300 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md inline-flex items-center justify-center" title="First page">
@@ -30,7 +40,7 @@
{% endif %}
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Previous page">
<a href="?{% add_to_qs page=page_obj.previous_page_number %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Previous page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -40,7 +50,7 @@
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Next page">
<a href="?{% add_to_qs page=page_obj.next_page_number %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Next page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -50,7 +60,7 @@
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.paginator.num_pages }}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Last page">
<a href="?{% add_to_qs page=page_obj.paginator.num_pages %}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-1 pt-1 mr-2 border-2 rounded-md hover:bg-slate-200 active:ring inline-flex items-center justify-center" title="Last page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %}
@@ -109,11 +119,11 @@ TODO
<tr class="border-slate-200 border-2 ">
<td class="p-4 font-bold text-slate-500 align-top">
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/">{{ event.digest_order }}</a>
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/{% current_qs %}">{{ event.digest_order }}</a>
</td>
<td class="p-4 font-bold text-slate-500 align-top"> {# how useful is this really? #}
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/">{{ event.id|truncatechars:9 }}</a>
<a href="/issues/issue/{{ issue.id }}/event/{{ event.id }}/{% current_qs %}">{{ event.id|truncatechars:9 }}</a>
</td>
<td class="p-4 font-mono whitespace-nowrap align-top">
@@ -140,6 +150,12 @@ TODO
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="p-4 text-slate-800 italic">
No events found{% if q %} for "{{ q }}"{% endif %}.
</td>
</tr>
{% endfor %}
{# note: no "empty" case; event-less issues are not something I expect to really support (for some definition of "really") #}
</tbody>

View File

@@ -10,7 +10,7 @@
{# event-nav only #}
<div class="flex">
<div class="overflow-hidden">
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})</div>
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})</div>
</div>
<div class="ml-auto flex-none">
@@ -31,7 +31,7 @@
<div class="flex">
<div class="overflow-hidden">
{% if forloop.counter0 == 0 %}
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }})</div>
<div class="italic">{{ event.ingested_at|date:"j M G:i T" }} (Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} total{% if q %} — {{ event_qs.count }} found by search{% endif %})</div>
{% endif %}
<h1 class="text-2xl font-bold {% if forloop.counter0 > 0 %}mt-4{% endif %} text-ellipsis whitespace-nowrap overflow-hidden">{{ exception.type }}</h1>
<div class="text-lg mb-4 text-ellipsis whitespace-nowrap overflow-hidden">{{ exception.value }}</div>

View File

@@ -22,6 +22,7 @@ def regex_converter(passed_regex):
register_converter(regex_converter("(first|last)"), "first-last")
register_converter(regex_converter("(prev|next)"), "prev-next")
register_converter(regex_converter("(none)"), "str-none")
urlpatterns = [
@@ -35,6 +36,11 @@ urlpatterns = [
path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/details/', issue_event_details, name="event_details"),
path('issue/<uuid:issue_pk>/event/<uuid:event_pk>/breadcrumbs/', issue_event_breadcrumbs, name="event_breadcrumbs"),
path('issue/<uuid:issue_pk>/event/<str-none:event_pk>/', issue_event_stacktrace, name="event_stacktrace"),
path('issue/<uuid:issue_pk>/event/<str-none:event_pk>/details/', issue_event_details, name="event_details"),
path('issue/<uuid:issue_pk>/event/<str-none:event_pk>/breadcrumbs/',
issue_event_breadcrumbs, name="event_breadcrumbs"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/', issue_event_stacktrace, name="event_stacktrace"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/details/', issue_event_details, name="event_details"),
path('issue/<uuid:issue_pk>/event/<int:digest_order>/breadcrumbs/', issue_event_breadcrumbs,

View File

@@ -3,6 +3,7 @@ import json
import sentry_sdk
import re
from django.db.models import Min, Max
from django.utils import timezone
from django.shortcuts import render, get_object_or_404, redirect
from django.db.models import Q, Subquery
@@ -26,7 +27,7 @@ from events.ua_stuff import get_contexts_enriched_with_ua
from compat.timestamp import format_timestamp
from projects.models import ProjectMembership
from tags.models import TagValue, IssueTag
from tags.models import TagValue, IssueTag, EventTag
from .models import Issue, IssueQuerysetStateManager, IssueStateManager, TurningPoint, TurningPointKind
from .forms import CommentForm
@@ -62,6 +63,13 @@ class KnownCountPaginator(Paginator):
return self._count
def _request_repr(parsed_data):
if "request" not in parsed_data:
return ""
return parsed_data["request"].get("method", "") + " " + parsed_data["request"].get("url", "")
def _is_valid_action(action, issue):
"""We take the 'strict' approach of complaining even when the action is simply a no-op, because you're already in
the desired state."""
@@ -240,6 +248,9 @@ def _and_join(q_objects):
def _search(project, issue_list, q):
if not q:
return issue_list
# The simplest possible query-language that could have any value: key:value is recognized as such; the rest is "free
# text"; no support for quoting of spaces.
slices_to_remove = []
@@ -274,6 +285,44 @@ def _search(project, issue_list, q):
return issue_list
def _search_events(project, event_list, q):
if not q:
return event_list
# The simplest possible query-language that could have any value: key:value is recognized as such; the rest is "free
# text"; no support for quoting of spaces.
slices_to_remove = []
clauses = []
for match in re.finditer(r"(\S+):(\S+)", q):
slices_to_remove.append(match.span())
key, value = match.groups()
try:
tag_value_obj = TagValue.objects.get(project=project, key__key=key, value=value)
except TagValue.DoesNotExist:
# if the tag doesn't exist, we can't have any events with it; the below short-circuit is fine, I think (I
# mean: we _could_ say "tag x is to blame" but that's not what one does generally in search, is it?
return event_list.none()
# TODO: Extensive performance testing of various choices here is necessary; in particular the choice of Subquery
# vs. joins; and the choice of a separate query to get TagValue v.s. doing everything in a single big query will
# have different trade-offs _in practice_.
clauses.append(
Q(id__in=Subquery(EventTag.objects.filter(value=tag_value_obj).values_list("event_id", flat=True))))
# this is really TSTTCPW (or more like a "fake it till you make it" thing); but I'd rather "have something" and then
# have really-good-search than to have either nothing at all, or half-baked search. Note that we didn't even bother
# to set indexes on the fields we search on (nor create a single searchable field for the whole of 'title').
plain_text_q = remove_slices(q, slices_to_remove).strip()
if plain_text_q:
clauses.append(Q(Q(calculated_type__icontains=plain_text_q) | Q(calculated_value__icontains=plain_text_q)))
# if we reach this point, there's always either a plain_text_q or some key/value pair (this is a condition for
# _and_join)
event_list = event_list.filter(_and_join(clauses))
return event_list
def _issue_list_pt_2(request, project, state_filter, unapplied_issue_ids):
d_state_filter = {
"open": lambda qs: qs.filter(is_resolved=False, is_muted=False),
@@ -336,38 +385,42 @@ def _handle_post(request, issue):
return HttpResponseRedirect(request.path_info)
def _get_event(issue, event_pk, digest_order, nav):
if nav is not None:
if nav == "first":
return Event.objects.filter(issue=issue).order_by("digest_order").first()
if nav == "last":
return Event.objects.filter(issue=issue).order_by("digest_order").last()
def _get_event(qs, event_pk, digest_order, nav):
"""Returns the event using the "url lookup"."""
if nav in ["prev", "next"]:
if nav is not None:
if nav not in ["first", "last", "prev", "next"]:
raise Http404("Cannot look up with '%s'" % nav)
if nav == "first":
result = qs.order_by("digest_order").first()
elif nav == "last":
result = qs.order_by("digest_order").last()
elif nav in ["prev", "next"]:
if digest_order is None:
raise Http404("Cannot look up with '%s' without digest_order" % nav)
if nav == "prev":
result = Event.objects.filter(
issue=issue, digest_order__lt=digest_order).order_by("-digest_order").first()
result = qs.filter(digest_order__lt=digest_order).order_by("-digest_order").first()
elif nav == "next":
result = Event.objects.filter(
issue=issue, digest_order__gt=digest_order).order_by("digest_order").first()
if result is None:
raise Event.DoesNotExist
return result
result = qs.filter(digest_order__gt=digest_order).order_by("digest_order").first()
raise Http404("Cannot look up with '%s'" % nav)
if result is None:
raise Event.DoesNotExist
return result
elif event_pk is not None:
# we match on both internal and external id, trying internal first
if event_pk == "none":
raise Event.DoesNotExist()
try:
return Event.objects.get(pk=event_pk)
except Event.DoesNotExist:
return Event.objects.get(issue=issue, event_id=event_pk)
return qs.get(event_id=event_pk)
elif digest_order is not None:
return Event.objects.get(issue=issue, digest_order=digest_order)
return qs.get(digest_order=digest_order)
else:
raise Http404("Either event_pk, nav, or digest_order must be provided")
@@ -378,10 +431,14 @@ def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None, nav
if request.method == "POST":
return _handle_post(request, issue)
event_qs = Event.objects.filter(issue=issue)
if request.GET.get("q"):
event_qs = _search_events(issue.project, event_qs, request.GET["q"])
try:
event = _get_event(issue, event_pk, digest_order, nav)
event = _get_event(event_qs, event_pk, digest_order, nav)
except Event.DoesNotExist:
return issue_event_404(request, issue, "stacktrace", "event_stacktrace")
return issue_event_404(request, issue, event_qs, "stacktrace", "event_stacktrace")
parsed_data = event.get_parsed_data()
@@ -425,16 +482,20 @@ def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None, nav
"event": event,
"is_event_page": True,
"parsed_data": parsed_data,
"request_repr": _request_repr(parsed_data),
"exceptions": exceptions,
"stack_of_plates": stack_of_plates,
"mute_options": GLOBAL_MUTE_OPTIONS,
"q": request.GET.get("q", ""),
"event_qs": event_qs,
**_has_next_prev(event, event_qs),
})
def issue_event_404(request, issue, tab, this_view):
def issue_event_404(request, issue, event_qs, tab, this_view):
"""If the Event is 404, but the issue is not, we can still show the issue page; we show a message for the event"""
last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs
last_event = event_qs.last() # used for switching to an event-page (using tabs)
return render(request, "issues/event_404.html", {
"tab": tab,
"this_view": this_view,
@@ -443,6 +504,9 @@ def issue_event_404(request, issue, tab, this_view):
"event": last_event,
"is_event_page": False, # this variable is used to denote "we have event-related info", which we don't
"mute_options": GLOBAL_MUTE_OPTIONS,
"event_qs": event_qs,
"q": request.GET.get("q", ""),
"event_qs_count": event_qs.count(), # avoids repeating the count() query
})
@@ -452,10 +516,14 @@ def issue_event_breadcrumbs(request, issue, event_pk=None, digest_order=None, na
if request.method == "POST":
return _handle_post(request, issue)
event_qs = Event.objects.filter(issue=issue)
if request.GET.get("q"):
event_qs = _search_events(issue.project, event_qs, request.GET["q"])
try:
event = _get_event(issue, event_pk, digest_order, nav)
event = _get_event(event_qs, event_pk, digest_order, nav)
except Event.DoesNotExist:
return issue_event_404(request, issue, "breadcrumbs", "event_breadcrumbs")
return issue_event_404(request, issue, event_qs, "breadcrumbs", "event_breadcrumbs")
parsed_data = event.get_parsed_data()
@@ -466,9 +534,12 @@ def issue_event_breadcrumbs(request, issue, event_pk=None, digest_order=None, na
"issue": issue,
"event": event,
"is_event_page": True,
"parsed_data": parsed_data,
"request_repr": _request_repr(parsed_data),
"breadcrumbs": get_values(parsed_data.get("breadcrumbs")),
"mute_options": GLOBAL_MUTE_OPTIONS,
"q": request.GET.get("q", ""),
"event_qs": event_qs,
**_has_next_prev(event, event_qs),
})
@@ -478,16 +549,28 @@ def _date_with_milis_html(timestamp):
'<span class="text-xs">' + date(timestamp, "u")[:3] + '</span>')
def _has_next_prev(event, event_qs):
d = event_qs.aggregate(lo=Min("digest_order"), hi=Max("digest_order"))
return {
"has_prev": event.digest_order > d["lo"] if d.get("lo") is not None else False,
"has_next": event.digest_order < d["hi"] if d.get("hi") is not None else False,
}
@atomic_for_request_method
@issue_membership_required
def issue_event_details(request, issue, event_pk=None, digest_order=None, nav=None):
if request.method == "POST":
return _handle_post(request, issue)
event_qs = Event.objects.filter(issue=issue)
if request.GET.get("q"):
event_qs = _search_events(issue.project, event_qs, request.GET["q"])
try:
event = _get_event(issue, event_pk, digest_order, nav)
event = _get_event(event_qs, event_pk, digest_order, nav)
except Event.DoesNotExist:
return issue_event_404(request, issue, "event-details", "event_details")
return issue_event_404(request, issue, event_qs, "event-details", "event_details")
parsed_data = event.get_parsed_data()
key_info = [
@@ -557,11 +640,15 @@ def issue_event_details(request, issue, event_pk=None, digest_order=None, nav=No
"event": event,
"is_event_page": True,
"parsed_data": parsed_data,
"request_repr": _request_repr(parsed_data),
"key_info": key_info,
"logentry_info": logentry_info,
"deployment_info": deployment_info,
"contexts": contexts,
"mute_options": GLOBAL_MUTE_OPTIONS,
"q": request.GET.get("q", ""),
"event_qs": event_qs,
**_has_next_prev(event, event_qs),
})
@@ -571,14 +658,15 @@ def issue_history(request, issue):
if request.method == "POST":
return _handle_post(request, issue)
last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs
event_qs = _search_events(issue.project, issue.event_set.order_by("digest_order"), request.GET.get("q", ""))
last_event = event_qs.last() # used for switching to an event-page (using tabs)
return render(request, "issues/history.html", {
"tab": "history",
"project": issue.project,
"issue": issue,
"event": last_event,
"is_event_page": False,
"parsed_data": last_event.get_parsed_data(),
"request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "",
"mute_options": GLOBAL_MUTE_OPTIONS,
})
@@ -589,14 +677,15 @@ def issue_tags(request, issue):
if request.method == "POST":
return _handle_post(request, issue)
last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs
event_qs = _search_events(issue.project, issue.event_set.order_by("digest_order"), request.GET.get("q", ""))
last_event = event_qs.last() # used for switching to an event-page (using tabs)
return render(request, "issues/tags.html", {
"tab": "tags",
"project": issue.project,
"issue": issue,
"event": last_event,
"is_event_page": False,
"parsed_data": last_event.get_parsed_data(),
"request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "",
"mute_options": GLOBAL_MUTE_OPTIONS,
})
@@ -607,14 +696,15 @@ def issue_grouping(request, issue):
if request.method == "POST":
return _handle_post(request, issue)
last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs
event_qs = _search_events(issue.project, issue.event_set.order_by("digest_order"), request.GET.get("q", ""))
last_event = event_qs.last() # used for switching to an event-page (using tabs)
return render(request, "issues/grouping.html", {
"tab": "grouping",
"project": issue.project,
"issue": issue,
"event": last_event,
"is_event_page": False,
"parsed_data": last_event.get_parsed_data(),
"request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "",
"mute_options": GLOBAL_MUTE_OPTIONS,
})
@@ -627,13 +717,18 @@ def issue_event_list(request, issue):
event_list = issue.event_set.order_by("digest_order")
# re 250: in general "big is good" because it allows a lot "at a glance".
paginator = KnownCountPaginator(event_list, 250, count=issue.stored_event_count)
if "q" in request.GET:
event_list = _search_events(issue.project, event_list, request.GET["q"])
paginator = Paginator(event_list, 250) # might as well use Paginator; the cost of .count() is incurred anyway
else:
# re 250: in general "big is good" because it allows a lot "at a glance".
paginator = KnownCountPaginator(event_list, 250, count=issue.stored_event_count)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
last_event = issue.event_set.order_by("digest_order").last() # the template needs this for the tabs
last_event = event_list.last() # used for switching to an event-page (using tabs)
return render(request, "issues/event_list.html", {
"tab": "event-list",
"project": issue.project,
@@ -641,8 +736,9 @@ def issue_event_list(request, issue):
"event": last_event,
"event_list": event_list,
"is_event_page": False,
"parsed_data": last_event.get_parsed_data(),
"request_repr": _request_repr(last_event.get_parsed_data()) if last_event is not None else "",
"mute_options": GLOBAL_MUTE_OPTIONS,
"q": request.GET.get("q", ""),
"page_obj": page_obj,
})

View File

@@ -7,6 +7,8 @@ register = template.Library()
@register.simple_tag(takes_context=True)
def add_to_qs(context, **kwargs):
"""add kwargs to query string"""
if 'request' not in context:
# "should not happen", because this tag is only assumed to be used in RequestContext templates, but it's not
# something I want to break for. Also: we have an answer that "mostly works" for that case, so let's do that.
@@ -15,3 +17,16 @@ def add_to_qs(context, **kwargs):
query = copy(context['request'].GET.dict())
query.update(kwargs)
return urlencode(query)
@register.simple_tag(takes_context=True)
def current_qs(context):
if 'request' not in context:
# "should not happen", because this tag is only assumed to be used in RequestContext templates, but it's not
# something I want to break for. Also: we have an answer that "mostly works" for that case, so let's do that.
return ""
query = copy(context['request'].GET.dict())
if query:
return '?' + urlencode(query)
return ""