Просмотр исходного кода

feat(feedback): remove feedback ingest endpoint (#62499)

This endpoint is no longer used and is deprecated. All Sentry feedbacks
should be coming through envelope ingestion now.
Josh Ferge 1 год назад
Родитель
Сommit
231b86cba0

+ 0 - 7
src/sentry/api/urls.py

@@ -57,7 +57,6 @@ from sentry.discover.endpoints.discover_saved_query_detail import (
     DiscoverSavedQueryDetailEndpoint,
     DiscoverSavedQueryVisitEndpoint,
 )
-from sentry.feedback.endpoints.feedback_ingest import FeedbackIngestEndpoint
 from sentry.feedback.endpoints.organization_feedback_index import OrganizationFeedbackIndexEndpoint
 from sentry.feedback.endpoints.project_feedback_details import ProjectFeedbackDetailsEndpoint
 from sentry.incidents.endpoints.organization_alert_rule_available_action_index import (
@@ -3038,12 +3037,6 @@ urlpatterns = [
         SetupWizard.as_view(),
         name="sentry-api-0-project-wizard",
     ),
-    # Feedback
-    re_path(
-        r"^feedback/$",
-        FeedbackIngestEndpoint.as_view(),
-        name="sentry-api-0-feedback-ingest",
-    ),
     # Internal
     re_path(
         r"^internal/",

+ 0 - 191
src/sentry/feedback/endpoints/feedback_ingest.py

@@ -1,191 +0,0 @@
-from __future__ import annotations
-
-import datetime
-from typing import Any, Dict
-from uuid import uuid4
-
-from rest_framework import serializers
-from rest_framework.request import Request
-from rest_framework.response import Response
-
-from sentry import features
-from sentry.api.api_owners import ApiOwner
-from sentry.api.api_publish_status import ApiPublishStatus
-from sentry.api.authentication import (
-    ApiKeyAuthentication,
-    DSNAuthentication,
-    OrgAuthTokenAuthentication,
-    UserAuthTokenAuthentication,
-)
-from sentry.api.base import Endpoint, region_silo_endpoint
-from sentry.api.bases.project import ProjectPermission
-from sentry.api.exceptions import ResourceDoesNotExist
-from sentry.constants import ObjectStatus
-from sentry.feedback.models import Feedback
-from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, create_feedback_issue
-from sentry.models.environment import Environment
-from sentry.models.organization import Organization
-from sentry.models.project import Project
-from sentry.models.projectkey import ProjectKey
-from sentry.utils.sdk import bind_organization_context, configure_scope
-
-
-class FeedbackValidator(serializers.Serializer):
-    # required fields
-    feedback = serializers.JSONField(required=True)
-    platform = serializers.CharField(required=True)
-    sdk = serializers.JSONField(required=True)
-    timestamp = serializers.FloatField(required=True)
-
-    # optional fields
-    release = serializers.CharField(required=False)
-    environment = serializers.CharField(required=False, allow_null=True, default="production")
-    dist = serializers.CharField(required=False)
-    event_id = serializers.CharField(required=False)
-    request = serializers.JSONField(required=False)
-    tags = serializers.JSONField(required=False)
-    user = serializers.JSONField(required=False)
-    contexts = serializers.JSONField(required=False)
-    BrowserContext = serializers.JSONField(required=False)
-    DeviceContext = serializers.JSONField(required=False)
-
-    def validate_environment(self, value):
-        if not Environment.is_valid_name(value):
-            raise serializers.ValidationError("Invalid value for environment")
-        return value
-
-    def validate(self, data):
-        try:
-            ret: Dict[str, Any] = {}
-            ret["data"] = {
-                "feedback": data["feedback"],
-                "platform": data["platform"],
-                "sdk": data["sdk"],
-                "release": data.get("release"),
-                "request": data.get("request"),
-                "user": data.get("user"),
-                "tags": data.get("tags"),
-                "dist": data.get("dist"),
-                "contexts": data.get("contexts"),
-                "browser": data.get("BrowserContext"),
-                "device": data.get("DeviceContext"),
-            }
-            ret["date_added"] = datetime.datetime.fromtimestamp(data["timestamp"])
-            ret["feedback_id"] = data.get("event_id") or uuid4().hex
-            ret["url"] = data["feedback"]["url"]
-            ret["message"] = data["feedback"]["message"]
-            ret["replay_id"] = data["feedback"].get("replay_id")
-            ret["project_id"] = self.context["project"].id
-            ret["organization_id"] = self.context["organization"].id
-            ret["environment"] = data.get("environment")
-            return ret
-        except KeyError:
-            raise serializers.ValidationError("Input has wrong field name or type")
-
-
-class FeedbackIngestPermission(ProjectPermission):
-    scope_map = {
-        "POST": ["project:read", "project:write", "project:admin"],
-    }
-
-
-@region_silo_endpoint
-class FeedbackIngestEndpoint(Endpoint):
-    publish_status = {
-        "POST": ApiPublishStatus.EXPERIMENTAL,
-    }
-    owner = ApiOwner.FEEDBACK
-
-    # Authentication code borrowed from the monitor endpoints (which will eventually be removed)
-    authentication_classes = (
-        DSNAuthentication,
-        UserAuthTokenAuthentication,
-        OrgAuthTokenAuthentication,
-        ApiKeyAuthentication,
-    )
-
-    permission_classes = (FeedbackIngestPermission,)
-
-    def convert_args(
-        self,
-        request: Request,
-        organization_slug: str | None = None,
-        *args,
-        **kwargs,
-    ):
-        using_dsn_auth = isinstance(request.auth, ProjectKey)
-
-        # When using DSN auth we're able to infer the organization slug
-        if not organization_slug and using_dsn_auth:
-            organization_slug = request.auth.project.organization.slug  # type: ignore
-
-        if organization_slug:
-            try:
-                organization = Organization.objects.get_from_cache(slug=organization_slug)
-                # Try lookup by slug first. This requires organization context since
-                # slugs are unique only to the organization
-            except Organization.DoesNotExist:
-                raise ResourceDoesNotExist
-
-        project = request.auth.project  # type: ignore
-
-        if project.status != ObjectStatus.ACTIVE:
-            raise ResourceDoesNotExist
-
-        if using_dsn_auth and project.id != request.auth.project_id:  # type: ignore
-            raise ResourceDoesNotExist
-
-        if organization_slug and project.organization.slug != organization_slug:
-            raise ResourceDoesNotExist
-
-        # Check project permission. Required for Token style authentication
-        self.check_object_permissions(request, project)
-
-        with configure_scope() as scope:
-            scope.set_tag("project", project.id)
-
-        bind_organization_context(project.organization)
-
-        request._request.organization = project.organization  # type: ignore
-
-        kwargs["organization"] = organization
-        kwargs["project"] = project
-        return args, kwargs
-
-    def post(self, request: Request, organization: Organization, project: Project) -> Response:
-        if not features.has(
-            "organizations:user-feedback-ingest", project.organization, actor=request.user
-        ):
-            return Response(status=404)
-
-        feedback_validator = FeedbackValidator(
-            data=request.data, context={"project": project, "organization": organization}
-        )
-        if not feedback_validator.is_valid():
-            return self.respond(feedback_validator.errors, status=400)
-
-        result = feedback_validator.validated_data
-
-        env = Environment.objects.get_or_create(
-            name=result["environment"], organization_id=organization.id
-        )[0]
-        result["environment"] = env
-
-        # FOR NOW CREATE BOTH A FEEDBACK ISSUE AND A FEEDBACK OBJECT
-        # WE MAY NOT END UP NEEDING A FEEDBACK OBJECT, BUT IT'S HERE FOR NOW
-        Feedback.objects.create(**result)
-
-        _convert_feedback_to_context(request.data)
-        create_feedback_issue(
-            request.data, project.id, FeedbackCreationSource.NEW_FEEDBACK_DJANGO_ENDPOINT
-        )
-
-        return self.respond(status=201)
-
-
-def _convert_feedback_to_context(event):
-    if event.get("feedback"):
-        if "contexts" not in event:
-            event["contexts"] = {}
-        event["contexts"]["feedback"] = event["feedback"]
-        del event["feedback"]

+ 0 - 243
tests/sentry/feedback/test_feedback_ingest.py

@@ -1,243 +0,0 @@
-from unittest.mock import patch
-
-from django.urls import reverse
-from rest_framework.exceptions import ErrorDetail
-
-from sentry.feedback.models import Feedback
-from sentry.testutils.cases import MonitorIngestTestCase
-
-test_data = {
-    "dist": "abc123",
-    "environment": "production",
-    "event_id": "1ffe0775ac0f4417aed9de36d9f6f8dc",
-    "feedback": {
-        "contact_email": "colton.allen@sentry.io",
-        "message": "I really like this user-feedback feature!",
-        "replay_id": "ec3b4dc8b79f417596f7a1aa4fcca5d2",
-        "url": "https://docs.sentry.io/platforms/javascript/",
-        "name": "Colton Allen",
-    },
-    "platform": "javascript",
-    "release": "version@1.3",
-    "request": {
-        "headers": {
-            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
-        }
-    },
-    "sdk": {"name": "sentry.javascript.react", "version": "6.18.1"},
-    "tags": {"key": "value"},
-    "timestamp": 1234456,
-    "user": {
-        "email": "username@example.com",
-        "id": "123",
-        "ip_address": "127.0.0.1",
-        "name": "user",
-        "username": "user2270129",
-    },
-    "contexts": {
-        "BrowserContext": {"name": "Chrome", "version": "116.0.0"},
-        "DeviceContext": {"family": "Mac", "model": "Mac", "brand": "Apple", "type": "device"},
-    },
-}
-
-
-class FeedbackIngestTest(MonitorIngestTestCase):
-    endpoint = "sentry-api-0-feedback-ingest"
-
-    @patch("sentry.feedback.usecases.create_feedback.produce_occurrence_to_kafka")
-    def test_save_feedback(self, mock_produce_occurrence_to_kafka):
-        # Feature enabled should lead to successful save
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(path, data=test_data, **self.dsn_auth_headers)
-            assert response.status_code == 201
-
-            # Feedback object exists
-            feedback_list = Feedback.objects.all()
-            assert len(feedback_list) == 1
-
-            # Feedback object is what was posted
-            feedback = feedback_list[0]
-            assert feedback.data["dist"] == "abc123"
-            assert feedback.environment.name == "production"
-            assert feedback.data["sdk"]["name"] == "sentry.javascript.react"
-            assert feedback.data["feedback"]["contact_email"] == "colton.allen@sentry.io"
-            assert (
-                feedback.data["feedback"]["message"] == "I really like this user-feedback feature!"
-            )
-            assert feedback.data["feedback"]["name"] == "Colton Allen"
-            assert feedback.data["tags"]["key"] == "value"
-            assert feedback.data["release"] == "version@1.3"
-            assert feedback.data["user"]["name"] == "user"
-            assert (
-                feedback.data["request"]["headers"]["User-Agent"]
-                == "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
-            )
-            assert feedback.data["platform"] == "javascript"
-
-            assert len(mock_produce_occurrence_to_kafka.mock_calls) == 1
-            mock_event_data = mock_produce_occurrence_to_kafka.call_args_list[0][1]["event_data"]
-            assert (
-                mock_event_data["contexts"]["feedback"]["contact_email"] == "colton.allen@sentry.io"
-            )
-            assert (
-                mock_event_data["contexts"]["feedback"]["message"]
-                == "I really like this user-feedback feature!"
-            )
-            assert mock_event_data["contexts"]["feedback"]["name"] == "Colton Allen"
-            assert mock_event_data["platform"] == "javascript"
-            assert "associated_event_id" not in mock_event_data["contexts"]["feedback"]
-            assert mock_event_data["level"] == "info"
-
-            self.project.refresh_from_db()
-            assert self.project.flags.has_feedbacks
-            assert self.project.flags.has_new_feedbacks
-
-    def test_no_feature_enabled(self):
-        # Feature disabled should lead to unsuccessful save
-        with self.feature({"organizations:user-feedback-ingest": False}):
-            path = reverse(self.endpoint)
-            response = self.client.post(path, data=test_data, **self.dsn_auth_headers)
-            assert response.status_code == 404
-
-    def test_not_authorized(self):
-        # No authorization should lead to unsuccessful save
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(path, data=test_data)
-            assert response.status_code == 401
-            assert response.data == {"detail": "Authentication credentials were not provided."}
-
-    def test_wrong_input(self):
-        # Wrong inputs should lead to failed validation
-        wrong_test_data = {
-            "dist!": "abc",
-            "environment": "production",
-            "feedback": {
-                "contact_email": "colton.allen@sentry.io",
-                "message": "I really like this user-feedback feature!",
-                "replay_id": "ec3b4dc8b79f417596f7a1aa4fcca5d2",
-                "url123": "https://docs.sentry.io/platforms/javascript/",
-            },
-            "platform": "javascript",
-            "release": "version@1.3",
-            "sdk": {"name": "sentry.javascript.react", "version": "6.18.1"},
-            "timestamp": 1234456,
-        }
-
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(path, data=wrong_test_data, **self.dsn_auth_headers)
-            assert response.status_code == 400
-            assert response.data == {
-                "non_field_errors": [
-                    ErrorDetail(string="Input has wrong field name or type", code="invalid")
-                ]
-            }
-
-    def test_no_timestamp(self):
-        # Timestamp field is required for a successful post
-        missing_timestamp_test_data = {
-            "dist": "abc123",
-            "feedback": {
-                "contact_email": "colton.allen@sentry.io",
-                "message": "I really like this user-feedback feature!",
-                "replay_id": "ec3b4dc8b79f417596f7a1aa4fcca5d2",
-                "url": "https://docs.sentry.io/platforms/javascript/",
-            },
-            "platform": "javascript",
-            "release": "version@1.3",
-            "request": {
-                "headers": {
-                    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"
-                }
-            },
-            "sdk": {"name": "sentry.javascript.react", "version": "6.18.1"},
-            "tags": {"key": "value"},
-        }
-
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(
-                path, data=missing_timestamp_test_data, **self.dsn_auth_headers
-            )
-            assert response.status_code == 400
-            assert response.data == {
-                "timestamp": [ErrorDetail(string="This field is required.", code="required")]
-            }
-
-    def test_wrong_type(self):
-        # Fields should be correct type
-        wrong_type_test_data = {
-            "feedback": {
-                "contact_email": "colton.allen@sentry.io",
-                "message": "I really like this user-feedback feature!",
-                "replay_id": "ec3b4dc8b79f417596f7a1aa4fcca5d2",
-                "url": "https://docs.sentry.io/platforms/javascript/",
-            },
-            "platform": "javascript",
-            "release": "1",
-            "sdk": {"name": "sentry.javascript.react", "version": "6.18.1"},
-            "timestamp": {},
-        }
-
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(path, data=wrong_type_test_data, **self.dsn_auth_headers)
-            assert response.status_code == 400
-            assert response.data == {
-                "timestamp": [ErrorDetail(string="A valid number is required.", code="invalid")]
-            }
-
-    def test_bad_slug_path(self):
-        # Bad slug in path should lead to unsuccessful save
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(path + "bad_slug", data=test_data, **self.dsn_auth_headers)
-            assert response.status_code == 404
-
-    def test_missing_optional_fields(self):
-        # Optional fields missing should still result in successful save
-        test_data_missing_optional_fields = {
-            "feedback": {
-                "message": "I really like this user-feedback feature!",
-                "url": "https://docs.sentry.io/platforms/javascript/",
-            },
-            "platform": "javascript",
-            "sdk": {"name": "sentry.javascript.react", "version": "6.18.1"},
-            "timestamp": 1234456,
-        }
-
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(
-                path,
-                data=test_data_missing_optional_fields,
-                **self.dsn_auth_headers,
-            )
-            assert response.status_code == 201, response.content
-
-    def test_env(self):
-        # No environment name in input should default the field to "production"
-        test_data_missing_optional_fields = {
-            "feedback": {
-                "contact_email": "colton.allen@sentry.io",
-                "message": "I really like this user-feedback feature!",
-                "url": "https://docs.sentry.io/platforms/javascript/",
-            },
-            "platform": "javascript",
-            "sdk": {"name": "sentry.javascript.react", "version": "6.18.1"},
-            "timestamp": 1234456,
-        }
-
-        with self.feature({"organizations:user-feedback-ingest": True}):
-            path = reverse(self.endpoint)
-            response = self.client.post(
-                path,
-                data=test_data_missing_optional_fields,
-                **self.dsn_auth_headers,
-            )
-            assert response.status_code == 201, response.content
-            feedback_list = Feedback.objects.all()
-            feedback = feedback_list[0]
-            assert feedback.environment.name == "production"