diff --git a/src/sentry/feedback/usecases/create_feedback.py b/src/sentry/feedback/usecases/create_feedback.py index b1cfc6ffb34ba7..80beca80ea18b6 100644 --- a/src/sentry/feedback/usecases/create_feedback.py +++ b/src/sentry/feedback/usecases/create_feedback.py @@ -96,9 +96,16 @@ def make_evidence(feedback, source: FeedbackCreationSource, is_message_spam: boo return evidence_data, evidence_display -def fix_for_issue_platform(event_data): - # the issue platform has slightly different requirements than ingest - # for event schema, so we need to massage the data a bit +def fix_for_issue_platform(event_data: dict[str, Any]) -> dict[str, Any]: + """ + The issue platform has slightly different requirements than ingest for event schema, + so we need to massage the data a bit. + * event["tags"] is coerced to a dict. + * If event["user"]["email"] is missing we try to set using the feedback context. + + Returns: + A dict[str, Any] conforming to sentry.issues.json_schemas.EVENT_PAYLOAD_SCHEMA. + """ ret_event: dict[str, Any] = {} ret_event["timestamp"] = datetime.fromtimestamp(event_data["timestamp"], UTC).isoformat() @@ -147,7 +154,6 @@ def fix_for_issue_platform(event_data): # Force `tags` to be a dict if it's initially a list, # since we can't guarantee its type here. - tags = event_data.get("tags", {}) tags_dict = {} if isinstance(tags, list): @@ -157,11 +163,6 @@ def fix_for_issue_platform(event_data): tags_dict = tags ret_event["tags"] = tags_dict - # Set the user.email tag since we want to be able to display user.email on the feedback UI as a tag - # as well as be able to write alert conditions on it - if not ret_event["tags"].get("user.email"): - ret_event["tags"]["user.email"] = contact_email - # Set the event message to the feedback message. ret_event["logentry"] = {"message": feedback_obj.get("message")} @@ -316,6 +317,17 @@ def create_feedback_issue(event, project_id: int, source: FeedbackCreationSource } event_fixed = fix_for_issue_platform(event_data) + # Set the user.email tag since we want to be able to display user.email on the feedback UI as a tag + # as well as be able to write alert conditions on it + user_email = get_path(event_fixed, "user", "email") + if user_email and "user.email" not in event_fixed["tags"]: + event_fixed["tags"]["user.email"] = user_email + + # Set the trace.id tag to expose it for the feedback UI. + trace_id = get_path(event_fixed, "contexts", "trace", "trace_id") + if trace_id: + event_fixed["tags"]["trace.id"] = trace_id + # make sure event data is valid for issue platform validate_issue_platform_event_schema(event_fixed) diff --git a/tests/sentry/feedback/usecases/test_create_feedback.py b/tests/sentry/feedback/usecases/test_create_feedback.py index f31422df29b91f..af56cfbaccaf04 100644 --- a/tests/sentry/feedback/usecases/test_create_feedback.py +++ b/tests/sentry/feedback/usecases/test_create_feedback.py @@ -190,7 +190,6 @@ def test_fix_for_issue_platform(): fixed_event = fix_for_issue_platform(event) validate_issue_platform_event_schema(fixed_event) assert fixed_event["contexts"]["replay"]["replay_id"] == "3d621c61593c4ff9b43f8490a78ae18e" - assert fixed_event["tags"]["user.email"] == "josh.ferge@sentry.io" assert fixed_event["contexts"]["feedback"] == { "contact_email": "josh.ferge@sentry.io", "name": "Josh Ferge", @@ -661,6 +660,64 @@ def test_create_feedback_spam_detection_project_option_false( assert found_is_spam is None +@django_db_all +def test_create_feedback_spam_detection_set_status_ignored( + default_project, + monkeypatch, +): + with Feature( + { + "organizations:user-feedback-spam-filter-actions": True, + "organizations:user-feedback-spam-filter-ingest": True, + } + ): + event = { + "project_id": default_project.id, + "request": { + "url": "https://sentry.sentry.io/feedback/?statsPeriod=14d", + "headers": { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" + }, + }, + "event_id": "56b08cf7852c42cbb95e4a6998c66ad6", + "timestamp": 1698255009.574, + "received": "2021-10-24T22:23:29.574000+00:00", + "environment": "prod", + "release": "frontend@daf1316f209d961443664cd6eb4231ca154db502", + "user": { + "ip_address": "72.164.175.154", + "email": "josh.ferge@sentry.io", + "id": 880461, + "isStaff": False, + "name": "Josh Ferge", + }, + "contexts": { + "feedback": { + "contact_email": "josh.ferge@sentry.io", + "name": "Josh Ferge", + "message": "This is definitely spam", + "replay_id": "3d621c61593c4ff9b43f8490a78ae18e", + "url": "https://sentry.sentry.io/feedback/?statsPeriod=14d", + }, + }, + "breadcrumbs": [], + "platform": "javascript", + } + + mock_openai = Mock() + mock_openai().chat.completions.create = create_dummy_response + + monkeypatch.setattr("sentry.llm.providers.openai.OpenAI", mock_openai) + + create_feedback_issue( + event, default_project.id, FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE + ) + + group = Group.objects.get() + assert group.status == GroupStatus.IGNORED + assert group.substatus == GroupSubStatus.FOREVER + + @django_db_all def test_create_feedback_adds_associated_event_id( default_project, mock_produce_occurrence_to_kafka @@ -714,61 +771,43 @@ def test_create_feedback_adds_associated_event_id( @django_db_all -def test_create_feedback_spam_detection_set_status_ignored( - default_project, - monkeypatch, -): - with Feature( - { - "organizations:user-feedback-spam-filter-actions": True, - "organizations:user-feedback-spam-filter-ingest": True, - } - ): - event = { - "project_id": default_project.id, - "request": { - "url": "https://sentry.sentry.io/feedback/?statsPeriod=14d", - "headers": { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" - }, - }, - "event_id": "56b08cf7852c42cbb95e4a6998c66ad6", - "timestamp": 1698255009.574, - "received": "2021-10-24T22:23:29.574000+00:00", - "environment": "prod", - "release": "frontend@daf1316f209d961443664cd6eb4231ca154db502", - "user": { - "ip_address": "72.164.175.154", - "email": "josh.ferge@sentry.io", - "id": 880461, - "isStaff": False, - "name": "Josh Ferge", - }, - "contexts": { - "feedback": { - "contact_email": "josh.ferge@sentry.io", - "name": "Josh Ferge", - "message": "This is definitely spam", - "replay_id": "3d621c61593c4ff9b43f8490a78ae18e", - "url": "https://sentry.sentry.io/feedback/?statsPeriod=14d", - }, - }, - "breadcrumbs": [], - "platform": "javascript", - } +def test_create_feedback_tags(default_project, mock_produce_occurrence_to_kafka): + """We want to surface these tags in the UI. We also use user.email for alert conditions.""" + event = mock_feedback_event(default_project.id, datetime.now(UTC)) + event["user"]["email"] = "josh.ferge@sentry.io" + event["contexts"]["feedback"]["contact_email"] = "andrew@sentry.io" + event["contexts"]["trace"] = {"trace_id": "abc123"} + create_feedback_issue(event, default_project.id, FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE) - mock_openai = Mock() - mock_openai().chat.completions.create = create_dummy_response + assert mock_produce_occurrence_to_kafka.call_count == 1 + produced_event = mock_produce_occurrence_to_kafka.call_args.kwargs["event_data"] + tags = produced_event["tags"] + assert tags["user.email"] == "josh.ferge@sentry.io" + assert tags["trace.id"] == "abc123" - monkeypatch.setattr("sentry.llm.providers.openai.OpenAI", mock_openai) + # Uses feedback contact_email if user context doesn't have one + del event["user"]["email"] + create_feedback_issue(event, default_project.id, FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE) - create_feedback_issue( - event, default_project.id, FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE - ) + assert mock_produce_occurrence_to_kafka.call_count == 2 # includes last feedback + produced_event = mock_produce_occurrence_to_kafka.call_args.kwargs["event_data"] + tags = produced_event["tags"] + assert tags["user.email"] == "andrew@sentry.io" - group = Group.objects.get() - assert group.status == GroupStatus.IGNORED - assert group.substatus == GroupSubStatus.FOREVER + +@django_db_all +def test_create_feedback_tags_skips_if_empty(default_project, mock_produce_occurrence_to_kafka): + event = mock_feedback_event(default_project.id, datetime.now(UTC)) + event["user"].pop("email", None) + event["contexts"]["feedback"].pop("contact_email", None) + event["contexts"].pop("trace", None) + create_feedback_issue(event, default_project.id, FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE) + + assert mock_produce_occurrence_to_kafka.call_count == 1 + produced_event = mock_produce_occurrence_to_kafka.call_args.kwargs["event_data"] + tags = produced_event["tags"] + assert "user.email" not in tags + assert "trace.id" not in tags @django_db_all