Skip to content

ref(feedback): extract utils and shim code from create_feedback #89458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/project_user_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from sentry.api.helpers.user_reports import user_reports_filter_to_unresolved
from sentry.api.paginator import DateTimePaginator
from sentry.api.serializers import UserReportWithGroupSerializer, serialize
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource
from sentry.feedback.lib.utils import FeedbackCreationSource
from sentry.ingest.userreport import Conflict, save_userreport
from sentry.models.environment import Environment
from sentry.models.userreport import UserReport
Expand Down
39 changes: 39 additions & 0 deletions src/sentry/feedback/lib/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from enum import Enum

from sentry import options
from sentry.models.organization import Organization

UNREAL_FEEDBACK_UNATTENDED_MESSAGE = "Sent in the unattended mode"


class FeedbackCreationSource(Enum):
NEW_FEEDBACK_ENVELOPE = "new_feedback_envelope"
USER_REPORT_DJANGO_ENDPOINT = "user_report_sentry_django_endpoint"
USER_REPORT_ENVELOPE = "user_report_envelope"
CRASH_REPORT_EMBED_FORM = "crash_report_embed_form"
UPDATE_USER_REPORTS_TASK = "update_user_reports_task"

@classmethod
def new_feedback_category_values(cls) -> set[str]:
return {
c.value
for c in [
cls.NEW_FEEDBACK_ENVELOPE,
]
}

@classmethod
def old_feedback_category_values(cls) -> set[str]:
return {
c.value
for c in [
cls.CRASH_REPORT_EMBED_FORM,
cls.USER_REPORT_ENVELOPE,
cls.USER_REPORT_DJANGO_ENDPOINT,
cls.UPDATE_USER_REPORTS_TASK,
]
}


def is_in_feedback_denylist(organization: Organization) -> bool:
return organization.slug in options.get("feedback.organizations.slug-denylist")
115 changes: 2 additions & 113 deletions src/sentry/feedback/usecases/create_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import logging
import random
from datetime import UTC, datetime
from enum import Enum
from typing import Any, TypedDict
from typing import Any
from uuid import uuid4

import jsonschema

from sentry import features, options
from sentry.constants import DataCategory
from sentry.eventstore.models import Event, GroupEvent
from sentry.feedback.lib.utils import UNREAL_FEEDBACK_UNATTENDED_MESSAGE, FeedbackCreationSource
from sentry.feedback.usecases.spam_detection import is_spam, spam_detection_enabled
from sentry.issues.grouptype import FeedbackGroup
from sentry.issues.issue_occurrence import IssueEvidence, IssueOccurrence
Expand All @@ -28,37 +27,6 @@

logger = logging.getLogger(__name__)

UNREAL_FEEDBACK_UNATTENDED_MESSAGE = "Sent in the unattended mode"


class FeedbackCreationSource(Enum):
NEW_FEEDBACK_ENVELOPE = "new_feedback_envelope"
USER_REPORT_DJANGO_ENDPOINT = "user_report_sentry_django_endpoint"
USER_REPORT_ENVELOPE = "user_report_envelope"
CRASH_REPORT_EMBED_FORM = "crash_report_embed_form"
UPDATE_USER_REPORTS_TASK = "update_user_reports_task"

@classmethod
def new_feedback_category_values(cls) -> set[str]:
return {
c.value
for c in [
cls.NEW_FEEDBACK_ENVELOPE,
]
}

@classmethod
def old_feedback_category_values(cls) -> set[str]:
return {
c.value
for c in [
cls.CRASH_REPORT_EMBED_FORM,
cls.USER_REPORT_ENVELOPE,
cls.USER_REPORT_DJANGO_ENDPOINT,
cls.UPDATE_USER_REPORTS_TASK,
]
}


def make_evidence(feedback, source: FeedbackCreationSource, is_message_spam: bool | None):
evidence_data = {}
Expand Down Expand Up @@ -426,82 +394,3 @@ def auto_ignore_spam_feedbacks(project, issue_fingerprint):
new_substatus=GroupSubStatus.FOREVER,
),
)


###########
# Shim code
###########


class UserReportShimDict(TypedDict):
name: str
email: str
comments: str
event_id: str
level: str


def shim_to_feedback(
report: UserReportShimDict,
event: Event | GroupEvent,
project: Project,
source: FeedbackCreationSource,
):
"""
takes user reports from the legacy user report form/endpoint and
user reports that come from relay envelope ingestion and
creates a new User Feedback from it.
User feedbacks are an event type, so we try and grab as much from the
legacy user report and event to create the new feedback.
"""
if is_in_feedback_denylist(project.organization):
track_outcome(
org_id=project.organization_id,
project_id=project.id,
key_id=None,
outcome=Outcome.RATE_LIMITED,
reason="feedback_denylist",
timestamp=datetime.fromisoformat(event.timestamp),
event_id=event.event_id,
category=DataCategory.USER_REPORT_V2,
quantity=1,
)
return

try:
feedback_event: dict[str, Any] = {
"contexts": {
"feedback": {
"name": report.get("name", ""),
"contact_email": report["email"],
"message": report["comments"],
},
},
}

feedback_event["contexts"]["feedback"]["associated_event_id"] = event.event_id

if get_path(event.data, "contexts", "replay", "replay_id"):
feedback_event["contexts"]["replay"] = event.data["contexts"]["replay"]
feedback_event["contexts"]["feedback"]["replay_id"] = event.data["contexts"]["replay"][
"replay_id"
]

if get_path(event.data, "contexts", "trace", "trace_id"):
feedback_event["contexts"]["trace"] = event.data["contexts"]["trace"]

feedback_event["timestamp"] = event.datetime.timestamp()
feedback_event["platform"] = event.platform
feedback_event["level"] = event.data["level"]
feedback_event["environment"] = event.get_environment().name
feedback_event["tags"] = [list(item) for item in event.tags]

# Entrypoint for "new" (issue platform based) feedback. This emits outcomes.
create_feedback_issue(feedback_event, project.id, source)
except Exception:
logger.exception("Error attempting to create new user feedback by shimming a user report")
metrics.incr("feedback.shim_to_feedback.failed", tags={"referrer": source.value})


def is_in_feedback_denylist(organization):
return organization.slug in options.get("feedback.organizations.slug-denylist")
84 changes: 84 additions & 0 deletions src/sentry/feedback/usecases/shim_to_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import logging
from datetime import datetime
from typing import Any, TypedDict

from sentry.constants import DataCategory
from sentry.eventstore.models import Event, GroupEvent
from sentry.feedback.lib.utils import FeedbackCreationSource, is_in_feedback_denylist
from sentry.feedback.usecases.create_feedback import create_feedback_issue
from sentry.models.project import Project
from sentry.utils import metrics
from sentry.utils.outcomes import Outcome, track_outcome
from sentry.utils.safe import get_path

logger = logging.getLogger(__name__)


class UserReportShimDict(TypedDict):
name: str
email: str
comments: str
event_id: str
level: str


def shim_to_feedback(
report: UserReportShimDict,
event: Event | GroupEvent,
project: Project,
source: FeedbackCreationSource,
):
"""
takes user reports from the legacy user report form/endpoint and
user reports that come from relay envelope ingestion and
creates a new User Feedback from it.
User feedbacks are an event type, so we try and grab as much from the
legacy user report and event to create the new feedback.
"""
if is_in_feedback_denylist(project.organization):
track_outcome(
org_id=project.organization_id,
project_id=project.id,
key_id=None,
outcome=Outcome.RATE_LIMITED,
reason="feedback_denylist",
timestamp=datetime.fromisoformat(event.timestamp),
event_id=event.event_id,
category=DataCategory.USER_REPORT_V2,
quantity=1,
)
return

try:
feedback_event: dict[str, Any] = {
"contexts": {
"feedback": {
"name": report.get("name", ""),
"contact_email": report["email"],
"message": report["comments"],
},
},
}

feedback_event["contexts"]["feedback"]["associated_event_id"] = event.event_id

if get_path(event.data, "contexts", "replay", "replay_id"):
feedback_event["contexts"]["replay"] = event.data["contexts"]["replay"]
feedback_event["contexts"]["feedback"]["replay_id"] = event.data["contexts"]["replay"][
"replay_id"
]

if get_path(event.data, "contexts", "trace", "trace_id"):
feedback_event["contexts"]["trace"] = event.data["contexts"]["trace"]

feedback_event["timestamp"] = event.datetime.timestamp()
feedback_event["platform"] = event.platform
feedback_event["level"] = event.data["level"]
feedback_event["environment"] = event.get_environment().name
feedback_event["tags"] = [list(item) for item in event.tags]

# Entrypoint for "new" (issue platform based) feedback. This emits outcomes.
create_feedback_issue(feedback_event, project.id, source)
except Exception:
logger.exception("Error attempting to create new user feedback by shimming a user report")
metrics.incr("feedback.shim_to_feedback.failed", tags={"referrer": source.value})
2 changes: 1 addition & 1 deletion src/sentry/ingest/consumer/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from sentry.attachments import CachedAttachment, attachment_cache
from sentry.event_manager import EventManager, save_attachment
from sentry.eventstore.processing import event_processing_store, transaction_processing_store
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, is_in_feedback_denylist
from sentry.feedback.lib.utils import FeedbackCreationSource, is_in_feedback_denylist
from sentry.ingest.types import ConsumerType
from sentry.ingest.userreport import Conflict, save_userreport
from sentry.killswitches import killswitch_matches_context
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/ingest/userreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
from sentry import eventstore, options
from sentry.constants import DataCategory
from sentry.eventstore.models import Event, GroupEvent
from sentry.feedback.usecases.create_feedback import (
from sentry.feedback.lib.utils import (
UNREAL_FEEDBACK_UNATTENDED_MESSAGE,
FeedbackCreationSource,
is_in_feedback_denylist,
shim_to_feedback,
)
from sentry.feedback.usecases.shim_to_feedback import shim_to_feedback
from sentry.models.project import Project
from sentry.models.userreport import UserReport
from sentry.signals import user_feedback_received
Expand Down
5 changes: 3 additions & 2 deletions src/sentry/tasks/post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -1308,7 +1308,7 @@ def wrapper(job):


def should_postprocess_feedback(job: PostProcessJob) -> bool:
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource
from sentry.feedback.lib.utils import FeedbackCreationSource

event = job["event"]

Expand Down Expand Up @@ -1393,7 +1393,8 @@ def check_has_high_priority_alerts(job: PostProcessJob) -> None:


def link_event_to_user_report(job: PostProcessJob) -> None:
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, shim_to_feedback
from sentry.feedback.lib.utils import FeedbackCreationSource
from sentry.feedback.usecases.shim_to_feedback import shim_to_feedback
from sentry.models.userreport import UserReport

event = job["event"]
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/tasks/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from sentry.constants import DEFAULT_STORE_NORMALIZER_ARGS
from sentry.datascrubbing import scrub_data
from sentry.eventstore import processing
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, create_feedback_issue
from sentry.feedback.lib.utils import FeedbackCreationSource
from sentry.feedback.usecases.create_feedback import create_feedback_issue
from sentry.ingest.types import ConsumerType
from sentry.killswitches import killswitch_matches_context
from sentry.lang.native.symbolicator import SymbolicatorTaskKind
Expand Down
7 changes: 2 additions & 5 deletions src/sentry/tasks/update_user_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@
from django.utils import timezone

from sentry import eventstore, quotas
from sentry.feedback.usecases.create_feedback import (
FeedbackCreationSource,
is_in_feedback_denylist,
shim_to_feedback,
)
from sentry.feedback.lib.utils import FeedbackCreationSource, is_in_feedback_denylist
from sentry.feedback.usecases.shim_to_feedback import shim_to_feedback
from sentry.models.project import Project
from sentry.models.userreport import UserReport
from sentry.silo.base import SiloMode
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/utils/mockdata/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
from sentry import buffer, roles, tsdb
from sentry.constants import ObjectStatus
from sentry.exceptions import HashDiscarded
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, create_feedback_issue
from sentry.feedback.lib.utils import FeedbackCreationSource
from sentry.feedback.usecases.create_feedback import create_feedback_issue
from sentry.incidents.logic import create_alert_rule, create_alert_rule_trigger, create_incident
from sentry.incidents.models.alert_rule import AlertRuleThresholdType
from sentry.incidents.models.incident import IncidentType
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/web/frontend/error_page_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from django.views.generic import View

from sentry import eventstore
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, shim_to_feedback
from sentry.feedback.lib.utils import FeedbackCreationSource
from sentry.feedback.usecases.shim_to_feedback import shim_to_feedback
from sentry.models.options.project_option import ProjectOption
from sentry.models.project import Project
from sentry.models.projectkey import ProjectKey
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import UTC, datetime, timedelta
from unittest.mock import patch

from sentry.feedback.usecases.create_feedback import FeedbackCreationSource
from sentry.feedback.lib.utils import FeedbackCreationSource
from sentry.ingest.userreport import save_userreport
from sentry.models.group import GroupStatus
from sentry.models.userreport import UserReport
Expand Down
16 changes: 16 additions & 0 deletions tests/sentry/feedback/lib/test_feedback_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from sentry.feedback.lib.utils import is_in_feedback_denylist
from sentry.testutils.pytest.fixtures import django_db_all


@django_db_all
def test_denylist(set_sentry_option, default_project):
with set_sentry_option(
"feedback.organizations.slug-denylist", [default_project.organization.slug]
):
assert is_in_feedback_denylist(default_project.organization) is True


@django_db_all
def test_denylist_not_in_list(set_sentry_option, default_project):
with set_sentry_option("feedback.organizations.slug-denylist", ["not-in-list"]):
assert is_in_feedback_denylist(default_project.organization) is False
Loading
Loading