Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(release-health): Sessions should not be default #67353

Merged
merged 16 commits into from Apr 3, 2024
6 changes: 2 additions & 4 deletions src/sentry/conf/server.py
Expand Up @@ -2332,13 +2332,11 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
SENTRY_METRICS_INDEXER_ENABLE_SLICED_PRODUCER = False

# Release Health
SENTRY_RELEASE_HEALTH = "sentry.release_health.sessions.SessionsReleaseHealthBackend"
SENTRY_RELEASE_HEALTH = "sentry.release_health.metrics.MetricsReleaseHealthBackend"
SENTRY_RELEASE_HEALTH_OPTIONS: dict[str, Any] = {}

# Release Monitor
SENTRY_RELEASE_MONITOR = (
"sentry.release_health.release_monitor.sessions.SessionReleaseMonitorBackend"
)
SENTRY_RELEASE_MONITOR = "sentry.release_health.release_monitor.metrics.MetricReleaseMonitorBackend"
Comment on lines -2335 to +2339
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this cause data loss in self-hosted? I don't remember if self-hosted ever had release health but if they did, they will lose access to that data now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not sure we ever put a proper migration in place for self hosted. We should make sure that self hosted keeps using the old backend until we do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No this won't, self-hosted has already cut over to use metrics release health.
https://github.com/getsentry/self-hosted/blob/b3d3ce06da1661eca62a5d1fd5112810d6bbd117/sentry/sentry.conf.example.py#L195

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we actually backfill release health from sessions over to metrics? lgtm if so

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We implemented a period where we dual wrote to sessions/metrics datasets. Then, after 3 months(~90 days), we cut over to reading from the metrics dataset

Copy link
Member

@wedamija wedamija Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does that work in self hosted? What if they come from a version that had sessions, and come straight to latest release using metric. How do you enforce a 90 day dual write there?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately we weren't able to provide a way to smoothly accommodate that scenario. We gave self-hosted users as many warnings as we could through documentation and release notes months in advance before cutting over the metrics.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I don't think this is a good way to handle situations like this going forward. We could have provided a dual write backend that logs a date, dual writes, and starts reading from sessions going forward. Another option could have been to write a backfill that is only applied in self hosted. We really shouldn't be making self hosted a second class citizen like this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree with you there. There are definitely ways that this could've gone better but it also feels like a part of the broader issue that teams are shipping features without keeping self-hosted in mind.

cc @chadwhitacre

SENTRY_RELEASE_MONITOR_OPTIONS: dict[str, Any] = {}

# Render charts on the backend. This uses the Chartcuterie external service.
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/release_health/metrics.py
Expand Up @@ -5,7 +5,7 @@
from typing import Any, Literal, TypeVar

from snuba_sdk import Column, Condition, Direction, Op
from snuba_sdk.expressions import Granularity, Limit
from snuba_sdk.expressions import Granularity, Limit, Offset

from sentry.models.environment import Environment
from sentry.models.project import Project
Expand Down Expand Up @@ -1716,6 +1716,7 @@ def get_project_releases_by_stability(
orderby=orderby,
groupby=groupby,
granularity=Granularity(LEGACY_SESSIONS_DEFAULT_ROLLUP),
offset=Offset(offset) if offset is not None else None,
limit=Limit(limit) if limit is not None else None,
include_series=False,
include_totals=True,
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/testutils/pytest/metrics.py
Expand Up @@ -66,7 +66,7 @@ def new_build_results(*args, **kwargs):
is_performance_metrics = False
is_metrics = False
if not isinstance(query, MetricsQuery) and not isinstance(query.match, Join):
is_performance_metrics = query.match.name.startswith("generic_metrics")
is_performance_metrics = query.match.name.startswith("generic")
is_metrics = "metrics" in query.match.name

if is_performance_metrics:
Expand All @@ -85,7 +85,7 @@ def new_create_snql_in_snuba(subscription, snuba_query, snql_query, entity_subsc
is_performance_metrics = False
is_metrics = False
if isinstance(query.match, Entity):
is_performance_metrics = query.match.name.startswith("generic_metrics")
is_performance_metrics = query.match.name.startswith("generic")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the best fix but it was the easiest for the scope of this PR. We might want in the future to improve this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, how does this change fix the tests?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The acceptance tests were running into some endpoints which were querying generic metrics but via the entity key generic_org_metrics_counters which is still on generic metrics but failed the generic_metrics check. For this reason I relaxed the condition but a better solution would be to have some sort of mapping or inferring the use case id from the query and marking it as performance if it's not sessions.

is_metrics = "metrics" in query.match.name

if is_performance_metrics:
Expand Down
3 changes: 3 additions & 0 deletions tests/acceptance/test_organization_dashboards.py
Expand Up @@ -33,6 +33,9 @@
EDIT_FEATURE = ["organizations:dashboards-edit"]


pytestmark = pytest.mark.sentry_metrics


@no_silo_test
class OrganizationDashboardsAcceptanceTest(AcceptanceTestCase):
def setUp(self):
Expand Down
3 changes: 3 additions & 0 deletions tests/acceptance/test_organization_switch.py
@@ -1,10 +1,13 @@
import pytest
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By

from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase
from sentry.testutils.silo import no_silo_test
from sentry.utils.retries import TimedRetryPolicy

pytestmark = pytest.mark.sentry_metrics


@no_silo_test
class OrganizationSwitchTest(AcceptanceTestCase, SnubaTestCase):
Expand Down
@@ -1,11 +1,14 @@
from datetime import UTC, datetime

import pytest
from django.test.client import RequestFactory
from django.urls import reverse

from fixtures.apidocs_test_case import APIDocsTestCase
from sentry.models.release import Release

pytestmark = pytest.mark.sentry_metrics


class OrganizationReleasesDocsTest(APIDocsTestCase):
def setUp(self):
Expand Down
@@ -1,9 +1,12 @@
import pytest
from django.test.client import RequestFactory
from django.urls import reverse

from fixtures.apidocs_test_case import APIDocsTestCase
from sentry.testutils.cases import SnubaTestCase

pytestmark = pytest.mark.sentry_metrics


class OrganizationSessionsDocsTest(APIDocsTestCase, SnubaTestCase):
def setUp(self):
Expand Down
Expand Up @@ -2,6 +2,7 @@
from datetime import UTC, datetime, timedelta
from unittest.mock import patch

import pytest
from django.urls import reverse

from sentry.api.endpoints.organization_release_details import OrganizationReleaseSerializer
Expand All @@ -24,7 +25,7 @@
from sentry.types.activity import ActivityType
from sentry.utils.security.orgauthtoken_token import generate_token, hash_token

pytestmark = [requires_snuba]
pytestmark = [pytest.mark.sentry_metrics, requires_snuba]


class ReleaseDetailsTest(APITestCase):
Expand Down
7 changes: 4 additions & 3 deletions tests/sentry/api/endpoints/test_organization_releases.py
Expand Up @@ -3,6 +3,7 @@
from functools import cached_property
from unittest.mock import patch

import pytest
from django.urls import reverse
from django.utils import timezone

Expand Down Expand Up @@ -36,9 +37,9 @@
from sentry.silo import SiloMode
from sentry.testutils.cases import (
APITestCase,
BaseMetricsTestCase,
ReleaseCommitPatchTest,
SetRefsTestCase,
SnubaTestCase,
TestCase,
)
from sentry.testutils.outbox import outbox_runner
Expand All @@ -47,10 +48,10 @@
from sentry.types.activity import ActivityType
from sentry.utils.security.orgauthtoken_token import generate_token, hash_token

pytestmark = [requires_snuba]
pytestmark = [requires_snuba, pytest.mark.sentry_metrics]


class OrganizationReleaseListTest(APITestCase, SnubaTestCase):
class OrganizationReleaseListTest(APITestCase, BaseMetricsTestCase):
endpoint = "sentry-api-0-organization-releases"

def assert_expected_versions(self, response, expected):
Expand Down
3 changes: 2 additions & 1 deletion tests/sentry/api/endpoints/test_project_release_details.py
@@ -1,6 +1,7 @@
import unittest
from datetime import UTC, datetime

import pytest
from django.urls import reverse

from sentry.api.serializers.rest_framework.release import ReleaseSerializer
Expand All @@ -16,7 +17,7 @@
from sentry.types.activity import ActivityType
from sentry.utils.security.orgauthtoken_token import generate_token, hash_token

pytestmark = [requires_snuba]
pytestmark = [pytest.mark.sentry_metrics, requires_snuba]


class ReleaseDetailsTest(APITestCase):
Expand Down
3 changes: 2 additions & 1 deletion tests/sentry/api/endpoints/test_project_release_stats.py
@@ -1,12 +1,13 @@
from datetime import UTC, datetime

import pytest
from django.urls import reverse

from sentry.models.release import Release
from sentry.testutils.cases import APITestCase
from sentry.testutils.skips import requires_snuba

pytestmark = [requires_snuba]
pytestmark = [requires_snuba, pytest.mark.sentry_metrics]


class ProjectReleaseStatsTest(APITestCase):
Expand Down
2 changes: 1 addition & 1 deletion tests/sentry/integrations/slack/test_unfurl.py
Expand Up @@ -21,7 +21,7 @@
from sentry.testutils.helpers.features import with_feature
from sentry.testutils.skips import requires_snuba

pytestmark = [requires_snuba]
pytestmark = [requires_snuba, pytest.mark.sentry_metrics]

INTERVAL_COUNT = 300
INTERVALS_PER_DAY = int(60 * 60 * 24 / INTERVAL_COUNT)
Expand Down
21 changes: 0 additions & 21 deletions tests/sentry/release_health/test_metrics_sessions_v2.py
Expand Up @@ -61,27 +61,6 @@ def get_sessions_data(self, groupby: list[str], interval):
assert response.status_code == 200
return response.data

def test_sessions_metrics_with_metrics_only_field(self):
"""
Tests whether the request of a metrics-only field forwarded to the SessionsReleaseHealthBackend
is handled with an empty response.

This test is designed to show an edge-case that can happen in case the duplexer makes the wrong
decision with respect to which backend to choose for satisfying the query.
"""
response = self.do_request(
{
"organization_slug": [self.organization1],
"project": [self.project1.id],
"field": ["crash_free_rate(session)"],
"groupBy": [],
"interval": "1d",
}
)

assert len(response.data["groups"]) == 0
assert response.status_code == 200


@pytest.mark.parametrize(
"input, expected_output, expected_status_filter",
Expand Down
4 changes: 3 additions & 1 deletion tests/sentry/rules/filters/test_latest_release.py
@@ -1,13 +1,15 @@
from datetime import UTC, datetime

import pytest

from sentry.models.release import Release
from sentry.models.rule import Rule
from sentry.rules.filters.latest_release import LatestReleaseFilter, get_project_release_cache_key
from sentry.testutils.cases import RuleTestCase
from sentry.testutils.skips import requires_snuba
from sentry.utils.cache import cache

pytestmark = [requires_snuba]
pytestmark = [requires_snuba, pytest.mark.sentry_metrics]


class LatestReleaseFilterTest(RuleTestCase):
Expand Down
5 changes: 4 additions & 1 deletion tests/snuba/api/endpoints/test_organization_sessions.py
Expand Up @@ -9,6 +9,7 @@
from sentry import release_health
from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment
from sentry.release_health.metrics import MetricsReleaseHealthBackend
from sentry.release_health.sessions import SessionsReleaseHealthBackend
from sentry.snuba.metrics import to_intervals
from sentry.testutils.cases import APITestCase, BaseMetricsTestCase, SnubaTestCase
from sentry.testutils.helpers.datetime import freeze_time
Expand Down Expand Up @@ -86,6 +87,8 @@ def adjust_end(end: datetime.datetime, interval: int) -> datetime.datetime:
return end


# TODO: why is this not patching the backend correctly?
@patch("sentry.release_health.backend", SessionsReleaseHealthBackend())
class OrganizationSessionsEndpointTest(APITestCase, SnubaTestCase):
def setUp(self):
super().setUp()
Expand Down Expand Up @@ -1313,7 +1316,7 @@ def test_release_stage_filter(self):

@patch("sentry.release_health.backend", MetricsReleaseHealthBackend())
class OrganizationSessionsEndpointMetricsTest(
BaseMetricsTestCase, OrganizationSessionsEndpointTest
OrganizationSessionsEndpointTest, BaseMetricsTestCase
):
"""Repeat all tests with metrics backend"""

Expand Down
2 changes: 2 additions & 0 deletions tests/snuba/api/serializers/test_group_stream.py
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
from unittest import mock

import pytest
from django.utils import timezone

from sentry.api.event_search import SearchFilter, SearchKey, SearchValue
Expand Down Expand Up @@ -54,6 +55,7 @@ def test_environment(self):
for args, kwargs in get_range.call_args_list:
assert kwargs["environment_ids"] is None

@pytest.mark.xfail(reason="Does not work with the metrics release health backend")
def test_session_count(self):
group = self.group
organization_id = group.project.organization_id
Expand Down
15 changes: 10 additions & 5 deletions tests/snuba/rules/conditions/test_event_frequency.py
Expand Up @@ -15,12 +15,17 @@
EventUniqueUserFrequencyCondition,
)
from sentry.testutils.abstract import Abstract
from sentry.testutils.cases import PerformanceIssueTestCase, RuleTestCase, SnubaTestCase
from sentry.testutils.cases import (
BaseMetricsTestCase,
PerformanceIssueTestCase,
RuleTestCase,
SnubaTestCase,
)
from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format
from sentry.testutils.skips import requires_snuba
from sentry.utils.samples import load_data

pytestmark = [requires_snuba]
pytestmark = [pytest.mark.sentry_metrics, requires_snuba]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any attempt to access metrics in tests seems to fail without it



class ErrorEventMixin(SnubaTestCase):
Expand Down Expand Up @@ -281,7 +286,7 @@ def increment(self, event, count, environment=None, timestamp=None):
)


class EventFrequencyPercentConditionTestCase(SnubaTestCase, RuleTestCase):
class EventFrequencyPercentConditionTestCase(BaseMetricsTestCase, RuleTestCase):
__test__ = Abstract(__module__, __qualname__)

rule_cls = EventFrequencyPercentCondition
Expand All @@ -306,7 +311,7 @@ def make_session(i):
duration=None,
errors=0,
# The line below is crucial to spread sessions throughout the time period.
started=received - i,
started=received - i - 1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had to do this since the new metrics based implementation has the end interval as non inclusive so we want data to fit within the interval because we use interval: x in tests which is going back x seconds from the current time.

received=received,
)

Expand Down Expand Up @@ -422,7 +427,7 @@ def test_comparison(self):

# Test data is 2 events in the current period and 1 events in the comparison period.
# Number of sessions is 20 in each period, so current period is 20% of sessions, prev
# is 10%. Overall a 100% increase comparitively.
# is 10%. Overall a 100% increase comparatively.
event = self.add_event(
data={"fingerprint": ["something_random"]},
project_id=self.project.id,
Expand Down