diff --git a/bugsink/settings/default.py b/bugsink/settings/default.py index 27be223..33d2527 100644 --- a/bugsink/settings/default.py +++ b/bugsink/settings/default.py @@ -66,8 +66,21 @@ INSTALLED_APPS = [ 'tailwind', # As currently set up, this is also needed in production (templatetags) 'admin_auto_filters', + 'rest_framework', ] +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', # from the tutorial + 'PAGE_SIZE': 10, + + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + ], +} + BUGSINK_APPS = [ 'bsmain', 'phonehome', diff --git a/bugsink/urls.py b/bugsink/urls.py index db2e052..6bf0531 100644 --- a/bugsink/urls.py +++ b/bugsink/urls.py @@ -5,6 +5,8 @@ from django.urls import include, path from django.contrib.auth import views as auth_views from django.views.generic import RedirectView, TemplateView +from rest_framework import routers + from alerts.views import debug_email as debug_alerts_email from users.views import debug_email as debug_users_email from teams.views import debug_email as debug_teams_email @@ -14,6 +16,12 @@ from ingest.views import download_envelope from files.views import chunk_upload, artifact_bundle_assemble, api_root, api_catch_all from bugsink.decorators import login_exempt +from events.api_views import EventViewSet +from issues.api_views import IssueViewSet, GroupingViewSet +from projects.api_views import ProjectViewSet +from releases.api_views import ReleaseViewSet +from teams.api_views import TeamViewSet + from .views import home, trigger_error, favicon, settings_view, silence_email_system_warning, counts, health_check_ready from .debug_views import csrf_debug @@ -23,6 +31,15 @@ admin.site.site_title = get_settings().SITE_TITLE admin.site.index_title = "Admin" # everyone calls this the "admin" anyway. Let's set the title accordingly. +api_router = routers.DefaultRouter() +api_router.register(r'events', EventViewSet) +api_router.register(r'issues', IssueViewSet) +api_router.register(r'groupings', GroupingViewSet) +api_router.register(r'projects', ProjectViewSet) +api_router.register(r'releases', ReleaseViewSet) +api_router.register(r'teams', TeamViewSet) + + urlpatterns = [ path('', home, name='home'), @@ -43,6 +60,8 @@ urlpatterns = [ # many user-related views are directly exposed above (/accounts/), the rest is here: path("users/", include("users.urls")), + path("api/canonical/0/", include(api_router.urls)), + # these are sentry-cli endpoint for uploading; they're unrelated to e.g. the ingestion API. # the /api/0/ is just a hard prefix (for the ingest API, that position indicates the project id, but here it's just # a prefix) diff --git a/events/api_views.py b/events/api_views.py new file mode 100644 index 0000000..874fa4c --- /dev/null +++ b/events/api_views.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets + +from .models import Event +from .serializers import EventSerializer + + +class EventViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Event.objects.all().order_by('digest_order') + serializer_class = EventSerializer + + # TODO: the idea of required filter-fields when listing. diff --git a/events/serializers.py b/events/serializers.py new file mode 100644 index 0000000..ae9bb26 --- /dev/null +++ b/events/serializers.py @@ -0,0 +1,25 @@ +from rest_framework import serializers + +from .models import Event + + +class EventSerializer(serializers.ModelSerializer): + class Meta: + model = Event + + # TODO better wording: + # This is the first attempt at getting the list of fields right. My belief is: this is a nice minimal list. + # it _does_ contain `data`, which is typically quite "fat", but I'd say that's the most useful field to have. + # and when you're actually in the business of looking at a specific event, you want to see the data. + fields = [ + "id", + "ingested_at", + "digested_at", + "issue", + "grouping", + "event_id", + "project", + "data", # TODO fetch from disk if so-configured + "timestamp", + "digest_order", + ] diff --git a/issues/api_views.py b/issues/api_views.py new file mode 100644 index 0000000..1dbdb8c --- /dev/null +++ b/issues/api_views.py @@ -0,0 +1,16 @@ +from rest_framework import viewsets + +from .models import Issue, Grouping +from .serializers import IssueSerializer, GroupingSerializer + + +class IssueViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Issue.objects.all().order_by('digest_order') # TBD + serializer_class = IssueSerializer + + +class GroupingViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Grouping.objects.all().order_by('grouping_key') # TBD + serializer_class = GroupingSerializer + + # TODO: the idea of required filter-fields when listing. diff --git a/issues/serializers.py b/issues/serializers.py new file mode 100644 index 0000000..0e9d127 --- /dev/null +++ b/issues/serializers.py @@ -0,0 +1,45 @@ +from rest_framework import serializers + +from .models import Issue, Grouping + + +class IssueSerializer(serializers.ModelSerializer): + class Meta: + model = Issue + + # TODO better wording: + # This is the first attempt at getting the list of fields right. My belief is: this is a nice minimal list. + # it _does_ contain `data`, which is typically quite "fat", but I'd say that's the most useful field to have. + # and when you're actually in the business of looking at a specific event, you want to see the data. + fields = [ + "id", + "project", + "is_deleted", + "digest_order", + "last_seen", + "first_seen", + "digested_event_count", + "stored_event_count", + "calculated_type", + "calculated_value", + "transaction", + # "last_frame_filename", + # "last_frame_module", + # "last_frame_function", + "is_resolved", + "is_resolved_by_next_release", + # "fixed_at", too "raw"? i.e. too implementation-tied? + # "events_at", too "raw"? i.e. too implementation-tied? + "is_muted", + # "unmute_on_volume_based_conditions", too "raw"? i.e. too implementation-tied? + ] + + +class GroupingSerializer(serializers.ModelSerializer): + class Meta: + model = Grouping + fields = [ + "project", + "grouping_key", + "issue", + ] diff --git a/projects/api_views.py b/projects/api_views.py new file mode 100644 index 0000000..6aadcf4 --- /dev/null +++ b/projects/api_views.py @@ -0,0 +1,9 @@ +from rest_framework import viewsets + +from .models import Project +from .serializers import ProjectSerializer + + +class ProjectViewSet(viewsets.ModelViewSet): + queryset = Project.objects.all().order_by('id') + serializer_class = ProjectSerializer diff --git a/projects/serializers.py b/projects/serializers.py new file mode 100644 index 0000000..1f5854f --- /dev/null +++ b/projects/serializers.py @@ -0,0 +1,24 @@ +from rest_framework import serializers + +from .models import Project + + +class ProjectSerializer(serializers.ModelSerializer): + class Meta: + model = Project + fields = [ + "id", + "team", + "name", + "slug", + "is_deleted", + "sentry_key", # or just: "dsn" + # users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, through="ProjectMembership") + "digested_event_count", + "stored_event_count", + "alert_on_new_issue", + "alert_on_regression", + "alert_on_unmute", + "visibility", + "retention_max_event_count", + ] diff --git a/releases/api_views.py b/releases/api_views.py new file mode 100644 index 0000000..e0dc27b --- /dev/null +++ b/releases/api_views.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets + +from .models import Release +from .serializers import ReleaseSerializer + + +class ReleaseViewSet(viewsets.ModelViewSet): + queryset = Release.objects.all().order_by('sort_epoch') + serializer_class = ReleaseSerializer + + # TODO: the idea of required filter-fields when listing; in particular: project is required. diff --git a/releases/serializers.py b/releases/serializers.py new file mode 100644 index 0000000..0cdd883 --- /dev/null +++ b/releases/serializers.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + +from .models import Release + + +class ReleaseSerializer(serializers.ModelSerializer): + class Meta: + model = Release + + # TODO: distinguish read vs write fields + fields = [ + "id", + "project", + "version", + "date_released", + "semver", + "is_semver", + "sort_epoch", + ] diff --git a/requirements.txt b/requirements.txt index 53b17bc..869d355 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,4 @@ user-agents==2.2.* fastjsonschema==2.21.* verbose_csrf_middleware==1.0.* ecma426>=0.2.0 +djangorestframework==3.16.* diff --git a/teams/api_views.py b/teams/api_views.py new file mode 100644 index 0000000..a2bd220 --- /dev/null +++ b/teams/api_views.py @@ -0,0 +1,9 @@ +from rest_framework import viewsets + +from .models import Team +from .serializers import TeamSerializer + + +class TeamViewSet(viewsets.ReadOnlyModelViewSet): # create? then we need a way to deal with visibility + queryset = Team.objects.all().order_by('name') # ordering: TBD + serializer_class = TeamSerializer diff --git a/teams/serializers.py b/teams/serializers.py new file mode 100644 index 0000000..1567ce3 --- /dev/null +++ b/teams/serializers.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from .models import Team + + +class TeamSerializer(serializers.ModelSerializer): + class Meta: + model = Team + fields = [ + "id", + "name", + # "visibility", + ]