Skip to content

feat(user feedback): update default title for tickets created from user feedback #94889

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
35 changes: 34 additions & 1 deletion src/sentry/feedback/usecases/create_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,39 @@ def should_filter_feedback(
return False, None


def get_feedback_title(feedback_message: str, max_words: int = 10) -> str:
"""
Generate a descriptive title for user feedback issues.
Format: "User Feedback: [first few words of message]"

Args:
feedback_message: The user's feedback message
max_words: Maximum number of words to include from the message

Returns:
A formatted title string
"""
stripped_message = feedback_message.strip()

# Clean and split the message into words
words = stripped_message.split()

if len(words) <= max_words:
summary = stripped_message
else:
summary = " ".join(words[:max_words])
if len(summary) < len(stripped_message):
summary += "..."

title = f"User Feedback: {summary}"

# Truncate if necessary (keeping some buffer for external system limits)
if len(title) > 200: # Conservative limit
title = title[:197] + "..."

return title


def create_feedback_issue(
event: dict[str, Any], project_id: int, source: FeedbackCreationSource
) -> dict[str, Any] | None:
Expand Down Expand Up @@ -343,7 +376,7 @@ def create_feedback_issue(
event_id=event.get("event_id") or uuid4().hex,
project_id=project_id,
fingerprint=issue_fingerprint, # random UUID for fingerprint so feedbacks are grouped individually
issue_title="User Feedback",
issue_title=get_feedback_title(feedback_message),
subtitle=feedback_message,
resource_id=None,
evidence_data=evidence_data,
Expand Down
59 changes: 59 additions & 0 deletions tests/sentry/feedback/usecases/test_create_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
FeedbackCreationSource,
create_feedback_issue,
fix_for_issue_platform,
get_feedback_title,
is_in_feedback_denylist,
shim_to_feedback,
validate_issue_platform_event_schema,
Expand Down Expand Up @@ -1143,3 +1144,61 @@ def test_shim_to_feedback_missing_fields(default_project, monkeypatch):
report_dict, event, default_project, FeedbackCreationSource.USER_REPORT_ENVELOPE # type: ignore[arg-type]
)
assert mock_create_feedback_issue.call_count == 0


@django_db_all
def test_get_feedback_title():
"""Test the get_feedback_title function with various message types."""

# Test normal short message
assert get_feedback_title("Login button broken") == "User Feedback: Login button broken"

# Test message with exactly 10 words (default max_words)
message_10_words = "This is a test message with exactly ten words total"
assert get_feedback_title(message_10_words) == f"User Feedback: {message_10_words}"

# Test message with more than 10 words (should truncate)
long_message = "This is a very long feedback message that goes on and on and describes many different issues"
expected = "User Feedback: This is a very long feedback message that goes on..."
assert get_feedback_title(long_message) == expected

# Test very short message
assert get_feedback_title("Bug") == "User Feedback: Bug"

# Test custom max_words parameter
message = "This is a test with custom word limit"
assert get_feedback_title(message, max_words=3) == "User Feedback: This is a..."

# Test message that would create a title longer than 200 characters
very_long_message = "a" * 300 # 300 character message
result = get_feedback_title(very_long_message)
assert len(result) <= 200
assert result.endswith("...")
assert result.startswith("User Feedback: ")

# Test message with special characters
special_message = "The @login button doesn't work! It's broken & needs fixing."
expected_special = "User Feedback: The @login button doesn't work! It's broken & needs fixing."
assert get_feedback_title(special_message) == expected_special


@django_db_all
def test_create_feedback_issue_title(default_project, mock_produce_occurrence_to_kafka):
"""Test that create_feedback_issue uses the generated title."""
long_message = "This is a very long feedback message that describes multiple issues with the application including performance problems, UI bugs, and various other concerns that users are experiencing"

event = mock_feedback_event(default_project.id)
event["contexts"]["feedback"]["message"] = long_message

create_feedback_issue(event, default_project.id, FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE)

assert mock_produce_occurrence_to_kafka.call_count == 1

call_args = mock_produce_occurrence_to_kafka.call_args
occurrence = call_args[1]["occurrence"]

# Check that the title is truncated properly
expected_title = (
"User Feedback: This is a very long feedback message that describes multiple..."
)
assert occurrence.issue_title == expected_title
18 changes: 14 additions & 4 deletions tests/sentry/tasks/test_post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from sentry.eventstore.models import Event
from sentry.eventstore.processing import event_processing_store
from sentry.eventstream.types import EventStreamEventType
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource
from sentry.feedback.usecases.create_feedback import FeedbackCreationSource, get_feedback_title
from sentry.integrations.models.integration import Integration
from sentry.integrations.source_code_management.commit_context import CommitInfo, FileBlameInfo
from sentry.issues.auto_source_code_config.utils.platform import get_supported_platforms
Expand Down Expand Up @@ -2076,6 +2076,11 @@ def test_user_report_shims_to_feedback(self, mock_produce_occurrence_to_kafka):
assert mock_event_data["contexts"]["feedback"]["associated_event_id"] == event.event_id
assert mock_event_data["level"] == "error"

occurrence = mock_produce_occurrence_to_kafka.call_args_list[0][1]["occurrence"]
assert occurrence.issue_title == get_feedback_title(
mock_event_data["contexts"]["feedback"]["message"]
)

@patch("sentry.feedback.usecases.create_feedback.produce_occurrence_to_kafka")
def test_user_reports_no_shim_if_group_exists_on_report(self, mock_produce_occurrence_to_kafka):
with self.feature("organizations:user-feedback-event-link-ingestion-changes"):
Expand Down Expand Up @@ -3190,6 +3195,9 @@ def create_event(
is_spam=False,
):
data["type"] = "generic"
if "message" not in data:
data["message"] = "It Broke!!!"

event = self.store_event(
data=data, project_id=project_id, assert_no_errors=assert_no_errors
)
Expand All @@ -3211,7 +3219,7 @@ def create_event(
**{
"id": uuid.uuid4().hex,
"fingerprint": ["c" * 32],
"issue_title": "User Feedback",
"issue_title": get_feedback_title(data["message"]),
"subtitle": "it was bad",
"culprit": "api/123",
"resource_id": "1234",
Expand Down Expand Up @@ -3351,7 +3359,7 @@ def test_not_ran_if_crash_report_setting_option_epoch_0(self):

def test_ran_if_default_on_new_projects(self):
event = self.create_event(
data={},
data={"message": "It Broke!!!"},
project_id=self.project.id,
feedback_type=FeedbackCreationSource.CRASH_REPORT_EMBED_FORM,
)
Expand All @@ -3372,10 +3380,11 @@ def test_ran_if_default_on_new_projects(self):
cache_key="total_rubbish",
)
assert mock_process_func.call_count == 1
assert event.occurrence.issue_title == "User Feedback: It Broke!!!"

def test_ran_if_crash_feedback_envelope(self):
event = self.create_event(
data={},
data={"message": "It Broke!!!"},
project_id=self.project.id,
feedback_type=FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE,
)
Expand All @@ -3396,6 +3405,7 @@ def test_ran_if_crash_feedback_envelope(self):
cache_key="total_rubbish",
)
assert mock_process_func.call_count == 1
assert event.occurrence.issue_title == "User Feedback: It Broke!!!"

def test_logs_if_source_missing(self):
event = self.create_event(
Expand Down
Loading