Files
2024-02-15 17:01:50 +00:00

3708 lines
142 KiB
Python

from __future__ import annotations
import logging
import uuid
from datetime import datetime, timedelta, timezone
from time import time
from unittest import mock
from unittest.mock import MagicMock, patch
import pytest
import responses
from arroyo.backends.kafka.consumer import KafkaPayload
from arroyo.backends.local.backend import LocalBroker
from arroyo.backends.local.storages.memory import MemoryMessageStorage
from arroyo.types import Partition, Topic
from django.conf import settings
from django.core.cache import cache
from django.test.utils import override_settings
from django.utils import timezone as django_timezone
from rest_framework.status import HTTP_404_NOT_FOUND
from fixtures.github import (
COMPARE_COMMITS_EXAMPLE_WITH_INTERMEDIATE,
EARLIER_COMMIT_SHA,
GET_COMMIT_EXAMPLE,
GET_LAST_2_COMMITS_EXAMPLE,
GET_PRIOR_COMMIT_EXAMPLE,
LATER_COMMIT_SHA,
)
from sentry import audit_log, eventstore, nodestore, tsdb
from sentry.attachments import CachedAttachment, attachment_cache
from sentry.constants import MAX_VERSION_LENGTH, DataCategory
from sentry.dynamic_sampling import (
ExtendedBoostedRelease,
Platform,
ProjectBoostedReleases,
get_redis_client_for_ds,
)
from sentry.event_manager import (
EventManager,
_get_event_instance,
_save_grouphash_and_group,
get_event_type,
has_pending_commit_resolution,
materialize_metadata,
)
from sentry.eventstore.models import Event
from sentry.exceptions import HashDiscarded
from sentry.grouping.api import GroupingConfig, load_grouping_config
from sentry.grouping.utils import hash_from_values
from sentry.ingest.inbound_filters import FilterStatKeys
from sentry.issues.grouptype import (
ErrorGroupType,
GroupCategory,
PerformanceNPlusOneGroupType,
PerformanceSlowDBQueryGroupType,
)
from sentry.issues.issue_occurrence import IssueEvidence
from sentry.models.activity import Activity
from sentry.models.auditlogentry import AuditLogEntry
from sentry.models.commit import Commit
from sentry.models.commitauthor import CommitAuthor
from sentry.models.environment import Environment
from sentry.models.group import Group, GroupStatus
from sentry.models.groupenvironment import GroupEnvironment
from sentry.models.grouphash import GroupHash
from sentry.models.grouplink import GroupLink
from sentry.models.grouprelease import GroupRelease
from sentry.models.groupresolution import GroupResolution
from sentry.models.grouptombstone import GroupTombstone
from sentry.models.integrations import Integration
from sentry.models.integrations.external_issue import ExternalIssue
from sentry.models.project import Project
from sentry.models.pullrequest import PullRequest, PullRequestCommit
from sentry.models.release import Release
from sentry.models.releasecommit import ReleaseCommit
from sentry.models.releaseheadcommit import ReleaseHeadCommit
from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
from sentry.models.userreport import UserReport
from sentry.options import set
from sentry.projectoptions.defaults import DEFAULT_GROUPING_CONFIG, LEGACY_GROUPING_CONFIG
from sentry.spans.grouping.utils import hash_values
from sentry.testutils.asserts import assert_mock_called_once_with_partial
from sentry.testutils.cases import (
PerformanceIssueTestCase,
SnubaTestCase,
TestCase,
TransactionTestCase,
)
from sentry.testutils.helpers import apply_feature_flag_on_cls, override_options
from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format
from sentry.testutils.outbox import outbox_runner
from sentry.testutils.performance_issues.event_generators import get_event
from sentry.testutils.pytest.fixtures import django_db_all
from sentry.testutils.silo import assume_test_silo_mode_of, region_silo_test
from sentry.testutils.skips import requires_snuba
from sentry.tsdb.base import TSDBModel
from sentry.types.activity import ActivityType
from sentry.usage_accountant import accountant
from sentry.utils import json
from sentry.utils.cache import cache_key_for_event
from sentry.utils.eventuser import EventUser
from sentry.utils.outcomes import Outcome
from sentry.utils.samples import load_data
pytestmark = [requires_snuba]
def make_event(**kwargs):
result = {
"event_id": uuid.uuid1().hex,
"level": logging.ERROR,
"logger": "default",
"tags": [],
}
result.update(kwargs)
return result
class EventManagerTestMixin:
def make_release_event(self, release_name, project_id):
manager = EventManager(make_event(release=release_name))
manager.normalize()
event = manager.save(project_id)
return event
@region_silo_test
class EventManagerTest(TestCase, SnubaTestCase, EventManagerTestMixin, PerformanceIssueTestCase):
def test_similar_message_prefix_doesnt_group(self):
# we had a regression which caused the default hash to just be
# 'event.message' instead of '[event.message]' which caused it to
# generate a hash per letter
manager = EventManager(make_event(event_id="a", message="foo bar"))
manager.normalize()
event1 = manager.save(self.project.id)
manager = EventManager(make_event(event_id="b", message="foo baz"))
manager.normalize()
event2 = manager.save(self.project.id)
assert event1.group_id != event2.group_id
def test_ephemeral_interfaces_removed_on_save(self):
manager = EventManager(make_event(platform="python"))
manager.normalize()
event = manager.save(self.project.id)
group = event.group
assert group is not None
assert group.platform == "python"
assert event.platform == "python"
@mock.patch("sentry.event_manager.eventstream.backend.insert")
def test_dupe_message_id(self, eventstream_insert):
# Saves the latest event to nodestore and eventstream
project_id = self.project.id
event_id = "a" * 32
node_id = Event.generate_node_id(project_id, event_id)
manager = EventManager(make_event(event_id=event_id, message="first"))
manager.normalize()
manager.save(project_id)
assert nodestore.backend.get(node_id)["logentry"]["formatted"] == "first"
manager = EventManager(make_event(event_id=event_id, message="second"))
manager.normalize()
manager.save(project_id)
assert nodestore.backend.get(node_id)["logentry"]["formatted"] == "second"
assert eventstream_insert.call_count == 2
def test_updates_group(self):
timestamp = time() - 300
manager = EventManager(
make_event(message="foo", event_id="a" * 32, checksum="a" * 32, timestamp=timestamp)
)
manager.normalize()
event = manager.save(self.project.id)
manager = EventManager(
make_event(
message="foo bar", event_id="b" * 32, checksum="a" * 32, timestamp=timestamp + 2.0
)
)
manager.normalize()
with self.tasks():
event2 = manager.save(self.project.id)
group = Group.objects.get(id=event.group_id)
assert group.times_seen == 2
assert group.last_seen == event2.datetime
assert group.message == event2.message
assert group.data.get("type") == "default"
assert group.data.get("metadata").get("title") == "foo bar"
def test_materialze_metadata_simple(self):
manager = EventManager(make_event(transaction="/dogs/are/great/"))
event = manager.save(self.project.id)
event_type = get_event_type(event.data)
event_metadata = event_type.get_metadata(event.data)
assert materialize_metadata(event.data, event_type, event_metadata) == {
"type": "default",
"culprit": "/dogs/are/great/",
"metadata": {"title": "<unlabeled event>"},
"title": "<unlabeled event>",
"location": None,
}
def test_materialze_metadata_preserves_existing_metadata(self):
manager = EventManager(make_event())
event = manager.save(self.project.id)
event.data.setdefault("metadata", {})
event.data["metadata"]["dogs"] = "are great" # should not get clobbered
event_type = get_event_type(event.data)
event_metadata_from_type = event_type.get_metadata(event.data)
materialized = materialize_metadata(event.data, event_type, event_metadata_from_type)
assert materialized["metadata"] == {"title": "<unlabeled event>", "dogs": "are great"}
@mock.patch("sentry.signals.issue_unresolved.send_robust")
def test_unresolves_group(self, send_robust):
ts = time() - 300
# N.B. EventManager won't unresolve the group unless the event2 has a
# later timestamp than event1.
manager = EventManager(make_event(event_id="a" * 32, checksum="a" * 32, timestamp=ts))
with self.tasks():
event = manager.save(self.project.id)
group = Group.objects.get(id=event.group_id)
group.status = GroupStatus.RESOLVED
group.substatus = None
group.save()
assert group.is_resolved()
manager = EventManager(make_event(event_id="b" * 32, checksum="a" * 32, timestamp=ts + 50))
event2 = manager.save(self.project.id)
assert event.group_id == event2.group_id
group = Group.objects.get(id=group.id)
assert not group.is_resolved()
assert send_robust.called
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_does_not_unresolve_group(self, plugin_is_regression):
# N.B. EventManager won't unresolve the group unless the event2 has a
# later timestamp than event1.
plugin_is_regression.return_value = False
manager = EventManager(
make_event(event_id="a" * 32, checksum="a" * 32, timestamp=1403007314)
)
with self.tasks():
manager.normalize()
event = manager.save(self.project.id)
group = Group.objects.get(id=event.group_id)
group.status = GroupStatus.RESOLVED
group.substatus = None
group.save()
assert group.is_resolved()
manager = EventManager(
make_event(event_id="b" * 32, checksum="a" * 32, timestamp=1403007315)
)
manager.normalize()
event2 = manager.save(self.project.id)
assert event.group_id == event2.group_id
group = Group.objects.get(id=group.id)
assert group.is_resolved()
@mock.patch("sentry.tasks.activity.send_activity_notifications.delay")
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_marks_as_unresolved_with_new_release(
self, plugin_is_regression, mock_send_activity_notifications_delay
):
plugin_is_regression.return_value = True
old_release = Release.objects.create(
version="a",
organization_id=self.project.organization_id,
date_added=django_timezone.now() - timedelta(minutes=30),
)
old_release.add_project(self.project)
manager = EventManager(
make_event(
event_id="a" * 32,
checksum="a" * 32,
timestamp=time() - 50000, # need to work around active_at
release=old_release.version,
)
)
event = manager.save(self.project.id)
assert event.group is not None
group = event.group
group.update(status=GroupStatus.RESOLVED, substatus=None)
resolution = GroupResolution.objects.create(release=old_release, group=group)
activity = Activity.objects.create(
group=group,
project=group.project,
type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
ident=resolution.id,
data={"version": ""},
)
manager = EventManager(
make_event(
event_id="b" * 32, checksum="a" * 32, timestamp=time(), release=old_release.version
)
)
event = manager.save(self.project.id)
assert event.group_id == group.id
group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.RESOLVED
activity = Activity.objects.get(id=activity.id)
assert activity.data["version"] == ""
assert GroupResolution.objects.filter(group=group).exists()
manager = EventManager(
make_event(event_id="c" * 32, checksum="a" * 32, timestamp=time(), release="b")
)
event = manager.save(self.project.id)
assert event.group_id == group.id
group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.UNRESOLVED
activity = Activity.objects.get(id=activity.id)
assert activity.data["version"] == "b"
assert not GroupResolution.objects.filter(group=group).exists()
activity = Activity.objects.get(group=group, type=ActivityType.SET_REGRESSION.value)
mock_send_activity_notifications_delay.assert_called_once_with(activity.id)
@mock.patch("sentry.tasks.activity.send_activity_notifications.delay")
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_that_release_in_latest_activity_prior_to_regression_is_not_overridden(
self, plugin_is_regression, mock_send_activity_notifications_delay
):
"""
Test that ensures in the case where a regression occurs, the release prior to the latest
activity to that regression is not overridden.
It should only be overridden if the activity was awaiting the upcoming release
"""
plugin_is_regression.return_value = True
# Create a release and a group associated with it
old_release = self.create_release(
version="foobar", date_added=django_timezone.now() - timedelta(minutes=30)
)
manager = EventManager(
make_event(
event_id="a" * 32,
checksum="a" * 32,
timestamp=time() - 50000, # need to work around active_at
release=old_release.version,
)
)
event = manager.save(self.project.id)
assert event.group is not None
group = event.group
group.update(status=GroupStatus.RESOLVED, substatus=None)
# Resolve the group in old_release
resolution = GroupResolution.objects.create(release=old_release, group=group)
activity = Activity.objects.create(
group=group,
project=group.project,
type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
ident=resolution.id,
data={"version": "foobar"},
)
# Create a regression
manager = EventManager(
make_event(event_id="c" * 32, checksum="a" * 32, timestamp=time(), release="b")
)
event = manager.save(self.project.id)
assert event.group_id == group.id
group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.UNRESOLVED
activity = Activity.objects.get(id=activity.id)
assert activity.data["version"] == "foobar"
regressed_activity = Activity.objects.get(
group=group, type=ActivityType.SET_REGRESSION.value
)
assert regressed_activity.data["version"] == "b"
assert regressed_activity.data["follows_semver"] is False
mock_send_activity_notifications_delay.assert_called_once_with(regressed_activity.id)
@mock.patch("sentry.tasks.activity.send_activity_notifications.delay")
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_current_release_version_in_latest_activity_prior_to_regression_is_not_overridden(
self, plugin_is_regression, mock_send_activity_notifications_delay
):
"""
Test that ensures in the case where a regression occurs, the release prior to the latest
activity to that regression is overridden with the release regression occurred in but the
value of `current_release_version` used for semver is not lost in the update.
"""
plugin_is_regression.return_value = True
# Create a release and a group associated with it
old_release = self.create_release(
version="a", date_added=django_timezone.now() - timedelta(minutes=30)
)
manager = EventManager(
make_event(
event_id="a" * 32,
checksum="a" * 32,
timestamp=time() - 50000, # need to work around active_at
release=old_release.version,
)
)
event = manager.save(self.project.id)
assert event.group is not None
group = event.group
group.update(status=GroupStatus.RESOLVED, substatus=None)
# Resolve the group in old_release
resolution = GroupResolution.objects.create(release=old_release, group=group)
activity = Activity.objects.create(
group=group,
project=group.project,
type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
ident=resolution.id,
data={"version": "", "current_release_version": "pre foobar"},
)
# Create a regression
manager = EventManager(
make_event(event_id="c" * 32, checksum="a" * 32, timestamp=time(), release="b")
)
event = manager.save(self.project.id)
assert event.group_id == group.id
group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.UNRESOLVED
activity = Activity.objects.get(id=activity.id)
assert activity.data["version"] == "b"
assert activity.data["current_release_version"] == "pre foobar"
regressed_activity = Activity.objects.get(
group=group, type=ActivityType.SET_REGRESSION.value
)
assert regressed_activity.data["version"] == "b"
mock_send_activity_notifications_delay.assert_called_once_with(regressed_activity.id)
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_resolved_in_release_regression_activity_follows_semver(self, plugin_is_regression):
"""
Issue was marked resolved in 1.0.0, regression occurred in 2.0.0.
If the project follows semver then the regression activity should have `follows_semver` set.
We should also record which version the issue was resolved in as `resolved_in_version`.
This allows the UI to say the issue was resolved in 1.0.0, regressed in 2.0.0 and
the versions were compared using semver.
"""
plugin_is_regression.return_value = True
# Create a release and a group associated with it
old_release = self.create_release(
version="[email protected]", date_added=django_timezone.now() - timedelta(minutes=30)
)
manager = EventManager(
make_event(
event_id="a" * 32,
checksum="a" * 32,
timestamp=time() - 50000, # need to work around active_at
release=old_release.version,
)
)
event = manager.save(self.project.id)
assert event.group is not None
group = event.group
group.update(status=GroupStatus.RESOLVED, substatus=None)
# Resolve the group in old_release
resolution = GroupResolution.objects.create(release=old_release, group=group)
activity = Activity.objects.create(
group=group,
project=group.project,
type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
ident=resolution.id,
data={"version": "[email protected]"},
)
# Create a regression
manager = EventManager(
make_event(event_id="c" * 32, checksum="a" * 32, timestamp=time(), release="[email protected]")
)
event = manager.save(self.project.id)
assert event.group_id == group.id
group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.UNRESOLVED
activity = Activity.objects.get(id=activity.id)
assert activity.data["version"] == "[email protected]"
regressed_activity = Activity.objects.get(
group=group, type=ActivityType.SET_REGRESSION.value
)
assert regressed_activity.data["version"] == "[email protected]"
assert regressed_activity.data["follows_semver"] is True
assert regressed_activity.data["resolved_in_version"] == "[email protected]"
def test_has_pending_commit_resolution(self):
project_id = self.project.id
event = self.make_release_event("1.0", project_id)
group = event.group
assert group is not None
assert group.first_release.version == "1.0"
assert not has_pending_commit_resolution(group)
# Add a commit with no associated release
repo = self.create_repo(project=group.project)
commit = Commit.objects.create(
organization_id=group.project.organization_id, repository_id=repo.id, key="a" * 40
)
GroupLink.objects.create(
group_id=group.id,
project_id=group.project_id,
linked_type=GroupLink.LinkedType.commit,
linked_id=commit.id,
relationship=GroupLink.Relationship.resolves,
)
assert has_pending_commit_resolution(group)
def test_multiple_pending_commit_resolution(self):
project_id = self.project.id
event = self.make_release_event("1.0", project_id)
group = event.group
assert group is not None
# Add a few commits with no associated release
repo = self.create_repo(project=group.project)
for key in ["a", "b", "c"]:
commit = Commit.objects.create(
organization_id=group.project.organization_id,
repository_id=repo.id,
key=key * 40,
)
GroupLink.objects.create(
group_id=group.id,
project_id=group.project_id,
linked_type=GroupLink.LinkedType.commit,
linked_id=commit.id,
relationship=GroupLink.Relationship.resolves,
)
pending = has_pending_commit_resolution(group)
assert pending
# Most recent commit has been associated with a release
latest_commit = Commit.objects.create(
organization_id=group.project.organization_id, repository_id=repo.id, key="d" * 40
)
GroupLink.objects.create(
group_id=group.id,
project_id=group.project_id,
linked_type=GroupLink.LinkedType.commit,
linked_id=latest_commit.id,
relationship=GroupLink.Relationship.resolves,
)
ReleaseCommit.objects.create(
organization_id=group.project.organization_id,
release=group.first_release,
commit=latest_commit,
order=0,
)
pending = has_pending_commit_resolution(group)
assert pending is False
def test_has_pending_commit_resolution_issue_regression(self):
project_id = self.project.id
event = self.make_release_event("1.0", project_id)
group = event.group
assert group is not None
repo = self.create_repo(project=group.project)
# commit that resolved the issue is part of a PR, but all commits within the PR are unreleased
commit = Commit.objects.create(
organization_id=group.project.organization_id, repository_id=repo.id, key="a" * 40
)
second_commit = Commit.objects.create(
organization_id=group.project.organization_id, repository_id=repo.id, key="b" * 40
)
GroupLink.objects.create(
group_id=group.id,
project_id=group.project_id,
linked_type=GroupLink.LinkedType.commit,
linked_id=commit.id,
relationship=GroupLink.Relationship.resolves,
)
pr = PullRequest.objects.create(
organization_id=group.project.organization_id,
repository_id=repo.id,
key="1",
)
PullRequestCommit.objects.create(pull_request_id=pr.id, commit_id=commit.id)
PullRequestCommit.objects.create(pull_request_id=pr.id, commit_id=second_commit.id)
assert PullRequestCommit.objects.filter(pull_request_id=pr.id, commit_id=commit.id).exists()
assert PullRequestCommit.objects.filter(
pull_request_id=pr.id, commit_id=second_commit.id
).exists()
assert not ReleaseCommit.objects.filter(commit__pullrequestcommit__id=commit.id).exists()
assert not ReleaseCommit.objects.filter(
commit__pullrequestcommit__id=second_commit.id
).exists()
pending = has_pending_commit_resolution(group)
assert pending
def test_has_pending_commit_resolution_issue_regression_released_commits(self):
project_id = self.project.id
event = self.make_release_event("1.0", project_id)
group = event.group
assert group is not None
release = self.create_release(project=self.project, version="1.1")
repo = self.create_repo(project=group.project)
# commit 1 is part of the PR, it resolves the issue in the commit message, and is unreleased
commit = Commit.objects.create(
organization_id=group.project.organization_id, repository_id=repo.id, key="a" * 38
)
GroupLink.objects.create(
group_id=group.id,
project_id=group.project_id,
linked_type=GroupLink.LinkedType.commit,
linked_id=commit.id,
relationship=GroupLink.Relationship.resolves,
)
# commit 2 is part of the PR, but does not resolve the issue, and is released
released_commit = Commit.objects.create(
organization_id=group.project.organization_id, repository_id=repo.id, key="b" * 38
)
# commit 3 is part of the PR, but does not resolve the issue, and is unreleased
unreleased_commit = Commit.objects.create(
organization_id=group.project.organization_id, repository_id=repo.id, key="c" * 38
)
pr = PullRequest.objects.create(
organization_id=group.project.organization_id,
repository_id=repo.id,
key="19",
)
PullRequestCommit.objects.create(pull_request_id=pr.id, commit_id=commit.id)
released_pr_commit = PullRequestCommit.objects.create(
pull_request_id=pr.id, commit_id=released_commit.id
)
unreleased_pr_commit = PullRequestCommit.objects.create(
pull_request_id=pr.id, commit_id=unreleased_commit.id
)
ReleaseCommit.objects.create(
organization_id=group.project.organization_id,
release=release,
commit=released_commit,
order=1,
)
assert Commit.objects.all().count() == 3
assert PullRequestCommit.objects.filter(pull_request_id=pr.id, commit_id=commit.id).exists()
assert PullRequestCommit.objects.filter(
pull_request_id=pr.id, commit_id=released_commit.id
).exists()
assert PullRequestCommit.objects.filter(commit__id=unreleased_pr_commit.commit.id).exists()
assert ReleaseCommit.objects.filter(
commit__pullrequestcommit__id=released_pr_commit.id
).exists()
pending = has_pending_commit_resolution(group)
assert pending is False
@mock.patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound")
@mock.patch("sentry.tasks.activity.send_activity_notifications.delay")
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_marks_as_unresolved_with_new_release_with_integration(
self,
plugin_is_regression,
mock_send_activity_notifications_delay,
mock_sync_status_outbound,
):
plugin_is_regression.return_value = True
old_release = Release.objects.create(
version="a",
organization_id=self.project.organization_id,
date_added=django_timezone.now() - timedelta(minutes=30),
)
old_release.add_project(self.project)
manager = EventManager(
make_event(
event_id="a" * 32,
checksum="a" * 32,
timestamp=time() - 50000, # need to work around active_at
release=old_release.version,
)
)
event = manager.save(self.project.id)
assert event.group is not None
group = event.group
org = group.organization
integration = self.create_integration(
organization=org,
external_id="example",
oi_params={
"config": {
"sync_comments": True,
"sync_status_outbound": True,
"sync_status_inbound": True,
"sync_assignee_outbound": True,
"sync_assignee_inbound": True,
}
},
provider="example",
name="Example",
)
external_issue = ExternalIssue.objects.get_or_create(
organization_id=org.id, integration_id=integration.id, key="APP-%s" % group.id
)[0]
GroupLink.objects.get_or_create(
group_id=group.id,
project_id=group.project_id,
linked_type=GroupLink.LinkedType.issue,
linked_id=external_issue.id,
relationship=GroupLink.Relationship.references,
)[0]
group.update(status=GroupStatus.RESOLVED, substatus=None)
resolution = GroupResolution.objects.create(release=old_release, group=group)
activity = Activity.objects.create(
group=group,
project=group.project,
type=ActivityType.SET_RESOLVED_IN_RELEASE.value,
ident=resolution.id,
data={"version": ""},
)
manager = EventManager(
make_event(
event_id="b" * 32, checksum="a" * 32, timestamp=time(), release=old_release.version
)
)
with self.tasks():
with self.feature({"organizations:integrations-issue-sync": True}):
event = manager.save(self.project.id)
assert event.group_id == group.id
group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.RESOLVED
activity = Activity.objects.get(id=activity.id)
assert activity.data["version"] == ""
assert GroupResolution.objects.filter(group=group).exists()
manager = EventManager(
make_event(event_id="c" * 32, checksum="a" * 32, timestamp=time(), release="b")
)
event = manager.save(self.project.id)
assert event.group is not None
mock_sync_status_outbound.assert_called_once_with(
external_issue, False, event.group.project_id
)
assert event.group_id == group.id
group = Group.objects.get(id=group.id)
assert group.status == GroupStatus.UNRESOLVED
activity = Activity.objects.get(id=activity.id)
assert activity.data["version"] == "b"
assert not GroupResolution.objects.filter(group=group).exists()
activity = Activity.objects.get(group=group, type=ActivityType.SET_REGRESSION.value)
mock_send_activity_notifications_delay.assert_called_once_with(activity.id)
@mock.patch("sentry.tasks.activity.send_activity_notifications.delay")
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_does_not_mark_as_unresolved_with_pending_commit(
self, plugin_is_regression, mock_send_activity_notifications_delay
):
plugin_is_regression.return_value = True
repo = self.create_repo(project=self.project)
commit = self.create_commit(repo=repo)
manager = EventManager(
make_event(
event_id="a" * 32,
checksum="a" * 32,
timestamp=time() - 50000, # need to work around active_at
)
)
event = manager.save(self.project.id)
group = event.group
assert group is not None
group.update(status=GroupStatus.RESOLVED, substatus=None)
GroupLink.objects.create(
group_id=group.id,
project_id=group.project_id,
linked_id=commit.id,
linked_type=GroupLink.LinkedType.commit,
relationship=GroupLink.Relationship.resolves,
)
manager = EventManager(make_event(event_id="b" * 32, checksum="a" * 32, timestamp=time()))
event = manager.save(self.project.id)
assert event.group is not None
assert event.group_id == group.id
assert Group.objects.get(id=group.id).status == GroupStatus.RESOLVED
@mock.patch("sentry.tasks.activity.send_activity_notifications.delay")
@mock.patch("sentry.event_manager.plugin_is_regression")
def test_mark_as_unresolved_with_released_commit(
self, plugin_is_regression, mock_send_activity_notifications_delay
):
plugin_is_regression.return_value = True
release = self.create_release(project=self.project)
repo = self.create_repo(project=self.project)
commit = self.create_commit(repo=repo, release=release, project=self.project)
manager = EventManager(
make_event(
event_id="a" * 32,
checksum="a" * 32,
timestamp=time() - 50000, # need to work around active_at
)
)
event = manager.save(self.project.id)
group = event.group
assert group is not None
group.update(status=GroupStatus.RESOLVED, substatus=None)
GroupLink.objects.create(
group_id=group.id,
project_id=group.project_id,
linked_id=commit.id,
linked_type=GroupLink.LinkedType.commit,
relationship=GroupLink.Relationship.resolves,
)
manager = EventManager(make_event(event_id="b" * 32, checksum="a" * 32, timestamp=time()))
event = manager.save(self.project.id)
assert event.group is not None
assert event.group_id == group.id
assert Group.objects.get(id=group.id).status == GroupStatus.UNRESOLVED
@mock.patch("sentry.models.Group.is_resolved")
def test_unresolves_group_with_auto_resolve(self, mock_is_resolved):
ts = time() - 100
mock_is_resolved.return_value = False
manager = EventManager(make_event(event_id="a" * 32, checksum="a" * 32, timestamp=ts))
with self.tasks():
event = manager.save(self.project.id)
assert event.group is not None
mock_is_resolved.return_value = True
manager = EventManager(make_event(event_id="b" * 32, checksum="a" * 32, timestamp=ts + 100))
with self.tasks():
event2 = manager.save(self.project.id)
assert event2.group is not None
assert event.group_id == event2.group_id
group = Group.objects.get(id=event.group.id)
assert group.active_at
assert group.active_at.replace(second=0) == event2.datetime.replace(second=0)
assert group.active_at.replace(second=0) != event.datetime.replace(second=0)
def test_invalid_transaction(self):
dict_input = {"messages": "foo"}
manager = EventManager(make_event(transaction=dict_input))
manager.normalize()
event = manager.save(self.project.id)
assert event.transaction is None
def test_transaction_as_culprit(self):
manager = EventManager(make_event(transaction="foobar"))
manager.normalize()
event = manager.save(self.project.id)
assert event.transaction == "foobar"
assert event.culprit == "foobar"
def test_culprit_is_not_transaction(self):
manager = EventManager(make_event(culprit="foobar"))
manager.normalize()
event1 = manager.save(self.project.id)
assert event1.transaction is None
assert event1.culprit == "foobar"
def test_culprit_after_stacktrace_processing(self):
from sentry.grouping.enhancer import Enhancements
enhancement = Enhancements.from_config_string(
"""
function:in_app_function +app
function:not_in_app_function -app
""",
)
manager = EventManager(
make_event(
platform="native",
exception={
"values": [
{
"type": "Hello",
"stacktrace": {
"frames": [
{
"function": "not_in_app_function",
},
{
"function": "in_app_function",
},
]
},
}
]
},
)
)
manager.normalize()
manager.get_data()["grouping_config"] = {
"enhancements": enhancement.dumps(),
"id": "legacy:2019-03-12",
}
event1 = manager.save(self.project.id)
assert event1.transaction is None
assert event1.culprit == "in_app_function"
def test_inferred_culprit_from_empty_stacktrace(self):
manager = EventManager(make_event(stacktrace={"frames": []}))
manager.normalize()
event = manager.save(self.project.id)
assert event.culprit == ""
def test_transaction_and_culprit(self):
manager = EventManager(make_event(transaction="foobar", culprit="baz"))
manager.normalize()
event1 = manager.save(self.project.id)
assert event1.transaction == "foobar"
assert event1.culprit == "baz"
def test_release_with_empty_version(self):
cases = ["", " ", "\t", "\n"]
for case in cases:
event = self.make_release_event(case, self.project.id)
assert not event.group.first_release
assert Release.objects.filter(projects__in=[self.project.id]).count() == 0
assert Release.objects.filter(organization_id=self.project.organization_id).count() == 0
def test_first_release(self):
project_id = self.project.id
event = self.make_release_event("1.0", project_id)
group = event.group
assert group is not None
assert group.first_release.version == "1.0"
event = self.make_release_event("2.0", project_id)
group = event.group
assert group is not None
assert group.first_release.version == "1.0"
def test_release_project_slug(self):
project = self.create_project(name="foo")
release = Release.objects.create(version="foo-1.0", organization=project.organization)
release.add_project(project)
event = self.make_release_event("1.0", project.id)
group = event.group
assert group is not None
assert group.first_release.version == "foo-1.0"
release_tag = [v for k, v in event.tags if k == "sentry:release"][0]
assert release_tag == "foo-1.0"
event = self.make_release_event("2.0", project.id)
group = event.group
assert group is not None
assert group.first_release.version == "foo-1.0"
def test_release_project_slug_long(self):
project = self.create_project(name="foo")
partial_version_len = MAX_VERSION_LENGTH - 4
release = Release.objects.create(
version="foo-{}".format("a" * partial_version_len), organization=project.organization
)
release.add_project(project)
event = self.make_release_event("a" * partial_version_len, project.id)
group = event.group
assert group is not None
assert group.first_release.version == "foo-{}".format("a" * partial_version_len)
release_tag = [v for k, v in event.tags if k == "sentry:release"][0]
assert release_tag == "foo-{}".format("a" * partial_version_len)
def test_group_release_no_env(self):
project_id = self.project.id
event = self.make_release_event("1.0", project_id)
release = Release.objects.get(version="1.0", projects=event.project_id)
assert GroupRelease.objects.filter(
release_id=release.id, group_id=event.group_id, environment=""
).exists()
# ensure we're not erroring on second creation
event = self.make_release_event("1.0", project_id)
def test_group_release_with_env(self):
manager = EventManager(make_event(release="1.0", environment="prod", event_id="a" * 32))
manager.normalize()
event = manager.save(self.project.id)
release = Release.objects.get(version="1.0", projects=event.project_id)
assert GroupRelease.objects.filter(
release_id=release.id, group_id=event.group_id, environment="prod"
).exists()
manager = EventManager(make_event(release="1.0", environment="staging", event_id="b" * 32))
event = manager.save(self.project.id)
release = Release.objects.get(version="1.0", projects=event.project_id)
assert GroupRelease.objects.filter(
release_id=release.id, group_id=event.group_id, environment="staging"
).exists()
def test_tsdb(self):
project = self.project
manager = EventManager(
make_event(
fingerprint=["totally unique super duper fingerprint"],
environment="totally unique super duper environment",
)
)
event = manager.save(project.id)
assert event.group is not None
def query(model, key, **kwargs):
return tsdb.backend.get_sums(
model,
[key],
event.datetime,
event.datetime,
tenant_ids={"organization_id": 123, "referrer": "r"},
**kwargs,
)[key]
assert query(TSDBModel.project, project.id) == 1
assert query(TSDBModel.group, event.group.id) == 1
environment_id = Environment.get_for_organization_id(
event.project.organization_id, "totally unique super duper environment"
).id
assert query(TSDBModel.project, project.id, environment_id=environment_id) == 1
assert query(TSDBModel.group, event.group.id, environment_id=environment_id) == 1
@pytest.mark.xfail
def test_record_frequencies(self):
project = self.project
manager = EventManager(make_event())
event = manager.save(project.id)
assert tsdb.backend.get_most_frequent(
TSDBModel.frequent_issues_by_project, (event.project.id,), event.datetime
) == {event.project.id: [(event.group_id, 1.0)]}
def test_event_user(self):
event_id = uuid.uuid4().hex
manager = EventManager(
make_event(
event_id=event_id, environment="totally unique environment", **{"user": {"id": "1"}}
)
)
manager.normalize()
with self.tasks():
event = manager.save(self.project.id)
assert event.group is not None
environment_id = Environment.get_for_organization_id(
event.project.organization_id, "totally unique environment"
).id
assert tsdb.backend.get_distinct_counts_totals(
TSDBModel.users_affected_by_group,
(event.group.id,),
event.datetime,
event.datetime,
tenant_ids={"referrer": "r", "organization_id": 123},
) == {event.group.id: 1}
assert tsdb.backend.get_distinct_counts_totals(
TSDBModel.users_affected_by_project,
(event.project.id,),
event.datetime,
event.datetime,
tenant_ids={"organization_id": 123, "referrer": "r"},
) == {event.project.id: 1}
assert tsdb.backend.get_distinct_counts_totals(
TSDBModel.users_affected_by_group,
(event.group.id,),
event.datetime,
event.datetime,
environment_id=environment_id,
tenant_ids={"organization_id": 123, "referrer": "r"},
) == {event.group.id: 1}
assert tsdb.backend.get_distinct_counts_totals(
TSDBModel.users_affected_by_project,
(event.project.id,),
event.datetime,
event.datetime,
environment_id=environment_id,
tenant_ids={"organization_id": 123, "referrer": "r"},
) == {event.project.id: 1}
saved_event = eventstore.backend.get_event_by_id(self.project.id, event_id)
euser = EventUser.from_event(saved_event)
assert event.get_tag("sentry:user") == euser.tag_value
# clear the cache otherwise the cached EventUser from prev
# manager.save() will be used instead of jane
cache.clear()
# ensure event user is mapped to tags in second attempt
event_id_2 = uuid.uuid4().hex
manager = EventManager(
make_event(event_id=event_id_2, **{"user": {"id": "1", "name": "jane"}})
)
manager.normalize()
with self.tasks():
manager.save(self.project.id)
saved_event = eventstore.backend.get_event_by_id(self.project.id, event_id_2)
euser = EventUser.from_event(saved_event)
assert event.get_tag("sentry:user") == euser.tag_value
assert euser.name == "jane"
assert euser.user_ident == "1"
def test_event_user_invalid_ip(self):
event_id = uuid.uuid4().hex
manager = EventManager(
make_event(
event_id=event_id, environment="totally unique environment", **{"user": {"id": "1"}}
)
)
manager.normalize()
# This can happen as part of PII stripping, which happens after normalization
manager._data["user"]["ip_address"] = "[ip]"
with self.tasks():
manager.save(self.project.id)
saved_event = eventstore.backend.get_event_by_id(self.project.id, event_id)
euser = EventUser.from_event(saved_event)
assert euser.ip_address is None
def test_event_user_unicode_identifier(self):
event_id = uuid.uuid4().hex
manager = EventManager(make_event(event_id=event_id, **{"user": {"username": "foô"}}))
manager.normalize()
with self.tasks():
manager.save(self.project.id)
saved_event = eventstore.backend.get_event_by_id(self.project.id, event_id)
euser = EventUser.from_event(saved_event)
assert euser.username == "foô"
def test_environment(self):
manager = EventManager(make_event(**{"environment": "beta"}))
manager.normalize()
event = manager.save(self.project.id)
assert dict(event.tags).get("environment") == "beta"
def test_invalid_environment(self):
manager = EventManager(make_event(**{"environment": "bad/name"}))
manager.normalize()
event = manager.save(self.project.id)
assert dict(event.tags).get("environment") is None
def test_invalid_tags(self):
manager = EventManager(make_event(**{"tags": [42]}))
manager.normalize()
assert None in manager.get_data().get("tags", [])
assert 42 not in manager.get_data().get("tags", [])
event = manager.save(self.project.id)
assert 42 not in event.tags
assert None not in event.tags
@mock.patch("sentry.event_manager.eventstream.backend.insert")
def test_group_environment(self, eventstream_insert):
release_version = "1.0"
def save_event():
manager = EventManager(
make_event(
**{
"message": "foo",
"event_id": uuid.uuid1().hex,
"environment": "beta",
"release": release_version,
}
)
)
manager.normalize()
return manager.save(self.project.id)
event = save_event()
# Ensure the `GroupEnvironment` record was created.
instance = GroupEnvironment.objects.get(
group_id=event.group_id,
environment_id=Environment.objects.get(
organization_id=self.project.organization_id, name=event.get_tag("environment")
).id,
)
assert Release.objects.get(id=instance.first_release_id).version == release_version
group_states1 = {
"is_new": True,
"is_regression": False,
"is_new_group_environment": True,
}
# Ensure that the first event in the (group, environment) pair is
# marked as being part of a new environment.
eventstream_insert.assert_called_with(
event=event,
**group_states1,
primary_hash="acbd18db4cc2f85cedef654fccc4a4d8",
skip_consume=False,
received_timestamp=event.data["received"],
group_states=[{"id": event.group.id, **group_states1}],
)
event = save_event()
group_states2 = {
"is_new": False,
"is_regression": False,
"is_new_group_environment": False,
}
# Ensure that the next event in the (group, environment) pair is *not*
# marked as being part of a new environment.
eventstream_insert.assert_called_with(
event=event,
**group_states2,
primary_hash="acbd18db4cc2f85cedef654fccc4a4d8",
skip_consume=False,
received_timestamp=event.data["received"],
group_states=[{"id": event.group.id, **group_states2}],
)
def test_user_report_gets_environment(self):
project = self.create_project()
environment = Environment.objects.create(
organization_id=project.organization_id, name="production"
)
environment.add_project(project)
event_id = "a" * 32
UserReport.objects.create(
project_id=project.id,
event_id=event_id,
name="foo",
email="[email protected]",
comments="It Broke!!!",
)
self.store_event(
data=make_event(environment=environment.name, event_id=event_id), project_id=project.id
)
assert UserReport.objects.get(event_id=event_id).environment_id == environment.id
def test_default_event_type(self):
manager = EventManager(make_event(message="foo bar"))
manager.normalize()
data = manager.get_data()
assert data["type"] == "default"
event = manager.save(self.project.id)
group = event.group
assert group is not None
assert group.data.get("type") == "default"
assert group.data.get("metadata")
assert group.data.get("metadata").get("title") == "foo bar" # type: ignore[union-attr]
def test_message_event_type(self):
manager = EventManager(
make_event(
**{
"message": "",
"logentry": {"formatted": "foo bar", "message": "foo %s", "params": ["bar"]},
}
)
)
manager.normalize()
data = manager.get_data()
assert data["type"] == "default"
event = manager.save(self.project.id)
group = event.group
assert group is not None
assert group.data.get("type") == "default"
assert group.data.get("metadata")
assert group.data.get("metadata").get("title") == "foo bar" # type: ignore[union-attr]
def test_error_event_type(self):
manager = EventManager(
make_event(**{"exception": {"values": [{"type": "Foo", "value": "bar"}]}})
)
manager.normalize()
data = manager.get_data()
assert data["type"] == "error"
event = manager.save(self.project.id)
group = event.group
assert group is not None
assert group.data.get("type") == "error"
assert group.data.get("metadata") == {
"type": "Foo",
"value": "bar",
"display_title_with_tree_label": False,
}
def test_csp_event_type(self):
manager = EventManager(
make_event(
**{
"csp": {
"effective_directive": "script-src",
"blocked_uri": "http://example.com",
},
# this normally is noramlized in relay as part of ingest
"logentry": {"message": "Blocked 'script' from 'example.com'"},
}
)
)
manager.normalize()
data = manager.get_data()
assert data["type"] == "csp"
event = manager.save(self.project.id)
group = event.group
assert group is not None
assert group.data.get("type") == "csp"
assert group.data.get("metadata") == {
"directive": "script-src",
"uri": "example.com",
"message": "Blocked 'script' from 'example.com'",
}
assert group.title == "Blocked 'script' from 'example.com'"
def test_transaction_event_type(self):
manager = EventManager(
make_event(
**{
"transaction": "wait",
"contexts": {
"trace": {
"parent_span_id": "bce14471e0e9654d",
"op": "foobar",
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"span_id": "bf5be759039ede9a",
}
},
"spans": [],
"timestamp": "2019-06-14T14:01:40Z",
"start_timestamp": "2019-06-14T14:01:40Z",
"type": "transaction",
}
)
)
manager.normalize()
data = manager.get_data()
assert data["type"] == "transaction"
def test_transaction_event_span_grouping(self):
manager = EventManager(
make_event(
**{
"transaction": "wait",
"contexts": {
"trace": {
"parent_span_id": "bce14471e0e9654d",
"op": "foobar",
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"span_id": "bf5be759039ede9a",
}
},
"spans": [
{
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"parent_span_id": "bf5be759039ede9a",
"span_id": "a" * 16,
"start_timestamp": 0,
"timestamp": 1,
"same_process_as_parent": True,
"op": "default",
"description": "span a",
},
{
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"parent_span_id": "bf5be759039ede9a",
"span_id": "b" * 16,
"start_timestamp": 0,
"timestamp": 1,
"same_process_as_parent": True,
"op": "default",
"description": "span a",
},
{
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"parent_span_id": "bf5be759039ede9a",
"span_id": "c" * 16,
"start_timestamp": 0,
"timestamp": 1,
"same_process_as_parent": True,
"op": "default",
"description": "span b",
},
],
"timestamp": "2019-06-14T14:01:40Z",
"start_timestamp": "2019-06-14T14:01:40Z",
"type": "transaction",
}
)
)
manager.normalize()
event = manager.save(self.project.id)
data = event.data
assert data["type"] == "transaction"
assert data["span_grouping_config"]["id"] == "default:2022-10-27"
spans = [{"hash": span["hash"]} for span in data["spans"]]
# the basic strategy is to simply use the description
assert spans == [{"hash": hash_values([span["description"]])} for span in data["spans"]]
def test_sdk(self):
manager = EventManager(make_event(**{"sdk": {"name": "sentry-unity", "version": "1.0"}}))
manager.normalize()
event = manager.save(self.project.id)
assert event.data["sdk"] == {
"name": "sentry-unity",
"version": "1.0",
"integrations": None,
"packages": None,
}
def test_sdk_group_tagging(self):
manager = EventManager(
make_event(**{"sdk": {"name": "sentry-native-unity", "version": "1.0"}})
)
manager.normalize()
event = manager.save(self.project.id)
assert (sdk_metadata := event.group.data.get("metadata").get("sdk")) # type: ignore[union-attr]
assert sdk_metadata.get("name") == "sentry-native-unity"
assert sdk_metadata.get("name_normalized") == "sentry.native.unity"
def test_no_message(self):
# test that the message is handled gracefully
manager = EventManager(
make_event(**{"message": None, "logentry": {"message": "hello world"}})
)
manager.normalize()
event = manager.save(self.project.id)
assert event.message == "hello world"
def test_search_message_simple(self):
manager = EventManager(
make_event(
**{
"message": "test",
"transaction": "sentry.tasks.process",
}
)
)
manager.normalize()
event = manager.save(self.project.id)
search_message = event.search_message
assert "test" in search_message
assert "sentry.tasks.process" in search_message
def test_search_message_prefers_log_entry_message(self):
manager = EventManager(
make_event(
**{
"message": "test",
"logentry": {"message": "hello world"},
"transaction": "sentry.tasks.process",
}
)
)
manager.normalize()
event = manager.save(self.project.id)
search_message = event.search_message
assert "test" not in search_message
assert "hello world" in search_message
assert "sentry.tasks.process" in search_message
def test_search_message_skips_requested_keys(self):
from sentry.eventstore import models
with patch.object(models, "SEARCH_MESSAGE_SKIPPED_KEYS", ("dogs",)):
manager = EventManager(
make_event(
**{
"logentry": {"message": "hello world"},
"transaction": "sentry.tasks.process",
}
)
)
manager.normalize()
# Normalizing nukes any metadata we might pass when creating the event and event
# manager, so we have to add it in here
manager._data["metadata"] = {"dogs": "are great", "maisey": "silly", "charlie": "goofy"}
event = manager.save(
self.project.id,
)
search_message = event.search_message
assert "hello world" in search_message
assert "sentry.tasks.process" in search_message
assert "silly" in search_message
assert "goofy" in search_message
assert "are great" not in search_message # "dogs" key is skipped
def test_search_message_skips_bools_and_numbers(self):
from sentry.eventstore import models
with patch.object(models, "SEARCH_MESSAGE_SKIPPED_KEYS", ("dogs",)):
manager = EventManager(
make_event(
**{
"logentry": {"message": "hello world"},
"transaction": "sentry.tasks.process",
}
)
)
manager.normalize()
# Normalizing nukes any metadata we might pass when creating the event and event
# manager, so we have to add it in here
manager._data["metadata"] = {
"dogs are great": True,
"maisey": 12312012,
"charlie": 1121.2012,
"adopt": "don't shop",
}
event = manager.save(
self.project.id,
)
search_message = event.search_message
assert "hello world" in search_message
assert "sentry.tasks.process" in search_message
assert "True" not in search_message # skipped because it's a boolean
assert "12312012" not in search_message # skipped because it's an int
assert "1121.2012" not in search_message # skipped because it's a float
assert "don't shop" in search_message
def test_stringified_message(self):
manager = EventManager(make_event(**{"message": 1234}))
manager.normalize()
event = manager.save(self.project.id)
assert event.data["logentry"] == {"formatted": "1234", "message": None, "params": None}
def test_bad_message(self):
# test that invalid messages are rejected
manager = EventManager(make_event(**{"message": ["asdf"]}))
manager.normalize()
event = manager.save(self.project.id)
assert event.message == '["asdf"]'
assert "logentry" in event.data
def test_message_attribute_goes_to_interface(self):
manager = EventManager(make_event(**{"message": "hello world"}))
manager.normalize()
event = manager.save(self.project.id)
assert event.data["logentry"] == {
"formatted": "hello world",
"message": None,
"params": None,
}
def test_message_attribute_shadowing(self):
# Logentry shadows the legacy message attribute.
manager = EventManager(
make_event(**{"message": "world hello", "logentry": {"message": "hello world"}})
)
manager.normalize()
event = manager.save(self.project.id)
assert event.data["logentry"] == {
"formatted": "hello world",
"message": None,
"params": None,
}
def test_message_attribute_interface_both_strings(self):
manager = EventManager(
make_event(**{"logentry": "a plain string", "message": "another string"})
)
manager.normalize()
event = manager.save(self.project.id)
assert event.data["logentry"] == {
"formatted": "a plain string",
"message": None,
"params": None,
}
def test_throws_when_matches_discarded_hash(self):
manager = EventManager(make_event(message="foo", event_id="a" * 32, fingerprint=["a" * 32]))
with self.tasks():
event = manager.save(self.project.id)
group = Group.objects.get(id=event.group_id)
tombstone = GroupTombstone.objects.create(
project_id=group.project_id,
level=group.level,
message=group.message,
culprit=group.culprit,
data=group.data,
previous_group_id=group.id,
)
GroupHash.objects.filter(group=group).update(group=None, group_tombstone_id=tombstone.id)
manager = EventManager(
make_event(message="foo", event_id="b" * 32, fingerprint=["a" * 32]),
project=self.project,
)
manager.normalize()
a1 = CachedAttachment(name="a1", data=b"hello")
a2 = CachedAttachment(name="a2", data=b"world")
cache_key = cache_key_for_event(manager.get_data())
attachment_cache.set(cache_key, attachments=[a1, a2])
from sentry.utils.outcomes import track_outcome
mock_track_outcome = mock.Mock(wraps=track_outcome)
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
with self.feature("organizations:event-attachments"):
with self.tasks():
with pytest.raises(HashDiscarded):
event = manager.save(self.project.id, cache_key=cache_key)
assert mock_track_outcome.call_count == 3
for o in mock_track_outcome.mock_calls:
assert o.kwargs["outcome"] == Outcome.FILTERED
assert o.kwargs["reason"] == FilterStatKeys.DISCARDED_HASH
o = mock_track_outcome.mock_calls[0]
assert o.kwargs["category"] == DataCategory.ERROR
for o in mock_track_outcome.mock_calls[1:]:
assert o.kwargs["category"] == DataCategory.ATTACHMENT
assert o.kwargs["quantity"] == 5
def test_honors_crash_report_limit(self):
from sentry.utils.outcomes import track_outcome
mock_track_outcome = mock.Mock(wraps=track_outcome)
# Allow exactly one crash report
self.project.update_option("sentry:store_crash_reports", 1)
manager = EventManager(
make_event(message="foo", event_id="a" * 32, fingerprint=["a" * 32]),
project=self.project,
)
manager.normalize()
a1 = CachedAttachment(name="a1", data=b"hello", type="event.minidump")
a2 = CachedAttachment(name="a2", data=b"world")
cache_key = cache_key_for_event(manager.get_data())
attachment_cache.set(cache_key, attachments=[a1, a2])
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
with self.feature("organizations:event-attachments"):
with self.tasks():
manager.save(self.project.id, cache_key=cache_key)
# The first minidump should be accepted, since the limit is 1
assert mock_track_outcome.call_count == 3
for o in mock_track_outcome.mock_calls:
assert o.kwargs["outcome"] == Outcome.ACCEPTED
mock_track_outcome.reset_mock()
manager = EventManager(
make_event(message="foo", event_id="b" * 32, fingerprint=["a" * 32]),
project=self.project,
)
manager.normalize()
cache_key = cache_key_for_event(manager.get_data())
attachment_cache.set(cache_key, attachments=[a1, a2])
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
with self.feature("organizations:event-attachments"):
with self.tasks():
event = manager.save(self.project.id, cache_key=cache_key)
assert event.data["metadata"]["stripped_crash"] is True
assert mock_track_outcome.call_count == 3
o = mock_track_outcome.mock_calls[0]
assert o.kwargs["outcome"] == Outcome.FILTERED
assert o.kwargs["category"] == DataCategory.ATTACHMENT
assert o.kwargs["reason"] == FilterStatKeys.CRASH_REPORT_LIMIT
for o in mock_track_outcome.mock_calls[1:]:
assert o.kwargs["outcome"] == Outcome.ACCEPTED
def test_event_accepted_outcome(self):
manager = EventManager(make_event(message="foo"))
manager.normalize()
mock_track_outcome = mock.Mock()
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
manager.save(self.project.id)
assert_mock_called_once_with_partial(
mock_track_outcome, outcome=Outcome.ACCEPTED, category=DataCategory.ERROR
)
def test_attachment_accepted_outcomes(self):
manager = EventManager(make_event(message="foo"), project=self.project)
manager.normalize()
a1 = CachedAttachment(name="a1", data=b"hello")
a2 = CachedAttachment(name="a2", data=b"limited", rate_limited=True)
a3 = CachedAttachment(name="a3", data=b"world")
cache_key = cache_key_for_event(manager.get_data())
attachment_cache.set(cache_key, attachments=[a1, a2, a3])
mock_track_outcome = mock.Mock()
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
with self.feature("organizations:event-attachments"):
manager.save(self.project.id, cache_key=cache_key)
assert mock_track_outcome.call_count == 3
for o in mock_track_outcome.mock_calls:
assert o.kwargs["outcome"] == Outcome.ACCEPTED
for o in mock_track_outcome.mock_calls[:2]:
assert o.kwargs["category"] == DataCategory.ATTACHMENT
assert o.kwargs["quantity"] == 5
final = mock_track_outcome.mock_calls[2]
assert final.kwargs["category"] == DataCategory.ERROR
def test_attachment_filtered_outcomes(self):
manager = EventManager(make_event(message="foo"), project=self.project)
manager.normalize()
# Disable storing all crash reports, which will drop the minidump but save the other
a1 = CachedAttachment(name="a1", data=b"minidump", type="event.minidump")
a2 = CachedAttachment(name="a2", data=b"limited", rate_limited=True)
a3 = CachedAttachment(name="a3", data=b"world")
cache_key = cache_key_for_event(manager.get_data())
attachment_cache.set(cache_key, attachments=[a1, a2, a3])
mock_track_outcome = mock.Mock()
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
with self.feature("organizations:event-attachments"):
manager.save(self.project.id, cache_key=cache_key)
assert mock_track_outcome.call_count == 3
# First outcome is the rejection of the minidump
o = mock_track_outcome.mock_calls[0]
assert o.kwargs["outcome"] == Outcome.FILTERED
assert o.kwargs["category"] == DataCategory.ATTACHMENT
assert o.kwargs["reason"] == FilterStatKeys.CRASH_REPORT_LIMIT
# Second outcome is acceptance of the "a3" attachment
o = mock_track_outcome.mock_calls[1]
assert o.kwargs["outcome"] == Outcome.ACCEPTED
assert o.kwargs["category"] == DataCategory.ATTACHMENT
assert o.kwargs["quantity"] == 5
# Last outcome is the event
o = mock_track_outcome.mock_calls[2]
assert o.kwargs["outcome"] == Outcome.ACCEPTED
assert o.kwargs["category"] == DataCategory.ERROR
def test_transaction_outcome_accepted(self):
"""
Without metrics extraction, we count the number of accepted transaction
events in the TRANSACTION data category. This maintains compatibility
with Sentry installations that do not have a metrics pipeline.
"""
manager = EventManager(
make_event(
transaction="wait",
contexts={
"trace": {
"parent_span_id": "bce14471e0e9654d",
"op": "foobar",
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"span_id": "bf5be759039ede9a",
}
},
spans=[],
timestamp=iso_format(before_now(minutes=5)),
start_timestamp=iso_format(before_now(minutes=5)),
type="transaction",
platform="python",
)
)
manager.normalize()
mock_track_outcome = mock.Mock()
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
with self.feature({"organizations:transaction-metrics-extraction": False}):
manager.save(self.project.id)
assert_mock_called_once_with_partial(
mock_track_outcome, outcome=Outcome.ACCEPTED, category=DataCategory.TRANSACTION
)
def test_transaction_indexed_outcome_accepted(self):
"""
With metrics extraction, we count the number of accepted transaction
events in the TRANSACTION_INDEXED data category. The TRANSACTION data
category contains the number of metrics from
``billing_metrics_consumer``.
"""
manager = EventManager(
make_event(
transaction="wait",
contexts={
"trace": {
"parent_span_id": "bce14471e0e9654d",
"op": "foobar",
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"span_id": "bf5be759039ede9a",
}
},
spans=[],
timestamp=iso_format(before_now(minutes=5)),
start_timestamp=iso_format(before_now(minutes=5)),
type="transaction",
platform="python",
)
)
manager.normalize()
mock_track_outcome = mock.Mock()
with mock.patch("sentry.event_manager.track_outcome", mock_track_outcome):
with self.feature("organizations:transaction-metrics-extraction"):
manager.save(self.project.id)
assert_mock_called_once_with_partial(
mock_track_outcome, outcome=Outcome.ACCEPTED, category=DataCategory.TRANSACTION_INDEXED
)
def test_checksum_rehashed(self):
checksum = "invalid checksum hash"
manager = EventManager(make_event(**{"checksum": checksum}))
manager.normalize()
event = manager.save(self.project.id)
hashes = [gh.hash for gh in GroupHash.objects.filter(group=event.group)]
assert sorted(hashes) == sorted([hash_from_values(checksum), checksum])
def test_legacy_attributes_moved(self):
event = make_event(
release="my-release",
environment="my-environment",
site="whatever",
server_name="foo.com",
event_id=uuid.uuid1().hex,
)
manager = EventManager(event)
event = manager.save(self.project.id)
# release and environment stay toplevel
assert event.data["release"] == "my-release"
assert event.data["environment"] == "my-environment"
# site is a legacy attribute that is just a tag
assert event.data.get("site") is None
tags = dict(event.tags)
assert tags["site"] == "whatever"
assert event.data.get("server_name") is None
tags = dict(event.tags)
assert tags["server_name"] == "foo.com"
@freeze_time()
def test_save_issueless_event(self):
manager = EventManager(
make_event(
transaction="wait",
contexts={
"trace": {
"parent_span_id": "bce14471e0e9654d",
"op": "foobar",
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"span_id": "bf5be759039ede9a",
}
},
spans=[],
timestamp=iso_format(before_now(minutes=5)),
start_timestamp=iso_format(before_now(minutes=5)),
type="transaction",
platform="python",
)
)
event = manager.save(self.project.id)
assert event.group is None
assert (
tsdb.backend.get_sums(
TSDBModel.project,
[self.project.id],
event.datetime,
event.datetime,
tenant_ids={"organization_id": 123, "referrer": "r"},
)[self.project.id]
== 0
)
def test_category_match_in_app(self):
"""
Regression test to ensure that grouping in-app enhancements work in
principle.
"""
from sentry.grouping.enhancer import Enhancements
enhancement = Enhancements.from_config_string(
"""
function:foo category=bar
function:foo2 category=bar
category:bar -app
""",
)
event = make_event(
platform="native",
exception={
"values": [
{
"type": "Hello",
"stacktrace": {
"frames": [
{
"function": "foo",
"in_app": True,
},
{"function": "bar"},
]
},
}
]
},
)
manager = EventManager(event)
manager.normalize()
manager.get_data()["grouping_config"] = {
"enhancements": enhancement.dumps(),
"id": "mobile:2021-02-12",
}
event1 = manager.save(self.project.id)
assert event1.data["exception"]["values"][0]["stacktrace"]["frames"][0]["in_app"] is False
event = make_event(
platform="native",
exception={
"values": [
{
"type": "Hello",
"stacktrace": {
"frames": [
{
"function": "foo2",
"in_app": True,
},
{"function": "bar"},
]
},
}
]
},
)
manager = EventManager(event)
manager.normalize()
manager.get_data()["grouping_config"] = {
"enhancements": enhancement.dumps(),
"id": "mobile:2021-02-12",
}
event2 = manager.save(self.project.id)
assert event2.data["exception"]["values"][0]["stacktrace"]["frames"][0]["in_app"] is False
assert event1.group_id == event2.group_id
def test_category_match_group(self):
"""
Regression test to ensure categories are applied consistently and don't
produce hash mismatches.
"""
from sentry.grouping.enhancer import Enhancements
enhancement = Enhancements.from_config_string(
"""
function:foo category=foo_like
category:foo_like -group
""",
)
event = make_event(
platform="native",
exception={
"values": [
{
"type": "Hello",
"stacktrace": {
"frames": [
{
"function": "foo",
},
{
"function": "bar",
},
]
},
}
]
},
)
manager = EventManager(event)
manager.normalize()
grouping_config: GroupingConfig = {
"enhancements": enhancement.dumps(),
"id": "mobile:2021-02-12",
}
manager.get_data()["grouping_config"] = grouping_config
event1 = manager.save(self.project.id)
event2 = Event(event1.project_id, event1.event_id, data=event1.data)
assert (
event1.get_hashes().hashes
== event2.get_hashes(load_grouping_config(grouping_config)).hashes
)
def test_write_none_tree_labels(self):
"""Write tree labels even if None"""
event = make_event(
platform="native",
exception={
"values": [
{
"type": "Hello",
"stacktrace": {
"frames": [
{
"function": "<redacted>",
},
{
"function": "<redacted>",
},
]
},
}
]
},
)
manager = EventManager(event)
manager.normalize()
manager.get_data()["grouping_config"] = {
"id": "mobile:2021-02-12",
}
event = manager.save(self.project.id)
assert event.data["hierarchical_tree_labels"] == [None]
def test_synthetic_exception_detection(self):
manager = EventManager(
make_event(
message="foo",
event_id="b" * 32,
exception={
"values": [
{
"type": "SIGABRT",
"mechanism": {"handled": False},
"stacktrace": {"frames": [{"function": "foo"}]},
}
]
},
),
project=self.project,
)
manager.normalize()
manager.get_data()["grouping_config"] = {
"id": "mobile:2021-02-12",
}
event = manager.save(self.project.id)
mechanism = event.interfaces["exception"].values[0].mechanism
assert mechanism is not None
assert mechanism.synthetic is True
assert event.title == "foo"
def test_auto_update_grouping(self):
with override_settings(SENTRY_GROUPING_AUTO_UPDATE_ENABLED=False):
# start out with legacy grouping, this should update us
self.project.update_option("sentry:grouping_config", LEGACY_GROUPING_CONFIG)
manager = EventManager(
make_event(
message="foo",
event_id="c" * 32,
),
project=self.project,
)
manager.normalize()
manager.save(self.project.id)
# No update yet
project = Project.objects.get(id=self.project.id)
assert project.get_option("sentry:grouping_config") == LEGACY_GROUPING_CONFIG
with override_settings(SENTRY_GROUPING_AUTO_UPDATE_ENABLED=1.0):
# start out with legacy grouping, this should update us
self.project.update_option("sentry:grouping_config", LEGACY_GROUPING_CONFIG)
manager = EventManager(
make_event(
message="foo",
event_id="c" * 32,
),
project=self.project,
)
manager.normalize()
with outbox_runner():
manager.save(self.project.id)
# This should have moved us back to the default grouping
project = Project.objects.get(id=self.project.id)
assert project.get_option("sentry:grouping_config") == DEFAULT_GROUPING_CONFIG
# and we should see an audit log record.
with assume_test_silo_mode_of(AuditLogEntry):
record = AuditLogEntry.objects.first()
assert record.event == audit_log.get_event_id("PROJECT_EDIT")
assert record.data["sentry:grouping_config"] == DEFAULT_GROUPING_CONFIG
assert record.data["slug"] == self.project.slug
@override_options({"performance.issues.all.problem-detection": 1.0})
@override_options({"performance.issues.n_plus_one_db.problem-creation": 1.0})
def test_perf_issue_creation(self):
with mock.patch("sentry_sdk.tracing.Span.containing_transaction"):
event = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view"))
)
data = event.data
assert event.get_event_type() == "transaction"
assert event.transaction == "/books/"
assert data["span_grouping_config"]["id"] == "default:2022-10-27"
span_hashes = [span["hash"] for span in data["spans"]]
assert span_hashes == [
"0f43fb6f6e01ca52",
"3dc5dd68b38e1730",
"424c6ae1641f0f0e",
"d5da18d7274b34a1",
"ac72fc0a4f5fe381",
"ac1468d8e11a0553",
"d8681423cab4275f",
"e853d2eb7fb9ebb0",
"6a992d5529f459a4",
"b640a0ce465fa2a4",
"a3605e201eaf6c45",
"061710eb39a66089",
"c031296784b22ea9",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
"d74ed7012596c3fb",
]
assert event.group
group = event.group
assert group is not None
assert group.title == "N+1 Query"
assert (
group.message
== "/books/ N+1 Query SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21"
)
assert group.culprit == "/books/"
assert group.get_event_type() == "transaction"
description = "SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21"
assert group.get_event_metadata() == {
"location": "/books/",
"title": "N+1 Query",
"value": description,
}
assert (
event.search_message
== "/books/ N+1 Query SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21"
)
assert group.location() == "/books/"
assert group.level == 40
assert group.issue_category == GroupCategory.PERFORMANCE
assert group.issue_type == PerformanceNPlusOneGroupType
assert event.occurrence
assert event.occurrence.evidence_display == [
IssueEvidence(
name="Offending Spans",
value="db - SELECT `books_author`.`id`, `books_author`.`name` "
"FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21",
important=True,
)
]
assert event.occurrence.evidence_data == {
"transaction_name": "/books/",
"op": "db",
"parent_span_ids": ["8dd7a5869a4f4583"],
"parent_span": "django.view - index",
"cause_span_ids": ["9179e43ae844b174"],
"offender_span_ids": [
"b8be6138369491dd",
"b2d4826e7b618f1b",
"b3fdeea42536dbf1",
"b409e78a092e642f",
"86d2ede57bbf48d4",
"8e554c84cdc9731e",
"94d6230f3f910e12",
"a210b87a2191ceb6",
"88a5ccaf25b9bd8f",
"bb32cf50fc56b296",
],
"repeating_spans": "db - SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21",
"repeating_spans_compact": "SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21",
"num_repeating_spans": "10",
}
@override_options({"performance.issues.all.problem-detection": 1.0})
@override_options({"performance.issues.n_plus_one_db.problem-creation": 1.0})
def test_perf_issue_update(self):
with mock.patch("sentry_sdk.tracing.Span.containing_transaction"):
event = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view"))
)
group = event.group
assert group is not None
assert group.issue_category == GroupCategory.PERFORMANCE
assert group.issue_type == PerformanceNPlusOneGroupType
group.data["metadata"] = {
"location": "hi",
"title": "lol",
}
group.culprit = "wat"
group.message = "nope"
group.save()
assert group.location() == "hi"
assert group.title == "lol"
with self.tasks():
self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view"))
)
# Make sure the original group is updated via buffers
group.refresh_from_db()
assert group.title == "N+1 Query"
assert group.get_event_metadata() == {
"location": "/books/",
"title": "N+1 Query",
"value": "SELECT `books_author`.`id`, `books_author`.`name` FROM `books_author` WHERE `books_author`.`id` = %s LIMIT 21",
}
assert group.location() == "/books/"
assert group.message == "nope"
assert group.culprit == "/books/"
@override_options({"performance.issues.all.problem-detection": 1.0})
@override_options({"performance.issues.n_plus_one_db.problem-creation": 1.0})
def test_error_issue_no_associate_perf_event(self):
"""Test that you can't associate a performance event with an error issue"""
with mock.patch("sentry_sdk.tracing.Span.containing_transaction"):
event = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view"))
)
assert event.group is not None
# sneakily make the group type wrong
group = event.group
assert group is not None
group.type = ErrorGroupType.type_id
group.save()
event = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view"))
)
assert event.group is None
@override_options({"performance.issues.all.problem-detection": 1.0})
@override_options({"performance.issues.n_plus_one_db.problem-creation": 1.0})
def test_perf_issue_no_associate_error_event(self):
"""Test that you can't associate an error event with a performance issue"""
with mock.patch("sentry_sdk.tracing.Span.containing_transaction"):
manager = EventManager(make_event())
manager.normalize()
event = manager.save(self.project.id)
assert len(event.groups) == 1
# sneakily make the group type wrong
group = event.group
assert group is not None
group.type = PerformanceNPlusOneGroupType.type_id
group.save()
manager = EventManager(make_event())
manager.normalize()
event = manager.save(self.project.id)
assert not event.group
@override_options({"performance.issues.all.problem-detection": 1.0})
@override_options({"performance.issues.n_plus_one_db.problem-creation": 1.0})
def test_perf_issue_creation_ignored(self):
with mock.patch("sentry_sdk.tracing.Span.containing_transaction"):
event = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view")),
noise_limit=2,
)
assert event.get_event_type() == "transaction"
assert event.group is None
@override_options({"performance.issues.all.problem-detection": 1.0})
@override_options({"performance.issues.n_plus_one_db.problem-creation": 1.0})
def test_perf_issue_creation_over_ignored_threshold(self):
with mock.patch("sentry_sdk.tracing.Span.containing_transaction"):
event_1 = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view")), noise_limit=3
)
event_2 = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view")), noise_limit=3
)
event_3 = self.create_performance_issue(
event_data=make_event(**get_event("n-plus-one-in-django-index-view")), noise_limit=3
)
assert event_1.get_event_type() == "transaction"
assert event_2.get_event_type() == "transaction"
assert event_3.get_event_type() == "transaction"
# only the third occurrence of the hash should create the group
assert event_1.group is None
assert event_2.group is None
assert event_3.group is not None
@override_options(
{
"performance.issues.slow_db_query.problem-creation": 1.0,
"performance_issue_creation_rate": 1.0,
"performance.issues.all.problem-detection": 1.0,
}
)
def test_perf_issue_slow_db_issue_is_created(self):
def attempt_to_generate_slow_db_issue() -> Event:
return self.create_performance_issue(
event_data=make_event(**get_event("slow-db-spans")),
issue_type=PerformanceSlowDBQueryGroupType,
)
# Should not create the group without the feature flag
last_event = attempt_to_generate_slow_db_issue()
assert not last_event.group
with self.feature({"organizations:performance-slow-db-issue": True}):
last_event = attempt_to_generate_slow_db_issue()
assert last_event.group
assert last_event.group.type == PerformanceSlowDBQueryGroupType.type_id
@patch("sentry.event_manager.metrics.incr")
def test_new_group_metrics_logging(self, mock_metrics_incr: MagicMock) -> None:
manager = EventManager(
make_event(platform="javascript", sdk={"name": "sentry.javascript.nextjs"})
)
manager.normalize()
manager.save(self.project.id)
mock_metrics_incr.assert_any_call(
"group.created",
skip_internal=True,
tags={
"platform": "javascript",
"sdk": "sentry.javascript.nextjs",
},
)
@patch("sentry.event_manager.metrics.incr")
def test_new_group_metrics_logging_no_platform_no_sdk(
self, mock_metrics_incr: MagicMock
) -> None:
manager = EventManager(make_event(platform=None, sdk=None))
manager.normalize()
manager.save(self.project.id)
mock_metrics_incr.assert_any_call(
"group.created",
skip_internal=True,
tags={
"platform": "other",
"sdk": "other",
},
)
@patch("sentry.event_manager.metrics.incr")
def test_new_group_metrics_logging_sdk_exist_but_null(
self, mock_metrics_incr: MagicMock
) -> None:
manager = EventManager(make_event(platform=None, sdk={"name": None}))
manager.normalize()
manager.save(self.project.id)
mock_metrics_incr.assert_any_call(
"group.created",
skip_internal=True,
tags={
"platform": "other",
"sdk": "other",
},
)
def test_new_group_metrics_logging_with_frame_mix(self) -> None:
with patch("sentry.event_manager.metrics.incr") as mock_metrics_incr:
manager = EventManager(
make_event(platform="javascript", sdk={"name": "sentry.javascript.nextjs"})
)
manager.normalize()
# IRL, `normalize_stacktraces_for_grouping` adds frame mix metadata to the event, but we
# can't mock that because it's imported inside its calling function to avoid circular imports
manager._data["metadata"] = {"in_app_frame_mix": "in-app-only"}
manager.save(self.project.id)
mock_metrics_incr.assert_any_call(
"grouping.in_app_frame_mix",
sample_rate=1.0,
tags={
"platform": "javascript",
"frame_mix": "in-app-only",
"sdk": "sentry.javascript.nextjs",
},
)
def test_new_group_metrics_logging_without_frame_mix(self) -> None:
with patch("sentry.event_manager.metrics.incr") as mock_metrics_incr:
manager = EventManager(make_event(platform="javascript"))
event = manager.save(self.project.id)
assert event.get_event_metadata().get("in_app_frame_mix") is None
metrics_logged = [call.args[0] for call in mock_metrics_incr.mock_calls]
assert "grouping.in_app_frame_mix" not in metrics_logged
@region_silo_test
class AutoAssociateCommitTest(TestCase, EventManagerTestMixin):
def setUp(self):
super().setUp()
self.repo_name = "example"
self.project = self.create_project(name="foo")
with assume_test_silo_mode_of(Integration):
self.org_integration = self.integration.add_organization(
self.project.organization, self.user
)
self.repo = self.create_repo(
project=self.project,
name=self.repo_name,
provider="integrations:github",
integration_id=self.integration.id,
)
self.repo.update(config={"name": self.repo_name})
self.create_code_mapping(
project=self.project,
repo=self.repo,
organization_integration=self.org_integration,
stack_root="/stack/root",
source_root="/source/root",
default_branch="main",
)
responses.add(
"GET",
f"https://api.github.com/repos/{self.repo_name}/commits/{LATER_COMMIT_SHA}",
json=json.loads(GET_COMMIT_EXAMPLE),
)
responses.add(
"GET",
f"https://api.github.com/repos/{self.repo_name}/commits/{EARLIER_COMMIT_SHA}",
json=json.loads(GET_PRIOR_COMMIT_EXAMPLE),
)
self.dummy_commit_sha = "a" * 40
responses.add(
responses.GET,
f"https://api.github.com/repos/{self.repo_name}/compare/{self.dummy_commit_sha}...{LATER_COMMIT_SHA}",
json=json.loads(COMPARE_COMMITS_EXAMPLE_WITH_INTERMEDIATE),
)
responses.add(
responses.GET,
f"https://api.github.com/repos/{self.repo_name}/commits?sha={LATER_COMMIT_SHA}",
json=json.loads(GET_LAST_2_COMMITS_EXAMPLE),
)
def _create_first_release_commit(self):
# Create a release
release = self.create_release(project=self.project, version="abcabcabc")
# Create a commit
commit = self.create_commit(
repo=self.repo,
key=self.dummy_commit_sha,
)
# Make a release head commit
ReleaseHeadCommit.objects.create(
organization_id=self.project.organization.id,
repository_id=self.repo.id,
release=release,
commit=commit,
)
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commits_on_sha_release_version(self, get_jwt):
with self.feature("projects:auto-associate-commits-to-release"):
self._create_first_release_commit()
# Make a new release with SHA checksum
with self.tasks():
_ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
release2 = Release.objects.get(version=LATER_COMMIT_SHA)
commit_list = list(
Commit.objects.filter(releasecommit__release=release2).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 2
assert commit_list[0].repository_id == self.repo.id
assert commit_list[0].organization_id == self.project.organization.id
assert commit_list[0].key == EARLIER_COMMIT_SHA
assert commit_list[1].repository_id == self.repo.id
assert commit_list[1].organization_id == self.project.organization.id
assert commit_list[1].key == LATER_COMMIT_SHA
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commits_first_release(self, get_jwt):
with self.feature("projects:auto-associate-commits-to-release"):
with self.tasks():
_ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
release2 = Release.objects.get(version=LATER_COMMIT_SHA)
commit_list = list(
Commit.objects.filter(releasecommit__release=release2).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 2
assert commit_list[0].repository_id == self.repo.id
assert commit_list[0].organization_id == self.project.organization.id
assert commit_list[0].key == EARLIER_COMMIT_SHA
assert commit_list[1].repository_id == self.repo.id
assert commit_list[1].organization_id == self.project.organization.id
assert commit_list[1].key == LATER_COMMIT_SHA
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commits_not_a_sha(self, get_jwt):
SHA = "not-a-sha"
with self.feature("projects:auto-associate-commits-to-release"):
with self.tasks():
_ = self.make_release_event(SHA, self.project.id)
release2 = Release.objects.get(version=SHA)
commit_list = list(
Commit.objects.filter(releasecommit__release=release2).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 0
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commit_not_found(self, get_jwt):
SHA = "b" * 40
responses.add(
"GET",
f"https://api.github.com/repos/{self.repo_name}/commits/{SHA}",
status=HTTP_404_NOT_FOUND,
)
with self.feature("projects:auto-associate-commits-to-release"):
with self.tasks():
_ = self.make_release_event(SHA, self.project.id)
release2 = Release.objects.get(version=SHA)
commit_list = list(
Commit.objects.filter(releasecommit__release=release2).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 0
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commits_release_conflict(self, get_jwt):
# Release is created but none of the commits, we should still associate commits
with self.feature("projects:auto-associate-commits-to-release"):
preexisting_release = self.create_release(
project=self.project, version=LATER_COMMIT_SHA
)
with self.tasks():
_ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
commit_releases = Release.objects.filter(version=LATER_COMMIT_SHA).all()
assert len(commit_releases) == 1
assert commit_releases[0].id == preexisting_release.id
commit_list = list(
Commit.objects.filter(releasecommit__release=preexisting_release).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 2
assert commit_list[0].repository_id == self.repo.id
assert commit_list[0].organization_id == self.project.organization.id
assert commit_list[0].key == EARLIER_COMMIT_SHA
assert commit_list[1].repository_id == self.repo.id
assert commit_list[1].organization_id == self.project.organization.id
assert commit_list[1].key == LATER_COMMIT_SHA
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commits_commit_conflict(self, get_jwt):
# A commit tied to the release is somehow created before the release itself is created.
# autoassociation should tie the existing commit to the new release
with self.feature("projects:auto-associate-commits-to-release"):
author = CommitAuthor.objects.create(
organization_id=self.organization.id,
email="[email protected]",
name="Monalisa Octocat",
)
# Values taken from commit generated from GH response fixtures
preexisting_commit = self.create_commit(
repo=self.repo,
project=self.project,
author=author,
key=EARLIER_COMMIT_SHA,
message="Fix all the bugs",
date_added=datetime(2011, 4, 14, 16, 0, 49, tzinfo=timezone.utc),
)
with self.tasks():
self.make_release_event(LATER_COMMIT_SHA, self.project.id)
new_release = Release.objects.get(version=LATER_COMMIT_SHA)
commit_list = list(
Commit.objects.filter(releasecommit__release=new_release).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 2
assert commit_list[0].id == preexisting_commit.id
assert commit_list[0].repository_id == self.repo.id
assert commit_list[0].organization_id == self.project.organization.id
assert commit_list[0].key == EARLIER_COMMIT_SHA
assert commit_list[1].repository_id == self.repo.id
assert commit_list[1].organization_id == self.project.organization.id
assert commit_list[1].key == LATER_COMMIT_SHA
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commits_feature_not_enabled(self, get_jwt):
with self.feature({"projects:auto-associate-commits-to-release": False}):
with self.tasks():
_ = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
release2 = Release.objects.get(version=LATER_COMMIT_SHA)
commit_list = list(
Commit.objects.filter(releasecommit__release=release2).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 0
@mock.patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
@responses.activate
def test_autoassign_commits_duplicate_events(self, get_jwt):
with self.feature({"projects:auto-associate-commits-to-release": True}):
with self.tasks():
event1 = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
event2 = self.make_release_event(LATER_COMMIT_SHA, self.project.id)
assert event1 != event2
assert event1.release == event2.release
releases = Release.objects.filter(version=LATER_COMMIT_SHA).all()
assert len(releases) == 1
commit_list = list(
Commit.objects.filter(releasecommit__release=releases[0]).order_by(
"releasecommit__order"
)
)
assert len(commit_list) == 2
assert commit_list[0].repository_id == self.repo.id
assert commit_list[0].organization_id == self.project.organization.id
assert commit_list[0].key == EARLIER_COMMIT_SHA
assert commit_list[1].repository_id == self.repo.id
assert commit_list[1].organization_id == self.project.organization.id
assert commit_list[1].key == LATER_COMMIT_SHA
@region_silo_test
class ReleaseIssueTest(TestCase):
def setUp(self):
self.project = self.create_project()
self.release = Release.get_or_create(self.project, "1.0")
self.environment1 = Environment.get_or_create(self.project, "prod")
self.environment2 = Environment.get_or_create(self.project, "staging")
self.timestamp = float(int(time() - 300))
def make_event(self, **kwargs):
result = {
"event_id": "a" * 32,
"message": "foo",
"timestamp": self.timestamp + 0.23,
"level": logging.ERROR,
"logger": "default",
"tags": [],
}
result.update(kwargs)
return result
def make_release_event(
self, release_version="1.0", environment_name="prod", project_id=1, **kwargs
):
event = make_event(
release=release_version, environment=environment_name, event_id=uuid.uuid1().hex
)
event.update(kwargs)
manager = EventManager(event)
with self.tasks():
event = manager.save(project_id)
return event
def convert_timestamp(self, timestamp):
date = datetime.fromtimestamp(timestamp)
date = date.replace(tzinfo=timezone.utc)
return date
def assert_release_project_environment(self, event, new_issues_count, first_seen, last_seen):
release = Release.objects.get(
organization=event.project.organization.id, version=event.get_tag("sentry:release")
)
release_project_envs = ReleaseProjectEnvironment.objects.filter(
release=release, project=event.project, environment=event.get_environment()
)
assert len(release_project_envs) == 1
release_project_env = release_project_envs[0]
assert release_project_env.new_issues_count == new_issues_count
assert release_project_env.first_seen == self.convert_timestamp(first_seen)
assert release_project_env.last_seen == self.convert_timestamp(last_seen)
def test_different_groups(self):
event1 = self.make_release_event(
release_version=self.release.version,
environment_name=self.environment1.name,
project_id=self.project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
self.assert_release_project_environment(
event=event1, new_issues_count=1, last_seen=self.timestamp, first_seen=self.timestamp
)
event2 = self.make_release_event(
release_version=self.release.version,
environment_name=self.environment1.name,
project_id=self.project.id,
checksum="b" * 32,
timestamp=self.timestamp + 100,
)
self.assert_release_project_environment(
event=event2,
new_issues_count=2,
last_seen=self.timestamp + 100,
first_seen=self.timestamp,
)
def test_same_group(self):
event1 = self.make_release_event(
release_version=self.release.version,
environment_name=self.environment1.name,
project_id=self.project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
self.assert_release_project_environment(
event=event1, new_issues_count=1, last_seen=self.timestamp, first_seen=self.timestamp
)
event2 = self.make_release_event(
release_version=self.release.version,
environment_name=self.environment1.name,
project_id=self.project.id,
checksum="a" * 32,
timestamp=self.timestamp + 100,
)
self.assert_release_project_environment(
event=event2,
new_issues_count=1,
last_seen=self.timestamp + 100,
first_seen=self.timestamp,
)
def test_same_group_different_environment(self):
event1 = self.make_release_event(
release_version=self.release.version,
environment_name=self.environment1.name,
project_id=self.project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
self.assert_release_project_environment(
event=event1, new_issues_count=1, last_seen=self.timestamp, first_seen=self.timestamp
)
event2 = self.make_release_event(
release_version=self.release.version,
environment_name=self.environment2.name,
project_id=self.project.id,
checksum="a" * 32,
timestamp=self.timestamp + 100,
)
self.assert_release_project_environment(
event=event1, new_issues_count=1, last_seen=self.timestamp, first_seen=self.timestamp
)
self.assert_release_project_environment(
event=event2,
new_issues_count=1,
last_seen=self.timestamp + 100,
first_seen=self.timestamp + 100,
)
@region_silo_test
@apply_feature_flag_on_cls("organizations:dynamic-sampling")
class DSLatestReleaseBoostTest(TestCase):
def setUp(self):
self.environment1 = Environment.get_or_create(self.project, "prod")
self.environment2 = Environment.get_or_create(self.project, "staging")
self.timestamp = float(int(time() - 300))
self.redis_client = get_redis_client_for_ds()
def make_transaction_event(self, **kwargs):
result = {
"transaction": "wait",
"contexts": {
"trace": {
"parent_span_id": "bce14471e0e9654d",
"op": "foobar",
"trace_id": "a0fa8803753e40fd8124b21eeb2986b5",
"span_id": "bf5be759039ede9a",
}
},
"spans": [],
"timestamp": self.timestamp + 0.23,
"start_timestamp": "2019-06-14T14:01:40Z",
"type": "transaction",
}
result.update(kwargs)
return result
def make_release_transaction(
self, release_version="1.0", environment_name="prod", project_id=1, **kwargs
):
transaction = (
self.make_transaction_event(
release=release_version, environment=environment_name, event_id=uuid.uuid1().hex
)
if environment_name is not None
else self.make_transaction_event(release=release_version, event_id=uuid.uuid1().hex)
)
transaction.update(kwargs)
manager = EventManager(transaction)
with self.tasks():
event = manager.save(project_id)
return event
@freeze_time("2022-11-03 10:00:00")
def test_boost_release_with_non_observed_release(self):
ts = datetime.now(timezone.utc).timestamp()
project = self.create_project(platform="python")
release_1 = Release.get_or_create(project=project, version="1.0", date_added=datetime.now())
release_2 = Release.get_or_create(
project=project, version="2.0", date_added=datetime.now() + timedelta(hours=1)
)
release_3 = Release.get_or_create(
project=project, version="3.0", date_added=datetime.now() + timedelta(hours=2)
)
for release, environment in (
(release_1, None),
(release_2, "prod"),
(release_3, "dev"),
):
self.make_release_transaction(
release_version=release.version,
environment_name=environment,
project_id=project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
env_postfix = f":e:{environment}" if environment is not None else ""
assert self.redis_client.get(f"ds::p:{project.id}:r:{release.id}{env_postfix}") == "1"
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release_1.id}": str(ts),
f"ds::r:{release_2.id}:e:prod": str(ts),
f"ds::r:{release_3.id}:e:dev": str(ts),
}
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release_1.id,
timestamp=ts,
environment=None,
cache_key=f"ds::r:{release_1.id}",
version=release_1.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release_2.id,
timestamp=ts,
environment="prod",
cache_key=f"ds::r:{release_2.id}:e:prod",
version=release_2.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release_3.id,
timestamp=ts,
environment="dev",
cache_key=f"ds::r:{release_3.id}:e:dev",
version=release_3.version,
platform=Platform(project.platform),
),
]
@freeze_time("2022-11-03 10:00:00")
def test_boost_release_boosts_only_latest_release(self):
ts = datetime.now(timezone.utc).timestamp()
project = self.create_project(platform="python")
release_1 = Release.get_or_create(project=project, version="1.0", date_added=datetime.now())
release_2 = Release.get_or_create(
project=project,
version="2.0",
# We must make sure the new release_2.date_added > release_1.date_added.
date_added=datetime.now() + timedelta(hours=1),
)
# We add a transaction for latest release release_2.
self.make_release_transaction(
release_version=release_2.version,
environment_name=self.environment1.name,
project_id=project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
# We add a transaction for release_1 which is not anymore the latest release, therefore we should skip this.
self.make_release_transaction(
release_version=release_1.version,
environment_name=self.environment1.name,
project_id=project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
assert (
self.redis_client.get(f"ds::p:{project.id}:r:{release_2.id}:e:{self.environment1.name}")
== "1"
)
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release_2.id}:e:{self.environment1.name}": str(ts),
}
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release_2.id,
timestamp=ts,
environment=self.environment1.name,
cache_key=f"ds::r:{release_2.id}:e:{self.environment1.name}",
version=release_2.version,
platform=Platform(project.platform),
)
]
@freeze_time("2022-11-03 10:00:00")
def test_boost_release_with_observed_release_and_different_environment(self):
project = self.create_project(platform="python")
release = Release.get_or_create(project=project, version="1.0", date_added=datetime.now())
self.make_release_transaction(
release_version=release.version,
environment_name=self.environment1.name,
project_id=project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
ts_1 = time()
assert (
self.redis_client.get(f"ds::p:{project.id}:r:{release.id}:e:{self.environment1.name}")
== "1"
)
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release.id}:e:{self.environment1.name}": str(ts_1)
}
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release.id,
timestamp=ts_1,
environment=self.environment1.name,
cache_key=f"ds::r:{release.id}:e:{self.environment1.name}",
version=release.version,
platform=Platform(project.platform),
)
]
# We simulate that a new transaction with same release but with a different environment value comes after
# 30 minutes to show that we expect the entry for that release-env to be added to the boosted releases.
with freeze_time("2022-11-03 10:30:00"):
self.make_release_transaction(
release_version=release.version,
environment_name=self.environment2.name,
project_id=project.id,
checksum="b" * 32,
timestamp=self.timestamp,
)
ts_2 = time()
assert (
self.redis_client.get(
f"ds::p:{project.id}:r:{release.id}:e:{self.environment2.name}"
)
== "1"
)
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release.id}:e:{self.environment1.name}": str(ts_1),
f"ds::r:{release.id}:e:{self.environment2.name}": str(ts_2),
}
assert ProjectBoostedReleases(
project_id=project.id
).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release.id,
timestamp=ts_1,
environment=self.environment1.name,
cache_key=f"ds::r:{release.id}:e:{self.environment1.name}",
version=release.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release.id,
timestamp=ts_2,
environment=self.environment2.name,
cache_key=f"ds::r:{release.id}:e:{self.environment2.name}",
version=release.version,
platform=Platform(project.platform),
),
]
# We also test the case in which no environment is set, which can be the case as per
# https://docs.sentry.io/platforms/javascript/configuration/options/#environment.
with freeze_time("2022-11-03 11:00:00"):
self.make_release_transaction(
release_version=release.version,
environment_name=None,
project_id=project.id,
checksum="b" * 32,
timestamp=self.timestamp,
)
ts_3 = time()
assert self.redis_client.get(f"ds::p:{project.id}:r:{release.id}") == "1"
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release.id}:e:{self.environment1.name}": str(ts_1),
f"ds::r:{release.id}:e:{self.environment2.name}": str(ts_2),
f"ds::r:{release.id}": str(ts_3),
}
assert ProjectBoostedReleases(
project_id=project.id
).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release.id,
timestamp=ts_1,
environment=self.environment1.name,
cache_key=f"ds::r:{release.id}:e:{self.environment1.name}",
version=release.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release.id,
timestamp=ts_2,
environment=self.environment2.name,
cache_key=f"ds::r:{release.id}:e:{self.environment2.name}",
version=release.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release.id,
timestamp=ts_3,
environment=None,
cache_key=f"ds::r:{release.id}",
version=release.version,
platform=Platform(project.platform),
),
]
@freeze_time("2022-11-03 10:00:00")
def test_release_not_boosted_with_observed_release_and_same_environment(self):
project = self.create_project(platform="python")
release = Release.get_or_create(project=project, version="1.0", date_added=datetime.now())
for environment in (self.environment1.name, self.environment2.name):
self.redis_client.set(
f"ds::p:{project.id}:r:{release.id}:e:{environment}", 1, 60 * 60 * 24
)
self.make_release_transaction(
release_version=release.version,
environment_name=environment,
project_id=project.id,
checksum="b" * 32,
timestamp=self.timestamp,
)
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {}
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == []
@freeze_time("2022-11-03 10:00:00")
def test_release_not_boosted_with_deleted_release_after_event_received(self):
ts = datetime.now(timezone.utc).timestamp()
project = self.create_project(platform="python")
release_1 = Release.get_or_create(project=project, version="1.0", date_added=datetime.now())
release_2 = Release.get_or_create(
project=project, version="2.0", date_added=datetime.now() + timedelta(hours=1)
)
self.make_release_transaction(
release_version=release_1.version,
environment_name=None,
project_id=project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
assert self.redis_client.get(f"ds::p:{project.id}:r:{release_1.id}") == "1"
self.make_release_transaction(
release_version=release_2.version,
environment_name=None,
project_id=project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
assert self.redis_client.get(f"ds::p:{project.id}:r:{release_2.id}") == "1"
# We simulate that the release_2 is deleted after the boost has been inserted.
release_2_id = release_2.id
release_2.delete()
# We expect the boosted release to be kept in Redis, if not queried by the ProjectBoostedReleases.
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release_1.id}": str(ts),
f"ds::r:{release_2_id}": str(ts),
}
# We expect to not see the release 2 because it will not be in the database anymore, thus we mark it as
# expired.
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release_1.id,
timestamp=ts,
environment=None,
cache_key=f"ds::r:{release_1.id}",
version=release_1.version,
platform=Platform(project.platform),
),
]
@freeze_time("2022-11-03 10:00:00")
def test_get_boosted_releases_with_old_and_new_cache_keys(self):
ts = datetime.now(timezone.utc).timestamp()
project = self.create_project(platform="python")
# Old cache key
release_1 = Release.get_or_create(project=project, version="1.0", date_added=datetime.now())
self.redis_client.hset(
f"ds::p:{project.id}:boosted_releases",
f"{release_1.id}",
ts,
)
# New cache key
release_2 = Release.get_or_create(
project=project, version="2.0", date_added=datetime.now() + timedelta(hours=1)
)
self.redis_client.hset(
f"ds::p:{project.id}:boosted_releases",
f"ds::r:{release_2.id}",
ts,
)
self.redis_client.hset(
f"ds::p:{project.id}:boosted_releases",
f"ds::r:{release_2.id}:e:{self.environment1.name}",
ts,
)
self.redis_client.hset(
f"ds::p:{project.id}:boosted_releases",
f"ds::r:{release_2.id}:e:{self.environment2.name}",
ts,
)
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release_1.id,
timestamp=ts,
environment=None,
# This item has the old cache key.
cache_key=f"{release_1.id}",
version=release_1.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release_2.id,
timestamp=ts,
environment=None,
cache_key=f"ds::r:{release_2.id}",
version=release_2.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release_2.id,
timestamp=ts,
environment=self.environment1.name,
cache_key=f"ds::r:{release_2.id}:e:{self.environment1.name}",
version=release_2.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release_2.id,
timestamp=ts,
environment=self.environment2.name,
cache_key=f"ds::r:{release_2.id}:e:{self.environment2.name}",
version=release_2.version,
platform=Platform(project.platform),
),
]
@freeze_time("2022-11-03 10:00:00")
def test_expired_boosted_releases_are_removed(self):
ts = datetime.now(timezone.utc).timestamp()
# We want to test with multiple platforms.
for platform in ("python", "java", None):
project = self.create_project(platform=platform)
for index, (release_version, environment) in enumerate(
(
(f"1.0-{platform}", self.environment1.name),
(f"2.0-{platform}", self.environment2.name),
)
):
release = Release.get_or_create(
project=project,
version=release_version,
date_added=datetime.now() + timedelta(hours=index),
)
self.redis_client.set(
f"ds::p:{project.id}:r:{release.id}:e:{environment}", 1, 60 * 60 * 24
)
self.redis_client.hset(
f"ds::p:{project.id}:boosted_releases",
f"ds::r:{release.id}:e:{environment}",
# We set the creation time in order to expire it by 1 second.
ts - Platform(platform).time_to_adoption - 1,
)
# We add a new boosted release that is not expired.
release_3 = Release.get_or_create(
project=project,
version=f"3.0-{platform}",
date_added=datetime.now() + timedelta(hours=2),
)
self.make_release_transaction(
release_version=release_3.version,
environment_name=self.environment1.name,
project_id=project.id,
checksum="b" * 32,
timestamp=self.timestamp,
)
assert (
self.redis_client.get(
f"ds::p:{project.id}:r:{release_3.id}:e:{self.environment1.name}"
)
== "1"
)
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release_3.id}:e:{self.environment1.name}": str(ts)
}
assert ProjectBoostedReleases(
project_id=project.id
).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release_3.id,
timestamp=ts,
environment=self.environment1.name,
cache_key=f"ds::r:{release_3.id}:e:{self.environment1.name}",
version=release_3.version,
platform=Platform(project.platform),
)
]
@mock.patch("sentry.event_manager.schedule_invalidate_project_config")
def test_project_config_invalidation_is_triggered_when_new_release_is_observed(
self, mocked_invalidate
):
self.make_release_transaction(
release_version=self.release.version,
environment_name=self.environment1.name,
project_id=self.project.id,
checksum="a" * 32,
timestamp=self.timestamp,
)
assert any(
o.kwargs["trigger"] == "dynamic_sampling:boost_release"
for o in mocked_invalidate.mock_calls
)
@freeze_time("2022-11-03 10:00:00")
@mock.patch("sentry.dynamic_sampling.rules.helpers.latest_releases.BOOSTED_RELEASES_LIMIT", 2)
def test_least_recently_boosted_release_is_removed_if_limit_is_exceeded(self):
ts = datetime.now(timezone.utc).timestamp()
project = self.create_project(platform="python")
release_1 = Release.get_or_create(
project=project,
version="1.0",
date_added=datetime.now(),
)
release_2 = Release.get_or_create(
project=project,
version="2.0",
date_added=datetime.now() + timedelta(hours=1),
)
# We boost with increasing timestamps, so that we know that the smallest will be evicted.
for release, boost_time in ((release_1, ts - 2), (release_2, ts - 1)):
self.redis_client.set(
f"ds::p:{project.id}:r:{release.id}",
1,
60 * 60 * 24,
)
self.redis_client.hset(
f"ds::p:{project.id}:boosted_releases",
f"ds::r:{release.id}",
boost_time,
)
release_3 = Release.get_or_create(
project=project,
version="3.0",
date_added=datetime.now() + timedelta(hours=2),
)
self.make_release_transaction(
release_version=release_3.version,
environment_name=self.environment1.name,
project_id=project.id,
checksum="b" * 32,
timestamp=self.timestamp,
)
assert (
self.redis_client.get(f"ds::p:{project.id}:r:{release_3.id}:e:{self.environment1.name}")
== "1"
)
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release_2.id}": str(ts - 1),
f"ds::r:{release_3.id}:e:{self.environment1.name}": str(ts),
}
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release_2.id,
timestamp=ts - 1,
environment=None,
cache_key=f"ds::r:{release_2.id}",
version=release_2.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release_3.id,
timestamp=ts,
environment=self.environment1.name,
cache_key=f"ds::r:{release_3.id}:e:{self.environment1.name}",
version=release_3.version,
platform=Platform(project.platform),
),
]
@freeze_time()
@mock.patch("sentry.dynamic_sampling.rules.helpers.latest_releases.BOOSTED_RELEASES_LIMIT", 2)
def test_removed_boost_not_added_again_if_limit_is_exceeded(self):
ts = datetime.now(timezone.utc).timestamp()
project = self.create_project(platform="python")
release_1 = Release.get_or_create(project=project, version="1.0", date_added=datetime.now())
# We want to test that if we have the same release, but we send different environments that go over the
# limit, and we evict an environment, but then we send a transaction with the evicted environment.
#
# As an example suppose the following history of transactions received in the form (release, env):
# (1, production) -> (1, staging) -> (1, None) -> (1, production)
#
# Once we receive the first two, we have reached maximum capacity. Then we receive (1, None) and evict boost
# for (1, production) which results in the following boosts (1, staging), (1, None). After that we receive
# (1, production) again but in this case we don't want to remove (1, staging) because we will end up in an
# infinite loop. Instead, we expect to mark (1, production) as observed and only un-observe it if it does
# not receive transactions within the next 24 hours.
environments_sequence = [
self.environment1.name,
self.environment2.name,
None,
self.environment1.name,
]
for environment in environments_sequence:
self.make_release_transaction(
release_version=release_1.version,
environment_name=environment,
project_id=project.id,
checksum="b" * 32,
timestamp=self.timestamp,
)
# We assert that all environments have been observed.
assert (
self.redis_client.get(f"ds::p:{project.id}:r:{release_1.id}:e:{self.environment1.name}")
== "1"
)
assert (
self.redis_client.get(f"ds::p:{project.id}:r:{release_1.id}:e:{self.environment2.name}")
== "1"
)
assert self.redis_client.get(f"ds::p:{project.id}:r:{release_1.id}") == "1"
# We assert that only the last 2 unseen (release, env) pairs are boosted.
assert self.redis_client.hgetall(f"ds::p:{project.id}:boosted_releases") == {
f"ds::r:{release_1.id}:e:{self.environment2.name}": str(ts),
f"ds::r:{release_1.id}": str(ts),
}
assert ProjectBoostedReleases(project_id=project.id).get_extended_boosted_releases() == [
ExtendedBoostedRelease(
id=release_1.id,
timestamp=ts,
environment=self.environment2.name,
cache_key=f"ds::r:{release_1.id}:e:{self.environment2.name}",
version=release_1.version,
platform=Platform(project.platform),
),
ExtendedBoostedRelease(
id=release_1.id,
timestamp=ts,
environment=None,
cache_key=f"ds::r:{release_1.id}",
version=release_1.version,
platform=Platform(project.platform),
),
]
class TestSaveGroupHashAndGroup(TransactionTestCase):
def test(self):
perf_data = load_data("transaction-n-plus-one", timestamp=before_now(minutes=10))
event = _get_event_instance(perf_data, project_id=self.project.id)
group_hash = "some_group"
group, created = _save_grouphash_and_group(self.project, event, group_hash)
assert created
group_2, created = _save_grouphash_and_group(self.project, event, group_hash)
assert group.id == group_2.id
assert not created
assert Group.objects.filter(grouphash__hash=group_hash).count() == 1
group_3, created = _save_grouphash_and_group(self.project, event, "new_hash")
assert created
assert group_2.id != group_3.id
assert Group.objects.filter(grouphash__hash=group_hash).count() == 1
example_transaction_event = {
"type": "transaction",
"timestamp": datetime.now().isoformat(),
"start_timestamp": (datetime.now() - timedelta(seconds=1)).isoformat(),
"spans": [],
"contexts": {
"trace": {
"parent_span_id": "8988cec7cc0779c1",
"type": "trace",
"op": "foobar",
"trace_id": "a7d67cf796774551a95be6543cacd459",
"span_id": "babaae0d4b7512d9",
"status": "ok",
}
},
}
example_error_event = {
"event_id": "80e3496eff734ab0ac993167aaa0d1cd",
"release": "5.222.5",
"type": "error",
"level": "fatal",
"platform": "cocoa",
"tags": {"level": "fatal"},
"environment": "test-app",
"sdk": {
"name": "sentry.cocoa",
"version": "8.2.0",
"integrations": [
"Crash",
"PerformanceTracking",
"MetricKit",
"WatchdogTerminationTracking",
"ViewHierarchy",
"NetworkTracking",
"ANRTracking",
"AutoBreadcrumbTracking",
"FramesTracking",
"AppStartTracking",
"Screenshot",
"FileIOTracking",
"UIEventTracking",
"AutoSessionTracking",
"CoreDataTracking",
"PreWarmedAppStartTracing",
],
},
"user": {
"id": "803F5C87-0F8B-41C7-8499-27BD71A92738",
"ip_address": "192.168.0.1",
"geo": {"country_code": "US", "region": "United States"},
},
"logger": "my.logger.name",
}
@pytest.mark.parametrize(
"event_data,expected_type",
[
pytest.param(
example_transaction_event,
"transactions",
id="transactions",
),
pytest.param(
example_error_event,
"errors",
id="errors",
),
],
)
@django_db_all
def test_cogs_event_manager(default_project, event_data, expected_type):
storage: MemoryMessageStorage[KafkaPayload] = MemoryMessageStorage()
broker = LocalBroker(storage)
topic = Topic("shared-resources-usage")
broker.create_topic(topic, 1)
producer = broker.get_producer()
set("shared_resources_accounting_enabled", [settings.COGS_EVENT_STORE_LABEL])
accountant.init_backend(producer)
raw_event = make_event(**event_data)
manager = EventManager(raw_event)
manager.normalize()
normalized_data = dict(manager.get_data())
_ = manager.save(default_project)
expected_len = len(json.dumps(normalized_data))
accountant._shutdown()
accountant.reset_backend()
msg1 = broker.consume(Partition(topic, 0), 0)
assert msg1 is not None
payload = msg1.payload
assert payload is not None
formatted = json.loads(payload.value.decode("utf-8"))
assert formatted["shared_resource_id"] == settings.COGS_EVENT_STORE_LABEL
assert formatted["app_feature"] == expected_type
assert formatted["usage_unit"] == "bytes"
# We cannot assert for exact length because manager save method adds some extra fields. So we
# assert that the length is at least greater than the expected length.
assert formatted["amount"] >= expected_len