From 2069179200f481585b92c741f72851083ce98534 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 10 Dec 2024 09:04:29 +0100 Subject: [PATCH 001/757] fix(on-demand-metrics): disallow usage of release.stage (#81900) --- static/app/utils/onDemandMetrics/constants.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/utils/onDemandMetrics/constants.tsx b/static/app/utils/onDemandMetrics/constants.tsx index 39c9ea4e3b9c0f..e9098e99155296 100644 --- a/static/app/utils/onDemandMetrics/constants.tsx +++ b/static/app/utils/onDemandMetrics/constants.tsx @@ -21,6 +21,7 @@ export const ON_DEMAND_METRICS_UNSUPPORTED_TAGS = new Set([ FieldKey.ID, FieldKey.MESSAGE, FieldKey.PROFILE_ID, + FieldKey.RELEASE_STAGE, FieldKey.TIMESTAMP_TO_DAY, FieldKey.TIMESTAMP_TO_HOUR, FieldKey.TIMESTAMP, From 45bd6e87cc45632e400934b2bf149b40e9d0407b Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 10 Dec 2024 09:18:17 +0100 Subject: [PATCH 002/757] fix(sidebar): Center items when horizontal (#81901) --- static/app/components/sidebar/index.tsx | 9 ++++++++- static/app/components/sidebar/newOnboardingStatus.tsx | 7 ++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/static/app/components/sidebar/index.tsx b/static/app/components/sidebar/index.tsx index bae6b5408a7e31..a61ae5df9637a0 100644 --- a/static/app/components/sidebar/index.tsx +++ b/static/app/components/sidebar/index.tsx @@ -640,7 +640,7 @@ function Sidebar() { )} - + {HookStore.get('sidebar:bottom-items').length > 0 && HookStore.get('sidebar:bottom-items')[0]({ orientation, @@ -803,6 +803,7 @@ const SubitemDot = styled('div')<{collapsed: boolean}>` `; const SidebarSection = styled(SidebarSectionGroup)<{ + centeredItems?: boolean; hasNewNav?: boolean; noMargin?: boolean; noPadding?: boolean; @@ -823,6 +824,12 @@ const SidebarSection = styled(SidebarSectionGroup)<{ } `} + ${p => + p.centeredItems && + css` + align-items: center; + `} + &:empty { display: none; } diff --git a/static/app/components/sidebar/newOnboardingStatus.tsx b/static/app/components/sidebar/newOnboardingStatus.tsx index 2cb24e6e1a6880..697ae8520d5e4f 100644 --- a/static/app/components/sidebar/newOnboardingStatus.tsx +++ b/static/app/components/sidebar/newOnboardingStatus.tsx @@ -151,6 +151,7 @@ export function NewOnboardingStatus({ aria-label={label} onClick={handleShowPanel} isActive={isActive} + showText={!shouldAccordionFloat} onMouseEnter={() => { refetch(); }} @@ -242,11 +243,11 @@ const hoverCss = (p: {theme: Theme}) => css` } `; -const Container = styled('div')<{isActive: boolean}>` - padding: 9px 19px 9px 16px; +const Container = styled('div')<{isActive: boolean; showText: boolean}>` + padding: 9px 16px; cursor: pointer; display: grid; - grid-template-columns: max-content 1fr; + grid-template-columns: ${p => (p.showText ? 'max-content 1fr' : 'max-content')}; gap: ${space(1.5)}; align-items: center; transition: background 100ms; From 2f8a2798c2c8b6b21a161d85f91d096615161422 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 10 Dec 2024 09:51:51 +0100 Subject: [PATCH 003/757] ref(quick-start): Mark 'Configure an Issue Alert' as complete by creating a project (#81797) --- src/sentry/receivers/onboarding.py | 2 -- src/sentry/receivers/rules.py | 32 +++++++++++++++++++++-- tests/sentry/receivers/test_onboarding.py | 29 ++++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/sentry/receivers/onboarding.py b/src/sentry/receivers/onboarding.py index 97d8976ebcba98..9d61d1c37f2713 100644 --- a/src/sentry/receivers/onboarding.py +++ b/src/sentry/receivers/onboarding.py @@ -641,8 +641,6 @@ def record_plugin_enabled(plugin, project, user, **kwargs): @alert_rule_created.connect(weak=False) def record_alert_rule_created(user, project: Project, rule_type: str, **kwargs): - # NOTE: This intentionally does not fire for the default issue alert rule - # that gets created on new project creation. task = OnboardingTask.METRIC_ALERT if rule_type == "metric" else OnboardingTask.ALERT_RULE rows_affected, created = OrganizationOnboardingTask.objects.create_or_update( organization_id=project.organization_id, diff --git a/src/sentry/receivers/rules.py b/src/sentry/receivers/rules.py index e024ff4fab2410..c07b27c62fe0e1 100644 --- a/src/sentry/receivers/rules.py +++ b/src/sentry/receivers/rules.py @@ -1,7 +1,13 @@ +import logging + +from sentry import features from sentry.models.project import Project from sentry.models.rule import Rule from sentry.notifications.types import FallthroughChoiceType -from sentry.signals import project_created +from sentry.signals import alert_rule_created, project_created +from sentry.users.services.user.model import RpcUser + +logger = logging.getLogger("sentry") DEFAULT_RULE_LABEL = "Send a notification for high priority issues" DEFAULT_RULE_ACTIONS = [ @@ -31,7 +37,29 @@ def create_default_rules(project: Project, default_rules=True, RuleModel=Rule, * return rule_data = DEFAULT_RULE_DATA - RuleModel.objects.create(project=project, label=DEFAULT_RULE_LABEL, data=rule_data) + rule = RuleModel.objects.create(project=project, label=DEFAULT_RULE_LABEL, data=rule_data) + + try: + user: RpcUser = project.organization.get_default_owner() + except IndexError: + logger.warning( + "Cannot record default rule created for organization (%s) due to missing owners", + project.organization_id, + ) + return + + if features.has("organizations:quick-start-updates", project.organization, actor=user): + # When a user creates a new project and opts to set up an issue alert within it, + # the corresponding task in the quick start sidebar is automatically marked as complete. + alert_rule_created.send( + user=user, + project=project, + rule_id=rule.id, + # The default rule created within a new project is always of type 'issue' + rule_type="issue", + sender=type(project), + is_api_token=False, + ) project_created.connect(create_default_rules, dispatch_uid="create_default_rules", weak=False) diff --git a/tests/sentry/receivers/test_onboarding.py b/tests/sentry/receivers/test_onboarding.py index 8bd7f5dbc51942..ee0e44aff25ffb 100644 --- a/tests/sentry/receivers/test_onboarding.py +++ b/tests/sentry/receivers/test_onboarding.py @@ -981,3 +981,32 @@ def test_release_received_through_transaction_event(self): status=OnboardingTaskStatus.COMPLETE, ) assert task is not None + + def test_issue_alert_received_through_project_creation(self): + with self.feature("organizations:quick-start-updates"): + now = timezone.now() + + first_organization = self.create_organization(owner=self.user, slug="first-org") + first_project = self.create_project(first_event=now, organization=first_organization) + # By default, the project creation will create a default rule + project_created.send(project=first_project, user=self.user, sender=type(first_project)) + assert OrganizationOnboardingTask.objects.filter( + organization=first_project.organization, + task=OnboardingTask.ALERT_RULE, + status=OnboardingTaskStatus.COMPLETE, + ).exists() + + second_organization = self.create_organization(owner=self.user, slug="second-org") + second_project = self.create_project(first_event=now, organization=second_organization) + # When creating a project, a user can opt out of creating a default rule + project_created.send( + project=second_project, + user=self.user, + sender=type(second_project), + default_rules=False, + ) + assert not OrganizationOnboardingTask.objects.filter( + organization=second_project.organization, + task=OnboardingTask.ALERT_RULE, + status=OnboardingTaskStatus.COMPLETE, + ).exists() From 87f0aae52d0ee7bd38669e386e1af59b6aa73c0c Mon Sep 17 00:00:00 2001 From: Joris Bayer Date: Tue, 10 Dec 2024 11:15:17 +0100 Subject: [PATCH 004/757] chore(outcomes): Clean up metric-to-outcome producer (#81799) - Remove unused functionality. - Avoid the name "billing" where possible. I started out with the intention to rename the consumer to clarify what it does, but that seems like a recipe for disaster (class is being referenced by name in the config, rename configs in ops and self-hosted is tricky). --- src/sentry/ingest/billing_metrics_consumer.py | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/sentry/ingest/billing_metrics_consumer.py b/src/sentry/ingest/billing_metrics_consumer.py index 471855aa356f6d..8009d57a6cdf9e 100644 --- a/src/sentry/ingest/billing_metrics_consumer.py +++ b/src/sentry/ingest/billing_metrics_consumer.py @@ -16,13 +16,7 @@ from sentry.constants import DataCategory from sentry.models.project import Project -from sentry.sentry_metrics.indexer.strings import ( - SHARED_TAG_STRINGS, - SPAN_METRICS_NAMES, - TRANSACTION_METRICS_NAMES, -) -from sentry.sentry_metrics.use_case_id_registry import UseCaseID -from sentry.sentry_metrics.utils import reverse_resolve_tag_value +from sentry.sentry_metrics.indexer.strings import SPAN_METRICS_NAMES, TRANSACTION_METRICS_NAMES from sentry.signals import first_custom_metric_received from sentry.snuba.metrics import parse_mri from sentry.snuba.metrics.naming_layer.mri import is_custom_metric @@ -48,9 +42,11 @@ def create_with_partitions( class BillingTxCountMetricConsumerStrategy(ProcessingStrategy[KafkaPayload]): - """A metrics consumer that generates a billing outcome for each processed - transaction, processing a bucket at a time. The transaction count is - directly taken from the `c:transactions/usage@none` counter metric. + """A metrics consumer that generates an accepted outcome for each processed (as opposed to indexed) + transaction or span, processing a bucket at a time. The transaction / span count is + directly taken from the `c:transactions/usage@none` or `c:spans/usage@none` counter metric. + + See https://develop.sentry.dev/application-architecture/dynamic-sampling/outcomes/. """ #: The IDs of the metrics used to count transactions or spans @@ -58,7 +54,6 @@ class BillingTxCountMetricConsumerStrategy(ProcessingStrategy[KafkaPayload]): TRANSACTION_METRICS_NAMES["c:transactions/usage@none"]: DataCategory.TRANSACTION, SPAN_METRICS_NAMES["c:spans/usage@none"]: DataCategory.SPAN, } - profile_tag_key = str(SHARED_TAG_STRINGS["has_profile"]) def __init__(self, next_step: ProcessingStrategy[Any]) -> None: self.__next_step = next_step @@ -79,7 +74,7 @@ def submit(self, message: Message[KafkaPayload]) -> None: payload = self._get_payload(message) - self._produce_billing_outcomes(payload) + self._produce_outcomes(payload) self._flag_metric_received_for_project(payload) self.__next_step.submit(message) @@ -106,25 +101,16 @@ def _count_processed_items(self, generic_metric: GenericMetric) -> Mapping[DataC return items - def _has_profile(self, generic_metric: GenericMetric) -> bool: - return bool( - (tag_value := generic_metric["tags"].get(self.profile_tag_key)) - and "true" - == reverse_resolve_tag_value( - UseCaseID.TRANSACTIONS, generic_metric["org_id"], tag_value - ) - ) - - def _produce_billing_outcomes(self, generic_metric: GenericMetric) -> None: + def _produce_outcomes(self, generic_metric: GenericMetric) -> None: for category, quantity in self._count_processed_items(generic_metric).items(): - self._produce_billing_outcome( + self._produce_accepted_outcome( org_id=generic_metric["org_id"], project_id=generic_metric["project_id"], category=category, quantity=quantity, ) - def _produce_billing_outcome( + def _produce_accepted_outcome( self, *, org_id: int, project_id: int, category: DataCategory, quantity: int ) -> None: if quantity < 1: From 09f033f0886e2a9f8581a409abbcc15258743d8d Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 10 Dec 2024 11:32:01 +0100 Subject: [PATCH 005/757] Fix sdk crash detection pthread_getcpuclockid path (#81800) Update `pthread_getcpuclockid` path_pattern as it should be `/apex/com.android.runtime/lib64/bionic/libc.so` instead of `/apex/com.android.art/lib64/bionic/libc.so` --- .../utils/sdk_crashes/sdk_crash_detection_config.py | 2 +- .../utils/sdk_crashes/test_sdk_crash_detection_java.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py index 0053f191a524c8..8c49681dd7d19f 100644 --- a/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py +++ b/src/sentry/utils/sdk_crashes/sdk_crash_detection_config.py @@ -227,7 +227,7 @@ def build_sdk_crash_detection_configs() -> Sequence[SDKCrashDetectionConfig]: function_and_path_patterns=[ FunctionAndPathPattern( function_pattern=r"*pthread_getcpuclockid*", - path_pattern=r"/apex/com.android.art/lib64/bionic/libc.so", + path_pattern=r"/apex/com.android.runtime/lib64/bionic/libc.so", ), FunctionAndPathPattern( function_pattern=r"*art::Trace::StopTracing*", diff --git a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_java.py b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_java.py index c25d055863d760..af5a121416d5c0 100644 --- a/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_java.py +++ b/tests/sentry/utils/sdk_crashes/test_sdk_crash_detection_java.py @@ -148,31 +148,31 @@ def test_sdk_crash_is_reported_with_android_paths( [ ( "pthread_getcpuclockid", - "/apex/com.android.art/lib64/bionic/libc.so", + "/apex/com.android.runtime/lib64/bionic/libc.so", "/apex/com.android.art/lib64/libart.so", True, ), ( "__pthread_getcpuclockid", - "/apex/com.android.art/lib64/bionic/libc.so", + "/apex/com.android.runtime/lib64/bionic/libc.so", "/apex/com.android.art/lib64/libart.so", True, ), ( "pthread_getcpuclockid(void*)", - "/apex/com.android.art/lib64/bionic/libc.so", + "/apex/com.android.runtime/lib64/bionic/libc.so", "/apex/com.android.art/lib64/libart.so", True, ), ( "pthread_getcpuclocki", - "/apex/com.android.art/lib64/bionic/libc.so", + "/apex/com.android.runtime/lib64/bionic/libc.so", "/apex/com.android.art/lib64/libart.so", False, ), ( "pthread_getcpuclockid", - "/apex/com.android.art/lib64/bionic/libc.s", + "/apex/com.android.runtime/lib64/bionic/libc.s", "/apex/com.android.art/lib64/libart.so", False, ), From 0396c6e78c02da86b4b5d7ca398743966dd14e03 Mon Sep 17 00:00:00 2001 From: Simon Hellmayr Date: Tue, 10 Dec 2024 11:53:00 +0100 Subject: [PATCH 006/757] ref(metrics): remove metrics extraction rule code (#81798) --- src/sentry/sentry_metrics/extraction_rules.py | 80 ------------------- .../sentry_metrics/test_extraction_rules.py | 39 --------- 2 files changed, 119 deletions(-) delete mode 100644 src/sentry/sentry_metrics/extraction_rules.py delete mode 100644 tests/sentry/sentry_metrics/test_extraction_rules.py diff --git a/src/sentry/sentry_metrics/extraction_rules.py b/src/sentry/sentry_metrics/extraction_rules.py deleted file mode 100644 index 38246de5e50dc4..00000000000000 --- a/src/sentry/sentry_metrics/extraction_rules.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from dataclasses import dataclass -from typing import Any - -from sentry.sentry_metrics.configuration import ( - AGGREGATE_TO_METRIC_TYPE, - ALLOWED_TYPES, - HARD_CODED_UNITS, -) -from sentry.sentry_metrics.use_case_utils import string_to_use_case_id - -METRICS_EXTRACTION_RULES_OPTION_KEY = "sentry:metrics_extraction_rules" -SPAN_ATTRIBUTE_PREFIX = "span_attribute_" - - -class MetricsExtractionRuleValidationError(ValueError): - pass - - -@dataclass -class MetricsExtractionRule: - def __init__( - self, - span_attribute: str, - type: str, - unit: str, - tags: set[str], - condition: str, - id: int, - ): - self.span_attribute = self.validate_span_attribute(span_attribute) - self.type = self.validate_type(type) - self.unit = HARD_CODED_UNITS.get(span_attribute, unit) - self.tags = set(tags) - self.condition = condition - self.id = id - - def validate_span_attribute(self, span_attribute: str) -> str: - if not isinstance(span_attribute, str): - raise ValueError("The span attribute must be of type string.") - return span_attribute - - def validate_type(self, type_value: str) -> str: - if not isinstance(type_value, str): - raise ValueError("The type must be of type string.") - - if type_value not in ALLOWED_TYPES: - raise ValueError( - "Type can only have the following values: 'c' for counter, 'd' for distribution, 'g' for gauge, or 's' for set." - ) - return type_value - - @classmethod - def infer_types(self, aggregates: set[str]) -> set[str]: - types: set[str] = set() - for aggregate in aggregates: - if new_type := AGGREGATE_TO_METRIC_TYPE.get(aggregate): - types.add(new_type) - - return types - - def to_dict(self) -> Mapping[str, Any]: - return { - "spanAttribute": self.span_attribute, - "type": self.type, - "unit": self.unit, - "tags": self.tags, - "condition": self.condition, - "id": self.id, - } - - def generate_mri(self, use_case: str = "custom"): - """Generate the Metric Resource Identifier (MRI) associated with the extraction rule.""" - use_case_id = string_to_use_case_id(use_case) - return f"{self.type}:{use_case_id.value}/{SPAN_ATTRIBUTE_PREFIX}{self.id}@none" - - def __hash__(self): - return hash(self.generate_mri()) diff --git a/tests/sentry/sentry_metrics/test_extraction_rules.py b/tests/sentry/sentry_metrics/test_extraction_rules.py deleted file mode 100644 index 68a423f581315e..00000000000000 --- a/tests/sentry/sentry_metrics/test_extraction_rules.py +++ /dev/null @@ -1,39 +0,0 @@ -import uuid - -import pytest - -from sentry.sentry_metrics.extraction_rules import MetricsExtractionRule - - -def _new_id(): - return str(uuid.uuid4()) - - -def test_generate_mri(): - rule = MetricsExtractionRule("count_clicks", "c", "none", {"tag_1", "tag_2"}, "", 12378) - mri = rule.generate_mri() - assert mri == "c:custom/span_attribute_12378@none" - - -def test_type_validation(): - rules = [ - MetricsExtractionRule("count_clicks", "c", "none", {"tag_1", "tag_2"}, "", 7423), - MetricsExtractionRule( - "process_latency", "d", "none", {"tag_3"}, "first:value second:value", 239478 - ), - MetricsExtractionRule("unique_ids", "s", "none", set(), "foo:bar", 278934), - ] - - mris = [rule.generate_mri() for rule in rules] - assert mris == [ - "c:custom/span_attribute_7423@none", - "d:custom/span_attribute_239478@none", - "s:custom/span_attribute_278934@none", - ] - - with pytest.raises(ValueError): - MetricsExtractionRule("count_clicks", "f", "none", {"tag_1", "tag_2"}, "", 128903) - with pytest.raises(ValueError): - MetricsExtractionRule( - "count_clicks", "distribution", "none", {"tag_1", "tag_2"}, "", 123678 - ) From 5d2afaf4d56bab2b4f1518c77d9f609c7a94de87 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 10 Dec 2024 13:33:54 +0100 Subject: [PATCH 007/757] fix(releases): Show onboarding panel (#81906) --- static/app/views/releases/list/index.spec.tsx | 4 ++-- static/app/views/releases/list/index.tsx | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/static/app/views/releases/list/index.spec.tsx b/static/app/views/releases/list/index.spec.tsx index db809e37ad3608..18f8f7ade1c03b 100644 --- a/static/app/views/releases/list/index.spec.tsx +++ b/static/app/views/releases/list/index.spec.tsx @@ -252,7 +252,7 @@ describe('ReleasesList', () => { statusCode: 400, }); - render(, { + render(, { router, organization, }); @@ -261,7 +261,7 @@ describe('ReleasesList', () => { // we want release header to be visible despite the error message expect( - await screen.getByRole('combobox', { + await screen.findByRole('combobox', { name: 'Add a search term', }) ).toBeInTheDocument(); diff --git a/static/app/views/releases/list/index.tsx b/static/app/views/releases/list/index.tsx index 7d34aadb6cbe56..49bef814bf077d 100644 --- a/static/app/views/releases/list/index.tsx +++ b/static/app/views/releases/list/index.tsx @@ -187,6 +187,12 @@ class ReleasesList extends DeprecatedAsyncView { getSelectedProject(): Project | undefined { const {selection, projects} = this.props; + // Return the first project when 'All Projects' is displayed. + // This ensures the onboarding panel is shown correctly, for example. + if (selection.projects.length === 0) { + return projects[0]; + } + const selectedProjectId = selection.projects && selection.projects.length === 1 && selection.projects[0]; return projects?.find(p => p.id === `${selectedProjectId}`); From afb0c19845793898ee210b2038211dcce1668827 Mon Sep 17 00:00:00 2001 From: Harshitha Durai <76853136+harshithadurai@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:51:16 -0500 Subject: [PATCH 008/757] fix(dashboards): hide some features on pre-built dashboards (#81909) Hide Edit Access and Favourite dashboard features on pre-built dashboards --- static/app/views/dashboards/controls.tsx | 70 +++++++++++++----------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/static/app/views/dashboards/controls.tsx b/static/app/views/dashboards/controls.tsx index 5b78f05f53866a..0c91dca78e6458 100644 --- a/static/app/views/dashboards/controls.tsx +++ b/static/app/views/dashboards/controls.tsx @@ -182,40 +182,44 @@ function Controls({ {t('Export Dashboard')} - - - - ))} - - - -
-

Sort:

-
{state.sort?.map(formatSort).join(', ')}
- -
- - ); -} - -function ColumnSelector({ - displayType, - fields, - dataset, - onChange, -}: { - dataset: WidgetType; - displayType: DisplayType; - fields: Column[]; - onChange: (newFields: Column[]) => void; -}) { - const organization = useOrganization(); - const datasetConfig = getDatasetConfig(dataset); - - const fieldOptions = datasetConfig.getTableFieldOptions(organization); - - return ( - true} - filterPrimaryOptions={() => true} - onChange={onChange} - /> - ); -} - -function YAxis({ - displayType, - widgetType, - aggregates, - onChange, -}: { - aggregates: Column[]; - displayType: DisplayType; - onChange: (newFields: Column[]) => void; - widgetType: WidgetType; -}) { - const organization = useOrganization(); - return ( - - - - ); -} - -function QueryField({ - query, - onSearch, -}: { - onSearch: (query: string) => void; - query: string; -}) { - return ( - Promise.resolve([])} - showUnsubmittedIndicator - /> - ); -} - -function SortSelector() { - const {state, dispatch} = useWidgetBuilderContext(); - - // There's a SortDirection enum in the widgetBuilder utils, but it's not used anywhere else - // so I'd rather just get rid of the dependency and use a new object that uses standard terms - const sortDirections = { - desc: 'High to low', - asc: 'Low to high', - }; - const direction = state.sort?.[0]?.kind; - const sortBy = state.sort?.[0]?.field; - - return ( -
- ({ - label: sortDirections[value], - value, - }))} - value={direction} - onChange={option => { - dispatch({ - type: BuilderStateAction.SET_SORT, - payload: [{field: sortBy ?? '', kind: option.value}], - }); - }} - /> - ({ - label: generateFieldAsString(field), - value: generateFieldAsString(field), - }))} - onChange={option => { - dispatch({ - type: BuilderStateAction.SET_SORT, - payload: [{field: option.value, kind: direction ?? 'asc'}], - }); - }} - /> -
- ); -} - -const Body = styled('div')` - margin: ${space(2)}; - padding: ${space(2)}; -`; - -const Section = styled('section')` - display: flex; - flex-direction: row; - justify-content: space-around; - border: 1px solid ${p => p.theme.border}; - align-items: center; - - > * { - flex: 1; - } -`; - -const SimpleInput = styled(Input)` - width: 100%; -`; - -export default DevBuilder; diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 10020cd7be614e..05d8bf8d85fdeb 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -8,7 +8,6 @@ import {space} from 'sentry/styles/space'; import {useParams} from 'sentry/utils/useParams'; import {DisplayType, type Widget} from 'sentry/views/dashboards/types'; import WidgetBuilderDatasetSelector from 'sentry/views/dashboards/widgetBuilder/components/datasetSelector'; -import DevBuilder from 'sentry/views/dashboards/widgetBuilder/components/devBuilder'; import WidgetBuilderFilterBar from 'sentry/views/dashboards/widgetBuilder/components/filtersBar'; import WidgetBuilderGroupBySelector from 'sentry/views/dashboards/widgetBuilder/components/groupBySelector'; import WidgetBuilderNameAndDescription from 'sentry/views/dashboards/widgetBuilder/components/nameAndDescFields'; @@ -77,7 +76,6 @@ function WidgetBuilderSlideout({isOpen, onClose, onSave}: WidgetBuilderSlideoutP - ); From ce49f02c38c69f30f7876ab7282ae25bb6cab730 Mon Sep 17 00:00:00 2001 From: Nikki Kapadia <72356613+nikkikapadia@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:40:17 -0500 Subject: [PATCH 027/757] feat(widget-builder): Add sort by field to slideout (#81876) Added the sort by field UI. It is not visible on big number widgets. I've recycled most of the code from the current widget builder which takes care of the limiting. Most of the functionality is there except for limits (not sure if anything is missing but let me know if there is). This is what it looks like: on chart widgets image on table widgets image --- .../components/newWidgetBuilder.spec.tsx | 61 ++++++++ .../components/sortBySelector.spec.tsx | 126 ++++++++++++++++ .../components/sortBySelector.tsx | 137 ++++++++++++++++++ .../components/widgetBuilderSlideout.tsx | 8 + .../hooks/useWidgetBuilderState.tsx | 3 + 5 files changed, 335 insertions(+) create mode 100644 static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx create mode 100644 static/app/views/dashboards/widgetBuilder/components/sortBySelector.tsx diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx index c17dddf26941b2..def81c37141c9f 100644 --- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.spec.tsx @@ -135,6 +135,11 @@ describe('NewWidgetBuiler', function () { expect(screen.getByPlaceholderText('Search')).toBeInTheDocument(); + // Test sort by selector for table display type + expect(screen.getByText('Sort by')).toBeInTheDocument(); + expect(screen.getByText('High to low')).toBeInTheDocument(); + expect(screen.getByText(`Select a column\u{2026}`)).toBeInTheDocument(); + expect(await screen.findByPlaceholderText('Name')).toBeInTheDocument(); expect(await screen.findByTestId('add-description')).toBeInTheDocument(); @@ -328,4 +333,60 @@ describe('NewWidgetBuiler', function () { expect(await screen.findByText('Select group')).toBeInTheDocument(); expect(await screen.findByText('Add Group')).toBeInTheDocument(); }); + + it('renders the limit sort by field on chart widgets', async function () { + const chartsRouter = RouterFixture({ + ...router, + location: { + ...router.location, + query: {...router.location.query, displayType: 'line'}, + }, + }); + + render( + , + { + router: chartsRouter, + organization, + } + ); + + expect(await screen.findByText('Limit to 5 results')).toBeInTheDocument(); + expect(await screen.findByText('High to low')).toBeInTheDocument(); + expect(await screen.findByText('(Required)')).toBeInTheDocument(); + }); + + it('does not render sort by field on big number widgets', async function () { + const bigNumberRouter = RouterFixture({ + ...router, + location: { + ...router.location, + query: {...router.location.query, displayType: 'big_number'}, + }, + }); + + render( + , + { + router: bigNumberRouter, + organization, + } + ); + + await waitFor(() => { + expect(screen.queryByText('Sort by')).not.toBeInTheDocument(); + }); + }); }); diff --git a/static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx new file mode 100644 index 00000000000000..8971f2229d993a --- /dev/null +++ b/static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx @@ -0,0 +1,126 @@ +import {RouterFixture} from 'sentry-fixture/routerFixture'; + +import {initializeOrg} from 'sentry-test/initializeOrg'; +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {DiscoverDatasets} from 'sentry/utils/discover/types'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import WidgetBuilderSortBySelector from 'sentry/views/dashboards/widgetBuilder/components/sortBySelector'; +import {WidgetBuilderProvider} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext'; +import {SpanTagsProvider} from 'sentry/views/explore/contexts/spanTagsContext'; + +jest.mock('sentry/utils/useNavigate', () => ({ + useNavigate: jest.fn(), +})); + +const {organization, router} = initializeOrg({ + organization: {features: ['global-views', 'open-membership', 'dashboards-eap']}, + projects: [], + router: { + location: { + pathname: '/organizations/org-slug/dashboard/1/', + query: { + displayType: 'line', + fields: ['transaction.duration', 'count()', 'id'], + }, + }, + params: {}, + }, +}); + +const mockUseNavigate = jest.mocked(useNavigate); + +describe('WidgetBuilderSortBySelector', function () { + beforeEach(function () { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/spans/fields/', + body: [], + }); + }); + + it('renders', async function () { + const mockNavigate = jest.fn(); + mockUseNavigate.mockReturnValue(mockNavigate); + + render( + + + + + , + { + router, + organization, + } + ); + + expect(await screen.findByText('Sort by')).toBeInTheDocument(); + expect(await screen.findByText('Limit to 5 results')).toBeInTheDocument(); + expect(await screen.findByText('High to low')).toBeInTheDocument(); + expect(await screen.findByText('(Required)')).toBeInTheDocument(); + }); + + it('renders correct fields for table widgets', async function () { + const tableRouter = RouterFixture({ + ...router, + location: { + ...router.location, + query: {...router.location.query, displayType: 'table'}, + }, + }); + + render( + + + + + , + { + router: tableRouter, + organization, + } + ); + + expect(await screen.findByText('Sort by')).toBeInTheDocument(); + expect(await screen.findByText('High to low')).toBeInTheDocument(); + expect(await screen.findByText(`Select a column\u{2026}`)).toBeInTheDocument(); + }); + + it('renders and functions correctly', async function () { + const mockNavigate = jest.fn(); + mockUseNavigate.mockReturnValue(mockNavigate); + + render( + + + + + , + {router, organization} + ); + + const sortDirectionSelector = await screen.findByText('High to low'); + const sortFieldSelector = await screen.findByText('(Required)'); + + expect(sortFieldSelector).toBeInTheDocument(); + + await userEvent.click(sortFieldSelector); + await userEvent.click(await screen.findByText('count()')); + + expect(mockNavigate).toHaveBeenLastCalledWith( + expect.objectContaining({ + ...router.location, + query: expect.objectContaining({sort: ['-count']}), + }) + ); + + await userEvent.click(sortDirectionSelector); + await userEvent.click(await screen.findByText('Low to high')); + expect(mockNavigate).toHaveBeenLastCalledWith( + expect.objectContaining({ + ...router.location, + query: expect.objectContaining({sort: ['count']}), + }) + ); + }); +}); diff --git a/static/app/views/dashboards/widgetBuilder/components/sortBySelector.tsx b/static/app/views/dashboards/widgetBuilder/components/sortBySelector.tsx new file mode 100644 index 00000000000000..fb519389871f1b --- /dev/null +++ b/static/app/views/dashboards/widgetBuilder/components/sortBySelector.tsx @@ -0,0 +1,137 @@ +import {Fragment, useState} from 'react'; +import styled from '@emotion/styled'; + +import SelectControl from 'sentry/components/forms/controls/selectControl'; +import FieldGroup from 'sentry/components/forms/fieldGroup'; +import {Tooltip} from 'sentry/components/tooltip'; +import {t, tn} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import type {SelectValue} from 'sentry/types/core'; +import type {TagCollection} from 'sentry/types/group'; +import useTags from 'sentry/utils/useTags'; +import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; +import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; +import {SortBySelectors} from 'sentry/views/dashboards/widgetBuilder/buildSteps/sortByStep/sortBySelectors'; +import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader'; +import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext'; +import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState'; +import { + DEFAULT_RESULTS_LIMIT, + getResultsLimit, + SortDirection, +} from 'sentry/views/dashboards/widgetBuilder/utils'; +import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget'; +import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext'; + +function WidgetBuilderSortBySelector() { + const {state, dispatch} = useWidgetBuilderContext(); + const widget = convertBuilderStateToWidget(state); + + const [limit, setLimit] = useState(DEFAULT_RESULTS_LIMIT); + + const datasetConfig = getDatasetConfig(state.dataset); + + let tags: TagCollection = useTags(); + const numericSpanTags = useSpanTags('number'); + const stringSpanTags = useSpanTags('string'); + if (state.dataset === WidgetType.SPANS) { + tags = {...numericSpanTags, ...stringSpanTags}; + } + + let disableSort = false; + let disableSortDirection = false; + let disableSortReason: string | undefined = undefined; + + if (datasetConfig.disableSortOptions) { + ({disableSort, disableSortDirection, disableSortReason} = + datasetConfig.disableSortOptions(widget.queries[0])); + } + + const displayType = state.displayType ?? DisplayType.TABLE; + + const isTimeseriesChart = [ + DisplayType.LINE, + DisplayType.BAR, + DisplayType.AREA, + ].includes(displayType); + + const maxLimit = getResultsLimit( + widget.queries.length, + widget.queries[0].aggregates.length + ); + + function handleSortByChange(newSortBy: string, sortDirection: 'asc' | 'desc') { + dispatch({ + type: BuilderStateAction.SET_SORT, + payload: [{field: newSortBy, kind: sortDirection}], + }); + } + + return ( + + + + + {isTimeseriesChart && limit && ( + { + const value = resultLimit + 1; + return { + label: tn('Limit to %s result', 'Limit to %s results', value), + value, + }; + })} + value={limit} + onChange={(option: SelectValue) => { + // TODO: implement this when limit is implemented in widget builder state + setLimit(option.value); + }} + /> + )} + { + const newSortDirection = + sortDirection === SortDirection.HIGH_TO_LOW ? 'desc' : 'asc'; + handleSortByChange(sortBy, newSortDirection); + }} + tags={tags} + /> + + + + ); +} + +export default WidgetBuilderSortBySelector; + +const ResultsLimitSelector = styled(SelectControl)` + margin-bottom: ${space(1)}; +`; diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 05d8bf8d85fdeb..6f3794fa22f1e6 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -13,6 +13,7 @@ import WidgetBuilderGroupBySelector from 'sentry/views/dashboards/widgetBuilder/ import WidgetBuilderNameAndDescription from 'sentry/views/dashboards/widgetBuilder/components/nameAndDescFields'; import WidgetBuilderQueryFilterBuilder from 'sentry/views/dashboards/widgetBuilder/components/queryFilterBuilder'; import SaveButton from 'sentry/views/dashboards/widgetBuilder/components/saveButton'; +import WidgetBuilderSortBySelector from 'sentry/views/dashboards/widgetBuilder/components/sortBySelector'; import WidgetBuilderTypeSelector from 'sentry/views/dashboards/widgetBuilder/components/typeSelector'; import Visualize from 'sentry/views/dashboards/widgetBuilder/components/visualize'; import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext'; @@ -32,6 +33,8 @@ function WidgetBuilderSlideout({isOpen, onClose, onSave}: WidgetBuilderSlideoutP state.displayType !== DisplayType.BIG_NUMBER && state.displayType !== DisplayType.TABLE; + const isNotBigNumberWidget = state.displayType !== DisplayType.BIG_NUMBER; + return ( )} + {isNotBigNumberWidget && ( +
+ +
+ )}
diff --git a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx index d6469b244db7dc..a0aa5412c524a3 100644 --- a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx +++ b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx @@ -106,6 +106,9 @@ function useWidgetBuilderState(): { setDescription(action.payload); break; case BuilderStateAction.SET_DISPLAY_TYPE: + if (action.payload === DisplayType.BIG_NUMBER) { + setSort([]); + } setDisplayType(action.payload); break; case BuilderStateAction.SET_DATASET: From 48b88e1c2266b395eaebc5c19ce3eea460839b75 Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Tue, 10 Dec 2024 14:42:17 -0500 Subject: [PATCH 028/757] chore(profiles): Adjust timeouts (#81861) --- src/sentry/profiles/task.py | 50 +++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/src/sentry/profiles/task.py b/src/sentry/profiles/task.py index 71cc3c98db3bf2..22f89d5c2c3fe3 100644 --- a/src/sentry/profiles/task.py +++ b/src/sentry/profiles/task.py @@ -42,19 +42,14 @@ from sentry.utils.sdk import set_measurement -class VroomTimeout(Exception): - pass - - @instrumented_task( name="sentry.profiles.task.process_profile", queue="profiles.process", - autoretry_for=(VroomTimeout,), # Retry when vroom returns a GCS timeout retry_backoff=True, - retry_backoff_max=60, # up to 1 min + retry_backoff_max=20, retry_jitter=True, default_retry_delay=5, # retries after 5s - max_retries=5, + max_retries=2, acks_late=True, task_time_limit=60, task_acks_on_failure_or_timeout=False, @@ -515,7 +510,10 @@ def symbolicate( classes=[], ) return symbolicator.process_payload( - platform=platform, stacktraces=stacktraces, modules=modules, apply_source_context=False + platform=platform, + stacktraces=stacktraces, + modules=modules, + apply_source_context=False, ) @@ -956,29 +954,27 @@ def _insert_vroom_profile(profile: Profile) -> bool: path = "/chunk" if "profiler_id" in profile else "/profile" response = get_from_profiling_service(method="POST", path=path, json_data=profile) + sentry_sdk.set_tag("vroom.response.status_code", str(response.status)) + + reason = "bad status" + if response.status == 204: return True elif response.status == 429: - raise VroomTimeout + reason = "gcs timeout" elif response.status == 412: - metrics.incr( - "process_profile.insert_vroom_profile.error", - tags={ - "platform": profile["platform"], - "reason": "duplicate profile", - }, - sample_rate=1.0, - ) - return False - else: - metrics.incr( - "process_profile.insert_vroom_profile.error", - tags={"platform": profile["platform"], "reason": "bad status"}, - sample_rate=1.0, - ) - return False - except VroomTimeout: - raise + reason = "duplicate profile" + + metrics.incr( + "process_profile.insert_vroom_profile.error", + tags={ + "platform": profile["platform"], + "reason": reason, + "status_code": response.status, + }, + sample_rate=1.0, + ) + return False except Exception as e: sentry_sdk.capture_exception(e) metrics.incr( From 474b5c5f3066ba2584d16ae19b5c9a14e7e10416 Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Tue, 10 Dec 2024 11:47:39 -0800 Subject: [PATCH 029/757] :recycle: ref(slo): Add SLOs for Slack Service (#81866) Slack Service makes some external API calls, so adding SLOs here. --- src/sentry/integrations/messaging/metrics.py | 3 + src/sentry/integrations/slack/metrics.py | 19 +++ src/sentry/integrations/slack/service.py | 117 ++++++++++++------ .../slack/service/test_slack_service.py | 16 ++- 4 files changed, 113 insertions(+), 42 deletions(-) diff --git a/src/sentry/integrations/messaging/metrics.py b/src/sentry/integrations/messaging/metrics.py index d2e1da56bb72b0..abbdb03a7f5aa5 100644 --- a/src/sentry/integrations/messaging/metrics.py +++ b/src/sentry/integrations/messaging/metrics.py @@ -46,6 +46,9 @@ class MessagingInteractionType(StrEnum): SEND_INCIDENT_ALERT_NOTIFICATION = "SEND_INCIDENT_ALERT_NOTIFICATION" SEND_ISSUE_ALERT_NOTIFICATION = "SEND_ISSUE_ALERT_NOTIFICATION" + SEND_ACTIVITY_NOTIFICATION = "SEND_ACTIVITY_NOTIFICATION" + SEND_GENERIC_NOTIFICATION = "SEND_GENERIC_NOTIFICATION" + @dataclass class MessagingInteractionEvent(IntegrationEventLifecycleMetric): diff --git a/src/sentry/integrations/slack/metrics.py b/src/sentry/integrations/slack/metrics.py index 18a6364c7877c3..1174d265489b69 100644 --- a/src/sentry/integrations/slack/metrics.py +++ b/src/sentry/integrations/slack/metrics.py @@ -1,5 +1,13 @@ # metrics constants +from slack_sdk.errors import SlackApiError + +from sentry.integrations.slack.utils.errors import ( + SLACK_SDK_HALT_ERROR_CATEGORIES, + unpack_slack_api_error, +) +from sentry.integrations.utils.metrics import EventLifecycle + SLACK_ISSUE_ALERT_SUCCESS_DATADOG_METRIC = "sentry.integrations.slack.issue_alert.success" SLACK_ISSUE_ALERT_FAILURE_DATADOG_METRIC = "sentry.integrations.slack.issue_alert.failure" SLACK_ACTIVITY_THREAD_SUCCESS_DATADOG_METRIC = "sentry.integrations.slack.activity_thread.success" @@ -79,3 +87,14 @@ # Middleware Parsers SLACK_MIDDLE_PARSERS_SUCCESS_DATADOG_METRIC = "sentry.middleware.integrations.slack.parsers.success" SLACK_MIDDLE_PARSERS_FAILURE_DATADOG_METRIC = "sentry.middleware.integrations.slack.parsers.failure" + + +def record_lifecycle_termination_level(lifecycle: EventLifecycle, error: SlackApiError) -> None: + if ( + (reason := unpack_slack_api_error(error)) + and reason is not None + and reason in SLACK_SDK_HALT_ERROR_CATEGORIES + ): + lifecycle.record_halt(reason.message) + else: + lifecycle.record_failure(error) diff --git a/src/sentry/integrations/slack/service.py b/src/sentry/integrations/slack/service.py index 9b2dd314ca53f2..df0511ab1510ab 100644 --- a/src/sentry/integrations/slack/service.py +++ b/src/sentry/integrations/slack/service.py @@ -10,6 +10,10 @@ from slack_sdk.errors import SlackApiError from sentry.constants import ISSUE_ALERTS_THREAD_DEFAULT +from sentry.integrations.messaging.metrics import ( + MessagingInteractionEvent, + MessagingInteractionType, +) from sentry.integrations.models.integration import Integration from sentry.integrations.notifications import get_context from sentry.integrations.repository import get_default_issue_alert_repository @@ -25,8 +29,10 @@ SLACK_ACTIVITY_THREAD_SUCCESS_DATADOG_METRIC, SLACK_NOTIFY_RECIPIENT_FAILURE_DATADOG_METRIC, SLACK_NOTIFY_RECIPIENT_SUCCESS_DATADOG_METRIC, + record_lifecycle_termination_level, ) from sentry.integrations.slack.sdk_client import SlackSdkClient +from sentry.integrations.slack.spec import SlackMessagingSpec from sentry.integrations.slack.threads.activity_notifications import ( AssignedActivityNotification, ExternalIssueCreatedActivityNotification, @@ -182,12 +188,23 @@ def notify_all_threads_for_activity(self, activity: Activity) -> None: slack_client = SlackSdkClient(integration_id=integration.id) # Get all parent notifications, which will have the message identifier to use to reply in a thread - parent_notifications = ( - self._notification_message_repository.get_all_parent_notification_messages_by_filters( + with MessagingInteractionEvent( + interaction_type=MessagingInteractionType.GET_PARENT_NOTIFICATION, + spec=SlackMessagingSpec(), + ).capture() as lifecycle: + lifecycle.add_extras( + { + "activity_id": activity.id, + "group_id": activity.group.id, + "project_id": activity.project.id, + } + ) + parent_notifications = self._notification_message_repository.get_all_parent_notification_messages_by_filters( group_ids=[activity.group.id], project_ids=[activity.project.id], ) - ) + + # We don't wrap this in a lifecycle because _handle_parent_notification is already wrapped in a lifecycle for parent_notification in parent_notifications: try: self._handle_parent_notification( @@ -196,6 +213,7 @@ def notify_all_threads_for_activity(self, activity: Activity) -> None: client=slack_client, ) except Exception as err: + # TODO(iamrajjoshi): We can probably swallow this error once we audit the lifecycle self._logger.info( "failed to send notification", exc_info=err, @@ -254,25 +272,33 @@ def _handle_parent_notification( "rule_action_uuid": parent_notification.rule_action_uuid, } - try: - client.chat_postMessage( - channel=channel_id, - thread_ts=parent_notification.message_identifier, - text=notification_to_send, - blocks=json_blocks, - ) - metrics.incr(SLACK_ACTIVITY_THREAD_SUCCESS_DATADOG_METRIC, sample_rate=1.0) - except SlackApiError as e: - self._logger.info( - "failed to post message to slack", - extra={"error": str(e), "blocks": json_blocks, **extra}, - ) - metrics.incr( - SLACK_ACTIVITY_THREAD_FAILURE_DATADOG_METRIC, - sample_rate=1.0, - tags={"ok": e.response.get("ok", False), "status": e.response.status_code}, - ) - raise + with MessagingInteractionEvent( + interaction_type=MessagingInteractionType.SEND_ACTIVITY_NOTIFICATION, + spec=SlackMessagingSpec(), + ).capture() as lifecycle: + try: + client.chat_postMessage( + channel=channel_id, + thread_ts=parent_notification.message_identifier, + text=notification_to_send, + blocks=json_blocks, + ) + # TODO(iamrajjoshi): Remove this after we validate lifecycle + metrics.incr(SLACK_ACTIVITY_THREAD_SUCCESS_DATADOG_METRIC, sample_rate=1.0) + except SlackApiError as e: + # TODO(iamrajjoshi): Remove this after we validate lifecycle + self._logger.info( + "failed to post message to slack", + extra={"error": str(e), "blocks": json_blocks, **extra}, + ) + metrics.incr( + SLACK_ACTIVITY_THREAD_FAILURE_DATADOG_METRIC, + sample_rate=1.0, + tags={"ok": e.response.get("ok", False), "status": e.response.status_code}, + ) + lifecycle.add_extras({"rule_action_uuid": parent_notification.rule_action_uuid}) + record_lifecycle_termination_level(lifecycle, e) + raise def _get_notification_message_to_send(self, activity: Activity) -> str | None: """ @@ -427,21 +453,32 @@ def send_message_to_slack_channel( """Execution of send_notification_as_slack.""" client = SlackSdkClient(integration_id=integration_id) - try: - client.chat_postMessage( - blocks=str(payload.get("blocks", "")), - text=str(payload.get("text", "")), - channel=str(payload.get("channel", "")), - unfurl_links=False, - unfurl_media=False, - callback_id=str(payload.get("callback_id", "")), - ) - metrics.incr(SLACK_NOTIFY_RECIPIENT_SUCCESS_DATADOG_METRIC, sample_rate=1.0) - except SlackApiError as e: - extra = {"error": str(e), **log_params} - self._logger.info(log_error_message, extra=extra) - metrics.incr( - SLACK_NOTIFY_RECIPIENT_FAILURE_DATADOG_METRIC, - sample_rate=1.0, - tags={"ok": e.response.get("ok", False), "status": e.response.status_code}, - ) + with MessagingInteractionEvent( + interaction_type=MessagingInteractionType.SEND_GENERIC_NOTIFICATION, + spec=SlackMessagingSpec(), + ).capture() as lifecycle: + try: + lifecycle.add_extras({"integration_id": integration_id}) + client.chat_postMessage( + blocks=str(payload.get("blocks", "")), + text=str(payload.get("text", "")), + channel=str(payload.get("channel", "")), + unfurl_links=False, + unfurl_media=False, + callback_id=str(payload.get("callback_id", "")), + ) + # TODO(iamrajjoshi): Remove this after we validate lifecycle + metrics.incr(SLACK_NOTIFY_RECIPIENT_SUCCESS_DATADOG_METRIC, sample_rate=1.0) + except SlackApiError as e: + # TODO(iamrajjoshi): Remove this after we validate lifecycle + extra = {"error": str(e), **log_params} + self._logger.info(log_error_message, extra=extra) + metrics.incr( + SLACK_NOTIFY_RECIPIENT_FAILURE_DATADOG_METRIC, + sample_rate=1.0, + tags={"ok": e.response.get("ok", False), "status": e.response.status_code}, + ) + lifecycle.add_extras( + {k: str(v) for k, v in log_params.items() if isinstance(v, (int, str))} + ) + record_lifecycle_termination_level(lifecycle, e) diff --git a/tests/sentry/integrations/slack/service/test_slack_service.py b/tests/sentry/integrations/slack/service/test_slack_service.py index 14493e427a4c16..19957e8508f9a8 100644 --- a/tests/sentry/integrations/slack/service/test_slack_service.py +++ b/tests/sentry/integrations/slack/service/test_slack_service.py @@ -9,11 +9,13 @@ from sentry.integrations.repository.issue_alert import IssueAlertNotificationMessage from sentry.integrations.slack.sdk_client import SlackSdkClient from sentry.integrations.slack.service import RuleDataError, SlackService +from sentry.integrations.types import EventLifecycleOutcome from sentry.models.activity import Activity from sentry.models.options.organization_option import OrganizationOption from sentry.models.rulefirehistory import RuleFireHistory from sentry.notifications.models.notificationmessage import NotificationMessage from sentry.silo.base import SiloMode +from sentry.testutils.asserts import assert_slo_metric from sentry.testutils.cases import TestCase from sentry.testutils.silo import assume_test_silo_mode from sentry.types.activity import ActivityType @@ -157,8 +159,9 @@ def test_no_parent_notification(self, mock_handle): self.service.notify_all_threads_for_activity(activity=self.activity) assert not mock_handle.called + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @mock.patch("sentry.integrations.slack.service.SlackService._handle_parent_notification") - def test_calls_handle_parent_notification_sdk_client(self, mock_handle): + def test_calls_handle_parent_notification_sdk_client(self, mock_handle, mock_record): parent_notification = IssueAlertNotificationMessage.from_model( instance=self.parent_notification ) @@ -170,6 +173,8 @@ def test_calls_handle_parent_notification_sdk_client(self, mock_handle): # check client type assert isinstance(mock_handle.call_args.kwargs["client"], SlackSdkClient) + assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + class TestHandleParentNotification(TestCase): def setUp(self) -> None: @@ -233,8 +238,9 @@ def setUp(self) -> None: rule_fire_history=self.slack_rule_fire_history, ) + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @mock.patch("slack_sdk.web.client.WebClient._perform_urllib_http_request") - def test_handles_parent_notification_sdk(self, mock_api_call): + def test_handles_parent_notification_sdk(self, mock_api_call, mock_record): mock_api_call.return_value = { "body": orjson.dumps({"ok": True}).decode(), "headers": {}, @@ -246,8 +252,12 @@ def test_handles_parent_notification_sdk(self, mock_api_call): client=SlackSdkClient(integration_id=self.integration.id), ) + assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") def test_handles_parent_notification_sdk_error( self, + mock_record, ) -> None: with pytest.raises(SlackApiError): self.service._handle_parent_notification( @@ -256,6 +266,8 @@ def test_handles_parent_notification_sdk_error( client=SlackSdkClient(integration_id=self.integration.id), ) + assert_slo_metric(mock_record, EventLifecycleOutcome.FAILURE) + def test_raises_exception_when_parent_notification_does_not_have_rule_fire_history_data( self, ) -> None: From f93a03089b8a75e92d52f7b5f4f0acacfabe5f14 Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Tue, 10 Dec 2024 15:09:11 -0500 Subject: [PATCH 030/757] test(widget-builder): Fix sort test (#81927) --- .../widgetBuilder/components/sortBySelector.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx index 8971f2229d993a..31c51c0661c454 100644 --- a/static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/sortBySelector.spec.tsx @@ -110,7 +110,7 @@ describe('WidgetBuilderSortBySelector', function () { expect(mockNavigate).toHaveBeenLastCalledWith( expect.objectContaining({ ...router.location, - query: expect.objectContaining({sort: ['-count']}), + query: expect.objectContaining({sort: ['-count()']}), }) ); @@ -119,7 +119,7 @@ describe('WidgetBuilderSortBySelector', function () { expect(mockNavigate).toHaveBeenLastCalledWith( expect.objectContaining({ ...router.location, - query: expect.objectContaining({sort: ['count']}), + query: expect.objectContaining({sort: ['count()']}), }) ); }); From a7387f1514d780ea96310b5a75d21bd947867a02 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:57:02 -0500 Subject: [PATCH 031/757] ref(dashboards): Export Widget component props (#81924) Useful to other components and these names are better. --- .../dashboards/widgets/bigNumberWidget/bigNumberWidget.tsx | 4 ++-- .../dashboards/widgets/lineChartWidget/lineChartWidget.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.tsx b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.tsx index a12f0c3e58112b..62856d97a67a8a 100644 --- a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.tsx +++ b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.tsx @@ -21,12 +21,12 @@ import type {StateProps} from '../common/types'; import {DEEMPHASIS_COLOR_NAME, LOADING_PLACEHOLDER} from './settings'; -interface Props +export interface BigNumberWidgetProps extends StateProps, Omit, Partial {} -export function BigNumberWidget(props: Props) { +export function BigNumberWidget(props: BigNumberWidgetProps) { const {value, previousPeriodValue, field} = props; if (props.isLoading) { diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx index 283dfa55c35d8a..5cf2284a646265 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx @@ -15,12 +15,12 @@ import { import {MISSING_DATA_MESSAGE, X_GUTTER, Y_GUTTER} from '../common/settings'; import type {StateProps} from '../common/types'; -interface Props +export interface LineChartWidgetProps extends StateProps, Omit, Partial {} -export function LineChartWidget(props: Props) { +export function LineChartWidget(props: LineChartWidgetProps) { const {timeseries} = props; if (props.isLoading) { From c3b94ea1f87a5b32aab6de2a5711dd467ec17444 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 10 Dec 2024 13:00:55 -0800 Subject: [PATCH 032/757] feat(ui): Add dark app loading theme (#81611) --- src/sentry/templates/sentry/base-react.html | 2 ++ src/sentry/web/frontend/react_page.py | 11 ++++++++++- static/less/base.less | 16 ++++++++++++++++ static/less/shared-components.less | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/sentry/templates/sentry/base-react.html b/src/sentry/templates/sentry/base-react.html index d8c0ffd686f3b1..ab3e218318e48a 100644 --- a/src/sentry/templates/sentry/base-react.html +++ b/src/sentry/templates/sentry/base-react.html @@ -18,3 +18,5 @@ {% endblock %} + +{% block wrapperclass %}{{ user_theme }}{% endblock %} diff --git a/src/sentry/web/frontend/react_page.py b/src/sentry/web/frontend/react_page.py index 81e5c4d26b8051..68e65ed8d01463 100644 --- a/src/sentry/web/frontend/react_page.py +++ b/src/sentry/web/frontend/react_page.py @@ -88,6 +88,14 @@ def dns_prefetch(self) -> list[str]: def handle_react(self, request: Request, **kwargs) -> HttpResponse: org_context = getattr(self, "active_organization", None) + react_config = get_client_config(request, org_context) + + user_theme = "" + if react_config.get("user", None) and react_config["user"].get("options", {}).get( + "theme", None + ): + user_theme = f"theme-{react_config['user']['options']['theme']}" + context = { "CSRF_COOKIE_NAME": settings.CSRF_COOKIE_NAME, "meta_tags": [ @@ -100,7 +108,8 @@ def handle_react(self, request: Request, **kwargs) -> HttpResponse: # Since we already have it here from the OrganizationMixin, we can # save some work and render it faster. "org_context": org_context, - "react_config": get_client_config(request, org_context), + "react_config": react_config, + "user_theme": user_theme, } # Force a new CSRF token to be generated and set in user's diff --git a/static/less/base.less b/static/less/base.less index d96c1dd87365bc..6db46ce9526263 100644 --- a/static/less/base.less +++ b/static/less/base.less @@ -32,6 +32,22 @@ body { min-height: 100vh; } +// Applied to body +body.theme-dark { + // theme.surface200 + background: #1A141F; + // theme.gray400 + color: #D6D0DC; +} +body.theme-system { + @media (prefers-color-scheme: dark) { + // theme.surface200 + background: #1A141F; + // theme.gray400 + color: #D6D0DC; + } +} + h1, h2, h3, diff --git a/static/less/shared-components.less b/static/less/shared-components.less index e32f2b1a5db5ec..d71932ee585f20 100644 --- a/static/less/shared-components.less +++ b/static/less/shared-components.less @@ -438,6 +438,11 @@ table.table.key-value { } } +.theme-dark .loading .loading-indicator { + // theme.surface200 + background: #1A141F; +} + @-webkit-keyframes loading { 0% { -webkit-transform: rotate(0deg); @@ -519,6 +524,19 @@ table.table.key-value { } } +.theme-dark .loading.triangle .loading-indicator { + filter: invert(100%); + opacity: 0.8; +} +body.theme-system { + @media (prefers-color-scheme: dark) { + .loading.triangle .loading-indicator { + filter: invert(100%); + opacity: 0.8; + } + } +} + /** * Box * ============================================================================ From c7ee8970a519ed8055f2afa029a5a3e739f6ed73 Mon Sep 17 00:00:00 2001 From: Harshitha Durai <76853136+harshithadurai@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:19:04 -0500 Subject: [PATCH 033/757] feat(dashboards): add success message when favoriting dashboards (#81887) Add success message after favoriting/unfavoriting dashboards --- static/app/actionCreators/dashboards.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/actionCreators/dashboards.tsx b/static/app/actionCreators/dashboards.tsx index 845fe1c8227db6..846145e26b331c 100644 --- a/static/app/actionCreators/dashboards.tsx +++ b/static/app/actionCreators/dashboards.tsx @@ -1,6 +1,6 @@ import omit from 'lodash/omit'; -import {addErrorMessage} from 'sentry/actionCreators/indicator'; +import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import type {Client} from 'sentry/api'; import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters'; import {t} from 'sentry/locale'; @@ -113,6 +113,7 @@ export async function updateDashboardFavorite( }, } ); + addSuccessMessage(isFavorited ? t('Added as favorite') : t('Removed as favorite')); } catch (response) { const errorResponse = response?.responseJSON ?? null; if (errorResponse) { From 040076dd95723adb4fd9587869817278868045f1 Mon Sep 17 00:00:00 2001 From: Neo Huang <126607112+kneeyo1@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:21:13 -0800 Subject: [PATCH 034/757] Better logging for backpressure (#81648) This just adds host/port of the service to the logs. --- src/sentry/processing/backpressure/memory.py | 31 ++++++++++++++++++- src/sentry/processing/backpressure/monitor.py | 10 ++++++ .../processing/backpressure/test_redis.py | 24 ++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/sentry/processing/backpressure/memory.py b/src/sentry/processing/backpressure/memory.py index 3a336377043ce2..7b2af742fe0597 100644 --- a/src/sentry/processing/backpressure/memory.py +++ b/src/sentry/processing/backpressure/memory.py @@ -13,6 +13,8 @@ class ServiceMemory: used: int available: int percentage: float + host: str | None = None + port: int | None = None def __init__(self, name: str, used: int, available: int): self.name = name @@ -21,6 +23,12 @@ def __init__(self, name: str, used: int, available: int): self.percentage = used / available +@dataclass +class NodeInfo: + host: str | None + port: int | None + + def query_rabbitmq_memory_usage(host: str) -> ServiceMemory: """Returns the currently used memory and the memory limit of a RabbitMQ host. @@ -51,6 +59,23 @@ def get_memory_usage(node_id: str, info: Mapping[str, Any]) -> ServiceMemory: return ServiceMemory(node_id, memory_used, memory_available) +def get_host_port_info(node_id: str, cluster: Cluster) -> NodeInfo: + """ + Extract the host and port of the redis node in the cluster. + """ + try: + if isinstance(cluster, RedisCluster): + # RedisCluster node mapping + node = cluster.connection_pool.nodes.nodes.get(node_id) + return NodeInfo(node["host"], node["port"]) + else: + # rb.Cluster node mapping + node = cluster.hosts[node_id] + return NodeInfo(node.host, node.port) + except Exception: + return NodeInfo(None, None) + + def iter_cluster_memory_usage(cluster: Cluster) -> Generator[ServiceMemory, None, None]: """ A generator that yields redis `INFO` results for each of the nodes in the `cluster`. @@ -65,4 +90,8 @@ def iter_cluster_memory_usage(cluster: Cluster) -> Generator[ServiceMemory, None cluster_info = promise.value for node_id, info in cluster_info.items(): - yield get_memory_usage(node_id, info) + node_info = get_host_port_info(node_id, cluster) + memory_usage = get_memory_usage(node_id, info) + memory_usage.host = node_info.host + memory_usage.port = node_info.port + yield memory_usage diff --git a/src/sentry/processing/backpressure/monitor.py b/src/sentry/processing/backpressure/monitor.py index f9c233d6dd409a..bcd856c6b00d0d 100644 --- a/src/sentry/processing/backpressure/monitor.py +++ b/src/sentry/processing/backpressure/monitor.py @@ -85,10 +85,12 @@ def check_service_health(services: Mapping[str, Service]) -> MutableMapping[str, reasons = [] logger.info("Checking service `%s` (configured high watermark: %s):", name, high_watermark) + memory = None try: for memory in check_service_memory(service): if memory.percentage >= high_watermark: reasons.append(memory) + logger.info("Checking node: %s:%s", memory.host, memory.port) logger.info( " name: %s, used: %s, available: %s, percentage: %s", memory.name, @@ -101,6 +103,14 @@ def check_service_health(services: Mapping[str, Service]) -> MutableMapping[str, scope.set_tag("service", name) sentry_sdk.capture_exception(e) unhealthy_services[name] = e + host = memory.host if memory else "unknown" + port = memory.port if memory else "unknown" + logger.exception( + "Error while processing node %s:%s for service %s", + host, + port, + service, + ) else: unhealthy_services[name] = reasons diff --git a/tests/sentry/processing/backpressure/test_redis.py b/tests/sentry/processing/backpressure/test_redis.py index 5c111c4e0f68f6..a327aa48cc96ca 100644 --- a/tests/sentry/processing/backpressure/test_redis.py +++ b/tests/sentry/processing/backpressure/test_redis.py @@ -21,6 +21,8 @@ def test_rb_cluster_returns_some_usage() -> None: assert memory.used > 0 assert memory.available > 0 assert 0.0 < memory.percentage < 1.0 + assert memory.host == "localhost" + assert memory.port == 6379 @use_redis_cluster() @@ -35,6 +37,8 @@ def test_redis_cluster_cluster_returns_some_usage() -> None: assert memory.used > 0 assert memory.available > 0 assert 0.0 < memory.percentage < 1.0 + assert memory.host == "127.0.0.1" + assert memory.port in {7000, 7001, 7002, 7003, 7004, 7005} @use_redis_cluster(high_watermark=100) @@ -47,6 +51,14 @@ def test_redis_health(): assert isinstance(redis_services, list) assert len(redis_services) == 0 + usage = list(iter_cluster_memory_usage(services["redis"].cluster)) + for memory in usage: + assert memory.used >= 0 + assert memory.available > 0 + assert 0.0 < memory.percentage <= 1.0 + assert memory.host == "127.0.0.1" + assert memory.port in {7000, 7001, 7002, 7003, 7004, 7005} + @use_redis_cluster(high_watermark=0) def test_redis_unhealthy_state(): @@ -57,3 +69,15 @@ def test_redis_unhealthy_state(): redis_services = unhealthy_services.get("redis") assert isinstance(redis_services, list) assert len(redis_services) == 6 + + usage = list(iter_cluster_memory_usage(services["redis"].cluster)) + for memory in usage: + assert memory.used >= 0 + assert memory.available > 0 + assert 0.0 < memory.percentage <= 1.0 + assert memory.host == "127.0.0.1" + assert memory.port in {7000, 7001, 7002, 7003, 7004, 7005} + + for memory in redis_services: + assert memory.host == "127.0.0.1" + assert memory.port in {7000, 7001, 7002, 7003, 7004, 7005} From 524f5ca6a715a8ee94de5254472fbc2095031a50 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:26:56 -0800 Subject: [PATCH 035/757] feat(sdk): Upgrade @sentry SDKs to v8.43.0 (#81925) Planning to instrument the new browser `featureFlagsIntegration` to track internal flag evaluations. We need the latest release for this (https://github.com/getsentry/sentry-javascript/releases/tag/8.43.0). @sentry/types was deprecated so I've changed imports to use @sentry/core --- build-utils/sentry-instrumentation.ts | 2 +- package.json | 12 +- static/app/bootstrap/initializeSdk.tsx | 3 +- static/app/components/badge/featureBadge.tsx | 2 +- .../devtoolbar/hooks/useReplayRecorder.tsx | 2 +- .../hooks/useSentryClientAndScope.tsx | 2 +- .../events/featureFlags/useIssueEvents.tsx | 2 +- .../featureFeedback/feedbackModal.tsx | 2 +- .../app/components/feedback/widget/types.ts | 2 +- .../components/group/externalIssueForm.tsx | 2 +- static/app/types/event.tsx | 2 +- static/app/utils/analytics.tsx | 2 +- static/app/utils/featureObserver.ts | 2 +- .../app/utils/performanceForSentry/index.tsx | 2 +- .../utils/profiling/profile/importProfile.tsx | 2 +- static/app/utils/profiling/profile/utils.tsx | 2 +- static/app/utils/useFeedbackForm.tsx | 2 +- static/app/views/routeError.tsx | 2 +- .../settings/featureFlags/useUserFromId.tsx | 2 +- yarn.lock | 550 +++++++++--------- 20 files changed, 298 insertions(+), 301 deletions(-) diff --git a/build-utils/sentry-instrumentation.ts b/build-utils/sentry-instrumentation.ts index c6797c1e22f490..b680f98a34b263 100644 --- a/build-utils/sentry-instrumentation.ts +++ b/build-utils/sentry-instrumentation.ts @@ -1,6 +1,6 @@ /* eslint-env node */ +import type {Span} from '@sentry/core'; import type * as Sentry from '@sentry/node'; -import type {Span} from '@sentry/types'; import crypto from 'node:crypto'; import https from 'node:https'; import os from 'node:os'; diff --git a/package.json b/package.json index 0a27d0d93efa85..72ef9ba42cfbd7 100644 --- a/package.json +++ b/package.json @@ -56,13 +56,13 @@ "@sentry-internal/rrweb": "2.26.0", "@sentry-internal/rrweb-player": "2.26.0", "@sentry-internal/rrweb-snapshot": "2.26.0", - "@sentry/core": "8.39.0-beta.0", - "@sentry/node": "8.39.0-beta.0", - "@sentry/react": "8.39.0-beta.0", + "@sentry/core": "8.43.0", + "@sentry/node": "8.43.0", + "@sentry/react": "8.43.0", "@sentry/release-parser": "^1.3.1", "@sentry/status-page-list": "^0.3.0", - "@sentry/types": "8.39.0-beta.0", - "@sentry/utils": "8.39.0-beta.0", + "@sentry/types": "8.43.0", + "@sentry/utils": "8.43.0", "@sentry/webpack-plugin": "^2.22.4", "@spotlightjs/spotlight": "^2.0.0-alpha.1", "@tanstack/react-query": "^5.56.2", @@ -179,7 +179,7 @@ "@emotion/eslint-plugin": "^11.12.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "@sentry/jest-environment": "6.0.0", - "@sentry/profiling-node": "8.39.0-beta.0", + "@sentry/profiling-node": "8.43.0", "@styled/typescript-styled-plugin": "^1.0.1", "@testing-library/dom": "10.1.0", "@testing-library/jest-dom": "6.4.5", diff --git a/static/app/bootstrap/initializeSdk.tsx b/static/app/bootstrap/initializeSdk.tsx index 80ecff878cc382..ab3a7671a80e64 100644 --- a/static/app/bootstrap/initializeSdk.tsx +++ b/static/app/bootstrap/initializeSdk.tsx @@ -1,7 +1,6 @@ // eslint-disable-next-line simple-import-sort/imports import * as Sentry from '@sentry/react'; -import {_browserPerformanceTimeOriginMode} from '@sentry/utils'; -import type {Event} from '@sentry/types'; +import {type Event, _browserPerformanceTimeOriginMode} from '@sentry/core'; import {SENTRY_RELEASE_VERSION, SPA_DSN} from 'sentry/constants'; import type {Config} from 'sentry/types/system'; diff --git a/static/app/components/badge/featureBadge.tsx b/static/app/components/badge/featureBadge.tsx index 1d400ef746e844..f88860a620c577 100644 --- a/static/app/components/badge/featureBadge.tsx +++ b/static/app/components/badge/featureBadge.tsx @@ -1,8 +1,8 @@ import {Fragment, type ReactNode} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import type {SeverityLevel} from '@sentry/core'; import {captureException, withScope} from '@sentry/react'; -import type {SeverityLevel} from '@sentry/types'; import Badge from 'sentry/components/badge/badge'; import CircleIndicator from 'sentry/components/circleIndicator'; diff --git a/static/app/components/devtoolbar/hooks/useReplayRecorder.tsx b/static/app/components/devtoolbar/hooks/useReplayRecorder.tsx index 70d1be99034da1..d88b66437c3631 100644 --- a/static/app/components/devtoolbar/hooks/useReplayRecorder.tsx +++ b/static/app/components/devtoolbar/hooks/useReplayRecorder.tsx @@ -1,6 +1,6 @@ import {useCallback, useEffect, useState} from 'react'; +import type {ReplayRecordingMode} from '@sentry/core'; import type {replayIntegration} from '@sentry/react'; -import type {ReplayRecordingMode} from '@sentry/types'; import useConfiguration from 'sentry/components/devtoolbar/hooks/useConfiguration'; import {useSessionStorage} from 'sentry/utils/useSessionStorage'; diff --git a/static/app/components/devtoolbar/hooks/useSentryClientAndScope.tsx b/static/app/components/devtoolbar/hooks/useSentryClientAndScope.tsx index b95f283e1c4dd7..3c492995693b36 100644 --- a/static/app/components/devtoolbar/hooks/useSentryClientAndScope.tsx +++ b/static/app/components/devtoolbar/hooks/useSentryClientAndScope.tsx @@ -1,4 +1,4 @@ -import type {Client, Scope} from '@sentry/types'; +import type {Client, Scope} from '@sentry/core'; type V8Carrier = { stack: { diff --git a/static/app/components/events/featureFlags/useIssueEvents.tsx b/static/app/components/events/featureFlags/useIssueEvents.tsx index 11c7fd95a66f3a..b4196feaedb113 100644 --- a/static/app/components/events/featureFlags/useIssueEvents.tsx +++ b/static/app/components/events/featureFlags/useIssueEvents.tsx @@ -1,4 +1,4 @@ -import type {Event} from '@sentry/types'; +import type {Event} from '@sentry/core'; import {useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; diff --git a/static/app/components/featureFeedback/feedbackModal.tsx b/static/app/components/featureFeedback/feedbackModal.tsx index 6e42a489b38c03..79767ae2bb9cd7 100644 --- a/static/app/components/featureFeedback/feedbackModal.tsx +++ b/static/app/components/featureFeedback/feedbackModal.tsx @@ -1,6 +1,7 @@ import {Fragment, useCallback, useMemo, useState} from 'react'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import type {Event} from '@sentry/core'; import { BrowserClient, captureFeedback, @@ -8,7 +9,6 @@ import { getDefaultIntegrations, makeFetchTransport, } from '@sentry/react'; -import type {Event} from '@sentry/types'; import cloneDeep from 'lodash/cloneDeep'; import {addSuccessMessage} from 'sentry/actionCreators/indicator'; diff --git a/static/app/components/feedback/widget/types.ts b/static/app/components/feedback/widget/types.ts index 8c3ef9b01c8292..a53a611c3a877e 100644 --- a/static/app/components/feedback/widget/types.ts +++ b/static/app/components/feedback/widget/types.ts @@ -1,4 +1,4 @@ -import type {Event} from '@sentry/types'; +import type {Event} from '@sentry/core'; /** * NOTE: These types are still considered Beta and subject to change. diff --git a/static/app/components/group/externalIssueForm.tsx b/static/app/components/group/externalIssueForm.tsx index df53acb18a931c..44d41805f05e5c 100644 --- a/static/app/components/group/externalIssueForm.tsx +++ b/static/app/components/group/externalIssueForm.tsx @@ -1,5 +1,5 @@ +import type {Span} from '@sentry/core'; import * as Sentry from '@sentry/react'; -import type {Span} from '@sentry/types'; import {addSuccessMessage} from 'sentry/actionCreators/indicator'; import type DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; diff --git a/static/app/types/event.tsx b/static/app/types/event.tsx index 070a1369c83750..51a24ea3b52a4b 100644 --- a/static/app/types/event.tsx +++ b/static/app/types/event.tsx @@ -1,4 +1,4 @@ -import type {CloudResourceContext} from '@sentry/types'; +import type {CloudResourceContext} from '@sentry/core'; import type {CultureContext} from 'sentry/components/events/contexts/knownContext/culture'; import type {MissingInstrumentationContext} from 'sentry/components/events/contexts/knownContext/missingInstrumentation'; diff --git a/static/app/utils/analytics.tsx b/static/app/utils/analytics.tsx index 35bd66487781cf..cdd067fbd01108 100644 --- a/static/app/utils/analytics.tsx +++ b/static/app/utils/analytics.tsx @@ -1,5 +1,5 @@ +import type {Span} from '@sentry/core'; import * as Sentry from '@sentry/react'; -import type {Span} from '@sentry/types'; import HookStore from 'sentry/stores/hookStore'; import type {Hooks} from 'sentry/types/hooks'; diff --git a/static/app/utils/featureObserver.ts b/static/app/utils/featureObserver.ts index 0d3a66043ce9d3..38df3ab8129e3b 100644 --- a/static/app/utils/featureObserver.ts +++ b/static/app/utils/featureObserver.ts @@ -1,4 +1,4 @@ -import type {FeatureFlagContext} from '@sentry/types/build/types/context'; +import type {FeatureFlagContext} from '@sentry/core/build/types/types-hoist/context'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; diff --git a/static/app/utils/performanceForSentry/index.tsx b/static/app/utils/performanceForSentry/index.tsx index 9845b2637aa224..de15d5d3b72911 100644 --- a/static/app/utils/performanceForSentry/index.tsx +++ b/static/app/utils/performanceForSentry/index.tsx @@ -1,7 +1,7 @@ import type {ProfilerOnRenderCallback, ReactNode} from 'react'; import {Fragment, Profiler, useEffect, useRef} from 'react'; +import type {MeasurementUnit, Span, TransactionEvent} from '@sentry/core'; import * as Sentry from '@sentry/react'; -import type {MeasurementUnit, Span, TransactionEvent} from '@sentry/types'; import { _browserPerformanceTimeOriginMode, browserPerformanceTimeOrigin, diff --git a/static/app/utils/profiling/profile/importProfile.tsx b/static/app/utils/profiling/profile/importProfile.tsx index 701ae5d446fd4d..ad50bb10e36d7a 100644 --- a/static/app/utils/profiling/profile/importProfile.tsx +++ b/static/app/utils/profiling/profile/importProfile.tsx @@ -1,5 +1,5 @@ +import type {Span} from '@sentry/core'; import * as Sentry from '@sentry/react'; -import type {Span} from '@sentry/types'; import type {Image} from 'sentry/types/debugImage'; diff --git a/static/app/utils/profiling/profile/utils.tsx b/static/app/utils/profiling/profile/utils.tsx index d23831ccd854ab..326c07dd6bc91f 100644 --- a/static/app/utils/profiling/profile/utils.tsx +++ b/static/app/utils/profiling/profile/utils.tsx @@ -1,5 +1,5 @@ +import type {Span} from '@sentry/core'; import * as Sentry from '@sentry/react'; -import type {Span} from '@sentry/types'; import {defined} from 'sentry/utils'; import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame'; diff --git a/static/app/utils/useFeedbackForm.tsx b/static/app/utils/useFeedbackForm.tsx index 070379cbe3357b..0fcf451e56dc0d 100644 --- a/static/app/utils/useFeedbackForm.tsx +++ b/static/app/utils/useFeedbackForm.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, } from 'react'; -import type {FeedbackModalIntegration} from '@sentry/types'; +import type {FeedbackModalIntegration} from '@sentry/core'; import { useFeedback, diff --git a/static/app/views/routeError.tsx b/static/app/views/routeError.tsx index 3f7acf24028084..530d986187cd4f 100644 --- a/static/app/views/routeError.tsx +++ b/static/app/views/routeError.tsx @@ -1,7 +1,7 @@ import {useEffect} from 'react'; import styled from '@emotion/styled'; +import type {Scope} from '@sentry/core'; import * as Sentry from '@sentry/react'; -import type {Scope} from '@sentry/types'; import {getLastEventId} from 'sentry/bootstrap/initializeSdk'; import {Alert} from 'sentry/components/alert'; diff --git a/static/app/views/settings/featureFlags/useUserFromId.tsx b/static/app/views/settings/featureFlags/useUserFromId.tsx index 837dbe8300218c..8b15847c47db1f 100644 --- a/static/app/views/settings/featureFlags/useUserFromId.tsx +++ b/static/app/views/settings/featureFlags/useUserFromId.tsx @@ -1,4 +1,4 @@ -import type {User} from '@sentry/types'; +import type {User} from '@sentry/core'; import {useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; diff --git a/yarn.lock b/yarn.lock index 31b26dad3c229c..8ee3923363182f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2105,17 +2105,10 @@ dependencies: "@opentelemetry/api" "^1.0.0" -"@opentelemetry/api-logs@0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz#c478cbd8120ec2547b64edfa03a552cfe42170be" - integrity sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw== - dependencies: - "@opentelemetry/api" "^1.0.0" - -"@opentelemetry/api-logs@0.54.2": - version "0.54.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.54.2.tgz#bb8aa11cdc69b327b58d7e10cc2bc26bf540421f" - integrity sha512-4MTVwwmLgUh5QrJnZpYo6YRO5IBLAggf2h8gWDblwRagDStY13aEvt7gGk3jewrMaPlHiF83fENhIx0HO97/cQ== +"@opentelemetry/api-logs@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz#68f8c51ca905c260b610c8a3c67d3f9fa3d59a45" + integrity sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g== dependencies: "@opentelemetry/api" "^1.3.0" @@ -2124,230 +2117,239 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== -"@opentelemetry/context-async-hooks@^1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz#810bff2fcab84ec51f4684aff2d21f6c057d9e73" - integrity sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ== +"@opentelemetry/context-async-hooks@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.29.0.tgz#3b3836c913834afa7720fdcf9687620f49b2cf37" + integrity sha512-TKT91jcFXgHyIDF1lgJF3BHGIakn6x0Xp7Tq3zoS3TMPzT9IlP0xEavWP8C1zGjU9UmZP2VR1tJhW9Az1A3w8Q== -"@opentelemetry/core@1.26.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.25.1", "@opentelemetry/core@^1.8.0": +"@opentelemetry/core@1.26.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.8.0": version "1.26.0" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.26.0.tgz#7d84265aaa850ed0ca5813f97d831155be42b328" integrity sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ== dependencies: "@opentelemetry/semantic-conventions" "1.27.0" -"@opentelemetry/instrumentation-amqplib@^0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.43.0.tgz#e18b7d763b69c605a7abf9869e1c278f9bfdc1eb" - integrity sha512-ALjfQC+0dnIEcvNYsbZl/VLh7D2P1HhFF4vicRKHhHFIUV3Shpg4kXgiek5PLhmeKSIPiUB25IYH5RIneclL4A== +"@opentelemetry/core@1.29.0", "@opentelemetry/core@^1.26.0", "@opentelemetry/core@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.29.0.tgz#a9397dfd9a8b37b2435b5e44be16d39ec1c82bd9" + integrity sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA== + dependencies: + "@opentelemetry/semantic-conventions" "1.28.0" + +"@opentelemetry/instrumentation-amqplib@^0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.45.0.tgz#747d72e38ff89266670e730ead90b85b6edc62d3" + integrity sha512-SlKLsOS65NGMIBG1Lh/hLrMDU9WzTUF25apnV6ZmWZB1bBmUwan7qrwwrTu1cL5LzJWCXOdZPuTaxP7pC9qxnQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-connect@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.40.0.tgz#cb151b860ad8a711ebce4d7e025dcde95e4ba2c5" - integrity sha512-3aR/3YBQ160siitwwRLjwqrv2KBT16897+bo6yz8wIfel6nWOxTZBJudcbsK3p42pTC7qrbotJ9t/1wRLpv79Q== +"@opentelemetry/instrumentation-connect@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.42.0.tgz#daebedbe65068746c9db0eee6e3a636a0912d251" + integrity sha512-bOoYHBmbnq/jFaLHmXJ55VQ6jrH5fHDMAPjFM0d3JvR0dvIqW7anEoNC33QqYGFYUfVJ50S0d/eoyF61ALqQuA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/connect" "3.4.36" -"@opentelemetry/instrumentation-dataloader@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.12.0.tgz#de03a3948dec4f15fed80aa424d6bd5d6a8d10c7" - integrity sha512-pnPxatoFE0OXIZDQhL2okF//dmbiWFzcSc8pUg9TqofCLYZySSxDCgQc69CJBo5JnI3Gz1KP+mOjS4WAeRIH4g== +"@opentelemetry/instrumentation-dataloader@0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.15.0.tgz#c3ac6f41672961a489080edd2c59aceebe412798" + integrity sha512-5fP35A2jUPk4SerVcduEkpbRAIoqa2PaP5rWumn01T1uSbavXNccAr3Xvx1N6xFtZxXpLJq4FYqGFnMgDWgVng== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-express@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.44.0.tgz#51dc11e3152ffbee1c4e389298aac30231c8270a" - integrity sha512-GWgibp6Q0wxyFaaU8ERIgMMYgzcHmGrw3ILUtGchLtLncHNOKk0SNoWGqiylXWWT4HTn5XdV8MGawUgpZh80cA== +"@opentelemetry/instrumentation-express@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.46.0.tgz#8dfbc9dc567e2e864a00a6a7edfbec2dd8482056" + integrity sha512-BCEClDj/HPq/1xYRAlOr6z+OUnbp2eFp18DSrgyQz4IT9pkdYk8eWHnMi9oZSqlC6J5mQzkFmaW5RrKb1GLQhg== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-fastify@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.41.0.tgz#5e1d00383756f3a8cc2ea4a9d15f9f7510cec571" - integrity sha512-pNRjFvf0mvqfJueaeL/qEkuGJwgtE5pgjIHGYwjc2rMViNCrtY9/Sf+Nu8ww6dDd/Oyk2fwZZP7i0XZfCnETrA== +"@opentelemetry/instrumentation-fastify@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.43.0.tgz#855e259733bd75e21cb54cc110a7910861b200a4" + integrity sha512-Lmdsg7tYiV+K3/NKVAQfnnLNGmakUOFdB0PhoTh2aXuSyCmyNnnDvhn2MsArAPTZ68wnD5Llh5HtmiuTkf+DyQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-fs@0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.16.0.tgz#aa1cc3aa81011ad9843a0156b200f06f31ffa03e" - integrity sha512-hMDRUxV38ln1R3lNz6osj3YjlO32ykbHqVrzG7gEhGXFQfu7LJUx8t9tEwE4r2h3CD4D0Rw4YGDU4yF4mP3ilg== +"@opentelemetry/instrumentation-fs@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.18.0.tgz#6ef0e58cda3212ce2cd17bddc4dd74f768fd74c0" + integrity sha512-kC40y6CEMONm8/MWwoF5GHWIC7gOdF+g3sgsjfwJaUkgD6bdWV+FgG0XApqSbTQndICKzw3RonVk8i7s6mHqhA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-generic-pool@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.39.0.tgz#2b9af16ad82d5cbe67125c0125753cecd162a728" - integrity sha512-y4v8Y+tSfRB3NNBvHjbjrn7rX/7sdARG7FuK6zR8PGb28CTa0kHpEGCJqvL9L8xkTNvTXo+lM36ajFGUaK1aNw== +"@opentelemetry/instrumentation-generic-pool@0.42.0": + version "0.42.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.42.0.tgz#6c6c6dcf2300e803acb22b2b914c6053acb80bf3" + integrity sha512-J4QxqiQ1imtB9ogzsOnHra0g3dmmLAx4JCeoK3o0rFes1OirljNHnO8Hsj4s1jAir8WmWvnEEQO1y8yk6j2tog== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-graphql@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.44.0.tgz#6fce8e2f303d16810bf8a03148cad6e8e6119de1" - integrity sha512-FYXTe3Bv96aNpYktqm86BFUTpjglKD0kWI5T5bxYkLUPEPvFn38vWGMJTGrDMVou/i55E4jlWvcm6hFIqLsMbg== +"@opentelemetry/instrumentation-graphql@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.46.0.tgz#fbcf0844656c759294c03c30c471fc4862209a01" + integrity sha512-tplk0YWINSECcK89PGM7IVtOYenXyoOuhOQlN0X0YrcDUfMS4tZMKkVc0vyhNWYYrexnUHwNry2YNBNugSpjlQ== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-hapi@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.41.0.tgz#de8711907256d8fae1b5faf71fc825cef4a7ddbb" - integrity sha512-jKDrxPNXDByPlYcMdZjNPYCvw0SQJjN+B1A+QH+sx+sAHsKSAf9hwFiJSrI6C4XdOls43V/f/fkp9ITkHhKFbQ== +"@opentelemetry/instrumentation-hapi@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.44.0.tgz#5b4524bef636209ba6cc95cfbb976b605c2946cd" + integrity sha512-4HdNIMNXWK1O6nsaQOrACo83QWEVoyNODTdVDbUqtqXiv2peDfD0RAPhSQlSGWLPw3S4d9UoOmrV7s2HYj6T2A== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-http@0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz#0d806adf1b3aba036bc46e16162e3c0dbb8a6b60" - integrity sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ== +"@opentelemetry/instrumentation-http@0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.56.0.tgz#f7a9e1bb4126c0d918775c1368a42b8afd5a48a2" + integrity sha512-/bWHBUAq8VoATnH9iLk5w8CE9+gj+RgYSUphe7hry472n6fYl7+4PvuScoQMdmSUTprKq/gyr2kOWL6zrC7FkQ== dependencies: - "@opentelemetry/core" "1.26.0" - "@opentelemetry/instrumentation" "0.53.0" - "@opentelemetry/semantic-conventions" "1.27.0" + "@opentelemetry/core" "1.29.0" + "@opentelemetry/instrumentation" "0.56.0" + "@opentelemetry/semantic-conventions" "1.28.0" + forwarded-parse "2.1.2" semver "^7.5.2" -"@opentelemetry/instrumentation-ioredis@0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.43.0.tgz#dbadabaeefc4cb47c406f878444f1bcac774fa89" - integrity sha512-i3Dke/LdhZbiUAEImmRG3i7Dimm/BD7t8pDDzwepSvIQ6s2X6FPia7561gw+64w+nx0+G9X14D7rEfaMEmmjig== +"@opentelemetry/instrumentation-ioredis@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.46.0.tgz#ec230466813f8ce82eb9ca9b23308ccfa460ce2b" + integrity sha512-sOdsq8oGi29V58p1AkefHvuB3l2ymP1IbxRIX3y4lZesQWKL8fLhBmy8xYjINSQ5gHzWul2yoz7pe7boxhZcqQ== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-kafkajs@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.4.0.tgz#c1fe0de45a65a66581be0d7422f6828cc806b3bb" - integrity sha512-I9VwDG314g7SDL4t8kD/7+1ytaDBRbZQjhVaQaVIDR8K+mlsoBhLsWH79yHxhHQKvwCSZwqXF+TiTOhoQVUt7A== +"@opentelemetry/instrumentation-kafkajs@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.6.0.tgz#5d1c6738da8e270acde9259521a9c6e0f421490c" + integrity sha512-MGQrzqEUAl0tacKJUFpuNHJesyTi51oUzSVizn7FdvJplkRIdS11FukyZBZJEscofSEdk7Ycmg+kNMLi5QHUFg== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-knex@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.41.0.tgz#74d611489e823003a825097bac019c6c2ad061a5" - integrity sha512-OhI1SlLv5qnsnm2dOVrian/x3431P75GngSpnR7c4fcVFv7prXGYu29Z6ILRWJf/NJt6fkbySmwdfUUnFnHCTg== +"@opentelemetry/instrumentation-knex@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.43.0.tgz#1f45cfea69212bd579e4fa95c6d5cccdd9626b8e" + integrity sha512-mOp0TRQNFFSBj5am0WF67fRO7UZMUmsF3/7HSDja9g3H4pnj+4YNvWWyZn4+q0rGrPtywminAXe0rxtgaGYIqg== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-koa@0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz#963fd192a1b5f6cbae5dabf4ec82e3105cbb23b1" - integrity sha512-lDAhSnmoTIN6ELKmLJBplXzT/Jqs5jGZehuG22EdSMaTwgjMpxMDI1YtlKEhiWPWkrz5LUsd0aOO0ZRc9vn3AQ== +"@opentelemetry/instrumentation-koa@0.46.0": + version "0.46.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.46.0.tgz#bcdfb29f3b41be45355a9aa278fb231e19eb02e5" + integrity sha512-RcWXMQdJQANnPUaXbHY5G0Fg6gmleZ/ZtZeSsekWPaZmQq12FGk0L1UwodIgs31OlYfviAZ4yTeytoSUkgo5vQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-lru-memoizer@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.40.0.tgz#dc60d7fdfd2a0c681cb23e7ed4f314d1506ccdc0" - integrity sha512-21xRwZsEdMPnROu/QsaOIODmzw59IYpGFmuC4aFWvMj6stA8+Ei1tX67nkarJttlNjoM94um0N4X26AD7ff54A== +"@opentelemetry/instrumentation-lru-memoizer@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.43.0.tgz#7d3f524a10715d9f681e8d4ee6bfe91be80c82cf" + integrity sha512-fZc+1eJUV+tFxaB3zkbupiA8SL3vhDUq89HbDNg1asweYrEb9OlHIB+Ot14ZiHUc1qCmmWmZHbPTwa56mVVwzg== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation-mongodb@0.48.0": - version "0.48.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.48.0.tgz#40fb8c705cb4bf8d8c5bf8752c60c5a0aaaaf617" - integrity sha512-9YWvaGvrrcrydMsYGLu0w+RgmosLMKe3kv/UNlsPy8RLnCkN2z+bhhbjjjuxtUmvEuKZMCoXFluABVuBr1yhjw== +"@opentelemetry/instrumentation-mongodb@0.50.0": + version "0.50.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.50.0.tgz#e5c60ad0bfbdd8ac3238c255a0662b7430083303" + integrity sha512-DtwJMjYFXFT5auAvv8aGrBj1h3ciA/dXQom11rxL7B1+Oy3FopSpanvwYxJ+z0qmBrQ1/iMuWELitYqU4LnlkQ== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-mongoose@0.42.0": - version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.42.0.tgz#375afd21adfcd897a8f521c1ffd2d91e6a428705" - integrity sha512-AnWv+RaR86uG3qNEMwt3plKX1ueRM7AspfszJYVkvkehiicC3bHQA6vWdb6Zvy5HAE14RyFbu9+2hUUjR2NSyg== +"@opentelemetry/instrumentation-mongoose@0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.45.0.tgz#c8179827769fac8528b681da5888ae1779bd844b" + integrity sha512-zHgNh+A01C5baI2mb5dAGyMC7DWmUpOfwpV8axtC0Hd5Uzqv+oqKgKbVDIVhOaDkPxjgVJwYF9YQZl2pw2qxIA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-mysql2@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.41.0.tgz#6377b6e2d2487fd88e1d79aa03658db6c8d51651" - integrity sha512-REQB0x+IzVTpoNgVmy5b+UnH1/mDByrneimP6sbDHkp1j8QOl1HyWOrBH/6YWR0nrbU3l825Em5PlybjT3232g== +"@opentelemetry/instrumentation-mysql2@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.44.0.tgz#309d3fa452d4fcb632c4facb68ed7ea74b6738f9" + integrity sha512-e9QY4AGsjGFwmfHd6kBa4yPaQZjAq2FuxMb0BbKlXCAjG+jwqw+sr9xWdJGR60jMsTq52hx3mAlE3dUJ9BipxQ== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@opentelemetry/sql-common" "^0.40.1" -"@opentelemetry/instrumentation-mysql@0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz#2d50691ead5219774bd36d66c35d5b4681485dd7" - integrity sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw== +"@opentelemetry/instrumentation-mysql@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.44.0.tgz#a29af4432d4289ed9d147d9c30038c57031d950c" + integrity sha512-al7jbXvT/uT1KV8gdNDzaWd5/WXf+mrjrsF0/NtbnqLa0UUFGgQnoK3cyborgny7I+KxWhL8h7YPTf6Zq4nKsg== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/mysql" "2.15.26" -"@opentelemetry/instrumentation-nestjs-core@0.40.0": - version "0.40.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.40.0.tgz#2c0e6405b56caaec32747d55c57ff9a034668ea8" - integrity sha512-WF1hCUed07vKmf5BzEkL0wSPinqJgH7kGzOjjMAiTGacofNXjb/y4KQ8loj2sNsh5C/NN7s1zxQuCgbWbVTGKg== +"@opentelemetry/instrumentation-nestjs-core@0.43.0": + version "0.43.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.43.0.tgz#c176409ab5ebfac862298e31a6a149126e278700" + integrity sha512-NEo4RU7HTjiaXk3curqXUvCb9alRiFWxQY//+hvDXwWLlADX2vB6QEmVCeEZrKO+6I/tBrI4vNdAnbCY9ldZVg== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-pg@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz#1e97a0aeb2dca068ee23ce75884a0a0063a7ce3f" - integrity sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ== +"@opentelemetry/instrumentation-pg@0.49.0": + version "0.49.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.49.0.tgz#47a6a461099fae8e1ffbb97b715a0c34f0aec0b6" + integrity sha512-3alvNNjPXVdAPdY1G7nGRVINbDxRK02+KAugDiEpzw0jFQfU8IzFkSWA4jyU4/GbMxKvHD+XIOEfSjpieSodKw== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/core" "^1.26.0" + "@opentelemetry/instrumentation" "^0.56.0" + "@opentelemetry/semantic-conventions" "1.27.0" "@opentelemetry/sql-common" "^0.40.1" "@types/pg" "8.6.1" "@types/pg-pool" "2.0.6" -"@opentelemetry/instrumentation-redis-4@0.42.0": - version "0.42.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz#fc01104cfe884c7546385eaae03c57a47edd19d1" - integrity sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg== +"@opentelemetry/instrumentation-redis-4@0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.45.0.tgz#34115d39f7050b8576344d9e7f7cb8ceebf85067" + integrity sha512-Sjgym1xn3mdxPRH5CNZtoz+bFd3E3NlGIu7FoYr4YrQouCc9PbnmoBcmSkEdDy5LYgzNildPgsjx9l0EKNjKTQ== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-tedious@0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.15.0.tgz#da82f4d153fb6ff7d1f85d39872ac40bf9db12ea" - integrity sha512-Kb7yo8Zsq2TUwBbmwYgTAMPK0VbhoS8ikJ6Bup9KrDtCx2JC01nCb+M0VJWXt7tl0+5jARUbKWh5jRSoImxdCw== +"@opentelemetry/instrumentation-tedious@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.17.0.tgz#689b7c87346f11b73488b3aa91661d15e8fa830c" + integrity sha512-yRBz2409an03uVd1Q2jWMt3SqwZqRFyKoWYYX3hBAtPDazJ4w5L+1VOij71TKwgZxZZNdDBXImTQjii+VeuzLg== dependencies: - "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/instrumentation" "^0.56.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/tedious" "^4.0.14" -"@opentelemetry/instrumentation-undici@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz#9436ee155c8dcb0b760b66947c0e0f347688a5ef" - integrity sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ== +"@opentelemetry/instrumentation-undici@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.9.0.tgz#c0be1854a90a5002d2345f8bc939d659a9ad76b1" + integrity sha512-lxc3cpUZ28CqbrWcUHxGW/ObDpMOYbuxF/ZOzeFZq54P9uJ2Cpa8gcrC9F716mtuiMaekwk8D6n34vg/JtkkxQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.56.0" -"@opentelemetry/instrumentation@0.53.0", "@opentelemetry/instrumentation@^0.53.0": - version "0.53.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz#e6369e4015eb5112468a4d45d38dcada7dad892d" - integrity sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A== +"@opentelemetry/instrumentation@0.56.0", "@opentelemetry/instrumentation@^0.56.0": + version "0.56.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.56.0.tgz#3330ce16d9235a548efa1019a4a7f01414edd44a" + integrity sha512-2KkGBKE+FPXU1F0zKww+stnlUxUTlBvLCiWdP63Z9sqXYeNI/ziNzsxAp4LAdUcTQmXjw1IWgvm5CAb/BHy99w== dependencies: - "@opentelemetry/api-logs" "0.53.0" + "@opentelemetry/api-logs" "0.56.0" "@types/shimmer" "^1.2.0" import-in-the-middle "^1.8.1" require-in-the-middle "^7.1.1" @@ -2366,24 +2368,12 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.54.0": - version "0.54.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.54.2.tgz#29693a33bbaaf0156634ac5d9e774636b8f17f73" - integrity sha512-go6zpOVoZVztT9r1aPd79Fr3OWiD4N24bCPJsIKkBses8oyFo12F/Ew3UBTdIu6hsW4HC4MVEJygG6TEyJI/lg== - dependencies: - "@opentelemetry/api-logs" "0.54.2" - "@types/shimmer" "^1.2.0" - import-in-the-middle "^1.8.1" - require-in-the-middle "^7.1.1" - semver "^7.5.2" - shimmer "^1.2.1" - "@opentelemetry/redis-common@^0.36.2": version "0.36.2" resolved "https://registry.yarnpkg.com/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz#906ac8e4d804d4109f3ebd5c224ac988276fdc47" integrity sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g== -"@opentelemetry/resources@1.26.0", "@opentelemetry/resources@^1.26.0": +"@opentelemetry/resources@1.26.0": version "1.26.0" resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.26.0.tgz#da4c7366018bd8add1f3aa9c91c6ac59fd503cef" integrity sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw== @@ -2391,7 +2381,15 @@ "@opentelemetry/core" "1.26.0" "@opentelemetry/semantic-conventions" "1.27.0" -"@opentelemetry/sdk-trace-base@^1.22", "@opentelemetry/sdk-trace-base@^1.26.0": +"@opentelemetry/resources@1.29.0", "@opentelemetry/resources@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.29.0.tgz#d170f39b2ac93d61b53d13dfcd96795181bdc372" + integrity sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ== + dependencies: + "@opentelemetry/core" "1.29.0" + "@opentelemetry/semantic-conventions" "1.28.0" + +"@opentelemetry/sdk-trace-base@^1.22": version "1.26.0" resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz#0c913bc6d2cfafd901de330e4540952269ae579c" integrity sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw== @@ -2400,11 +2398,25 @@ "@opentelemetry/resources" "1.26.0" "@opentelemetry/semantic-conventions" "1.27.0" +"@opentelemetry/sdk-trace-base@^1.29.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.29.0.tgz#f48d95dae0e58e601d0596bd2e482122d2688fb8" + integrity sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ== + dependencies: + "@opentelemetry/core" "1.29.0" + "@opentelemetry/resources" "1.29.0" + "@opentelemetry/semantic-conventions" "1.28.0" + "@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.27.0": version "1.27.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c" integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg== +"@opentelemetry/semantic-conventions@1.28.0", "@opentelemetry/semantic-conventions@^1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" + integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== + "@opentelemetry/sql-common@^0.40.1": version "0.40.1" resolved "https://registry.yarnpkg.com/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz#93fbc48d8017449f5b3c3274f2268a08af2b83b6" @@ -3176,23 +3188,19 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry-internal/browser-utils@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.39.0-beta.0.tgz#aaec8215fffb34e7906cdca87725b81b6da735aa" - integrity sha512-J92C1qjuzLtMPfFFs9gKMlphUEgrLXLwVPbnDujZhFhh+anxZhwUVVsCtUN7ivDjkrWiQO8+e1ZRDRPR9fXf9g== +"@sentry-internal/browser-utils@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.43.0.tgz#b064908a537d1cc17d8ddaf0f4c5d712557cbf40" + integrity sha512-5WhJZ3SA5sZVDBwOsChDd5JCzYcwBX7sEqBqEcm3pFru6TUihEnFIJmDIbreIyrQMwUhs3dTxnfnidgjr5z1Ag== dependencies: - "@sentry/core" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry/core" "8.43.0" -"@sentry-internal/feedback@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.39.0-beta.0.tgz#444c62e140656db70cc3574a9e60e2963291848b" - integrity sha512-eh4NMPbMzrevycp+DCGvfzYHe1biTNK9J6clEhgvqUbWbxzGLXzb6pP4paXNCx8KO/AeFYHlFiVPSOPE+baEvg== +"@sentry-internal/feedback@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.43.0.tgz#9477b999c9bca62335eb944a6f7246a96beb0111" + integrity sha512-rcGR2kzFu4vLXBQbI9eGJwjyToyjl36O2q/UKbiZBNJ5IFtDvKRLke6jIHq/YqiHPfFGpVtq5M/lYduDfA/eaQ== dependencies: - "@sentry/core" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry/core" "8.43.0" "@sentry-internal/global-search@^1.0.0": version "1.0.0" @@ -3208,25 +3216,21 @@ htmlparser2 "^4.1.0" title-case "^3.0.2" -"@sentry-internal/replay-canvas@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.39.0-beta.0.tgz#77c19dfd2f685dcbb18e50713d9aa1c1b122215b" - integrity sha512-axIAQsUX1VDGJwYibkcWvYNa9SvfrG1h2b8Nf1PGUwdiWVCVyq0D9FeLWhC/9CizwxrqJ84VOLs8OLRpJJTIqg== +"@sentry-internal/replay-canvas@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.43.0.tgz#f5672a08c9eb588afa0bf36f07b9f5c29b5c9920" + integrity sha512-rL8G7E1GtozH8VNalRrBQNjYDJ5ChWS/vpQI5hUG11PZfvQFXEVatLvT3uO2l0xIlHm4idTsHOSLTe/usxnogQ== dependencies: - "@sentry-internal/replay" "8.39.0-beta.0" - "@sentry/core" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry-internal/replay" "8.43.0" + "@sentry/core" "8.43.0" -"@sentry-internal/replay@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.39.0-beta.0.tgz#2944e8431e798bea4b837d97fcc2bebe72522220" - integrity sha512-0Nf5S/8hcLg9BhpsgBQP9QZqVmHwG1EtA428JMauPHpHzUoR73VK1CLA0Kh4qAB86PzKuShFS+5L7BuRWM/Yow== +"@sentry-internal/replay@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.43.0.tgz#4e2e3844f52b47b16bf816d21857921bbfe85d62" + integrity sha512-geV5/zejLfGGwWHjylzrb1w8NI3U37GMG9/53nmv13FmTXUDF5XF2lh41KXFVYwvp7Ha4bd1FRQ9IU9YtBWskw== dependencies: - "@sentry-internal/browser-utils" "8.39.0-beta.0" - "@sentry/core" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry-internal/browser-utils" "8.43.0" + "@sentry/core" "8.43.0" "@sentry-internal/rrdom@2.26.0": version "2.26.0" @@ -3275,18 +3279,16 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.4.tgz#c5adef7201a799c971cdccc5ba11c97d4609b1a2" integrity sha512-hbSq067KwmeKIEkmyzkTNJbmbtx2KRqvpiy9Q/DynI5Z46Nko/ppvgIfyFXK9DelwvEPOqZic4WXTIhO4iv3DA== -"@sentry/browser@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.39.0-beta.0.tgz#3f02b9445adf7dfbe96f260fa19ee815d4799bf9" - integrity sha512-73xm8DrC9XKR4EJqBuhBrU1wgYUeLtt4ACYmL5/VRz1/UoJ50kjz6f1sgIisO6aScODKlEovp9qO822e72Z/hw== +"@sentry/browser@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.43.0.tgz#4eec67bc6fb278727304045b612ac392674cade6" + integrity sha512-LGvLLnfmR8+AEgFmd7Q7KHiOTiV0P1Lvio2ENDELhEqJOIiICauttibVmig+AW02qg4kMeywvleMsUYaZv2RVA== dependencies: - "@sentry-internal/browser-utils" "8.39.0-beta.0" - "@sentry-internal/feedback" "8.39.0-beta.0" - "@sentry-internal/replay" "8.39.0-beta.0" - "@sentry-internal/replay-canvas" "8.39.0-beta.0" - "@sentry/core" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry-internal/browser-utils" "8.43.0" + "@sentry-internal/feedback" "8.43.0" + "@sentry-internal/replay" "8.43.0" + "@sentry-internal/replay-canvas" "8.43.0" + "@sentry/core" "8.43.0" "@sentry/bundler-plugin-core@2.22.4": version "2.22.4" @@ -3356,92 +3358,81 @@ "@sentry/cli-win32-i686" "2.36.2" "@sentry/cli-win32-x64" "2.36.2" -"@sentry/core@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.39.0-beta.0.tgz#31f95bad5e53c9781a9a47116463569213631f2d" - integrity sha512-M680BSZJtnvQBlRknBZAoIoP6m+f9UDgp9+Fe7+Zf/lInpQs6XEt/MDa6gD+9AY7M5nkBK2k6gc+yVmFxbYMIg== - dependencies: - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" +"@sentry/core@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.43.0.tgz#e96a489e87a9999199f5ac27d8860da37c1fa8b4" + integrity sha512-ktyovtjkTMNud+kC/XfqHVCoQKreIKgx/hgeRvzPwuPyd1t1KzYmRL3DBkbcWVnyOPpVTHn+RsEI1eRcVYHtvw== "@sentry/jest-environment@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@sentry/jest-environment/-/jest-environment-6.0.0.tgz#7d77ac2e18e3fe4f0da2c143bc0985ad3d3c2dcb" integrity sha512-e4ZTE/h1/wNITyIzZUqka3KOmLMICVYXLxGiM7OdQy8zS7J7j/HCKbUPIZ9ozk5RVQJDwG68U2pNB4YX2Ka3xQ== -"@sentry/node@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.39.0-beta.0.tgz#b2187cda24a24dd698197253633f73fdc3a330b7" - integrity sha512-Uvv5j6VAdbFBMRrIzoE7U/qREr0HhcC+sRHHa6y5+rIEOasU8b5uGXe33vjghjJ1XE8lDGXMaPh5gvV6hpUYxg== +"@sentry/node@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.43.0.tgz#e7417a6c262f9492f68b522934bb75201b84abe1" + integrity sha512-qCQU9vFxf03ejw1h+qWJXCf0erV56HBi5xgi262lHiBLcRtuwj1xjufMVKOWX0sQEvAxzqpMZmqRE64lXLY4Ig== dependencies: "@opentelemetry/api" "^1.9.0" - "@opentelemetry/context-async-hooks" "^1.25.1" - "@opentelemetry/core" "^1.25.1" - "@opentelemetry/instrumentation" "^0.54.0" - "@opentelemetry/instrumentation-amqplib" "^0.43.0" - "@opentelemetry/instrumentation-connect" "0.40.0" - "@opentelemetry/instrumentation-dataloader" "0.12.0" - "@opentelemetry/instrumentation-express" "0.44.0" - "@opentelemetry/instrumentation-fastify" "0.41.0" - "@opentelemetry/instrumentation-fs" "0.16.0" - "@opentelemetry/instrumentation-generic-pool" "0.39.0" - "@opentelemetry/instrumentation-graphql" "0.44.0" - "@opentelemetry/instrumentation-hapi" "0.41.0" - "@opentelemetry/instrumentation-http" "0.53.0" - "@opentelemetry/instrumentation-ioredis" "0.43.0" - "@opentelemetry/instrumentation-kafkajs" "0.4.0" - "@opentelemetry/instrumentation-knex" "0.41.0" - "@opentelemetry/instrumentation-koa" "0.43.0" - "@opentelemetry/instrumentation-lru-memoizer" "0.40.0" - "@opentelemetry/instrumentation-mongodb" "0.48.0" - "@opentelemetry/instrumentation-mongoose" "0.42.0" - "@opentelemetry/instrumentation-mysql" "0.41.0" - "@opentelemetry/instrumentation-mysql2" "0.41.0" - "@opentelemetry/instrumentation-nestjs-core" "0.40.0" - "@opentelemetry/instrumentation-pg" "0.44.0" - "@opentelemetry/instrumentation-redis-4" "0.42.0" - "@opentelemetry/instrumentation-tedious" "0.15.0" - "@opentelemetry/instrumentation-undici" "0.6.0" - "@opentelemetry/resources" "^1.26.0" - "@opentelemetry/sdk-trace-base" "^1.26.0" - "@opentelemetry/semantic-conventions" "^1.27.0" + "@opentelemetry/context-async-hooks" "^1.29.0" + "@opentelemetry/core" "^1.29.0" + "@opentelemetry/instrumentation" "^0.56.0" + "@opentelemetry/instrumentation-amqplib" "^0.45.0" + "@opentelemetry/instrumentation-connect" "0.42.0" + "@opentelemetry/instrumentation-dataloader" "0.15.0" + "@opentelemetry/instrumentation-express" "0.46.0" + "@opentelemetry/instrumentation-fastify" "0.43.0" + "@opentelemetry/instrumentation-fs" "0.18.0" + "@opentelemetry/instrumentation-generic-pool" "0.42.0" + "@opentelemetry/instrumentation-graphql" "0.46.0" + "@opentelemetry/instrumentation-hapi" "0.44.0" + "@opentelemetry/instrumentation-http" "0.56.0" + "@opentelemetry/instrumentation-ioredis" "0.46.0" + "@opentelemetry/instrumentation-kafkajs" "0.6.0" + "@opentelemetry/instrumentation-knex" "0.43.0" + "@opentelemetry/instrumentation-koa" "0.46.0" + "@opentelemetry/instrumentation-lru-memoizer" "0.43.0" + "@opentelemetry/instrumentation-mongodb" "0.50.0" + "@opentelemetry/instrumentation-mongoose" "0.45.0" + "@opentelemetry/instrumentation-mysql" "0.44.0" + "@opentelemetry/instrumentation-mysql2" "0.44.0" + "@opentelemetry/instrumentation-nestjs-core" "0.43.0" + "@opentelemetry/instrumentation-pg" "0.49.0" + "@opentelemetry/instrumentation-redis-4" "0.45.0" + "@opentelemetry/instrumentation-tedious" "0.17.0" + "@opentelemetry/instrumentation-undici" "0.9.0" + "@opentelemetry/resources" "^1.29.0" + "@opentelemetry/sdk-trace-base" "^1.29.0" + "@opentelemetry/semantic-conventions" "^1.28.0" "@prisma/instrumentation" "5.19.1" - "@sentry/core" "8.39.0-beta.0" - "@sentry/opentelemetry" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry/core" "8.43.0" + "@sentry/opentelemetry" "8.43.0" import-in-the-middle "^1.11.2" -"@sentry/opentelemetry@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.39.0-beta.0.tgz#76abbf1d7d329f881a90f63424e909f74d3ce725" - integrity sha512-0BNHhyEd38n0dgZAJ9UdYSkwruULU5Iej8pIsOmT5mciDOwrqLil1xwfjkvElUnRnz/igR7Ghl4vVByRzqGC9A== +"@sentry/opentelemetry@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.43.0.tgz#9823a3e4162bf464c12092149fe714ba5e5ba0b9" + integrity sha512-Ey+z1+JuMsb+LNY5MddJhjJpCnmkVwGZwoc5T/wWfh+5WKnvZ5RSNwaUl71Ho0lpVhhejwuUtaNxc4Ilk1KjhA== dependencies: - "@sentry/core" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry/core" "8.43.0" -"@sentry/profiling-node@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/profiling-node/-/profiling-node-8.39.0-beta.0.tgz#f25b701708d5806865a0c153c40f7307bf7f0678" - integrity sha512-qoOky56SqqxxhDabMkDJGLWjEbjrn6xY5MuGH8PZXHDtAX+vmnD1lln5Y4Ka0wFuqJM+/YvXCLijib5W/3zMRQ== +"@sentry/profiling-node@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/profiling-node/-/profiling-node-8.43.0.tgz#6461a15ed94a30ed7787dd254d800500a05509ec" + integrity sha512-WyPknmDWA1nChFHv22u1k+nC7zo0iGpNLf9JuhpFxD0tAbltiXz414DQE/ISZvtPkDng89dVzKy0FJEAaGg7Dg== dependencies: - "@sentry/core" "8.39.0-beta.0" - "@sentry/node" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry/core" "8.43.0" + "@sentry/node" "8.43.0" detect-libc "^2.0.2" node-abi "^3.61.0" -"@sentry/react@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.39.0-beta.0.tgz#e08f8de27c61656453bf8faac866f8a5fe36f017" - integrity sha512-iLxwSmVByNY4APUUWEfFu/nqgnB7I1j6xHdfql1wcI+mvTE7rLAneMa59dvvLFj6+0OqauKIAw22ZFT63b1ZKw== +"@sentry/react@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.43.0.tgz#ad49bd16b0b1897613ef5cbd2f0a49b2b41f98a9" + integrity sha512-PsTzLrYio/FOJU537Y5Gj9jJi7OMHEjdttsC9INUxy5062LOd8ObtHsjE0mopLaSYEwUfSROQOBZCwmISh8ByQ== dependencies: - "@sentry/browser" "8.39.0-beta.0" - "@sentry/core" "8.39.0-beta.0" - "@sentry/types" "8.39.0-beta.0" - "@sentry/utils" "8.39.0-beta.0" + "@sentry/browser" "8.43.0" + "@sentry/core" "8.43.0" hoist-non-react-statics "^3.3.2" "@sentry/release-parser@^1.3.1": @@ -3454,17 +3445,19 @@ resolved "https://registry.yarnpkg.com/@sentry/status-page-list/-/status-page-list-0.3.0.tgz#d5520057007be1a021933aae26dfa6a4a3981c40" integrity sha512-v/MkVOvs48QioXt7Ex8gmZEFGvjukWqx2DlIej+Ac4pVQJAfzF6/DFFVT3IK8/owIqv/IdEhY0XzHOcIB0yBIA== -"@sentry/types@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.39.0-beta.0.tgz#2e267f587d7ffa1b8cb7e21823c869efc09b893d" - integrity sha512-gJvUZNvbxlucEp3ktfgVRGMP6IJXofUN0Zqc0V1F6ia3cs43DRyW+k1E0KXFFMoIxg9EjkLthAor3TZd5Zw4fg== +"@sentry/types@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.43.0.tgz#abe8ad46e26563260fd66f715a1133c4696d1b8e" + integrity sha512-NdTfD3S+od4RTw7xn+4sVSO5n63N/9pUsNT5s0D1QiGGZw0DpENoIb3J/PiGfuWL5f02Bmv7l9vVW0ovWZDWPg== + dependencies: + "@sentry/core" "8.43.0" -"@sentry/utils@8.39.0-beta.0": - version "8.39.0-beta.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.39.0-beta.0.tgz#7312aa9c1c57121f8f853806485b1594b5f6587a" - integrity sha512-bSjvF/caPGMrhWDWZLR0irTYoHJQxatdHOzC5f6mq4sOQZ+Ah4ZSPLVtxeq9Q5A/kooY5xmjH8yFBb5dGnfUgQ== +"@sentry/utils@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.43.0.tgz#d9ec0ceeea59116bf573a006b2b34229f6420e90" + integrity sha512-z7wD8OgRSn7R9n0kSPo/TTBrPXyNmIivQMqMHhXGOOtA9VfHJ7advkn0eQ5Ohq9/euiDDYWG3AUo+nO4rh4nCA== dependencies: - "@sentry/types" "8.39.0-beta.0" + "@sentry/core" "8.43.0" "@sentry/webpack-plugin@^2.22.4": version "2.22.4" @@ -7136,6 +7129,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +forwarded-parse@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" + integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw== + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" From a3abebb3208bb8e3df6ef0a38148fb221bfcebf9 Mon Sep 17 00:00:00 2001 From: Rohan Agarwal <47861399+roaga@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:35:34 -0500 Subject: [PATCH 036/757] feat(issue summary): Change 3-dot menu to dropdown (#81928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Aligns the button with the header properly 2. Changes the popup to a dropdown for consistency 3. Adds a feedback button to the dropdown Screenshot 2024-12-10 at 1 04 28 PM Screenshot 2024-12-10 at 1 06 54 PM Screenshot 2024-12-10 at 1 07 12 PM --- static/app/components/group/groupSummary.tsx | 166 ++++++++----------- 1 file changed, 65 insertions(+), 101 deletions(-) diff --git a/static/app/components/group/groupSummary.tsx b/static/app/components/group/groupSummary.tsx index 67b204f318e1f1..879d9b359fb5d1 100644 --- a/static/app/components/group/groupSummary.tsx +++ b/static/app/components/group/groupSummary.tsx @@ -1,10 +1,9 @@ -import {useEffect, useRef, useState} from 'react'; +import {useEffect, useState} from 'react'; import styled from '@emotion/styled'; -import {Button} from 'sentry/components/button'; -import Link from 'sentry/components/links/link'; +import {DropdownMenu} from 'sentry/components/dropdownMenu'; import Placeholder from 'sentry/components/placeholder'; -import {IconEllipsis, IconFatal, IconFocus, IconRefresh, IconSpan} from 'sentry/icons'; +import {IconEllipsis, IconFatal, IconFocus, IconSpan} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; @@ -13,6 +12,7 @@ import type {Project} from 'sentry/types/project'; import marked from 'sentry/utils/marked'; import {type ApiQueryKey, useApiQuery, useQueryClient} from 'sentry/utils/queryClient'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; +import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; import useOrganization from 'sentry/utils/useOrganization'; import {useAiConfig} from 'sentry/views/issueDetails/streamline/hooks/useAiConfig'; @@ -90,7 +90,7 @@ export function GroupSummary({ }) { const organization = useOrganization(); const [forceEvent, setForceEvent] = useState(false); - const [showEventDetails, setShowEventDetails] = useState(false); + const openFeedbackForm = useFeedbackForm(); const {data, isPending, isError, refresh} = useGroupSummary( group, event, @@ -98,9 +98,6 @@ export function GroupSummary({ forceEvent ); - const popupRef = useRef(null); - const buttonRef = useRef(null); - useEffect(() => { if (forceEvent && !isPending) { refresh(); @@ -108,58 +105,51 @@ export function GroupSummary({ } }, [forceEvent, isPending, refresh]); - useEffect(() => { - function handleClickOutside(e: MouseEvent) { - if ( - showEventDetails && - popupRef.current && - buttonRef.current && - !popupRef.current.contains(e.target as Node) && - !buttonRef.current.contains(e.target as Node) - ) { - setShowEventDetails(false); - } - } - - document.addEventListener('click', handleClickOutside); - return () => { - document.removeEventListener('click', handleClickOutside); - }; - }, [showEventDetails]); - - const tooltipContent = data?.eventId ? ( - event?.id === data.eventId ? ( - t('Based on this event') - ) : ( - - - {t('Based on event ')} - - {data.eventId.substring(0, 8)} - - - - + + + ); } +function ScrollIntoViewButton({ + children, + enabled, +}: { + children: React.ReactElement; + enabled: boolean; +}) { + if (React.Children.count(children) !== 1) { + throw new Error('ScrollIntoViewButton only accepts a single child'); + } + + const [isVisible, setIsVisible] = useState(false); + const [targetElement, setTargetElement] = useState(); + + useEffect(() => { + if (!targetElement || !enabled) { + return () => {}; + } + + const observer = new IntersectionObserver( + observerEntries => { + const entry = observerEntries[0]; + setIsVisible(!entry.isIntersecting); + }, + { + root: null, + threshold: 0.5, + } + ); + + observer.observe(targetElement); + return () => { + observer.disconnect(); + setIsVisible(false); + }; + }, [targetElement, enabled]); + + return ( + + {React.cloneElement(children, {ref: setTargetElement})} + {isVisible && ( + + { + targetElement?.scrollIntoView({behavior: 'smooth'}); + }} + initial={{opacity: 0, scale: 0.5}} + animate={{opacity: 1, scale: 1}} + transition={{ + ease: [0, 0.71, 0.2, 1.4], + }} + > + + + + )} + + ); +} + +const FloatingButton = styled(motion.button)` + position: fixed; + bottom: ${space(4)}; + right: ${space(1)}; + border-radius: 50%; + border: 1px solid ${p => p.theme.border}; + background-color: ${p => p.theme.purple400}; + color: ${p => p.theme.white}; + box-shadow: ${p => p.theme.dropShadowHeavy}; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; +`; + const FormActions = styled('div')` display: grid; grid-template-columns: repeat(2, max-content); From b9a2b3b0751c8207b11da7e6a70d728b0de5997d Mon Sep 17 00:00:00 2001 From: ArthurKnaus Date: Fri, 20 Dec 2024 13:19:33 +0100 Subject: [PATCH 393/757] ref(dynamic-sampling): Adjust table headings (#82450) Change `per month` & `per day` to `(30D)` and `(24H)` --- static/app/views/settings/dynamicSampling/projectsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/settings/dynamicSampling/projectsTable.tsx b/static/app/views/settings/dynamicSampling/projectsTable.tsx index 023b8a2df24e63..e9e8bbba23d1e1 100644 --- a/static/app/views/settings/dynamicSampling/projectsTable.tsx +++ b/static/app/views/settings/dynamicSampling/projectsTable.tsx @@ -72,7 +72,7 @@ export function ProjectsTable({ {t('Accepted Spans')} , - period === '24h' ? t('Stored Spans per day') : t('Stored Spans per month'), + period === '24h' ? t('Stored Spans (24h)') : t('Stored Spans (30d)'), rateHeader, ]} > From 0f081ee6eca44bb3a47380f34501f282926adeb5 Mon Sep 17 00:00:00 2001 From: Joris Bayer Date: Fri, 20 Dec 2024 13:52:26 +0100 Subject: [PATCH 394/757] doc(inbound-filters): Clarify help message (#82451) --- static/app/data/forms/inboundFilters.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/data/forms/inboundFilters.tsx b/static/app/data/forms/inboundFilters.tsx index c82ad9aa5d709a..2ee1c4d994b64d 100644 --- a/static/app/data/forms/inboundFilters.tsx +++ b/static/app/data/forms/inboundFilters.tsx @@ -77,7 +77,8 @@ export const customFilterFields: Field[] = [ help: ( {t('Filter events by error messages. ')} - {newLineHelpText} {globHelpText} + {newLineHelpText} {globHelpText}{' '} + {t('Exceptions are matched on ": ", for example "TypeError: *".')} ), getData: getOptionsData, From f51dfb1211041a7010695fa6d9e94ce5d7b0955e Mon Sep 17 00:00:00 2001 From: "Armen Zambrano G." <44410+armenzg@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:36:29 -0500 Subject: [PATCH 395/757] chore(similarity): Enable Native platforms (#82401) We have in place the protection of 30-frames and unsymbolicated stack traces do not have file names or functions to call seer. --- src/sentry/seer/similarity/utils.py | 15 +-------------- .../tasks/test_backfill_seer_grouping_records.py | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index 91ec1009771d6d..dd8af36a7286e5 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -19,15 +19,7 @@ # this separately from backfill status allows us to backfill projects which have events from # multiple platforms, some supported and some not, and not worry about events from the unsupported # platforms getting sent to Seer during ingest. -SEER_INELIGIBLE_EVENT_PLATFORMS = frozenset( - [ - # Native platforms - "c", - "native", - # We don't know what's in the event - "other", - ] -) +SEER_INELIGIBLE_EVENT_PLATFORMS = frozenset(["other"]) # We don't know what's in the event # Event platforms corresponding to project platforms which were backfilled before we started # blocking events with more than `MAX_FRAME_COUNT` frames from being sent to Seer (which we do to # prevent possible over-grouping). Ultimately we want a more unified solution, but for now, we're @@ -46,11 +38,6 @@ # platforms shouldn't have Seer enabled. SEER_INELIGIBLE_PROJECT_PLATFORMS = frozenset( [ - # Native platforms - "c", - "minidump", - "native", - "native-qt", # We have no clue what's in these projects "other", "", diff --git a/tests/sentry/tasks/test_backfill_seer_grouping_records.py b/tests/sentry/tasks/test_backfill_seer_grouping_records.py index ef9c4c8dc08a73..6f8535e616c5f5 100644 --- a/tests/sentry/tasks/test_backfill_seer_grouping_records.py +++ b/tests/sentry/tasks/test_backfill_seer_grouping_records.py @@ -1818,7 +1818,7 @@ def test_backfill_seer_grouping_records_cohort_creation_not_seer_eligible( project_same_cohort_not_eligible = self.create_project( organization=self.organization, id=self.project.id + thread_number ) - project_same_cohort_not_eligible.platform = "c" # Not currently eligible + project_same_cohort_not_eligible.platform = "other" # Not currently eligible project_same_cohort_not_eligible.save() self.create_event(project_same_cohort_not_eligible.id, times_seen=5) From adb2f304a8ce3bf5a1c3baa18c2b6e68120904aa Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 20 Dec 2024 09:19:42 -0500 Subject: [PATCH 396/757] feat(explore): Support explicit typed tags in search syntax (#82352) Explore introduces an additional syntax to search for numeric attributes on spans. This looks like `tags[foo,number]` where `foo` is the name of the numeric attribute. This change ensures the search bar correctly syntax highlights these keys and works with them nicely. --- .../search-syntax/explicit_number_tag.json | 43 ++++++++++++ .../explicit_number_tags_in_filter.json | 34 ++++++++++ .../search-syntax/explicit_string_tag.json | 43 ++++++++++++ .../explicit_string_tags_in_filter.json | 65 +++++++++++++++++++ .../app/components/searchSyntax/grammar.pegjs | 14 +++- static/app/components/searchSyntax/parser.tsx | 49 ++++++++++++-- .../app/components/searchSyntax/renderer.tsx | 8 ++- static/app/components/searchSyntax/utils.tsx | 37 ++++++++++- tests/sentry/api/test_event_search.py | 6 ++ 9 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 fixtures/search-syntax/explicit_number_tag.json create mode 100644 fixtures/search-syntax/explicit_number_tags_in_filter.json create mode 100644 fixtures/search-syntax/explicit_string_tag.json create mode 100644 fixtures/search-syntax/explicit_string_tags_in_filter.json diff --git a/fixtures/search-syntax/explicit_number_tag.json b/fixtures/search-syntax/explicit_number_tag.json new file mode 100644 index 00000000000000..57337d8d6f7fda --- /dev/null +++ b/fixtures/search-syntax/explicit_number_tag.json @@ -0,0 +1,43 @@ +[ + { + "query": "tags[foo,number]:456 release:1.2.1 tags[project_id,number]:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitNumberTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "foo", "quoted": false} + }, + "operator": "", + "value": {"type": "valueText", "value": "456", "quoted": false} + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": {"type": "valueText", "value": "1.2.1", "quoted": false} + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitNumberTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "project_id", "quoted": false} + }, + "operator": "", + "value": {"type": "valueText", "value": "123", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/fixtures/search-syntax/explicit_number_tags_in_filter.json b/fixtures/search-syntax/explicit_number_tags_in_filter.json new file mode 100644 index 00000000000000..2e7cd62cc6321d --- /dev/null +++ b/fixtures/search-syntax/explicit_number_tags_in_filter.json @@ -0,0 +1,34 @@ +[ + { + "query": "tags[foo,number]:[123, 456]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitNumberTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "foo", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "123", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "456", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] + diff --git a/fixtures/search-syntax/explicit_string_tag.json b/fixtures/search-syntax/explicit_string_tag.json new file mode 100644 index 00000000000000..8dd326ff2ed9fe --- /dev/null +++ b/fixtures/search-syntax/explicit_string_tag.json @@ -0,0 +1,43 @@ +[ + { + "query": "tags[fruit,string]:apple release:1.2.1 tags[project_id,string]:123", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": {"type": "valueText", "value": "apple", "quoted": false} + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": {"type": "keySimple", "value": "release", "quoted": false}, + "operator": "", + "value": {"type": "valueText", "value": "1.2.1", "quoted": false} + }, + {"type": "spaces", "value": " "}, + { + "type": "filter", + "filter": "text", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "project_id", "quoted": false} + }, + "operator": "", + "value": {"type": "valueText", "value": "123", "quoted": false} + }, + {"type": "spaces", "value": ""} + ] + } +] diff --git a/fixtures/search-syntax/explicit_string_tags_in_filter.json b/fixtures/search-syntax/explicit_string_tags_in_filter.json new file mode 100644 index 00000000000000..ebf6b7038d9da8 --- /dev/null +++ b/fixtures/search-syntax/explicit_string_tags_in_filter.json @@ -0,0 +1,65 @@ +[ + { + "query": "tags[fruit,string]:[apple, pear]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "apple", "quoted": false} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "pear", "quoted": false} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + }, + { + "query": "tags[fruit,string]:[\"apple wow\", \"pear\"]", + "result": [ + {"type": "spaces", "value": ""}, + { + "type": "filter", + "filter": "textIn", + "negated": false, + "key": { + "type": "keyExplicitStringTag", + "prefix": "tags", + "key": {"type": "keySimple", "value": "fruit", "quoted": false} + }, + "operator": "", + "value": { + "type": "valueTextList", + "items": [ + { + "separator": "", + "value": {"type": "valueText", "value": "apple wow", "quoted": true} + }, + { + "separator": ", ", + "value": {"type": "valueText", "value": "pear", "quoted": true} + } + ] + } + }, + {"type": "spaces", "value": ""} + ] + } +] + diff --git a/static/app/components/searchSyntax/grammar.pegjs b/static/app/components/searchSyntax/grammar.pegjs index 9a8bfb2685c575..55eedb460fe05b 100644 --- a/static/app/components/searchSyntax/grammar.pegjs +++ b/static/app/components/searchSyntax/grammar.pegjs @@ -234,6 +234,16 @@ explicit_tag_key return tc.tokenKeyExplicitTag(prefix, key); } +explicit_string_tag_key + = prefix:"tags" open_bracket key:search_key spaces comma spaces 'string' closed_bracket { + return tc.tokenKeyExplicitStringTag(prefix, key) + } + +explicit_number_tag_key + = prefix:"tags" open_bracket key:search_key spaces comma spaces 'number' closed_bracket { + return tc.tokenKeyExplicitNumberTag(prefix, key) + } + aggregate_key = name:key open_paren s1:spaces args:function_args? s2:spaces closed_paren { return tc.tokenKeyAggregate(name, args, s1, s2); @@ -259,10 +269,10 @@ quoted_aggregate_param } search_key - = key / quoted_key + = explicit_number_tag_key / key / quoted_key text_key - = explicit_tag_key / search_key + = explicit_tag_key / explicit_string_tag_key / search_key // Filter values diff --git a/static/app/components/searchSyntax/parser.tsx b/static/app/components/searchSyntax/parser.tsx index 9f445bab3534a5..9111eba6f6fb38 100644 --- a/static/app/components/searchSyntax/parser.tsx +++ b/static/app/components/searchSyntax/parser.tsx @@ -43,6 +43,8 @@ export enum Token { LOGIC_BOOLEAN = 'logicBoolean', KEY_SIMPLE = 'keySimple', KEY_EXPLICIT_TAG = 'keyExplicitTag', + KEY_EXPLICIT_NUMBER_TAG = 'keyExplicitNumberTag', + KEY_EXPLICIT_STRING_TAG = 'keyExplicitStringTag', KEY_AGGREGATE = 'keyAggregate', KEY_AGGREGATE_ARGS = 'keyAggregateArgs', KEY_AGGREGATE_PARAMS = 'keyAggregateParam', @@ -127,7 +129,11 @@ export const interchangeableFilterOperators = { [FilterType.DATE]: [FilterType.SPECIFIC_DATE], }; -const textKeys = [Token.KEY_SIMPLE, Token.KEY_EXPLICIT_TAG] as const; +const textKeys = [ + Token.KEY_SIMPLE, + Token.KEY_EXPLICIT_TAG, + Token.KEY_EXPLICIT_STRING_TAG, +] as const; /** * This constant-type configuration object declares how each filter type @@ -486,6 +492,26 @@ export class TokenConverter { key, }); + tokenKeyExplicitStringTag = ( + prefix: string, + key: ReturnType + ) => ({ + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_STRING_TAG as const, + prefix, + key, + }); + + tokenKeyExplicitNumberTag = ( + prefix: string, + key: ReturnType + ) => ({ + ...this.defaultTokenFields, + type: Token.KEY_EXPLICIT_NUMBER_TAG as const, + prefix, + key, + }); + tokenKeyAggregateParam = (value: string, quoted: boolean) => ({ ...this.defaultTokenFields, type: Token.KEY_AGGREGATE_PARAMS as const, @@ -771,13 +797,25 @@ export class TokenConverter { */ checkFilterWarning = (key: FilterMap[T]['key']) => { if ( - ![Token.KEY_SIMPLE, Token.KEY_EXPLICIT_TAG, Token.KEY_AGGREGATE].includes(key.type) + ![ + Token.KEY_SIMPLE, + Token.KEY_EXPLICIT_TAG, + Token.KEY_AGGREGATE, + Token.KEY_EXPLICIT_NUMBER_TAG, + Token.KEY_EXPLICIT_STRING_TAG, + ].includes(key.type) ) { return null; } const keyName = getKeyName( - key as TokenResult + key as TokenResult< + | Token.KEY_SIMPLE + | Token.KEY_EXPLICIT_TAG + | Token.KEY_AGGREGATE + | Token.KEY_EXPLICIT_NUMBER_TAG + | Token.KEY_EXPLICIT_STRING_TAG + > ); return this.config.getFilterTokenWarning?.(keyName) ?? null; }; @@ -838,7 +876,10 @@ export class TokenConverter { */ checkInvalidTextFilter = (key: TextFilter['key'], value: TextFilter['value']) => { // Explicit tag keys will always be treated as text filters - if (key.type === Token.KEY_EXPLICIT_TAG) { + if ( + key.type === Token.KEY_EXPLICIT_TAG || + key.type === Token.KEY_EXPLICIT_STRING_TAG + ) { return this.checkInvalidTextValue(value); } diff --git a/static/app/components/searchSyntax/renderer.tsx b/static/app/components/searchSyntax/renderer.tsx index f8784c5025d4ce..e1854d464f7965 100644 --- a/static/app/components/searchSyntax/renderer.tsx +++ b/static/app/components/searchSyntax/renderer.tsx @@ -224,7 +224,13 @@ function KeyToken({ token, negated, }: { - token: TokenResult; + token: TokenResult< + | Token.KEY_SIMPLE + | Token.KEY_AGGREGATE + | Token.KEY_EXPLICIT_TAG + | Token.KEY_EXPLICIT_NUMBER_TAG + | Token.KEY_EXPLICIT_STRING_TAG + >; negated?: boolean; }) { let value: React.ReactNode = token.text; diff --git a/static/app/components/searchSyntax/utils.tsx b/static/app/components/searchSyntax/utils.tsx index e9d8c669d08f17..7c7a35137f6b65 100644 --- a/static/app/components/searchSyntax/utils.tsx +++ b/static/app/components/searchSyntax/utils.tsx @@ -104,6 +104,12 @@ export function treeResultLocator({ nodeVisitor(token.argsSpaceBefore); nodeVisitor(token.argsSpaceAfter); break; + case Token.KEY_EXPLICIT_NUMBER_TAG: + nodeVisitor(token.key); + break; + case Token.KEY_EXPLICIT_STRING_TAG: + nodeVisitor(token.key); + break; case Token.LOGIC_GROUP: token.inner.forEach(nodeVisitor); break; @@ -172,6 +178,16 @@ export function treeTransformer({tree, transform}: TreeTransformerOpts) { argsSpaceBefore: nodeVisitor(token.argsSpaceBefore), argsSpaceAfter: nodeVisitor(token.argsSpaceAfter), }); + case Token.KEY_EXPLICIT_NUMBER_TAG: + return transform({ + ...token, + key: nodeVisitor(token.key), + }); + case Token.KEY_EXPLICIT_STRING_TAG: + return transform({ + ...token, + key: nodeVisitor(token.key), + }); case Token.LOGIC_GROUP: return transform({ ...token, @@ -213,7 +229,13 @@ type GetKeyNameOpts = { * Utility to get the string name of any type of key. */ export const getKeyName = ( - key: TokenResult, + key: TokenResult< + | Token.KEY_SIMPLE + | Token.KEY_EXPLICIT_TAG + | Token.KEY_AGGREGATE + | Token.KEY_EXPLICIT_NUMBER_TAG + | Token.KEY_EXPLICIT_STRING_TAG + >, options: GetKeyNameOpts = {} ) => { const {aggregateWithArgs, showExplicitTagPrefix = false} = options; @@ -229,6 +251,15 @@ export const getKeyName = ( return aggregateWithArgs ? `${key.name.value}(${key.args ? key.args.text : ''})` : key.name.value; + case Token.KEY_EXPLICIT_NUMBER_TAG: + // number tags always need to be expressed with the + // explicit tag prefix + type + return key.text; + case Token.KEY_EXPLICIT_STRING_TAG: + if (showExplicitTagPrefix) { + return key.text; + } + return key.key.value; default: return ''; } @@ -295,6 +326,10 @@ export function stringifyToken(token: TokenResult) { return token.text; case Token.KEY_EXPLICIT_TAG: return `${token.prefix}[${token.key.value}]`; + case Token.KEY_EXPLICIT_NUMBER_TAG: + return `${token.prefix}[${token.key.value},number]`; + case Token.KEY_EXPLICIT_STRING_TAG: + return `${token.prefix}[${token.key.value},string]`; case Token.VALUE_TEXT: return token.quoted ? `"${token.value}"` : token.value; case Token.VALUE_RELATIVE_DATE: diff --git a/tests/sentry/api/test_event_search.py b/tests/sentry/api/test_event_search.py index 6fbe14fd9f821d..8da6f3c184e64d 100644 --- a/tests/sentry/api/test_event_search.py +++ b/tests/sentry/api/test_event_search.py @@ -93,6 +93,12 @@ def node_visitor(token): if token["type"] == "keyExplicitTag": return SearchKey(name=f"tags[{token['key']['value']}]") + if token["type"] == "keyExplicitStringTag": + return SearchKey(name=f"tags[{token['key']['value']},string]") + + if token["type"] == "keyExplicitNumberTag": + return SearchKey(name=f"tags[{token['key']['value']},number]") + if token["type"] == "keyAggregate": name = node_visitor(token["name"]).name # Consistent join aggregate function parameters From 328c1e8eb196a5197d81efb11c4dd87a09e801cb Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Fri, 20 Dec 2024 06:56:24 -0800 Subject: [PATCH 397/757] :wrench: chore(slack): add logging to investigate team linking behavior (#82397) --- src/sentry/integrations/slack/webhooks/base.py | 2 +- src/sentry/integrations/slack/webhooks/command.py | 8 +++----- .../slack/webhooks/commands/test_link_team.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sentry/integrations/slack/webhooks/base.py b/src/sentry/integrations/slack/webhooks/base.py index fba29ed49d3b62..83db81f63d922e 100644 --- a/src/sentry/integrations/slack/webhooks/base.py +++ b/src/sentry/integrations/slack/webhooks/base.py @@ -215,7 +215,7 @@ def unlink_team_handler(self, input: CommandInput) -> IntegrationResponse[Respon for message, reason in self.TEAM_HALT_MAPPINGS.items(): if message in str(response.data): return IntegrationResponse( - interaction_result=EventLifecycleOutcome.SUCCESS, + interaction_result=EventLifecycleOutcome.HALTED, response=response, outcome_reason=str(reason), ) diff --git a/src/sentry/integrations/slack/webhooks/command.py b/src/sentry/integrations/slack/webhooks/command.py index ea0f111566da4d..957f2f545be530 100644 --- a/src/sentry/integrations/slack/webhooks/command.py +++ b/src/sentry/integrations/slack/webhooks/command.py @@ -27,7 +27,7 @@ from sentry.models.organizationmember import OrganizationMember from sentry.utils import metrics -_logger = logging.getLogger(__name__) +_logger = logging.getLogger("sentry.integration.slack.bot-commands") from .base import SlackDMEndpoint @@ -86,9 +86,7 @@ def link_team(self, slack_request: SlackDMRequest) -> Response: if slack_request.channel_name == DIRECT_MESSAGE_CHANNEL_NAME: return self.reply(slack_request, LINK_FROM_CHANNEL_MESSAGE) - logger_params = { - "slack_request": slack_request, - } + logger_params = {} identity_user = slack_request.get_identity_user() if not identity_user: @@ -113,7 +111,7 @@ def link_team(self, slack_request: SlackDMRequest) -> Response: has_valid_role = True if not has_valid_role: - _logger.info("insufficient-role", extra=logger_params) + _logger.error("insufficient-role", extra=logger_params) metrics.incr( self._METRICS_FAILURE_KEY + ".link_team.insufficient_role", sample_rate=1.0 ) diff --git a/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py b/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py index b452311e66772c..a90a4057ae0183 100644 --- a/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py +++ b/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py @@ -208,7 +208,7 @@ def test_unlink_no_team(self, mock_record): ) assert TEAM_NOT_LINKED_MESSAGE in get_response_text(data) - assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) @responses.activate @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") From b13a0112a6222ed0629e271913f4647e990ecdfc Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Fri, 20 Dec 2024 10:11:48 -0500 Subject: [PATCH 398/757] feat(widget-builder): Add default title (#82456) Because we have no form validation, the dashboard currently tries to submit without a title and sometimes drops a widget because the request fails. Adding a default for now so we don't accidentally drop a widget in progress. --- .../widgetBuilder/utils/getDefaultWidget.spec.tsx | 8 ++++---- .../dashboards/widgetBuilder/utils/getDefaultWidget.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.spec.tsx b/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.spec.tsx index 38422505109d04..2dc0f335cfb38e 100644 --- a/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.spec.tsx @@ -8,7 +8,7 @@ describe('getDefaultWidget', () => { expect(widget).toEqual({ displayType: DisplayType.TABLE, interval: '', - title: '', + title: 'Custom Widget', widgetType: WidgetType.ERRORS, queries: [ { @@ -29,7 +29,7 @@ describe('getDefaultWidget', () => { expect(widget).toEqual({ displayType: DisplayType.TABLE, interval: '', - title: '', + title: 'Custom Widget', widgetType: WidgetType.SPANS, queries: [ { @@ -50,7 +50,7 @@ describe('getDefaultWidget', () => { expect(widget).toEqual({ displayType: DisplayType.TABLE, interval: '', - title: '', + title: 'Custom Widget', widgetType: WidgetType.ISSUE, queries: [ { @@ -71,7 +71,7 @@ describe('getDefaultWidget', () => { expect(widget).toEqual({ displayType: DisplayType.TABLE, interval: '', - title: '', + title: 'Custom Widget', widgetType: WidgetType.RELEASE, queries: [ { diff --git a/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.tsx b/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.tsx index 0865e3eaeb052d..b221822f7e692c 100644 --- a/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.tsx +++ b/static/app/views/dashboards/widgetBuilder/utils/getDefaultWidget.tsx @@ -6,7 +6,7 @@ export function getDefaultWidget(widgetType: WidgetType): Widget { return { displayType: DisplayType.TABLE, interval: '', - title: '', + title: 'Custom Widget', widgetType, queries: [config.defaultWidgetQuery], }; From 8cb0eb6377bce72653e15e761ad9acd902f035b2 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:25:25 -0500 Subject: [PATCH 399/757] feat(dashboards): Return `meta` with `useDiscoverSeries` results (#82099) Right now, if you use `useDiscoverSeries` but you request multiple series (e.g., a Top N query, or multiple YAxis) you cannot get the series meta back, there's simply nowhere for it to go! The reason is that `useWrappedDiscoverTimeseriesQuery` drops the meta, and returns just the actual data. After looking at it for a while, I decided to skip `useWrappedDiscoverTimeseriesQuery` and use `useGenericDiscoverQuery` instead. This approach skips all the wacky processing that's leftover from the Starfish days, and cleanly re-formats and returns the data! As a benefit, this also preserves the `meta` so that it can be passed to generic Dashboard widgets for rendering. --- .../common/queries/useDiscoverSeries.spec.tsx | 84 +++++++++++++++++ .../common/queries/useDiscoverSeries.ts | 89 ++++++++++++++----- 2 files changed, 152 insertions(+), 21 deletions(-) diff --git a/static/app/views/insights/common/queries/useDiscoverSeries.spec.tsx b/static/app/views/insights/common/queries/useDiscoverSeries.spec.tsx index 438973c1d7416e..ba990742b34f33 100644 --- a/static/app/views/insights/common/queries/useDiscoverSeries.spec.tsx +++ b/static/app/views/insights/common/queries/useDiscoverSeries.spec.tsx @@ -196,6 +196,14 @@ describe('useSpanMetricsSeries', () => { [1699907700, [{count: 7810.2}]], [1699908000, [{count: 1216.8}]], ], + meta: { + fields: { + 'spm()': 'rate', + }, + units: { + 'spm()': '1/minute', + }, + }, }, }); @@ -217,6 +225,14 @@ describe('useSpanMetricsSeries', () => { {name: '2023-11-13T20:35:00+00:00', value: 7810.2}, {name: '2023-11-13T20:40:00+00:00', value: 1216.8}, ], + meta: { + fields: { + 'spm()': 'rate', + }, + units: { + 'spm()': '1/minute', + }, + }, seriesName: 'spm()', }, }); @@ -232,12 +248,28 @@ describe('useSpanMetricsSeries', () => { [1699907700, [{count: 10.1}]], [1699908000, [{count: 11.2}]], ], + meta: { + fields: { + 'http_response_rate(3)': 'rate', + }, + units: { + 'http_response_rate(3)': '1/minute', + }, + }, }, 'http_response_rate(4)': { data: [ [1699907700, [{count: 12.6}]], [1699908000, [{count: 13.8}]], ], + meta: { + fields: { + 'http_response_rate(4)': 'rate', + }, + units: { + 'http_response_rate(4)': '1/minute', + }, + }, }, }, }); @@ -263,6 +295,14 @@ describe('useSpanMetricsSeries', () => { {name: '2023-11-13T20:35:00+00:00', value: 10.1}, {name: '2023-11-13T20:40:00+00:00', value: 11.2}, ], + meta: { + fields: { + 'http_response_rate(3)': 'rate', + }, + units: { + 'http_response_rate(3)': '1/minute', + }, + }, seriesName: 'http_response_rate(3)', }, 'http_response_rate(4)': { @@ -270,6 +310,50 @@ describe('useSpanMetricsSeries', () => { {name: '2023-11-13T20:35:00+00:00', value: 12.6}, {name: '2023-11-13T20:40:00+00:00', value: 13.8}, ], + meta: { + fields: { + 'http_response_rate(4)': 'rate', + }, + units: { + 'http_response_rate(4)': '1/minute', + }, + }, + seriesName: 'http_response_rate(4)', + }, + }); + }); + + it('returns a series for all requested yAxis even without data', async () => { + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events-stats/`, + method: 'GET', + body: {}, + }); + + const {result} = renderHook( + ({yAxis}) => useSpanMetricsSeries({yAxis}, 'span-metrics-series'), + { + wrapper: Wrapper, + initialProps: { + yAxis: [ + 'http_response_rate(3)', + 'http_response_rate(4)', + ] as SpanMetricsProperty[], + }, + } + ); + + await waitFor(() => expect(result.current.isPending).toEqual(false)); + + expect(result.current.data).toEqual({ + 'http_response_rate(3)': { + data: [], + meta: undefined, + seriesName: 'http_response_rate(3)', + }, + 'http_response_rate(4)': { + data: [], + meta: undefined, seriesName: 'http_response_rate(4)', }, }); diff --git a/static/app/views/insights/common/queries/useDiscoverSeries.ts b/static/app/views/insights/common/queries/useDiscoverSeries.ts index 9be9f5386efbbf..f9d956e709aa37 100644 --- a/static/app/views/insights/common/queries/useDiscoverSeries.ts +++ b/static/app/views/insights/common/queries/useDiscoverSeries.ts @@ -1,11 +1,21 @@ -import keyBy from 'lodash/keyBy'; +import moment from 'moment-timezone'; import type {Series} from 'sentry/types/echarts'; +import {encodeSort, type EventsMetaType} from 'sentry/utils/discover/eventView'; +import { + type DiscoverQueryProps, + useGenericDiscoverQuery, +} from 'sentry/utils/discover/genericDiscoverQuery'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import type {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {getSeriesEventView} from 'sentry/views/insights/common/queries/getSeriesEventView'; -import {useWrappedDiscoverTimeseriesQuery} from 'sentry/views/insights/common/queries/useSpansQuery'; +import { + getRetryDelay, + shouldRetryHandler, +} from 'sentry/views/insights/common/utils/retryHandlers'; import type { MetricsProperty, SpanFunctions, @@ -13,11 +23,17 @@ import type { SpanMetricsProperty, } from 'sentry/views/insights/types'; +import {DATE_FORMAT} from './useSpansQuery'; + export interface MetricTimeseriesRow { [key: string]: number; interval: number; } +type DiscoverSeries = Series & { + meta?: EventsMetaType; +}; + interface UseMetricsSeriesOptions { enabled?: boolean; interval?: string; @@ -66,6 +82,8 @@ const useDiscoverSeries = ( const {search = undefined, yAxis = [], interval = undefined} = options; const pageFilters = usePageFilters(); + const location = useLocation(); + const organization = useOrganization(); const eventView = getSeriesEventView( search, @@ -80,28 +98,57 @@ const useDiscoverSeries = ( eventView.interval = interval; } - const result = useWrappedDiscoverTimeseriesQuery({ + const result = useGenericDiscoverQuery< + { + data: any[]; + meta: EventsMetaType; + }, + DiscoverQueryProps + >({ + route: 'events-stats', eventView, - initialData: [], + location, + orgSlug: organization.slug, + getRequestPayload: () => ({ + ...eventView.getEventsAPIPayload(location), + yAxis: eventView.yAxis, + topEvents: eventView.topEvents, + excludeOther: 0, + partial: 1, + orderby: eventView.sorts?.[0] ? encodeSort(eventView.sorts?.[0]) : undefined, + interval: eventView.interval, + }), + options: { + enabled: options.enabled && pageFilters.isReady, + refetchOnWindowFocus: false, + retry: shouldRetryHandler, + retryDelay: getRetryDelay, + staleTime: Infinity, + }, referrer, - enabled: options.enabled, - overriddenRoute: options.overriddenRoute, }); - const parsedData = keyBy( - yAxis.map(seriesName => { - const series: Series = { - seriesName, - data: (result?.data ?? []).map(datum => ({ - value: datum[seriesName], - name: datum?.interval, - })), - }; - - return series; - }), - 'seriesName' - ) as Record; + const parsedData: Record = {}; + + yAxis.forEach(seriesName => { + const dataSeries = result.data?.[seriesName] ?? result?.data ?? {}; + const convertedSeries: DiscoverSeries = { + seriesName, + data: convertDiscoverTimeseriesResponse(dataSeries?.data ?? []), + meta: dataSeries?.meta, + }; - return {...result, data: parsedData}; + parsedData[seriesName] = convertedSeries; + }); + + return {...result, data: parsedData as Record}; }; + +function convertDiscoverTimeseriesResponse(data: any[]): DiscoverSeries['data'] { + return data.map(([timestamp, [{count: value}]]) => { + return { + name: moment(parseInt(timestamp, 10) * 1000).format(DATE_FORMAT), + value, + }; + }); +} From 0949dd809d91ad5987aba2a7d2d405fd7c2bf03c Mon Sep 17 00:00:00 2001 From: Nikki Kapadia <72356613+nikkikapadia@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:03:41 -0500 Subject: [PATCH 400/757] fix(dashboards): Glitching bar charts on url changes (#82457) The glitching bar charts every time the url changes was annoying and it was happening due to the bar chart animations (where it rises from 0). I turned off the animations for dashboard bar charts. I've also snuck in a fix for the widget preview height on big number widgets. --- .../components/newWidgetBuilder.tsx | 19 ++++++++++++++----- .../app/views/dashboards/widgetCard/chart.tsx | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx index 33071fd2860a82..d8aaeaa8e58c84 100644 --- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx @@ -193,6 +193,19 @@ export function WidgetPreviewContainer({ position: isDragEnabled ? 'fixed' : undefined, }; + const getPreviewHeight = () => { + if (isDragEnabled) { + return DRAGGABLE_PREVIEW_HEIGHT_PX; + } + if (state.displayType === DisplayType.TABLE) { + return 'auto'; + } + if (state.displayType === DisplayType.BIG_NUMBER && !isSmallScreen) { + return '20vw'; + } + return PREVIEW_HEIGHT_PX; + }; + return ( @@ -227,11 +240,7 @@ export function WidgetPreviewContainer({ }} style={{ width: isDragEnabled ? DRAGGABLE_PREVIEW_WIDTH_PX : undefined, - height: isDragEnabled - ? DRAGGABLE_PREVIEW_HEIGHT_PX - : state.displayType === DisplayType.TABLE - ? 'auto' - : PREVIEW_HEIGHT_PX, + height: getPreviewHeight(), outline: isDragEnabled ? `${space(1)} solid ${theme.border}` : undefined, diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index b648a20fb370f4..59a4a9e5364f59 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -272,7 +272,7 @@ class WidgetCardChart extends Component { switch (widget.displayType) { case 'bar': - return ; + return ; case 'area': case 'top_n': return ; From 94fa257bb049f30027a23864f2b036133d5782b9 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 20 Dec 2024 11:18:31 -0500 Subject: [PATCH 401/757] feat(uptime): Add list uptime alerts endpoint (#82420) Adds an endpoint to list all uptime alerts for an organization. Supports typical project / environment filtering as well as some querying of URL / name --- src/sentry/api/urls.py | 9 ++ src/sentry/apidocs/parameters.py | 7 + .../organiation_uptime_alert_index.py | 120 ++++++++++++++++++ .../test_organization_uptime_alert_index.py | 81 ++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 src/sentry/uptime/endpoints/organiation_uptime_alert_index.py create mode 100644 tests/sentry/uptime/endpoints/test_organization_uptime_alert_index.py diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 555f5b63ebd125..7548b5ca23becf 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -315,6 +315,9 @@ ) from sentry.tempest.endpoints.tempest_credentials import TempestCredentialsEndpoint from sentry.tempest.endpoints.tempest_credentials_details import TempestCredentialsDetailsEndpoint +from sentry.uptime.endpoints.organiation_uptime_alert_index import ( + OrganizationUptimeAlertIndexEndpoint, +) from sentry.uptime.endpoints.project_uptime_alert_details import ProjectUptimeAlertDetailsEndpoint from sentry.uptime.endpoints.project_uptime_alert_index import ProjectUptimeAlertIndexEndpoint from sentry.users.api.endpoints.authenticator_index import AuthenticatorIndexEndpoint @@ -2199,6 +2202,12 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: OrganizationForkEndpoint.as_view(), name="sentry-api-0-organization-fork", ), + # Uptime + re_path( + r"^(?P[^\/]+)/uptime/$", + OrganizationUptimeAlertIndexEndpoint.as_view(), + name="sentry-api-0-organization-uptime-alert-index", + ), ] PROJECT_URLS: list[URLPattern | URLResolver] = [ diff --git a/src/sentry/apidocs/parameters.py b/src/sentry/apidocs/parameters.py index 863f74aa30d05e..17a4aad234d340 100644 --- a/src/sentry/apidocs/parameters.py +++ b/src/sentry/apidocs/parameters.py @@ -418,6 +418,13 @@ class UptimeParams: type=int, description="The ID of the uptime alert rule you'd like to query.", ) + OWNER = OpenApiParameter( + name="owner", + location="query", + required=False, + type=str, + description="The owner of the uptime alert, in the format `user:id` or `team:id`. May be specified multiple times.", + ) class EventParams: diff --git a/src/sentry/uptime/endpoints/organiation_uptime_alert_index.py b/src/sentry/uptime/endpoints/organiation_uptime_alert_index.py new file mode 100644 index 00000000000000..3961bb0f93656a --- /dev/null +++ b/src/sentry/uptime/endpoints/organiation_uptime_alert_index.py @@ -0,0 +1,120 @@ +from django.db.models import Q +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.response import Response + +from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus +from sentry.api.base import region_silo_endpoint +from sentry.api.bases import NoProjects +from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission +from sentry.api.helpers.teams import get_teams +from sentry.api.paginator import OffsetPaginator +from sentry.api.serializers import serialize +from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED +from sentry.apidocs.parameters import GlobalParams, OrganizationParams, UptimeParams +from sentry.apidocs.utils import inline_sentry_response_serializer +from sentry.db.models.query import in_iexact +from sentry.models.organization import Organization +from sentry.search.utils import tokenize_query +from sentry.types.actor import Actor +from sentry.uptime.endpoints.serializers import ( + ProjectUptimeSubscriptionSerializer, + ProjectUptimeSubscriptionSerializerResponse, +) +from sentry.uptime.models import ProjectUptimeSubscription + + +@region_silo_endpoint +@extend_schema(tags=["Crons"]) +class OrganizationUptimeAlertIndexEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.EXPERIMENTAL, + } + owner = ApiOwner.CRONS + permission_classes = (OrganizationPermission,) + + @extend_schema( + operation_id="Retrieve Uptime Alets for an Organization", + parameters=[ + GlobalParams.ORG_ID_OR_SLUG, + OrganizationParams.PROJECT, + GlobalParams.ENVIRONMENT, + UptimeParams.OWNER, + ], + responses={ + 200: inline_sentry_response_serializer( + "UptimeAlertList", list[ProjectUptimeSubscriptionSerializerResponse] + ), + 401: RESPONSE_UNAUTHORIZED, + 403: RESPONSE_FORBIDDEN, + 404: RESPONSE_NOT_FOUND, + }, + ) + def get(self, request: Request, organization: Organization) -> Response: + """ + Lists uptime alerts. May be filtered to a project or environment. + """ + try: + filter_params = self.get_filter_params(request, organization, date_filter_optional=True) + except NoProjects: + return self.respond([]) + + queryset = ProjectUptimeSubscription.objects.filter( + project__organization_id=organization.id, project_id__in=filter_params["project_id"] + ) + query = request.GET.get("query") + owners = request.GET.getlist("owner") + + if "environment" in filter_params: + queryset = queryset.filter(environment__in=filter_params["environment_objects"]) + + if owners: + owners_set = set(owners) + + # Remove special values from owners, this can't be parsed as an Actor + include_myteams = "myteams" in owners_set + owners_set.discard("myteams") + include_unassigned = "unassigned" in owners_set + owners_set.discard("unassigned") + + actors = [Actor.from_identifier(identifier) for identifier in owners_set] + + user_ids = [actor.id for actor in actors if actor.is_user] + team_ids = [actor.id for actor in actors if actor.is_team] + + teams = get_teams( + request, + organization, + teams=[*team_ids, *(["myteams"] if include_myteams else [])], + ) + team_ids = [team.id for team in teams] + + owner_filter = Q(owner_user_id__in=user_ids) | Q(owner_team_id__in=team_ids) + + if include_unassigned: + unassigned_filter = Q(owner_user_id=None) & Q(owner_team_id=None) + queryset = queryset.filter(unassigned_filter | owner_filter) + else: + queryset = queryset.filter(owner_filter) + + if query: + tokens = tokenize_query(query) + for key, value in tokens.items(): + if key == "query": + query_value = " ".join(value) + queryset = queryset.filter( + Q(name__icontains=query_value) + | Q(uptime_subscription__url__icontains=query_value) + ) + elif key == "name": + queryset = queryset.filter(in_iexact("name", value)) + else: + queryset = queryset.none() + + return self.paginate( + request=request, + queryset=queryset, + on_results=lambda x: serialize(x, request.user, ProjectUptimeSubscriptionSerializer()), + paginator_cls=OffsetPaginator, + ) diff --git a/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index.py b/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index.py new file mode 100644 index 00000000000000..240e1e9fa28711 --- /dev/null +++ b/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index.py @@ -0,0 +1,81 @@ +from sentry.api.serializers import serialize +from tests.sentry.uptime.endpoints import UptimeAlertBaseEndpointTest + + +class OrganizationUptimeAlertIndexBaseEndpointTest(UptimeAlertBaseEndpointTest): + endpoint = "sentry-api-0-organization-uptime-alert-index" + + +class OrganizationUptimeAlertIndexEndpointTest(OrganizationUptimeAlertIndexBaseEndpointTest): + method = "get" + + def check_valid_response(self, response, expected_alerts): + assert [serialize(uptime_alert) for uptime_alert in expected_alerts] == response.data + + def test(self): + alert_1 = self.create_project_uptime_subscription(name="test1") + alert_2 = self.create_project_uptime_subscription(name="test2") + resp = self.get_success_response(self.organization.slug) + self.check_valid_response(resp, [alert_1, alert_2]) + + def test_search_by_url(self): + self.create_project_uptime_subscription() + santry_monitor = self.create_project_uptime_subscription( + uptime_subscription=self.create_uptime_subscription(url="https://santry.com") + ) + + response = self.get_success_response(self.organization.slug, query="santry") + self.check_valid_response(response, [santry_monitor]) + + def test_environment_filter(self): + env = self.create_environment() + self.create_project_uptime_subscription() + env_monitor = self.create_project_uptime_subscription(env=env) + + response = self.get_success_response(self.organization.slug, environment=[env.name]) + self.check_valid_response(response, [env_monitor]) + + def test_owner_filter(self): + user_1 = self.create_user() + user_2 = self.create_user() + team_1 = self.create_team() + team_2 = self.create_team() + self.create_team_membership(team_2, user=self.user) + + uptime_a = self.create_project_uptime_subscription(owner=user_1) + uptime_b = self.create_project_uptime_subscription(owner=user_2) + uptime_c = self.create_project_uptime_subscription(owner=team_1) + uptime_d = self.create_project_uptime_subscription(owner=team_2) + uptime_e = self.create_project_uptime_subscription(owner=None) + + # Monitor by user + response = self.get_success_response(self.organization.slug, owner=[f"user:{user_1.id}"]) + self.check_valid_response(response, [uptime_a]) + + # Monitors by users and teams + response = self.get_success_response( + self.organization.slug, + owner=[f"user:{user_1.id}", f"user:{user_2.id}", f"team:{team_1.id}"], + ) + self.check_valid_response(response, [uptime_a, uptime_b, uptime_c]) + + # myteams + response = self.get_success_response( + self.organization.slug, + owner=["myteams"], + ) + self.check_valid_response(response, [uptime_d]) + + # unassigned monitors + response = self.get_success_response( + self.organization.slug, + owner=["unassigned", f"user:{user_1.id}"], + ) + self.check_valid_response(response, [uptime_a, uptime_e]) + + # Invalid user ID + response = self.get_success_response( + self.organization.slug, + owner=["user:12345"], + ) + self.check_valid_response(response, []) From 5ef4ce3b03c270e2cc20a738db4f77dd75d86603 Mon Sep 17 00:00:00 2001 From: Harshitha Durai <76853136+harshithadurai@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:25:52 -0500 Subject: [PATCH 402/757] fix(dashboards): modify myDashboards sorting logic (#82364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modify `myDashboards` sorting logic. Previously, the `myDashboards` sort displayed the user's dashboards first followed by other dashboards sorted by `creator id`. This PR changes that so that the other dashboards are sorted by the creator's `name`. Screenshot 2024-12-19 at 9 58 56 AM --- .../api/endpoints/organization_dashboards.py | 49 ++++++++++++--- .../endpoints/test_organization_dashboards.py | 60 +++++++++++++++++++ 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/sentry/api/endpoints/organization_dashboards.py b/src/sentry/api/endpoints/organization_dashboards.py index bd7471784a7f04..fd6b9096f8e8f0 100644 --- a/src/sentry/api/endpoints/organization_dashboards.py +++ b/src/sentry/api/endpoints/organization_dashboards.py @@ -1,7 +1,7 @@ from __future__ import annotations from django.db import IntegrityError, router, transaction -from django.db.models import Case, Exists, IntegerField, OuterRef, When +from django.db.models import Case, Exists, IntegerField, OuterRef, Value, When from drf_spectacular.utils import extend_schema from rest_framework.request import Request from rest_framework.response import Response @@ -28,8 +28,10 @@ from sentry.apidocs.examples.dashboard_examples import DashboardExamples from sentry.apidocs.parameters import CursorQueryParam, GlobalParams, VisibilityParams from sentry.apidocs.utils import inline_sentry_response_serializer +from sentry.db.models.fields.text import CharField from sentry.models.dashboard import Dashboard, DashboardFavoriteUser from sentry.models.organization import Organization +from sentry.users.services.user.service import user_service MAX_RETRIES = 2 DUPLICATE_TITLE_PATTERN = r"(.*) copy(?:$|\s(\d+))" @@ -163,14 +165,43 @@ def get(self, request: Request, organization) -> Response: order_by = ["last_visited" if desc else "-last_visited"] elif sort_by == "mydashboards": - order_by = [ - Case( - When(created_by_id=request.user.id, then=-1), - default="created_by_id", - output_field=IntegerField(), - ), - "-date_added", - ] + if features.has( + "organizations:dashboards-table-view", organization, actor=request.user + ): + user_name_dict = { + user.id: user.name + for user in user_service.get_many_by_id( + ids=list(dashboards.values_list("created_by_id", flat=True)) + ) + } + dashboards = dashboards.annotate( + user_name=Case( + *[ + When(created_by_id=user_id, then=Value(user_name)) + for user_id, user_name in user_name_dict.items() + ], + default=Value(""), + output_field=CharField(), + ) + ) + order_by = [ + Case( + When(created_by_id=request.user.id, then=-1), + default=1, + output_field=IntegerField(), + ), + "-user_name" if desc else "user_name", + "-date_added", + ] + else: + order_by = [ + Case( + When(created_by_id=request.user.id, then=-1), + default="created_by_id", + output_field=IntegerField(), + ), + "-date_added", + ] elif sort_by == "myDashboardsAndRecentlyViewed": order_by = [ diff --git a/tests/sentry/api/endpoints/test_organization_dashboards.py b/tests/sentry/api/endpoints/test_organization_dashboards.py index fb3916ddaf6722..4515974c380820 100644 --- a/tests/sentry/api/endpoints/test_organization_dashboards.py +++ b/tests/sentry/api/endpoints/test_organization_dashboards.py @@ -175,6 +175,13 @@ def test_get_sortby_mydashboards(self): values = [int(row["createdBy"]["id"]) for row in response.data if row["dateCreated"]] assert values == [self.user.id, self.user.id, user_1.id, user_2.id] + with self.feature("organizations:dashboards-table-view"): + response = self.client.get(self.url, data={"sort": "mydashboards"}) + assert response.status_code == 200, response.content + + values = [int(row["createdBy"]["id"]) for row in response.data if row["dateCreated"]] + assert values == [self.user.id, self.user.id, user_2.id, user_1.id] + def test_get_sortby_mydashboards_and_recently_viewed(self): user_1 = self.create_user(username="user_1") self.create_member(organization=self.organization, user=user_1) @@ -219,6 +226,59 @@ def test_get_sortby_mydashboards_and_recently_viewed(self): "Dashboard 3", ] + def test_get_sortby_mydashboards_with_owner_name(self): + user_1 = self.create_user(username="user_1", name="Cat") + self.create_member(organization=self.organization, user=user_1) + + user_2 = self.create_user(username="user_2", name="Pineapple") + self.create_member(organization=self.organization, user=user_2) + + user_3 = self.create_user(username="user_3", name="Banana") + self.create_member(organization=self.organization, user=user_3) + + user_4 = self.create_user(username="user_4", name="Aapple") + self.create_member(organization=self.organization, user=user_4) + + Dashboard.objects.create(title="A", created_by_id=user_1.id, organization=self.organization) + Dashboard.objects.create(title="B", created_by_id=user_2.id, organization=self.organization) + Dashboard.objects.create(title="C", created_by_id=user_3.id, organization=self.organization) + Dashboard.objects.create(title="D", created_by_id=user_4.id, organization=self.organization) + Dashboard.objects.create(title="E", created_by_id=user_2.id, organization=self.organization) + Dashboard.objects.create(title="F", created_by_id=user_1.id, organization=self.organization) + + self.login_as(user_1) + with self.feature("organizations:dashboards-table-view"): + response = self.client.get(self.url, data={"sort": "mydashboards"}) + assert response.status_code == 200, response.content + + values = [row["createdBy"]["name"] for row in response.data if row["dateCreated"]] + assert values == [ + "Cat", + "Cat", + "admin@localhost", # name is empty + "admin@localhost", + "Aapple", + "Banana", + "Pineapple", + "Pineapple", + ] + + # descending + response = self.client.get(self.url, data={"sort": "-mydashboards"}) + assert response.status_code == 200, response.content + + values = [row["createdBy"]["name"] for row in response.data if row["dateCreated"]] + assert values == [ + "Cat", + "Cat", + "Pineapple", + "Pineapple", + "Banana", + "Aapple", + "admin@localhost", # name is empty + "admin@localhost", + ] + def test_get_only_favorites_no_sort(self): user_1 = self.create_user(username="user_1") self.create_member(organization=self.organization, user=user_1) From 70f801ad17018d21c20ff239c2f83065b600fb60 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:40:24 -0500 Subject: [PATCH 403/757] ref: remove more usage of iso_format in more tests (#82460) --- .../test_organization_group_index.py | 363 +++++++++--------- tests/sentry/receivers/test_onboarding.py | 6 +- tests/sentry/snuba/test_discover_query.py | 34 +- .../snuba/test_discover_timeseries_query.py | 24 +- tests/sentry/snuba/test_errors.py | 24 +- tests/sentry/snuba/test_transactions.py | 34 +- .../test_transactions_timeseries_query.py | 10 +- .../web/frontend/test_group_tag_export.py | 8 +- .../test_organization_event_details.py | 4 +- .../api/endpoints/test_organization_events.py | 14 +- ..._organization_events_facets_performance.py | 4 +- ...ion_events_facets_performance_histogram.py | 3 +- .../test_organization_events_histogram.py | 4 +- ...est_organization_events_spans_histogram.py | 18 +- ...t_organization_events_spans_performance.py | 70 ++-- .../test_organization_events_stats.py | 92 ++--- .../test_organization_events_stats_mep.py | 16 +- .../test_organization_events_trends.py | 12 +- .../test_organization_group_index_stats.py | 4 +- .../test_organization_tagkey_values.py | 56 +-- .../api/endpoints/test_organization_tags.py | 18 +- .../endpoints/test_project_event_details.py | 6 +- .../api/endpoints/test_project_group_index.py | 30 +- tests/snuba/api/serializers/test_group.py | 36 +- .../api/serializers/test_group_stream.py | 4 +- tests/snuba/models/test_group.py | 28 +- .../rules/conditions/test_event_frequency.py | 10 +- tests/snuba/search/test_backend.py | 152 ++++---- tests/snuba/tagstore/test_tagstore_backend.py | 34 +- tests/snuba/tasks/test_unmerge.py | 14 +- tests/snuba/test_snuba.py | 6 +- tests/snuba/tsdb/test_tsdb_backend.py | 6 +- 32 files changed, 574 insertions(+), 570 deletions(-) diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index 1f600634383566..441633389a92b8 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -1,10 +1,9 @@ import functools -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta from time import sleep from unittest.mock import MagicMock, Mock, call, patch from uuid import uuid4 -from dateutil.parser import parse as parse_datetime from django.urls import reverse from django.utils import timezone @@ -59,7 +58,7 @@ from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers import parse_link_header -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.features import Feature, apply_feature_flag_on_cls, with_feature from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import assume_test_silo_mode @@ -102,7 +101,7 @@ def get_response(self, *args, **kwargs): def test_sort_by_date_with_tag(self, _: MagicMock) -> None: # XXX(dcramer): this tests a case where an ambiguous column name existed event = self.store_event( - data={"event_id": "a" * 32, "timestamp": iso_format(before_now(seconds=1))}, + data={"event_id": "a" * 32, "timestamp": before_now(seconds=1).isoformat()}, project_id=self.project.id, ) group = event.group @@ -114,7 +113,7 @@ def test_sort_by_date_with_tag(self, _: MagicMock) -> None: def test_query_for_archived(self, _: MagicMock) -> None: event = self.store_event( - data={"event_id": "a" * 32, "timestamp": iso_format(before_now(seconds=1))}, + data={"event_id": "a" * 32, "timestamp": before_now(seconds=1).isoformat()}, project_id=self.project.id, ) group = event.group @@ -130,21 +129,21 @@ def test_query_for_archived(self, _: MagicMock) -> None: def test_sort_by_trends(self, mock_query: MagicMock) -> None: group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=10)), + "timestamp": before_now(seconds=10).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, ).group self.store_event( data={ - "timestamp": iso_format(before_now(seconds=10)), + "timestamp": before_now(seconds=10).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(before_now(hours=13)), + "timestamp": before_now(hours=13).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, @@ -152,14 +151,14 @@ def test_sort_by_trends(self, mock_query: MagicMock) -> None: group_2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=5)), + "timestamp": before_now(seconds=5).isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, ).group self.store_event( data={ - "timestamp": iso_format(before_now(hours=13)), + "timestamp": before_now(hours=13).isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, @@ -180,8 +179,8 @@ def test_sort_by_trends(self, mock_query: MagicMock) -> None: sort="trends", query="is:unresolved", limit=25, - start=iso_format(before_now(days=1)), - end=iso_format(before_now(seconds=1)), + start=before_now(days=1).isoformat(), + end=before_now(seconds=1).isoformat(), **aggregate_kwargs, ) assert len(response.data) == 2 @@ -192,7 +191,7 @@ def test_sort_by_inbox(self, _: MagicMock) -> None: group_1 = self.store_event( data={ "event_id": "a" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, @@ -201,7 +200,7 @@ def test_sort_by_inbox(self, _: MagicMock) -> None: group_2 = self.store_event( data={ "event_id": "a" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, @@ -227,7 +226,7 @@ def test_sort_by_inbox_me_or_none(self, _: MagicMock) -> None: group_1 = self.store_event( data={ "event_id": "a" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, @@ -236,7 +235,7 @@ def test_sort_by_inbox_me_or_none(self, _: MagicMock) -> None: group_2 = self.store_event( data={ "event_id": "b" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, @@ -253,7 +252,7 @@ def test_sort_by_inbox_me_or_none(self, _: MagicMock) -> None: owner_by_other = self.store_event( data={ "event_id": "c" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-3"], }, project_id=self.project.id, @@ -272,7 +271,7 @@ def test_sort_by_inbox_me_or_none(self, _: MagicMock) -> None: owned_me_assigned_to_other = self.store_event( data={ "event_id": "d" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-4"], }, project_id=self.project.id, @@ -291,7 +290,7 @@ def test_sort_by_inbox_me_or_none(self, _: MagicMock) -> None: unowned_assigned_to_other = self.store_event( data={ "event_id": "e" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-5"], }, project_id=self.project.id, @@ -312,7 +311,7 @@ def test_trace_search(self, _: MagicMock) -> None: event = self.store_event( data={ "event_id": "a" * 32, - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "contexts": { "trace": { "parent_span_id": "8988cec7cc0779c1", @@ -412,12 +411,12 @@ def test_invalid_sort_key(self, _: MagicMock) -> None: def test_simple_pagination(self, _: MagicMock) -> None: event1 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=2)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=2).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) group1 = event1.group event2 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=1)), "fingerprint": ["group-2"]}, + data={"timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-2"]}, project_id=self.project.id, ) group2 = event2.group @@ -460,7 +459,7 @@ def test_environment(self, _: MagicMock) -> None: self.store_event( data={ "fingerprint": ["put-me-in-group1"], - "timestamp": iso_format(self.min_ago), + "timestamp": self.min_ago.isoformat(), "environment": "production", }, project_id=self.project.id, @@ -468,7 +467,7 @@ def test_environment(self, _: MagicMock) -> None: self.store_event( data={ "fingerprint": ["put-me-in-group2"], - "timestamp": iso_format(self.min_ago), + "timestamp": self.min_ago.isoformat(), "environment": "staging", }, project_id=self.project.id, @@ -487,7 +486,7 @@ def test_project(self, _: MagicMock) -> None: self.store_event( data={ "fingerprint": ["put-me-in-group1"], - "timestamp": iso_format(self.min_ago), + "timestamp": self.min_ago.isoformat(), "environment": "production", }, project_id=self.project.id, @@ -505,11 +504,11 @@ def test_auto_resolved(self, _: MagicMock) -> None: project = self.project project.update_option("sentry:resolve_age", 1) self.store_event( - data={"event_id": "a" * 32, "timestamp": iso_format(before_now(seconds=1))}, + data={"event_id": "a" * 32, "timestamp": before_now(seconds=1).isoformat()}, project_id=project.id, ) event2 = self.store_event( - data={"event_id": "b" * 32, "timestamp": iso_format(before_now(seconds=1))}, + data={"event_id": "b" * 32, "timestamp": before_now(seconds=1).isoformat()}, project_id=project.id, ) group2 = event2.group @@ -537,7 +536,7 @@ def test_lookup_by_event_id(self, _: MagicMock) -> None: project.update_option("sentry:resolve_age", 1) event_id = "c" * 32 event = self.store_event( - data={"event_id": event_id, "timestamp": iso_format(self.min_ago)}, + data={"event_id": event_id, "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) @@ -551,12 +550,12 @@ def test_lookup_by_event_id(self, _: MagicMock) -> None: def test_lookup_by_event_id_incorrect_project_id(self, _: MagicMock) -> None: self.store_event( - data={"event_id": "a" * 32, "timestamp": iso_format(self.min_ago)}, + data={"event_id": "a" * 32, "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) event_id = "b" * 32 event = self.store_event( - data={"event_id": event_id, "timestamp": iso_format(self.min_ago)}, + data={"event_id": event_id, "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) @@ -577,7 +576,7 @@ def test_lookup_by_event_id_with_whitespace(self, _: MagicMock) -> None: project.update_option("sentry:resolve_age", 1) event_id = "c" * 32 event = self.store_event( - data={"event_id": event_id, "timestamp": iso_format(self.min_ago)}, + data={"event_id": event_id, "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) @@ -610,7 +609,7 @@ def test_lookup_by_short_id(self, _: MagicMock) -> None: def test_lookup_by_short_id_alias(self, _: MagicMock) -> None: event_id = "f" * 32 group = self.store_event( - data={"event_id": event_id, "timestamp": iso_format(before_now(seconds=1))}, + data={"event_id": event_id, "timestamp": before_now(seconds=1).isoformat()}, project_id=self.project.id, ).group short_id = group.qualified_short_id @@ -625,11 +624,11 @@ def test_lookup_by_multiple_short_id_alias(self, _: MagicMock) -> None: project = self.project project2 = self.create_project(name="baz", organization=project.organization) event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=2))}, + data={"timestamp": before_now(seconds=2).isoformat()}, project_id=project.id, ) event2 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=1))}, + data={"timestamp": before_now(seconds=1).isoformat()}, project_id=project2.id, ) with self.feature("organizations:global-views"): @@ -708,11 +707,11 @@ def test_lookup_by_first_release(self, _: MagicMock) -> None: release.add_project(project) release.add_project(project2) event = self.store_event( - data={"release": release.version, "timestamp": iso_format(before_now(seconds=2))}, + data={"release": release.version, "timestamp": before_now(seconds=2).isoformat()}, project_id=project.id, ) event2 = self.store_event( - data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))}, + data={"release": release.version, "timestamp": before_now(seconds=1).isoformat()}, project_id=project2.id, ) @@ -741,7 +740,7 @@ def test_lookup_by_release(self, _: MagicMock) -> None: release.add_project(project) event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "tags": {"sentry:release": release.version}, }, project_id=project.id, @@ -765,7 +764,7 @@ def test_release_package_in(self, _: MagicMock) -> None: event1 = self.store_event( data={ "release": release1.version, - "timestamp": iso_format(before_now(seconds=3)), + "timestamp": before_now(seconds=3).isoformat(), "fingerprint": ["1"], }, project_id=project.id, @@ -773,7 +772,7 @@ def test_release_package_in(self, _: MagicMock) -> None: event2 = self.store_event( data={ "release": release2.version, - "timestamp": iso_format(before_now(seconds=2)), + "timestamp": before_now(seconds=2).isoformat(), "fingerprint": ["2"], }, project_id=project.id, @@ -781,7 +780,7 @@ def test_release_package_in(self, _: MagicMock) -> None: self.store_event( data={ "release": release3.version, - "timestamp": iso_format(before_now(seconds=2)), + "timestamp": before_now(seconds=2).isoformat(), "fingerprint": ["3"], }, project_id=project.id, @@ -801,7 +800,7 @@ def test_lookup_by_release_wildcard(self, _: MagicMock) -> None: release.add_project(project) event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "tags": {"sentry:release": release.version}, }, project_id=project.id, @@ -818,7 +817,7 @@ def test_lookup_by_regressed_in_release(self, _: MagicMock) -> None: release = self.create_release() event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "tags": {"sentry:release": release.version}, }, project_id=project.id, @@ -836,7 +835,7 @@ def test_pending_delete_pending_merge_excluded(self, _: MagicMock) -> None: data={ "event_id": i * 32, "fingerprint": [i], - "timestamp": iso_format(self.min_ago), + "timestamp": self.min_ago.isoformat(), }, project_id=self.project.id, ) @@ -874,7 +873,7 @@ def test_token_auth(self, _: MagicMock) -> None: def test_date_range(self, _: MagicMock) -> None: with self.options({"system.event-retention-days": 2}): event = self.store_event( - data={"timestamp": iso_format(before_now(hours=5))}, project_id=self.project.id + data={"timestamp": before_now(hours=5).isoformat()}, project_id=self.project.id ) group = event.group @@ -931,7 +930,7 @@ def test_assigned_to_pagination(self, patched_params_update: MagicMock, _: Magic ] group = self.store_event( data={ - "timestamp": iso_format(before_now(days=day)), + "timestamp": before_now(days=day).isoformat(), "fingerprint": [f"group-{day}"], }, project_id=self.project.id, @@ -980,7 +979,7 @@ def test_assigned_me_none(self, _: MagicMock) -> None: for i in range(5): group = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=10, days=i)), + "timestamp": before_now(minutes=10, days=i).isoformat(), "fingerprint": [f"group-{i}"], }, project_id=self.project.id, @@ -1005,7 +1004,7 @@ def test_assigned_me_none(self, _: MagicMock) -> None: def test_seen_stats(self, _: MagicMock) -> None: self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) before_now_300_seconds = before_now(seconds=300).isoformat() @@ -1018,7 +1017,7 @@ def test_seen_stats(self, _: MagicMock) -> None: group2.first_seen = datetime.fromisoformat(before_now_350_seconds) group2.times_seen = 55 group2.save() - before_now_250_seconds = iso_format(before_now(seconds=250)) + before_now_250_seconds = before_now(seconds=250).replace(microsecond=0).isoformat() self.store_event( data={ "timestamp": before_now_250_seconds, @@ -1029,13 +1028,13 @@ def test_seen_stats(self, _: MagicMock) -> None: ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-1"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, project_id=self.project.id, ) - before_now_150_seconds = iso_format(before_now(seconds=150)) + before_now_150_seconds = before_now(seconds=150).replace(microsecond=0).isoformat() self.store_event( data={ "timestamp": before_now_150_seconds, @@ -1044,7 +1043,7 @@ def test_seen_stats(self, _: MagicMock) -> None: }, project_id=self.project.id, ) - before_now_100_seconds = iso_format(before_now(seconds=100)) + before_now_100_seconds = before_now(seconds=100).replace(microsecond=0).isoformat() self.store_event( data={ "timestamp": before_now_100_seconds, @@ -1066,21 +1065,21 @@ def test_seen_stats(self, _: MagicMock) -> None: assert response.data[0]["lifetime"]["stats"] is None assert response.data[0]["filtered"]["stats"] != response.data[0]["stats"] - assert response.data[0]["lifetime"]["firstSeen"] == parse_datetime( + assert response.data[0]["lifetime"]["firstSeen"] == datetime.fromisoformat( before_now_350_seconds # Should match overridden value, not event value - ).replace(tzinfo=UTC) - assert response.data[0]["lifetime"]["lastSeen"] == parse_datetime( + ) + assert response.data[0]["lifetime"]["lastSeen"] == datetime.fromisoformat( before_now_100_seconds - ).replace(tzinfo=UTC) + ) assert response.data[0]["lifetime"]["count"] == "55" assert response.data[0]["filtered"]["count"] == "2" - assert response.data[0]["filtered"]["firstSeen"] == parse_datetime( + assert response.data[0]["filtered"]["firstSeen"] == datetime.fromisoformat( before_now_250_seconds - ).replace(tzinfo=UTC) - assert response.data[0]["filtered"]["lastSeen"] == parse_datetime( + ) + assert response.data[0]["filtered"]["lastSeen"] == datetime.fromisoformat( before_now_150_seconds - ).replace(tzinfo=UTC) + ) # Empty filter test: response = self.get_response(sort_by="date", limit=10, query="") @@ -1092,12 +1091,12 @@ def test_seen_stats(self, _: MagicMock) -> None: assert response.data[0]["lifetime"]["stats"] is None assert response.data[0]["lifetime"]["count"] == "55" - assert response.data[0]["lifetime"]["firstSeen"] == parse_datetime( + assert response.data[0]["lifetime"]["firstSeen"] == datetime.fromisoformat( before_now_350_seconds # Should match overridden value, not event value - ).replace(tzinfo=UTC) - assert response.data[0]["lifetime"]["lastSeen"] == parse_datetime( + ) + assert response.data[0]["lifetime"]["lastSeen"] == datetime.fromisoformat( before_now_100_seconds - ).replace(tzinfo=UTC) + ) # now with useGroupSnubaDataset = 1 response = self.get_response(sort_by="date", limit=10, query="server:example.com") @@ -1113,7 +1112,7 @@ def test_semver_seen_stats(self, _: MagicMock) -> None: release_1_e_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=5)), + "timestamp": before_now(minutes=5).replace(microsecond=0).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -1123,7 +1122,7 @@ def test_semver_seen_stats(self, _: MagicMock) -> None: release_2_e_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).replace(microsecond=0).isoformat(), "fingerprint": ["group-1"], "release": release_2.version, }, @@ -1132,7 +1131,7 @@ def test_semver_seen_stats(self, _: MagicMock) -> None: release_3_e_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).replace(microsecond=0).isoformat(), "fingerprint": ["group-1"], "release": release_3.version, }, @@ -1181,7 +1180,7 @@ def test_semver_seen_stats(self, _: MagicMock) -> None: def test_inbox_search(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-1"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -1190,7 +1189,7 @@ def test_inbox_search(self, _: MagicMock) -> None: event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-2"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -1199,7 +1198,7 @@ def test_inbox_search(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-3"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -1226,8 +1225,8 @@ def test_inbox_search_outside_retention(self, _: MagicMock) -> None: query="is:unresolved is:for_review", collapse="stats", expand=["inbox", "owners"], - start=iso_format(before_now(days=20)), - end=iso_format(before_now(days=15)), + start=before_now(days=20).isoformat(), + end=before_now(days=15).isoformat(), ) assert response.status_code == 200 assert len(response.data) == 0 @@ -1236,7 +1235,7 @@ def test_inbox_search_outside_retention(self, _: MagicMock) -> None: def test_assigned_or_suggested_search(self, _: MagicMock) -> None: event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=180)), + "timestamp": before_now(seconds=180).isoformat(), "fingerprint": ["group-1"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -1244,7 +1243,7 @@ def test_assigned_or_suggested_search(self, _: MagicMock) -> None: ) event1 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=185)), + "timestamp": before_now(seconds=185).isoformat(), "fingerprint": ["group-2"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -1252,7 +1251,7 @@ def test_assigned_or_suggested_search(self, _: MagicMock) -> None: ) event2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=190)), + "timestamp": before_now(seconds=190).isoformat(), "fingerprint": ["group-3"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -1261,7 +1260,7 @@ def test_assigned_or_suggested_search(self, _: MagicMock) -> None: assigned_event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-4"], }, project_id=self.project.id, @@ -1269,7 +1268,7 @@ def test_assigned_or_suggested_search(self, _: MagicMock) -> None: assigned_to_other_event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-5"], }, project_id=self.project.id, @@ -1419,7 +1418,7 @@ def test_semver(self, _: MagicMock) -> None: release_1_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -1427,7 +1426,7 @@ def test_semver(self, _: MagicMock) -> None: ).group.id release_1_g_2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-2"], "release": release_1.version, }, @@ -1435,7 +1434,7 @@ def test_semver(self, _: MagicMock) -> None: ).group.id release_2_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-3"], "release": release_2.version, }, @@ -1443,7 +1442,7 @@ def test_semver(self, _: MagicMock) -> None: ).group.id release_2_g_2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=4)), + "timestamp": before_now(minutes=4).isoformat(), "fingerprint": ["group-4"], "release": release_2.version, }, @@ -1451,7 +1450,7 @@ def test_semver(self, _: MagicMock) -> None: ).group.id release_3_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=5)), + "timestamp": before_now(minutes=5).isoformat(), "fingerprint": ["group-5"], "release": release_3.version, }, @@ -1459,7 +1458,7 @@ def test_semver(self, _: MagicMock) -> None: ).group.id release_3_g_2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=6)), + "timestamp": before_now(minutes=6).isoformat(), "fingerprint": ["group-6"], "release": release_3.version, }, @@ -1519,7 +1518,7 @@ def test_release_stage(self, _: MagicMock) -> None: adopted_release_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group-1"], "release": adopted_release.version, "environment": self.environment.name, @@ -1528,7 +1527,7 @@ def test_release_stage(self, _: MagicMock) -> None: ).group.id adopted_release_g_2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-2"], "release": adopted_release.version, "environment": self.environment.name, @@ -1537,7 +1536,7 @@ def test_release_stage(self, _: MagicMock) -> None: ).group.id replaced_release_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-3"], "release": replaced_release.version, "environment": self.environment.name, @@ -1546,7 +1545,7 @@ def test_release_stage(self, _: MagicMock) -> None: ).group.id replaced_release_g_2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=4)), + "timestamp": before_now(minutes=4).isoformat(), "fingerprint": ["group-4"], "release": replaced_release.version, "environment": self.environment.name, @@ -1613,7 +1612,7 @@ def test_semver_package(self, _: MagicMock) -> None: release_1_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -1621,7 +1620,7 @@ def test_semver_package(self, _: MagicMock) -> None: ).group.id release_1_g_2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-2"], "release": release_1.version, }, @@ -1629,7 +1628,7 @@ def test_semver_package(self, _: MagicMock) -> None: ).group.id release_2_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-3"], "release": release_2.version, }, @@ -1657,7 +1656,7 @@ def test_semver_build(self, _: MagicMock) -> None: release_1_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -1665,7 +1664,7 @@ def test_semver_build(self, _: MagicMock) -> None: ).group.id release_1_g_2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-2"], "release": release_1.version, }, @@ -1673,7 +1672,7 @@ def test_semver_build(self, _: MagicMock) -> None: ).group.id release_2_g_1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-3"], "release": release_2.version, }, @@ -1698,7 +1697,7 @@ def test_semver_build(self, _: MagicMock) -> None: def test_aggregate_stats_regression_test(self, _: MagicMock) -> None: self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) @@ -1712,12 +1711,12 @@ def test_aggregate_stats_regression_test(self, _: MagicMock) -> None: def test_skipped_fields(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-1"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -1726,7 +1725,7 @@ def test_skipped_fields(self, _: MagicMock) -> None: query = "server:example.com" query += " status:unresolved" - query += " first_seen:" + iso_format(before_now(seconds=500)) + query += " first_seen:" + before_now(seconds=500).isoformat() self.login_as(user=self.user) response = self.get_response(sort_by="date", limit=10, query=query) @@ -1739,7 +1738,7 @@ def test_skipped_fields(self, _: MagicMock) -> None: def test_inbox_fields(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) add_group_to_inbox(event.group, GroupInboxReason.NEW) @@ -1773,7 +1772,7 @@ def test_inbox_fields(self, _: MagicMock) -> None: def test_inbox_fields_issue_states(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) add_group_to_inbox(event.group, GroupInboxReason.NEW) @@ -1805,7 +1804,7 @@ def test_inbox_fields_issue_states(self, _: MagicMock) -> None: def test_expand_string(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) add_group_to_inbox(event.group, GroupInboxReason.NEW) @@ -1821,7 +1820,7 @@ def test_expand_string(self, _: MagicMock) -> None: def test_expand_plugin_actions_and_issues(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) query = "status:unresolved" @@ -1845,7 +1844,7 @@ def test_expand_plugin_actions_and_issues(self, _: MagicMock) -> None: def test_expand_integration_issues(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) query = "status:unresolved" @@ -1896,7 +1895,7 @@ def test_expand_integration_issues(self, _: MagicMock) -> None: def test_expand_sentry_app_issues(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) query = "status:unresolved" @@ -1951,7 +1950,7 @@ def test_expand_sentry_app_issues(self, _: MagicMock) -> None: @with_feature("organizations:event-attachments") def test_expand_latest_event_has_attachments(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) query = "status:unresolved" @@ -1995,7 +1994,7 @@ def test_expand_no_latest_event_has_no_attachments( self, _: MagicMock, mock_latest_event: MagicMock ) -> None: self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) query = "status:unresolved" @@ -2010,7 +2009,7 @@ def test_expand_no_latest_event_has_no_attachments( def test_expand_owners(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) query = "status:unresolved" @@ -2071,11 +2070,11 @@ def test_expand_owners(self, _: MagicMock) -> None: def test_default_search(self, _: MagicMock) -> None: event1 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event2 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-2"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-2"]}, project_id=self.project.id, ) event2.group.update(status=GroupStatus.RESOLVED, substatus=None) @@ -2087,13 +2086,13 @@ def test_default_search(self, _: MagicMock) -> None: def test_default_search_with_priority(self, _: MagicMock) -> None: event1 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event1.group.priority = PriorityLevel.HIGH event1.group.save() event2 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-3"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-3"]}, project_id=self.project.id, ) event2.group.status = GroupStatus.RESOLVED @@ -2102,7 +2101,7 @@ def test_default_search_with_priority(self, _: MagicMock) -> None: event2.group.save() event3 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=400)), "fingerprint": ["group-2"]}, + data={"timestamp": before_now(seconds=400).isoformat(), "fingerprint": ["group-2"]}, project_id=self.project.id, ) event3.group.priority = PriorityLevel.LOW @@ -2117,7 +2116,7 @@ def test_default_search_with_priority(self, _: MagicMock) -> None: def test_collapse_stats(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -2137,7 +2136,7 @@ def test_collapse_stats(self, _: MagicMock) -> None: def test_collapse_lifetime(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -2156,7 +2155,7 @@ def test_collapse_lifetime(self, _: MagicMock) -> None: def test_collapse_filtered(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -2175,7 +2174,7 @@ def test_collapse_filtered(self, _: MagicMock) -> None: def test_collapse_lifetime_and_filtered(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -2194,7 +2193,7 @@ def test_collapse_lifetime_and_filtered(self, _: MagicMock) -> None: def test_collapse_base(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -2218,7 +2217,7 @@ def test_collapse_stats_group_snooze_bug(self, _: MagicMock) -> None: # There was a bug where we tried to access attributes on seen_stats if this feature is active # but seen_stats could be null when we collapse stats. event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) GroupSnooze.objects.create( @@ -2240,7 +2239,7 @@ def test_collapse_stats_group_snooze_bug(self, _: MagicMock) -> None: @with_feature("organizations:issue-stream-performance") def test_collapse_unhandled(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -2261,7 +2260,7 @@ def test_selected_saved_search(self, _: MagicMock) -> None: ) event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"], "message": "ZeroDivisionError", }, @@ -2270,7 +2269,7 @@ def test_selected_saved_search(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-2"], "message": "TypeError", }, @@ -2299,7 +2298,7 @@ def test_pinned_saved_search(self, _: MagicMock) -> None: ) event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"], "message": "ZeroDivisionError", }, @@ -2308,7 +2307,7 @@ def test_pinned_saved_search(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-2"], "message": "TypeError", }, @@ -2336,7 +2335,7 @@ def test_pinned_saved_search_with_query(self, _: MagicMock) -> None: ) event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"], "message": "ZeroDivisionError", }, @@ -2345,7 +2344,7 @@ def test_pinned_saved_search_with_query(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-2"], "message": "TypeError", }, @@ -2383,7 +2382,7 @@ def test_user_default_custom_view_query(self, _: MagicMock) -> None: ) event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"], "message": "ZeroDivisionError", }, @@ -2392,7 +2391,7 @@ def test_user_default_custom_view_query(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-2"], "message": "TypeError", }, @@ -2432,7 +2431,7 @@ def test_non_default_custom_view_query(self, _: MagicMock) -> None: event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"], "message": "ZeroDivisionError", }, @@ -2455,7 +2454,7 @@ def test_non_default_custom_view_query(self, _: MagicMock) -> None: def test_global_default_custom_view_query(self, _: MagicMock) -> None: event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"], "message": "ZeroDivisionError", }, @@ -2475,7 +2474,7 @@ def test_global_default_custom_view_query(self, _: MagicMock) -> None: def test_query_status_and_substatus_overlapping(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event.group.update(status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.ONGOING) @@ -2529,7 +2528,7 @@ def test_query_status_and_substatus_overlapping(self, _: MagicMock) -> None: def test_query_status_and_substatus_nonoverlapping(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event.group.update(status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.ONGOING) @@ -2579,7 +2578,7 @@ def test_query_status_and_substatus_nonoverlapping(self, _: MagicMock) -> None: def test_use_group_snuba_dataset(self, mock_query: MagicMock) -> None: self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -2620,15 +2619,15 @@ def test_snuba_order_by_first_seen_of_issue(self, _: MagicMock) -> None: def test_snuba_order_by_freq(self, mock_query: MagicMock) -> None: event1 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=3)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=3).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(before_now(seconds=2)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=2).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event2 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=1)), "fingerprint": ["group-2"]}, + data={"timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["group-2"]}, project_id=self.project.id, ) @@ -2658,7 +2657,7 @@ def test_snuba_order_by_user_count(self, mock_query: MagicMock) -> None: # 2 events, 2 users event1 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=6)), + "timestamp": before_now(seconds=6).isoformat(), "fingerprint": ["group-1"], "user": user2, }, @@ -2666,7 +2665,7 @@ def test_snuba_order_by_user_count(self, mock_query: MagicMock) -> None: ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=5)), + "timestamp": before_now(seconds=5).isoformat(), "fingerprint": ["group-1"], "user": user3, }, @@ -2676,7 +2675,7 @@ def test_snuba_order_by_user_count(self, mock_query: MagicMock) -> None: # 3 events, 1 user for group 1 event2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=4)), + "timestamp": before_now(seconds=4).isoformat(), "fingerprint": ["group-2"], "user": user1, }, @@ -2684,7 +2683,7 @@ def test_snuba_order_by_user_count(self, mock_query: MagicMock) -> None: ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=3)), + "timestamp": before_now(seconds=3).isoformat(), "fingerprint": ["group-2"], "user": user1, }, @@ -2692,7 +2691,7 @@ def test_snuba_order_by_user_count(self, mock_query: MagicMock) -> None: ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=2)), + "timestamp": before_now(seconds=2).isoformat(), "fingerprint": ["group-2"], "user": user1, }, @@ -3158,7 +3157,7 @@ def test_snuba_perf_issue(self, mock_query: MagicMock) -> None: user={"email": "myemail@example.com"}, event_data={ "type": "transaction", - "start_timestamp": iso_format(datetime.now() - timedelta(minutes=1)), + "start_timestamp": (datetime.now() - timedelta(minutes=1)).isoformat(), "contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}}, }, ) @@ -3217,7 +3216,7 @@ def test_snuba_type_and_category( [f"{PerformanceRenderBlockingAssetSpanGroupType.type_id}-group1"], event_data={ "type": "transaction", - "start_timestamp": iso_format(datetime.now() - timedelta(minutes=1)), + "start_timestamp": (datetime.now() - timedelta(minutes=1)).isoformat(), "contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}}, }, override_occurrence_data={ @@ -3233,7 +3232,7 @@ def test_snuba_type_and_category( [f"{PerformanceNPlusOneGroupType.type_id}-group2"], event_data={ "type": "transaction", - "start_timestamp": iso_format(datetime.now() - timedelta(minutes=1)), + "start_timestamp": (datetime.now() - timedelta(minutes=1)).isoformat(), "contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}}, }, override_occurrence_data={ @@ -3330,7 +3329,7 @@ def test_pagination_and_x_hits_header(self, _: MagicMock) -> None: for i in range(30): self.store_event( data={ - "timestamp": iso_format(before_now(seconds=i)), + "timestamp": before_now(seconds=i).isoformat(), "fingerprint": [f"group-{i}"], }, project_id=self.project.id, @@ -3385,7 +3384,7 @@ def test_find_error_by_message_with_snuba_only_search(self, _: MagicMock) -> Non # Simulate sending an event with Kafka enabled event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "message": "OutOfMemoryError", "tags": {"level": "error"}, }, @@ -3394,7 +3393,7 @@ def test_find_error_by_message_with_snuba_only_search(self, _: MagicMock) -> Non # Simulate sending another event that matches the wildcard filter event2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "message": "MemoryError", "tags": {"level": "error"}, }, @@ -3404,7 +3403,7 @@ def test_find_error_by_message_with_snuba_only_search(self, _: MagicMock) -> Non # Simulate sending another event that doesn't match the filter self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "message": "NullPointerException", "tags": {"level": "error"}, }, @@ -3439,7 +3438,7 @@ def test_first_seen_and_last_seen_filters(self, _: MagicMock) -> None: for i, (time1, time2) in enumerate(times): self.store_event( data={ - "timestamp": iso_format(time1), + "timestamp": time1.isoformat(), "message": f"Error {i}", "fingerprint": [f"group-{i}"], }, @@ -3447,7 +3446,7 @@ def test_first_seen_and_last_seen_filters(self, _: MagicMock) -> None: ) self.store_event( data={ - "timestamp": iso_format(time2), + "timestamp": time2.isoformat(), "message": f"Error {i} - additional event", "fingerprint": [f"group-{i}"], }, @@ -3455,7 +3454,7 @@ def test_first_seen_and_last_seen_filters(self, _: MagicMock) -> None: ) # Test firstSeen filter - twenty_four_hours_ago = iso_format(before_now(hours=24)) + twenty_four_hours_ago = before_now(hours=24).isoformat() response = self.get_success_response(query=f"firstSeen:<{twenty_four_hours_ago}") assert len(response.data) == 0 response = self.get_success_response(query="firstSeen:-24h") @@ -3469,7 +3468,7 @@ def test_first_seen_and_last_seen_filters(self, _: MagicMock) -> None: assert len(response.data) == 4 # Test lastSeen filter with an absolute date using before_now - absolute_date = iso_format(before_now(days=1)) # Assuming 365 days before now as an example + absolute_date = before_now(days=1).isoformat() # Assuming 365 days before now as an example response = self.get_success_response(query=f"lastSeen:>{absolute_date}") assert len(response.data) == 4 response = self.get_success_response(query=f"lastSeen:<{absolute_date}") @@ -3483,7 +3482,7 @@ def test_filter_by_bookmarked_by(self, _: MagicMock) -> None: # Create two issues, one bookmarked by each user event1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "message": "Error 1", "fingerprint": ["group-1"], }, @@ -3494,7 +3493,7 @@ def test_filter_by_bookmarked_by(self, _: MagicMock) -> None: event2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "message": "Error 2", "fingerprint": ["group-2"], }, @@ -3520,7 +3519,7 @@ def test_filter_by_linked(self, _: MagicMock) -> None: # Create two issues, one linked and one not linked event1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "message": "Error 1", "fingerprint": ["group-1"], }, @@ -3535,7 +3534,7 @@ def test_filter_by_linked(self, _: MagicMock) -> None: ) event2 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "message": "Error 2", "fingerprint": ["group-2"], }, @@ -3560,7 +3559,7 @@ def test_filter_by_subscribed_by(self, _: MagicMock) -> None: # Create two issues, one subscribed by user1 and one not subscribed event1 = self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "message": "Error 1", "fingerprint": ["group-1"], }, @@ -3575,7 +3574,7 @@ def test_filter_by_subscribed_by(self, _: MagicMock) -> None: ) self.store_event( data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "message": "Error 2", "fingerprint": ["group-2"], }, @@ -3597,7 +3596,7 @@ def test_snuba_search_lookup_by_regressed_in_release(self, _: MagicMock) -> None release = self.create_release() event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "tags": {"sentry:release": release.version}, }, project_id=project.id, @@ -3618,7 +3617,7 @@ def test_lookup_by_release_build(self, _: MagicMock) -> None: release = self.create_release(version="steve@1.2.7+123") event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "tags": {"sentry:release": release.version}, }, project_id=project.id, @@ -3638,7 +3637,7 @@ def test_snuba_search_lookup_by_stack_filename(self, _: MagicMock) -> None: project = self.project event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "fingerprint": ["unique-fingerprint-1"], "exception": { "values": [ @@ -3662,7 +3661,7 @@ def test_snuba_search_lookup_by_stack_filename(self, _: MagicMock) -> None: ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=2)), + "timestamp": before_now(seconds=2).isoformat(), "fingerprint": ["unique-fingerprint-2"], "exception": { "values": [ @@ -3699,7 +3698,7 @@ def test_error_main_thread_condition(self, _: MagicMock) -> None: # Simulate sending an event with main_thread set to true event1 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), "message": "MainThreadError", "exception": { "values": [ @@ -3717,7 +3716,7 @@ def test_error_main_thread_condition(self, _: MagicMock) -> None: # Simulate sending an event with main_thread set to false event2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=2)), + "timestamp": before_now(seconds=2).isoformat(), "message": "WorkerThreadError", "exception": { "values": [ @@ -3747,7 +3746,7 @@ def test_error_main_thread_condition(self, _: MagicMock) -> None: def test_snuba_heavy_search_aggregate_stats_regression_test(self, _: MagicMock) -> None: self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) @@ -3764,7 +3763,7 @@ def test_snuba_heavy_search_aggregate_stats_regression_test(self, _: MagicMock) def test_snuba_heavy_search_inbox_search(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-1"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -3773,7 +3772,7 @@ def test_snuba_heavy_search_inbox_search(self, _: MagicMock) -> None: event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-2"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -3782,7 +3781,7 @@ def test_snuba_heavy_search_inbox_search(self, _: MagicMock) -> None: self.store_event( data={ - "timestamp": iso_format(before_now(seconds=200)), + "timestamp": before_now(seconds=200).isoformat(), "fingerprint": ["group-3"], "tags": {"server": "example.com", "trace": "woof", "message": "foo"}, }, @@ -3830,7 +3829,7 @@ def test_snuba_heavy_advanced_search_errors(self, mock_record: MagicMock, _: Mag def test_snuba_heavy_filter_not_unresolved(self, _: MagicMock) -> None: event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event.group.update(status=GroupStatus.RESOLVED, substatus=None) @@ -3849,7 +3848,7 @@ def test_snuba_heavy_sdk_name_with_negations_and_positive_checks(self, _: MagicM # Store an event with sdk.name as sentry.python event_python = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=500)), + "timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"], "sdk": {"name": "sentry.python", "version": "0.13.19"}, }, @@ -3859,7 +3858,7 @@ def test_snuba_heavy_sdk_name_with_negations_and_positive_checks(self, _: MagicM # Store another event with sdk.name as sentry.javascript event_javascript = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=400)), + "timestamp": before_now(seconds=400).isoformat(), "fingerprint": ["group-2"], "sdk": {"name": "sentry.javascript", "version": "2.1.1"}, }, @@ -3903,7 +3902,7 @@ def test_snuba_heavy_error_handled_boolean(self, _: MagicMock) -> None: # Create an event with an unhandled exception unhandled_event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=300)), + "timestamp": before_now(seconds=300).isoformat(), "level": "error", "fingerprint": ["unhandled-group"], "exception": { @@ -3922,7 +3921,7 @@ def test_snuba_heavy_error_handled_boolean(self, _: MagicMock) -> None: # Create an event with a handled exception handled_event = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=300)), + "timestamp": before_now(seconds=300).isoformat(), "fingerprint": ["handled-group"], "exception": { "values": [ @@ -3971,7 +3970,7 @@ def run_feedback_filtered_by_default_test(self, use_group_snuba_dataset: bool): } ): event = self.store_event( - data={"event_id": uuid4().hex, "timestamp": iso_format(before_now(seconds=1))}, + data={"event_id": uuid4().hex, "timestamp": before_now(seconds=1).isoformat()}, project_id=self.project.id, ) assert event.group is not None @@ -4005,7 +4004,7 @@ def run_feedback_category_filter_test(self, use_group_snuba_dataset: bool): } ): event = self.store_event( - data={"event_id": uuid4().hex, "timestamp": iso_format(before_now(seconds=1))}, + data={"event_id": uuid4().hex, "timestamp": before_now(seconds=1).isoformat()}, project_id=self.project.id, ) assert event.group is not None @@ -4156,7 +4155,7 @@ def test_bulk_resolve(self) -> None: self.store_event( data={ "fingerprint": [i], - "timestamp": iso_format(self.min_ago - timedelta(seconds=i)), + "timestamp": (self.min_ago - timedelta(seconds=i)).isoformat(), }, project_id=self.project.id, ) @@ -4180,7 +4179,7 @@ def test_resolve_with_integration(self, mock_sync_status_outbound: MagicMock) -> integration = self.create_provider_integration(provider="example", name="Example") integration.add_organization(org, self.user) event = self.store_event( - data={"timestamp": iso_format(self.min_ago)}, project_id=self.project.id + data={"timestamp": self.min_ago.isoformat()}, project_id=self.project.id ) group = event.group @@ -4354,7 +4353,7 @@ def test_in_semver_projects_group_resolution_stores_current_release_version(self self.store_event( data={ - "timestamp": iso_format(before_now(seconds=10)), + "timestamp": before_now(seconds=10).isoformat(), "fingerprint": ["group-1"], "release": release_21_1_1.version, }, @@ -4362,7 +4361,7 @@ def test_in_semver_projects_group_resolution_stores_current_release_version(self ) group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=12)), + "timestamp": before_now(seconds=12).isoformat(), "fingerprint": ["group-1"], "release": release_21_1_0.version, }, @@ -4420,7 +4419,7 @@ def test_in_non_semver_projects_group_resolution_stores_current_release_version( group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=12)), + "timestamp": before_now(seconds=12).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -4469,7 +4468,7 @@ def test_in_non_semver_projects_store_actual_current_release_version_not_cached_ group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=12)), + "timestamp": before_now(seconds=12).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -4484,7 +4483,7 @@ def test_in_non_semver_projects_store_actual_current_release_version_not_cached_ self.store_event( data={ - "timestamp": iso_format(before_now(seconds=0)), + "timestamp": before_now(seconds=0).isoformat(), "fingerprint": ["group-1"], "release": release_2.version, }, @@ -4527,7 +4526,7 @@ def test_in_non_semver_projects_resolved_in_next_release_is_equated_to_in_releas group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=12)), + "timestamp": before_now(seconds=12).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -4675,7 +4674,7 @@ def test_in_semver_projects_set_resolved_in_explicit_release(self) -> None: group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=10)), + "timestamp": before_now(seconds=10).isoformat(), "fingerprint": ["group-1"], "release": release_1.version, }, @@ -5006,7 +5005,7 @@ def test_snooze_user_count(self) -> None: data={ "fingerprint": ["put-me-in-group-1"], "user": {"id": str(i)}, - "timestamp": iso_format(self.min_ago + timedelta(seconds=i)), + "timestamp": (self.min_ago + timedelta(seconds=i)).isoformat(), }, project_id=self.project.id, ) diff --git a/tests/sentry/receivers/test_onboarding.py b/tests/sentry/receivers/test_onboarding.py index a6f9aa52c92044..b893890bef22f0 100644 --- a/tests/sentry/receivers/test_onboarding.py +++ b/tests/sentry/receivers/test_onboarding.py @@ -31,7 +31,7 @@ ) from sentry.silo.base import SiloMode from sentry.testutils.cases import TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.outbox import outbox_runner from sentry.testutils.silo import assume_test_silo_mode from sentry.testutils.skips import requires_snuba @@ -1068,7 +1068,7 @@ def test_new_onboarding_complete(self, record_analytics): data={ "event_id": "c" * 32, "message": "this is bad.", - "timestamp": iso_format(timezone.now()), + "timestamp": timezone.now().isoformat(), "type": "error", }, project_id=project.id, @@ -1328,7 +1328,7 @@ def test_source_maps_as_required_task(self, record_analytics): data={ "event_id": "c" * 32, "message": "this is bad.", - "timestamp": iso_format(timezone.now()), + "timestamp": timezone.now().isoformat(), "type": "error", "release": "my-first-release", }, diff --git a/tests/sentry/snuba/test_discover_query.py b/tests/sentry/snuba/test_discover_query.py index bec9522663f07f..a4970721a9a38a 100644 --- a/tests/sentry/snuba/test_discover_query.py +++ b/tests/sentry/snuba/test_discover_query.py @@ -24,7 +24,7 @@ from sentry.search.events.types import SnubaParams from sentry.snuba import discover from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data ARRAY_COLUMNS = ["measurements", "span_op_breakdowns"] @@ -383,8 +383,8 @@ def test_timestamp_rounding_fields(self): hour = self.event_time.replace(minute=0, second=0, microsecond=0) day = hour.replace(hour=0) - assert [item["timestamp.to_hour"] for item in data] == [f"{iso_format(hour)}+00:00"] - assert [item["timestamp.to_day"] for item in data] == [f"{iso_format(day)}+00:00"] + assert [item["timestamp.to_hour"] for item in data] == [hour.isoformat()] + assert [item["timestamp.to_day"] for item in data] == [day.isoformat()] def test_timestamp_rounding_filters(self): one_day_ago = before_now(days=1) @@ -406,7 +406,7 @@ def test_timestamp_rounding_filters(self): result = discover.query( selected_columns=["timestamp.to_hour", "timestamp.to_day"], - query=f"timestamp.to_hour:<{iso_format(one_day_ago)} timestamp.to_day:<{iso_format(one_day_ago)}", + query=f"timestamp.to_hour:<{one_day_ago.isoformat()} timestamp.to_day:<{one_day_ago.isoformat()}", snuba_params=self.params, referrer="test_discover_query", ) @@ -415,8 +415,8 @@ def test_timestamp_rounding_filters(self): hour = two_day_ago.replace(minute=0, second=0, microsecond=0) day = hour.replace(hour=0) - assert [item["timestamp.to_hour"] for item in data] == [f"{iso_format(hour)}+00:00"] - assert [item["timestamp.to_day"] for item in data] == [f"{iso_format(day)}+00:00"] + assert [item["timestamp.to_hour"] for item in data] == [hour.isoformat()] + assert [item["timestamp.to_day"] for item in data] == [day.isoformat()] def test_user_display(self): # `user.display` should give `username` @@ -2254,8 +2254,8 @@ def test_field_aliasing_in_selected_columns(self): assert data[0]["user"] == "id:99" assert data[0]["release"] == "first-release" - event_hour = self.event_time.replace(minute=0, second=0) - assert data[0]["timestamp.to_hour"] == iso_format(event_hour) + "+00:00" + event_hour = self.event_time.replace(minute=0, second=0, microsecond=0) + assert data[0]["timestamp.to_hour"] == event_hour.isoformat() assert len(result["meta"]["fields"]) == 4 assert result["meta"]["fields"] == { @@ -2849,8 +2849,8 @@ def test_conditions_with_timestamps(self): results = discover.query( selected_columns=["transaction", "count()"], query="event.type:transaction AND (timestamp:<{} OR timestamp:>{})".format( - iso_format(self.now - timedelta(seconds=5)), - iso_format(self.now - timedelta(seconds=3)), + (self.now - timedelta(seconds=5)).isoformat(), + (self.now - timedelta(seconds=3)).isoformat(), ), snuba_params=SnubaParams( projects=[self.project], @@ -2873,7 +2873,7 @@ def test_timestamp_rollup_filter(self): event_hour = self.event_time.replace(minute=0, second=0) result = discover.query( selected_columns=["project.id", "user", "release"], - query="timestamp.to_hour:" + iso_format(event_hour), + query="timestamp.to_hour:" + event_hour.isoformat(), snuba_params=self.params, referrer="discover", ) @@ -3149,8 +3149,8 @@ def setUp(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 1500 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) self.params = SnubaParams( projects=[self.project], @@ -3251,8 +3251,8 @@ def test_orderby_equation(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 300 * i - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) results = discover.query( selected_columns=[ @@ -3401,8 +3401,8 @@ def test_nan_equation_results(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 0 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) results = discover.query( selected_columns=[ diff --git a/tests/sentry/snuba/test_discover_timeseries_query.py b/tests/sentry/snuba/test_discover_timeseries_query.py index 119bc1f4a57470..c074e22ad0c107 100644 --- a/tests/sentry/snuba/test_discover_timeseries_query.py +++ b/tests/sentry/snuba/test_discover_timeseries_query.py @@ -9,7 +9,7 @@ from sentry.snuba import discover from sentry.snuba.dataset import Dataset from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data ARRAY_COLUMNS = ["measurements", "span_op_breakdowns"] @@ -26,7 +26,7 @@ def setUp(self): data={ "event_id": "a" * 32, "message": "very bad", - "timestamp": iso_format(self.day_ago + timedelta(hours=1)), + "timestamp": (self.day_ago + timedelta(hours=1)).isoformat(), "fingerprint": ["group1"], "tags": {"important": "yes"}, "user": {"id": 1}, @@ -37,7 +37,7 @@ def setUp(self): data={ "event_id": "b" * 32, "message": "oh my", - "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=1)), + "timestamp": (self.day_ago + timedelta(hours=1, minutes=1)).isoformat(), "fingerprint": ["group2"], "tags": {"important": "no"}, }, @@ -47,7 +47,7 @@ def setUp(self): data={ "event_id": "c" * 32, "message": "very bad", - "timestamp": iso_format(self.day_ago + timedelta(hours=2, minutes=1)), + "timestamp": (self.day_ago + timedelta(hours=2, minutes=1)).isoformat(), "fingerprint": ["group2"], "tags": {"important": "yes"}, }, @@ -167,7 +167,7 @@ def test_comparison_aggregate_function_invalid(self): def test_comparison_aggregate_function(self): self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(hours=1)), + "timestamp": (self.day_ago + timedelta(hours=1)).isoformat(), "user": {"id": 1}, }, project_id=self.project.id, @@ -191,21 +191,21 @@ def test_comparison_aggregate_function(self): self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=1)), + "timestamp": (self.day_ago + timedelta(days=-1, hours=1)).isoformat(), "user": {"id": 1}, }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=1, minutes=2)), + "timestamp": (self.day_ago + timedelta(days=-1, hours=1, minutes=2)).isoformat(), "user": {"id": 2}, }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=2, minutes=1)), + "timestamp": (self.day_ago + timedelta(days=-1, hours=2, minutes=1)).isoformat(), }, project_id=self.project.id, ) @@ -253,8 +253,8 @@ def test_count_miserable(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 300 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) ProjectTransactionThreshold.objects.create( project=self.project, @@ -294,8 +294,8 @@ def test_count_miserable_with_arithmetic(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 300 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) ProjectTransactionThreshold.objects.create( project=self.project, diff --git a/tests/sentry/snuba/test_errors.py b/tests/sentry/snuba/test_errors.py index bd639ce7da06ce..378411769b9f33 100644 --- a/tests/sentry/snuba/test_errors.py +++ b/tests/sentry/snuba/test_errors.py @@ -16,7 +16,7 @@ from sentry.search.events.types import SnubaParams from sentry.snuba import errors from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data ARRAY_COLUMNS = ["measurements", "span_op_breakdowns"] @@ -296,8 +296,8 @@ def test_timestamp_rounding_fields(self): hour = self.event_time.replace(minute=0, second=0, microsecond=0) day = hour.replace(hour=0) - assert [item["timestamp.to_hour"] for item in data] == [f"{iso_format(hour)}+00:00"] - assert [item["timestamp.to_day"] for item in data] == [f"{iso_format(day)}+00:00"] + assert [item["timestamp.to_hour"] for item in data] == [hour.isoformat()] + assert [item["timestamp.to_day"] for item in data] == [day.isoformat()] def test_timestamp_rounding_filters(self): one_day_ago = before_now(days=1) @@ -319,7 +319,7 @@ def test_timestamp_rounding_filters(self): result = errors.query( selected_columns=["timestamp.to_hour", "timestamp.to_day"], - query=f"timestamp.to_hour:<{iso_format(one_day_ago)} timestamp.to_day:<{iso_format(one_day_ago)}", + query=f"timestamp.to_hour:<{one_day_ago.isoformat()} timestamp.to_day:<{one_day_ago.isoformat()}", snuba_params=self.snuba_params, referrer="test_discover_query", ) @@ -328,8 +328,8 @@ def test_timestamp_rounding_filters(self): hour = two_day_ago.replace(minute=0, second=0, microsecond=0) day = hour.replace(hour=0) - assert [item["timestamp.to_hour"] for item in data] == [f"{iso_format(hour)}+00:00"] - assert [item["timestamp.to_day"] for item in data] == [f"{iso_format(day)}+00:00"] + assert [item["timestamp.to_hour"] for item in data] == [hour.isoformat()] + assert [item["timestamp.to_day"] for item in data] == [day.isoformat()] def test_user_display(self): # `user.display` should give `username` @@ -1033,8 +1033,8 @@ def test_field_aliasing_in_selected_columns(self): assert data[0]["user"] == "id:99" assert data[0]["release"] == "first-release" - event_hour = self.event_time.replace(minute=0, second=0) - assert data[0]["timestamp.to_hour"] == iso_format(event_hour) + "+00:00" + event_hour = self.event_time.replace(minute=0, second=0, microsecond=0) + assert data[0]["timestamp.to_hour"] == event_hour.isoformat() assert len(result["meta"]["fields"]) == 4 assert result["meta"]["fields"] == { @@ -1596,8 +1596,8 @@ def test_conditions_with_timestamps(self): results = errors.query( selected_columns=["transaction", "count()"], query="event.type:error AND (timestamp:<{} OR timestamp:>{})".format( - iso_format(self.now - timedelta(seconds=5)), - iso_format(self.now - timedelta(seconds=3)), + (self.now - timedelta(seconds=5)).isoformat(), + (self.now - timedelta(seconds=3)).isoformat(), ), snuba_params=SnubaParams( projects=[self.project], @@ -1620,7 +1620,7 @@ def test_timestamp_rollup_filter(self): event_hour = self.event_time.replace(minute=0, second=0) result = errors.query( selected_columns=["project.id", "user", "release"], - query="timestamp.to_hour:" + iso_format(event_hour), + query="timestamp.to_hour:" + event_hour.isoformat(), snuba_params=self.snuba_params, referrer="discover", ) @@ -1757,7 +1757,7 @@ def setUp(self): self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0) self.now = before_now() event_data = load_data("javascript") - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) self.snuba_params = SnubaParams( projects=[self.project], diff --git a/tests/sentry/snuba/test_transactions.py b/tests/sentry/snuba/test_transactions.py index 600e4a3de378e1..67592362bf0916 100644 --- a/tests/sentry/snuba/test_transactions.py +++ b/tests/sentry/snuba/test_transactions.py @@ -24,7 +24,7 @@ from sentry.search.events.types import SnubaParams from sentry.snuba import discover, transactions from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data ARRAY_COLUMNS = ["measurements", "span_op_breakdowns"] @@ -288,8 +288,8 @@ def test_field_aliasing_in_selected_columns(self): assert data[0]["user"] == "id:99" assert data[0]["release"] == "first-release" - event_hour = self.event_time.replace(minute=0, second=0) - assert data[0]["timestamp.to_hour"] == iso_format(event_hour) + "+00:00" + event_hour = self.event_time.replace(minute=0, second=0, microsecond=0) + assert data[0]["timestamp.to_hour"] == event_hour.isoformat() assert len(result["meta"]["fields"]) == 4 assert result["meta"]["fields"] == { @@ -991,8 +991,8 @@ def test_timestamp_rounding_fields(self): hour = self.event_time.replace(minute=0, second=0, microsecond=0) day = hour.replace(hour=0) - assert [item["timestamp.to_hour"] for item in data] == [f"{iso_format(hour)}+00:00"] - assert [item["timestamp.to_day"] for item in data] == [f"{iso_format(day)}+00:00"] + assert [item["timestamp.to_hour"] for item in data] == [hour.isoformat()] + assert [item["timestamp.to_day"] for item in data] == [day.isoformat()] def test_timestamp_rounding_filters(self): one_day_ago = before_now(days=1) @@ -1005,7 +1005,7 @@ def test_timestamp_rounding_filters(self): result = transactions.query( selected_columns=["timestamp.to_hour", "timestamp.to_day"], - query=f"timestamp.to_hour:<{iso_format(one_day_ago)} timestamp.to_day:<{iso_format(one_day_ago)}", + query=f"timestamp.to_hour:<{one_day_ago.isoformat()} timestamp.to_day:<{one_day_ago.isoformat()}", snuba_params=self.snuba_params, referrer="test_discover_query", ) @@ -1014,8 +1014,8 @@ def test_timestamp_rounding_filters(self): hour = two_day_ago.replace(minute=0, second=0, microsecond=0) day = hour.replace(hour=0) - assert [item["timestamp.to_hour"] for item in data] == [f"{iso_format(hour)}+00:00"] - assert [item["timestamp.to_day"] for item in data] == [f"{iso_format(day)}+00:00"] + assert [item["timestamp.to_hour"] for item in data] == [hour.isoformat()] + assert [item["timestamp.to_day"] for item in data] == [day.isoformat()] def test_user_display(self): # `user.display` should give `username` @@ -2678,8 +2678,8 @@ def test_conditions_with_timestamps(self): results = transactions.query( selected_columns=["transaction", "count()"], query="event.type:transaction AND (timestamp:<{} OR timestamp:>{})".format( - iso_format(self.now - timedelta(seconds=5)), - iso_format(self.now - timedelta(seconds=3)), + (self.now - timedelta(seconds=5)).isoformat(), + (self.now - timedelta(seconds=3)).isoformat(), ), snuba_params=SnubaParams( start=self.two_min_ago, @@ -2704,7 +2704,7 @@ def test_timestamp_rollup_filter(self): event_hour = self.event_time.replace(minute=0, second=0) result = transactions.query( selected_columns=["project.id", "user", "release"], - query="timestamp.to_hour:" + iso_format(event_hour), + query="timestamp.to_hour:" + event_hour.isoformat(), snuba_params=self.snuba_params, referrer="discover", ) @@ -2865,8 +2865,8 @@ def setUp(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 1500 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) self.snuba_params = SnubaParams( projects=[self.project], @@ -2967,8 +2967,8 @@ def test_orderby_equation(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 300 * i - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) results = transactions.query( selected_columns=[ @@ -3117,8 +3117,8 @@ def test_nan_equation_results(self): event_data = load_data("transaction") # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 0 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) results = transactions.query( selected_columns=[ diff --git a/tests/sentry/snuba/test_transactions_timeseries_query.py b/tests/sentry/snuba/test_transactions_timeseries_query.py index 696dc34bf5e643..5a4700c8a3228a 100644 --- a/tests/sentry/snuba/test_transactions_timeseries_query.py +++ b/tests/sentry/snuba/test_transactions_timeseries_query.py @@ -9,7 +9,7 @@ from sentry.snuba import transactions from sentry.snuba.dataset import Dataset from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data ARRAY_COLUMNS = ["measurements", "span_op_breakdowns"] @@ -245,8 +245,8 @@ def test_count_miserable(self): event_data["transaction"] = "api/foo/" # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 300 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) ProjectTransactionThreshold.objects.create( project=self.project, @@ -287,8 +287,8 @@ def test_count_miserable_with_arithmetic(self): event_data["transaction"] = "api/foo/" # Half of duration so we don't get weird rounding differences when comparing the results event_data["breakdowns"]["span_ops"]["ops.http"]["value"] = 300 - event_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) - event_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=3)) + event_data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() + event_data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=3)).isoformat() self.store_event(data=event_data, project_id=self.project.id) ProjectTransactionThreshold.objects.create( project=self.project, diff --git a/tests/sentry/web/frontend/test_group_tag_export.py b/tests/sentry/web/frontend/test_group_tag_export.py index 95ce98be0fadb9..c7b612cdd77b49 100644 --- a/tests/sentry/web/frontend/test_group_tag_export.py +++ b/tests/sentry/web/frontend/test_group_tag_export.py @@ -3,7 +3,7 @@ from django.urls import reverse from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import create_test_regions, region_silo_test @@ -16,7 +16,7 @@ def setUp(self): self.value = "b\xe4r" self.project = self.create_project() - event_timestamp = iso_format(before_now(seconds=1)) + event_timestamp = before_now(seconds=1).replace(microsecond=0).isoformat() self.event = self.store_event( data={ @@ -30,9 +30,7 @@ def setUp(self): self.group = self.event.group - self.first_seen = datetime.strptime(event_timestamp, "%Y-%m-%dT%H:%M:%S").strftime( - "%Y-%m-%dT%H:%M:%S.%fZ" - ) + self.first_seen = datetime.fromisoformat(event_timestamp).strftime("%Y-%m-%dT%H:%M:%S.%fZ") self.last_seen = self.first_seen self.login_as(user=self.user) diff --git a/tests/snuba/api/endpoints/test_organization_event_details.py b/tests/snuba/api/endpoints/test_organization_event_details.py index aa983b470c65a6..01c83b6b45da38 100644 --- a/tests/snuba/api/endpoints/test_organization_event_details.py +++ b/tests/snuba/api/endpoints/test_organization_event_details.py @@ -6,7 +6,7 @@ from sentry.models.group import Group from sentry.search.events import constants from sentry.testutils.cases import APITestCase, MetricsEnhancedPerformanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.options import override_options from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import OccurrenceTestMixin @@ -214,7 +214,7 @@ def test_long_trace_description(self): data = load_data("transaction") data["event_id"] = "d" * 32 data["timestamp"] = before_now(minutes=1).isoformat() - data["start_timestamp"] = iso_format(before_now(minutes=1) - timedelta(seconds=5)) + data["start_timestamp"] = (before_now(minutes=1) - timedelta(seconds=5)).isoformat() data["contexts"]["trace"]["description"] = "b" * 512 self.store_event(data=data, project_id=self.project.id) diff --git a/tests/snuba/api/endpoints/test_organization_events.py b/tests/snuba/api/endpoints/test_organization_events.py index 70cbb82e396398..233ebbb697caa7 100644 --- a/tests/snuba/api/endpoints/test_organization_events.py +++ b/tests/snuba/api/endpoints/test_organization_events.py @@ -36,7 +36,7 @@ SpanTestCase, ) from sentry.testutils.helpers import parse_link_header -from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format +from sentry.testutils.helpers.datetime import before_now, freeze_time from sentry.testutils.helpers.discover import user_misery_formula from sentry.types.group import GroupSubStatus from sentry.utils import json @@ -56,7 +56,7 @@ def setUp(self): super().setUp() self.nine_mins_ago = before_now(minutes=9) self.ten_mins_ago = before_now(minutes=10) - self.ten_mins_ago_iso = iso_format(self.ten_mins_ago) + self.ten_mins_ago_iso = self.ten_mins_ago.replace(microsecond=0).isoformat() self.eleven_mins_ago = before_now(minutes=11) self.eleven_mins_ago_iso = self.eleven_mins_ago.isoformat() self.transaction_data = load_data("transaction", timestamp=self.ten_mins_ago) @@ -3041,9 +3041,13 @@ def test_any_field_alias(self): result = {r["any(user.display)"] for r in data} assert result == {"cathy@example.com"} result = {r["any(timestamp.to_day)"][:19] for r in data} - assert result == {iso_format(day_ago.replace(hour=0, minute=0, second=0, microsecond=0))} + assert result == { + day_ago.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None).isoformat() + } result = {r["any(timestamp.to_hour)"][:19] for r in data} - assert result == {iso_format(day_ago.replace(minute=0, second=0, microsecond=0))} + assert result == { + day_ago.replace(minute=0, second=0, microsecond=0, tzinfo=None).isoformat() + } def test_field_aliases_in_conflicting_functions(self): self.store_event( @@ -3334,7 +3338,7 @@ def test_all_aggregates_in_columns(self): assert response.status_code == 200, response.content data = response.data["data"] assert len(data) == 1 - assert self.ten_mins_ago_iso[:-5] in data[0]["last_seen()"] + assert self.ten_mins_ago_iso == data[0]["last_seen()"] assert data[0]["latest_event()"] == event.event_id query = { diff --git a/tests/snuba/api/endpoints/test_organization_events_facets_performance.py b/tests/snuba/api/endpoints/test_organization_events_facets_performance.py index 00b5051a37ee72..fdae7b997e4be3 100644 --- a/tests/snuba/api/endpoints/test_organization_events_facets_performance.py +++ b/tests/snuba/api/endpoints/test_organization_events_facets_performance.py @@ -3,7 +3,7 @@ from django.urls import reverse from sentry.testutils.cases import APITestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data @@ -72,7 +72,7 @@ def store_transaction( { "transaction": name, "event_id": f"{self._transaction_count:02x}".rjust(32, "0"), - "start_timestamp": iso_format(self.two_mins_ago - timedelta(seconds=duration)), + "start_timestamp": (self.two_mins_ago - timedelta(seconds=duration)).isoformat(), "timestamp": self.two_mins_ago.isoformat(), } ) diff --git a/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py b/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py index 4c61734ba33f19..8647fda3db5f78 100644 --- a/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py +++ b/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py @@ -2,7 +2,6 @@ from django.urls import reverse -from sentry.testutils.helpers.datetime import iso_format from sentry.utils.cursors import Cursor from sentry.utils.samples import load_data from tests.snuba.api.endpoints.test_organization_events_facets_performance import ( @@ -67,7 +66,7 @@ def store_transaction( { "transaction": name, "event_id": f"{self._transaction_count:02x}".rjust(32, "0"), - "start_timestamp": iso_format(self.two_mins_ago - timedelta(seconds=duration)), + "start_timestamp": (self.two_mins_ago - timedelta(seconds=duration)).isoformat(), "timestamp": self.two_mins_ago.isoformat(), } ) diff --git a/tests/snuba/api/endpoints/test_organization_events_histogram.py b/tests/snuba/api/endpoints/test_organization_events_histogram.py index 59529a36e3acd8..31a99c44b0c73b 100644 --- a/tests/snuba/api/endpoints/test_organization_events_histogram.py +++ b/tests/snuba/api/endpoints/test_organization_events_histogram.py @@ -11,7 +11,7 @@ from sentry.sentry_metrics.aggregation_option_registry import AggregationOption from sentry.testutils.cases import APITestCase, MetricsEnhancedPerformanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data from sentry.utils.snuba import get_array_column_alias @@ -43,7 +43,7 @@ def populate_events(self, specs): breakdown_name = f"ops.{suffix_key}" data["timestamp"] = start.isoformat() - data["start_timestamp"] = iso_format(start - timedelta(seconds=i)) + data["start_timestamp"] = (start - timedelta(seconds=i)).isoformat() value = random.random() * (spec.end - spec.start) + spec.start data["transaction"] = f"/measurement/{measurement_name}/value/{value}" diff --git a/tests/snuba/api/endpoints/test_organization_events_spans_histogram.py b/tests/snuba/api/endpoints/test_organization_events_spans_histogram.py index 8953e8887533b8..0f7a9274f48189 100644 --- a/tests/snuba/api/endpoints/test_organization_events_spans_histogram.py +++ b/tests/snuba/api/endpoints/test_organization_events_spans_histogram.py @@ -4,7 +4,7 @@ from rest_framework.exceptions import ErrorDetail from sentry.testutils.cases import APITestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data @@ -32,8 +32,8 @@ def create_event(self, **kwargs): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 3.0, @@ -44,8 +44,8 @@ def create_event(self, **kwargs): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=5)), + "start_timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=5)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 10.0, @@ -335,8 +335,8 @@ def test_histogram_all_data_filter(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": "e" * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 60.0, @@ -364,8 +364,8 @@ def test_histogram_exclude_outliers_data_filter(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": "e" * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 60.0, diff --git a/tests/snuba/api/endpoints/test_organization_events_spans_performance.py b/tests/snuba/api/endpoints/test_organization_events_spans_performance.py index 1c551ff9c4d004..c3736bd28a8bb0 100644 --- a/tests/snuba/api/endpoints/test_organization_events_spans_performance.py +++ b/tests/snuba/api/endpoints/test_organization_events_spans_performance.py @@ -10,7 +10,7 @@ from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers import parse_link_header -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data @@ -58,8 +58,8 @@ def create_event(self, **kwargs): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "hash": "2b9cbb96dbf59baa", @@ -72,8 +72,8 @@ def create_event(self, **kwargs): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=5)), + "start_timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=5)).isoformat(), "op": "django.view", "description": "view span", "hash": "be5e3378d9f64175", @@ -1244,8 +1244,8 @@ def test_span_filters(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": "b" * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": test_op, "description": "middleware span", "hash": "ab" * 8, @@ -1256,8 +1256,8 @@ def test_span_filters(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": "c" * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.view", "description": "middleware span", "hash": test_hash, @@ -1284,8 +1284,8 @@ def test_span_filters_with_min_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": "b" * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": test_op, "description": "middleware span", "hash": "ab" * 8, @@ -1295,8 +1295,8 @@ def test_span_filters_with_min_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": "b" * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": test_op, "description": "middleware span", "hash": "ab" * 8, @@ -1306,8 +1306,8 @@ def test_span_filters_with_min_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": "c" * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.view", "description": "middleware span", "hash": test_hash, @@ -1359,8 +1359,8 @@ def test_one_span_with_min(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 5.0, @@ -1398,8 +1398,8 @@ def test_one_span_with_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "hash": "cd" * 8, @@ -1438,8 +1438,8 @@ def test_one_span_with_min_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 5.0, @@ -1450,8 +1450,8 @@ def test_one_span_with_min_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=5)), + "start_timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=5)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 3.0, @@ -1542,8 +1542,8 @@ def test_per_page_with_min(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 5.0, @@ -1554,8 +1554,8 @@ def test_per_page_with_min(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=5)), + "start_timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=5)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 3.0, @@ -1615,8 +1615,8 @@ def test_per_page_with_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 5.0, @@ -1627,8 +1627,8 @@ def test_per_page_with_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=5)), + "start_timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=5)).isoformat(), "op": "django.middleware", "description": "middleware span", "exclusive_time": 3.0, @@ -1688,8 +1688,8 @@ def test_per_page_with_min_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=1)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=4)), + "start_timestamp": (self.min_ago + timedelta(seconds=1)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), "op": "django.middleware", "description": "middleware span", "hash": "2b9cbb96dbf59baa", @@ -1701,8 +1701,8 @@ def test_per_page_with_min_max(self): "same_process_as_parent": True, "parent_span_id": "a" * 16, "span_id": x * 16, - "start_timestamp": iso_format(self.min_ago + timedelta(seconds=4)), - "timestamp": iso_format(self.min_ago + timedelta(seconds=5)), + "start_timestamp": (self.min_ago + timedelta(seconds=4)).isoformat(), + "timestamp": (self.min_ago + timedelta(seconds=5)).isoformat(), "op": "django.middleware", "description": "middleware span", "hash": "2b9cbb96dbf59baa", diff --git a/tests/snuba/api/endpoints/test_organization_events_stats.py b/tests/snuba/api/endpoints/test_organization_events_stats.py index 5187a3b8f867c2..050fb0b90ac7d7 100644 --- a/tests/snuba/api/endpoints/test_organization_events_stats.py +++ b/tests/snuba/api/endpoints/test_organization_events_stats.py @@ -20,7 +20,7 @@ from sentry.models.transaction_threshold import ProjectTransactionThreshold, TransactionMetric from sentry.snuba.discover import OTHER_KEY from sentry.testutils.cases import APITestCase, ProfilesSnubaTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import SearchIssueTestMixin @@ -51,7 +51,7 @@ def setUp(self): data={ "event_id": "a" * 32, "message": "very bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=1)), + "timestamp": (self.day_ago + timedelta(minutes=1)).isoformat(), "fingerprint": ["group1"], "tags": {"sentry:user": self.user.email}, }, @@ -61,7 +61,7 @@ def setUp(self): data={ "event_id": "b" * 32, "message": "oh my", - "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=1)), + "timestamp": (self.day_ago + timedelta(hours=1, minutes=1)).isoformat(), "fingerprint": ["group2"], "tags": {"sentry:user": self.user2.email}, }, @@ -71,7 +71,7 @@ def setUp(self): data={ "event_id": "c" * 32, "message": "very bad", - "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)), + "timestamp": (self.day_ago + timedelta(hours=1, minutes=2)).isoformat(), "fingerprint": ["group2"], "tags": {"sentry:user": self.user2.email}, }, @@ -244,7 +244,7 @@ def test_user_count(self): data={ "event_id": "d" * 32, "message": "something", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "tags": {"sentry:user": self.user2.email}, "fingerprint": ["group2"], }, @@ -713,7 +713,7 @@ def test_transaction_events(self): data = prototype.copy() data["event_id"] = fixture[0] data["timestamp"] = fixture[1].isoformat() - data["start_timestamp"] = iso_format(fixture[1] - timedelta(seconds=1)) + data["start_timestamp"] = (fixture[1] - timedelta(seconds=1)).isoformat() self.store_event(data=data, project_id=self.project.id) for dataset in ["discover", "transactions"]: @@ -894,7 +894,7 @@ def test_large_interval_no_drop_values(self): data={ "event_id": "d" * 32, "message": "not good", - "timestamp": iso_format(self.day_ago - timedelta(minutes=10)), + "timestamp": (self.day_ago - timedelta(minutes=10)).isoformat(), "fingerprint": ["group3"], }, project_id=self.project.id, @@ -1013,8 +1013,8 @@ def test_with_zerofill(self): ] def test_without_zerofill(self): - start = iso_format(self.day_ago) - end = iso_format(self.day_ago + timedelta(hours=2)) + start = self.day_ago.isoformat() + end = (self.day_ago + timedelta(hours=2)).isoformat() response = self.do_request( data={ "start": start, @@ -1039,19 +1039,19 @@ def test_without_zerofill(self): def test_comparison_error_dataset(self): self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=1)), + "timestamp": (self.day_ago + timedelta(days=-1, minutes=1)).isoformat(), }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=2)), + "timestamp": (self.day_ago + timedelta(days=-1, minutes=2)).isoformat(), }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=1, minutes=1)), + "timestamp": (self.day_ago + timedelta(days=-1, hours=1, minutes=1)).isoformat(), }, project_id=self.project2.id, ) @@ -1075,19 +1075,19 @@ def test_comparison_error_dataset(self): def test_comparison(self): self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=1)), + "timestamp": (self.day_ago + timedelta(days=-1, minutes=1)).isoformat(), }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, minutes=2)), + "timestamp": (self.day_ago + timedelta(days=-1, minutes=2)).isoformat(), }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(days=-1, hours=1, minutes=1)), + "timestamp": (self.day_ago + timedelta(days=-1, hours=1, minutes=1)).isoformat(), }, project_id=self.project2.id, ) @@ -1176,7 +1176,7 @@ def test_tag_with_conflicting_function_alias_simple(self): for _ in range(7): self.store_event( data={ - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "tags": {"count": "9001"}, }, project_id=self.project2.id, @@ -1211,7 +1211,7 @@ def test_group_id_tag_simple(self): event_data: _EventDataDict = { "data": { "message": "poof", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"group_id": "testing"}, "fingerprint": ["group1"], @@ -1258,14 +1258,14 @@ def setUp(self): self.project2 = self.create_project() self.user2 = self.create_user() transaction_data = load_data("transaction") - transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2)) - transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=4)) + transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat() + transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=4)).isoformat() transaction_data["tags"] = {"shared-tag": "yup"} self.event_data: list[_EventDataDict] = [ { "data": { "message": "poof", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"shared-tag": "yup"}, "fingerprint": ["group1"], @@ -1276,7 +1276,7 @@ def setUp(self): { "data": { "message": "voof", - "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)), + "timestamp": (self.day_ago + timedelta(hours=1, minutes=2)).isoformat(), "fingerprint": ["group2"], "user": {"email": self.user2.email}, "tags": {"shared-tag": "yup"}, @@ -1287,7 +1287,7 @@ def setUp(self): { "data": { "message": "very bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group3"], "user": {"email": "foo@example.com"}, "tags": {"shared-tag": "yup"}, @@ -1298,7 +1298,7 @@ def setUp(self): { "data": { "message": "oh no", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group4"], "user": {"email": "bar@example.com"}, "tags": {"shared-tag": "yup"}, @@ -1311,7 +1311,7 @@ def setUp(self): { "data": { "message": "sorta bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group5"], "user": {"email": "bar@example.com"}, "tags": {"shared-tag": "yup"}, @@ -1322,7 +1322,7 @@ def setUp(self): { "data": { "message": "not so bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group6"], "user": {"email": "bar@example.com"}, "tags": {"shared-tag": "yup"}, @@ -1526,7 +1526,7 @@ def test_tag_with_conflicting_function_alias_simple(self): event_data: _EventDataDict = { "data": { "message": "poof", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"count": "9001"}, "fingerprint": ["group1"], @@ -1622,7 +1622,7 @@ def test_tag_with_conflicting_function_alias_with_other_multiple_groupings(self) { "data": { "message": "abc", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"count": "2"}, "fingerprint": ["group1"], @@ -1633,7 +1633,7 @@ def test_tag_with_conflicting_function_alias_with_other_multiple_groupings(self) { "data": { "message": "def", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"count": "9001"}, "fingerprint": ["group1"], @@ -1669,7 +1669,7 @@ def test_group_id_tag_simple(self): event_data: _EventDataDict = { "data": { "message": "poof", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"group_id": "the tag"}, "fingerprint": ["group1"], @@ -1925,8 +1925,8 @@ def test_top_events_with_functions(self): def test_top_events_with_functions_on_different_transactions(self): """Transaction2 has less events, but takes longer so order should be self.transaction then transaction2""" transaction_data = load_data("transaction") - transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2)) - transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6)) + transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat() + transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=6)).isoformat() transaction_data["transaction"] = "/foo_bar/" transaction2 = self.store_event(transaction_data, project_id=self.project.id) with self.feature(self.enabled_features): @@ -1959,8 +1959,8 @@ def test_top_events_with_functions_on_different_transactions(self): def test_top_events_with_query(self): transaction_data = load_data("transaction") - transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2)) - transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6)) + transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat() + transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=6)).isoformat() transaction_data["transaction"] = "/foo_bar/" self.store_event(transaction_data, project_id=self.project.id) with self.feature(self.enabled_features): @@ -2142,7 +2142,7 @@ def test_top_events_with_error_unhandled(self): prototype["logentry"] = {"formatted": "not handled"} prototype["exception"]["values"][0]["value"] = "not handled" prototype["exception"]["values"][0]["mechanism"]["handled"] = False - prototype["timestamp"] = iso_format(self.day_ago + timedelta(minutes=2)) + prototype["timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat() self.store_event(data=prototype, project_id=project.id) with self.feature(self.enabled_features): @@ -2654,7 +2654,7 @@ def test_top_events_timestamp_fields(self): timestamp_days = [timestamp.replace(hour=0, minute=0, second=0) for timestamp in timestamps] for ts, ts_hr, ts_day in zip(timestamps, timestamp_hours, timestamp_days): - key = f"{iso_format(ts)}+00:00,{iso_format(ts_day)}+00:00,{iso_format(ts_hr)}+00:00" + key = f"{ts.isoformat()},{ts_day.isoformat()},{ts_hr.isoformat()}" count = sum(e["count"] for e in self.event_data if e["data"]["timestamp"] == ts) results = data[key] assert [{"count": count}] in [attrs for time, attrs in results["data"]] @@ -2693,8 +2693,8 @@ def test_top_events_other_with_matching_columns(self): def test_top_events_with_field_overlapping_other_key(self): transaction_data = load_data("transaction") - transaction_data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=2)) - transaction_data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=6)) + transaction_data["start_timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat() + transaction_data["timestamp"] = (self.day_ago + timedelta(minutes=6)).isoformat() transaction_data["transaction"] = OTHER_KEY for i in range(5): data = transaction_data.copy() @@ -2990,7 +2990,7 @@ def setUp(self): { "data": { "message": "poof", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"shared-tag": "yup"}, "fingerprint": ["group1"], @@ -3001,7 +3001,7 @@ def setUp(self): { "data": { "message": "voof", - "timestamp": iso_format(self.day_ago + timedelta(hours=1, minutes=2)), + "timestamp": (self.day_ago + timedelta(hours=1, minutes=2)).isoformat(), "fingerprint": ["group2"], "user": {"email": self.user2.email}, "tags": {"shared-tag": "yup"}, @@ -3012,7 +3012,7 @@ def setUp(self): { "data": { "message": "very bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group3"], "user": {"email": "foo@example.com"}, "tags": {"shared-tag": "yup"}, @@ -3023,7 +3023,7 @@ def setUp(self): { "data": { "message": "oh no", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group4"], "user": {"email": "bar@example.com"}, "tags": {"shared-tag": "yup"}, @@ -3034,7 +3034,7 @@ def setUp(self): { "data": { "message": "kinda bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"shared-tag": "yup"}, "fingerprint": ["group7"], @@ -3046,7 +3046,7 @@ def setUp(self): { "data": { "message": "sorta bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group5"], "user": {"email": "bar@example.com"}, "tags": {"shared-tag": "yup"}, @@ -3057,7 +3057,7 @@ def setUp(self): { "data": { "message": "not so bad", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "fingerprint": ["group6"], "user": {"email": "bar@example.com"}, "tags": {"shared-tag": "yup"}, @@ -3268,7 +3268,7 @@ def test_group_id_tag_simple(self): event_data: _EventDataDict = { "data": { "message": "poof", - "timestamp": iso_format(self.day_ago + timedelta(minutes=2)), + "timestamp": (self.day_ago + timedelta(minutes=2)).isoformat(), "user": {"email": self.user.email}, "tags": {"group_id": "the tag"}, "fingerprint": ["group1"], @@ -3315,7 +3315,7 @@ def test_top_events_with_error_unhandled(self): prototype["logentry"] = {"formatted": "not handled"} prototype["exception"]["values"][0]["value"] = "not handled" prototype["exception"]["values"][0]["mechanism"]["handled"] = False - prototype["timestamp"] = iso_format(self.day_ago + timedelta(minutes=2)) + prototype["timestamp"] = (self.day_ago + timedelta(minutes=2)).isoformat() self.store_event(data=prototype, project_id=project.id) with self.feature(self.enabled_features): diff --git a/tests/snuba/api/endpoints/test_organization_events_stats_mep.py b/tests/snuba/api/endpoints/test_organization_events_stats_mep.py index 573c7de7d5e492..f0bc7d9d31b0ae 100644 --- a/tests/snuba/api/endpoints/test_organization_events_stats_mep.py +++ b/tests/snuba/api/endpoints/test_organization_events_stats_mep.py @@ -14,7 +14,7 @@ from sentry.sentry_metrics.use_case_id_registry import UseCaseID from sentry.snuba.metrics.extraction import MetricSpecType, OnDemandMetricSpec from sentry.testutils.cases import MetricsEnhancedPerformanceTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.on_demand import create_widget from sentry.utils.samples import load_data @@ -1585,8 +1585,8 @@ def test_top_events_with_transaction_on_demand_passing_widget_id_unsaved_error( "event_id": "a" * 32, "message": "very bad", "type": "error", - "start_timestamp": iso_format(self.day_ago + timedelta(hours=1)), - "timestamp": iso_format(self.day_ago + timedelta(hours=1)), + "start_timestamp": (self.day_ago + timedelta(hours=1)).isoformat(), + "timestamp": (self.day_ago + timedelta(hours=1)).isoformat(), "tags": {"customtag1": "error_value", "query.dataset": "foo"}, }, project_id=self.project.id, @@ -1596,8 +1596,8 @@ def test_top_events_with_transaction_on_demand_passing_widget_id_unsaved_error( "event_id": "b" * 32, "message": "very bad 2", "type": "error", - "start_timestamp": iso_format(self.day_ago + timedelta(hours=1)), - "timestamp": iso_format(self.day_ago + timedelta(hours=1)), + "start_timestamp": (self.day_ago + timedelta(hours=1)).isoformat(), + "timestamp": (self.day_ago + timedelta(hours=1)).isoformat(), "tags": {"customtag1": "error_value2", "query.dataset": "foo"}, }, project_id=self.project.id, @@ -1657,15 +1657,15 @@ def test_top_events_with_transaction_on_demand_passing_widget_id_unsaved_discove "event_id": "a" * 32, "message": "very bad", "type": "error", - "timestamp": iso_format(self.day_ago + timedelta(hours=1)), + "timestamp": (self.day_ago + timedelta(hours=1)).isoformat(), "tags": {"customtag1": "error_value", "query.dataset": "foo"}, }, project_id=self.project.id, ) transaction = load_data("transaction") - transaction["timestamp"] = iso_format(self.day_ago + timedelta(hours=1)) - transaction["start_timestamp"] = iso_format(self.day_ago + timedelta(hours=1)) + transaction["timestamp"] = (self.day_ago + timedelta(hours=1)).isoformat() + transaction["start_timestamp"] = (self.day_ago + timedelta(hours=1)).isoformat() transaction["tags"] = {"customtag1": "transaction_value", "query.dataset": "foo"} self.store_event( diff --git a/tests/snuba/api/endpoints/test_organization_events_trends.py b/tests/snuba/api/endpoints/test_organization_events_trends.py index efab6966d6e538..2f340065089be3 100644 --- a/tests/snuba/api/endpoints/test_organization_events_trends.py +++ b/tests/snuba/api/endpoints/test_organization_events_trends.py @@ -4,7 +4,7 @@ from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers import parse_link_header -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data @@ -16,16 +16,18 @@ def setUp(self): self.day_ago = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0) self.prototype = load_data("transaction") data = self.prototype.copy() - data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) + data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() data["user"] = {"email": "foo@example.com"} - data["timestamp"] = iso_format(self.day_ago + timedelta(minutes=30, seconds=2)) + data["timestamp"] = (self.day_ago + timedelta(minutes=30, seconds=2)).isoformat() data["measurements"]["lcp"]["value"] = 2000 self.store_event(data, project_id=self.project.id) second = [0, 2, 10] for i in range(3): data = self.prototype.copy() - data["start_timestamp"] = iso_format(self.day_ago + timedelta(hours=1, minutes=30 + i)) + data["start_timestamp"] = ( + self.day_ago + timedelta(hours=1, minutes=30 + i) + ).isoformat() data["timestamp"] = ( self.day_ago + timedelta(hours=1, minutes=30 + i, seconds=second[i]) ).isoformat() @@ -846,7 +848,7 @@ def setUp(self): for j in range(2): data = self.prototype.copy() data["user"] = {"email": "foo@example.com"} - data["start_timestamp"] = iso_format(self.day_ago + timedelta(minutes=30)) + data["start_timestamp"] = (self.day_ago + timedelta(minutes=30)).isoformat() data["timestamp"] = ( self.day_ago + timedelta(hours=j, minutes=30, seconds=2) ).isoformat() diff --git a/tests/snuba/api/endpoints/test_organization_group_index_stats.py b/tests/snuba/api/endpoints/test_organization_group_index_stats.py index fbaeef4405188e..a792a7dc24dcc1 100644 --- a/tests/snuba/api/endpoints/test_organization_group_index_stats.py +++ b/tests/snuba/api/endpoints/test_organization_group_index_stats.py @@ -3,7 +3,7 @@ from sentry.issues.grouptype import ProfileFileIOGroupType from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers import parse_link_header, with_feature -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from tests.sentry.issues.test_utils import OccurrenceTestMixin @@ -221,7 +221,7 @@ def test_query_timestamp(self): self.login_as(user=self.user) response = self.get_response( - query=f"timestamp:>{before_now(seconds=3)} timestamp:<{iso_format(before_now(seconds=1))}", + query=f"timestamp:>{before_now(seconds=3)} timestamp:<{before_now(seconds=1).isoformat()}", groups=[group_a.id, group_c.id], ) diff --git a/tests/snuba/api/endpoints/test_organization_tagkey_values.py b/tests/snuba/api/endpoints/test_organization_tagkey_values.py index 5acbdb7710785f..760f7ea6837b65 100644 --- a/tests/snuba/api/endpoints/test_organization_tagkey_values.py +++ b/tests/snuba/api/endpoints/test_organization_tagkey_values.py @@ -9,7 +9,7 @@ from sentry.search.events.constants import RELEASE_ALIAS, SEMVER_ALIAS from sentry.snuba.dataset import Dataset from sentry.testutils.cases import APITestCase, ReplaysSnubaTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import OccurrenceTestMixin @@ -46,19 +46,19 @@ def group(self): class OrganizationTagKeyValuesTest(OrganizationTagKeyTestCase): def test_simple(self): self.store_event( - data={"timestamp": iso_format(self.day_ago), "tags": {"fruit": "apple"}}, + data={"timestamp": self.day_ago.isoformat(), "tags": {"fruit": "apple"}}, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(self.min_ago), "tags": {"fruit": "orange"}}, + data={"timestamp": self.min_ago.isoformat(), "tags": {"fruit": "orange"}}, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(self.min_ago), "tags": {"some_tag": "some_value"}}, + data={"timestamp": self.min_ago.isoformat(), "tags": {"some_tag": "some_value"}}, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(self.min_ago), "tags": {"fruit": "orange"}}, + data={"timestamp": self.min_ago.isoformat(), "tags": {"fruit": "orange"}}, project_id=self.project.id, ) @@ -73,12 +73,12 @@ def test_simple(self): def test_env(self): env2 = self.create_environment() self.store_event( - data={"timestamp": iso_format(self.day_ago), "tags": {"fruit": "apple"}}, + data={"timestamp": self.day_ago.isoformat(), "tags": {"fruit": "apple"}}, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago), + "timestamp": self.day_ago.isoformat(), "tags": {"fruit": "apple"}, "environment": self.environment.name, }, @@ -86,14 +86,14 @@ def test_env(self): ) self.store_event( data={ - "timestamp": iso_format(self.day_ago), + "timestamp": self.day_ago.isoformat(), "tags": {"fruit": "apple"}, "environment": env2.name, }, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(self.min_ago), "tags": {"fruit": "orange"}}, + data={"timestamp": self.min_ago.isoformat(), "tags": {"fruit": "orange"}}, project_id=self.project.id, ) self.run_test( @@ -107,7 +107,7 @@ def test_env_with_order_by_count(self): for minute in range(1, 6): self.store_event( data={ - "timestamp": iso_format(before_now(minutes=minute * 10)), + "timestamp": before_now(minutes=minute * 10).isoformat(), "tags": {"fruit": "apple"}, "environment": self.environment.name, }, @@ -117,7 +117,7 @@ def test_env_with_order_by_count(self): for minute in range(1, 5): self.store_event( data={ - "timestamp": iso_format(self.min_ago), + "timestamp": self.min_ago.isoformat(), "tags": {"fruit": "orange"}, "environment": self.environment.name, }, @@ -140,7 +140,7 @@ def test_env_with_order_by_count(self): def test_invalid_sort_field(self): self.store_event( - data={"timestamp": iso_format(self.day_ago), "tags": {"fruit": "apple"}}, + data={"timestamp": self.day_ago.isoformat(), "tags": {"fruit": "apple"}}, project_id=self.project.id, ) response = self.get_response("fruit", sort="invalid_field") @@ -171,23 +171,23 @@ def test_bad_key(self): def test_snuba_column(self): self.store_event( - data={"timestamp": iso_format(self.day_ago), "user": {"email": "foo@example.com"}}, + data={"timestamp": self.day_ago.isoformat(), "user": {"email": "foo@example.com"}}, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(self.min_ago), "user": {"email": "bar@example.com"}}, + data={"timestamp": self.min_ago.isoformat(), "user": {"email": "bar@example.com"}}, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=10)), + "timestamp": before_now(seconds=10).isoformat(), "user": {"email": "baz@example.com"}, }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=10)), + "timestamp": before_now(seconds=10).isoformat(), "user": {"email": "baz@example.com"}, }, project_id=self.project.id, @@ -199,20 +199,20 @@ def test_snuba_column(self): def test_release(self): self.store_event( - data={"timestamp": iso_format(self.day_ago), "tags": {"sentry:release": "3.1.2"}}, + data={"timestamp": self.day_ago.isoformat(), "tags": {"sentry:release": "3.1.2"}}, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(self.min_ago), "tags": {"sentry:release": "4.1.2"}}, + data={"timestamp": self.min_ago.isoformat(), "tags": {"sentry:release": "4.1.2"}}, project_id=self.project.id, ) self.store_event( - data={"timestamp": iso_format(self.day_ago), "tags": {"sentry:release": "3.1.2"}}, + data={"timestamp": self.day_ago.isoformat(), "tags": {"sentry:release": "3.1.2"}}, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(before_now(seconds=10)), + "timestamp": before_now(seconds=10).isoformat(), "tags": {"sentry:release": "5.1.2"}, }, project_id=self.project.id, @@ -221,11 +221,11 @@ def test_release(self): def test_user_tag(self): self.store_event( - data={"tags": {"sentry:user": "1"}, "timestamp": iso_format(self.day_ago)}, + data={"tags": {"sentry:user": "1"}, "timestamp": self.day_ago.isoformat()}, project_id=self.project.id, ) self.store_event( - data={"tags": {"sentry:user": "2"}, "timestamp": iso_format(self.min_ago)}, + data={"tags": {"sentry:user": "2"}, "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) self.store_event( @@ -317,7 +317,7 @@ def test_disabled_tag_keys(self): def test_group_id_tag(self): self.store_event( data={ - "timestamp": iso_format(self.day_ago - timedelta(minutes=1)), + "timestamp": (self.day_ago - timedelta(minutes=1)).isoformat(), "tags": {"group_id": "not-a-group-id-but-a-string"}, }, project_id=self.project.id, @@ -327,21 +327,21 @@ def test_group_id_tag(self): def test_user_display(self): self.store_event( data={ - "timestamp": iso_format(self.day_ago - timedelta(minutes=1)), + "timestamp": (self.day_ago - timedelta(minutes=1)).isoformat(), "user": {"email": "foo@example.com", "ip_address": "127.0.0.1"}, }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago - timedelta(minutes=2)), + "timestamp": (self.day_ago - timedelta(minutes=2)).isoformat(), "user": {"username": "bazz", "ip_address": "192.168.0.1"}, }, project_id=self.project.id, ) self.store_event( data={ - "timestamp": iso_format(self.day_ago - timedelta(minutes=3)), + "timestamp": (self.day_ago - timedelta(minutes=3)).isoformat(), "user": {"ip_address": "127.0.0.1"}, }, project_id=self.project.id, @@ -479,8 +479,8 @@ def setUp(self): self.transaction.update( { "transaction": "/city_by_code/", - "timestamp": iso_format(before_now(seconds=30)), - "start_timestamp": iso_format(before_now(seconds=35)), + "timestamp": before_now(seconds=30).isoformat(), + "start_timestamp": before_now(seconds=35).isoformat(), } ) self.transaction["contexts"]["trace"].update( diff --git a/tests/snuba/api/endpoints/test_organization_tags.py b/tests/snuba/api/endpoints/test_organization_tags.py index f3bce93ff3f786..3c66e2568253cc 100644 --- a/tests/snuba/api/endpoints/test_organization_tags.py +++ b/tests/snuba/api/endpoints/test_organization_tags.py @@ -6,7 +6,7 @@ from sentry.replays.testutils import mock_replay from sentry.testutils.cases import APITestCase, ReplaysSnubaTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import OccurrenceTestMixin @@ -14,7 +14,7 @@ class OrganizationTagsTest(APITestCase, OccurrenceTestMixin, SnubaTestCase): def setUp(self): super().setUp() - self.min_ago = iso_format(before_now(minutes=1)) + self.min_ago = before_now(minutes=1).isoformat() def test_simple(self): user = self.create_user() @@ -331,8 +331,8 @@ def test_different_times_caching(self, mock_snuba_query): self.login_as(user=user) with self.options({"snuba.tagstore.cache-tagkeys-rate": 1.0}): - start = iso_format(before_now(minutes=10)) - end = iso_format(before_now(minutes=5)) + start = before_now(minutes=10).isoformat() + end = before_now(minutes=5).isoformat() url = reverse( "sentry-api-0-organization-tags", kwargs={"organization_id_or_slug": org.slug} ) @@ -343,8 +343,8 @@ def test_different_times_caching(self, mock_snuba_query): assert mock_snuba_query.call_count == 1 # 5 minutes later, cache_key should be different - start = iso_format(before_now(minutes=5)) - end = iso_format(before_now(minutes=0)) + start = before_now(minutes=5).isoformat() + end = before_now(minutes=0).isoformat() response = self.client.get( url, {"use_cache": "1", "start": start, "end": end}, format="json" ) @@ -359,9 +359,9 @@ def test_different_times_retrieves_cache(self): project = self.create_project(organization=org, teams=[team]) with self.options({"snuba.tagstore.cache-tagkeys-rate": 1.0}): - start = iso_format(before_now(minutes=10)) - middle = iso_format(before_now(minutes=5)) - end = iso_format(before_now(minutes=0)) + start = before_now(minutes=10).isoformat() + middle = before_now(minutes=5).isoformat() + end = before_now(minutes=0).isoformat() # Throw an event in the middle of the time window, since end might get rounded down a bit self.store_event( data={"event_id": "a" * 32, "tags": {"fruit": "apple"}, "timestamp": middle}, diff --git a/tests/snuba/api/endpoints/test_project_event_details.py b/tests/snuba/api/endpoints/test_project_event_details.py index 3285296bfe23fc..ecd46717e864de 100644 --- a/tests/snuba/api/endpoints/test_project_event_details.py +++ b/tests/snuba/api/endpoints/test_project_event_details.py @@ -1,7 +1,7 @@ from django.urls import reverse from sentry.testutils.cases import APITestCase, PerformanceIssueTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import OccurrenceTestMixin @@ -321,7 +321,7 @@ def setUp(self): self.login_as(user=self.user) self.event_id = "c" * 32 self.fingerprint = ["group_2"] - self.min_ago = iso_format(before_now(minutes=1)) + self.min_ago = before_now(minutes=1).replace(microsecond=0).isoformat() self.event = self.store_event( data={ "event_id": self.event_id, @@ -343,7 +343,7 @@ def setUp(self): def assert_event(self, data): assert data["event_id"] == self.event_id assert data["user"]["email"] == self.user.email - assert data["datetime"][:19] == self.min_ago + assert data["datetime"] == self.min_ago assert data["fingerprint"] == self.fingerprint def test_simple(self): diff --git a/tests/snuba/api/endpoints/test_project_group_index.py b/tests/snuba/api/endpoints/test_project_group_index.py index 29c029f447f5d6..48c78569f0fd78 100644 --- a/tests/snuba/api/endpoints/test_project_group_index.py +++ b/tests/snuba/api/endpoints/test_project_group_index.py @@ -33,7 +33,7 @@ from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, SnubaTestCase from sentry.testutils.helpers import Feature, parse_link_header, with_feature -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import assume_test_silo_mode from sentry.types.activity import ActivityType from sentry.users.models.user_option import UserOption @@ -79,14 +79,14 @@ def test_simple_pagination(self): event1 = self.store_event( data={ "fingerprint": ["put-me-in-group-1"], - "timestamp": iso_format(self.min_ago - timedelta(seconds=2)), + "timestamp": (self.min_ago - timedelta(seconds=2)).isoformat(), }, project_id=self.project.id, ) event2 = self.store_event( data={ "fingerprint": ["put-me-in-group-2"], - "timestamp": iso_format(self.min_ago - timedelta(seconds=1)), + "timestamp": (self.min_ago - timedelta(seconds=1)).isoformat(), }, project_id=self.project.id, ) @@ -136,7 +136,7 @@ def test_environment(self): self.store_event( data={ "fingerprint": ["put-me-in-group1"], - "timestamp": iso_format(self.min_ago), + "timestamp": self.min_ago.isoformat(), "environment": "production", }, project_id=self.project.id, @@ -144,7 +144,7 @@ def test_environment(self): self.store_event( data={ "fingerprint": ["put-me-in-group2"], - "timestamp": iso_format(self.min_ago), + "timestamp": self.min_ago.isoformat(), "environment": "staging", }, project_id=self.project.id, @@ -177,7 +177,7 @@ def test_lookup_by_event_id(self): project.update_option("sentry:resolve_age", 1) event_id = "c" * 32 event = self.store_event( - data={"event_id": event_id, "timestamp": iso_format(self.min_ago)}, + data={"event_id": event_id, "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -194,7 +194,7 @@ def test_lookup_by_event_with_matching_environment(self): self.create_environment(name="test", project=project) event = self.store_event( - data={"environment": "test", "timestamp": iso_format(self.min_ago)}, + data={"environment": "test", "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) @@ -213,7 +213,7 @@ def test_lookup_by_event_id_with_whitespace(self): project = self.project project.update_option("sentry:resolve_age", 1) event = self.store_event( - data={"event_id": "c" * 32, "timestamp": iso_format(self.min_ago)}, + data={"event_id": "c" * 32, "timestamp": self.min_ago.isoformat()}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -271,11 +271,11 @@ def test_lookup_by_first_release(self): release.add_project(project) release.add_project(project2) group = self.store_event( - data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))}, + data={"release": release.version, "timestamp": before_now(seconds=1).isoformat()}, project_id=project.id, ).group self.store_event( - data={"release": release.version, "timestamp": iso_format(before_now(seconds=1))}, + data={"release": release.version, "timestamp": before_now(seconds=1).isoformat()}, project_id=project2.id, ) url = "{}?query={}".format(self.path, 'first-release:"%s"' % release.version) @@ -347,7 +347,7 @@ def test_token_auth(self): def test_filter_not_unresolved(self): event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event.group.update(status=GroupStatus.RESOLVED, substatus=None) @@ -358,7 +358,7 @@ def test_filter_not_unresolved(self): def test_single_group_by_hash(self): event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) @@ -371,12 +371,12 @@ def test_single_group_by_hash(self): def test_multiple_groups_by_hashes(self): event = self.store_event( - data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + data={"timestamp": before_now(seconds=500).isoformat(), "fingerprint": ["group-1"]}, project_id=self.project.id, ) event2 = self.store_event( - data={"timestamp": iso_format(before_now(seconds=400)), "fingerprint": ["group-2"]}, + data={"timestamp": before_now(seconds=400).isoformat(), "fingerprint": ["group-2"]}, project_id=self.project.id, ) self.login_as(user=self.user) @@ -1136,7 +1136,7 @@ def test_snooze_user_count(self): data={ "fingerprint": ["put-me-in-group-1"], "user": {"id": str(i)}, - "timestamp": iso_format(self.min_ago + timedelta(seconds=i)), + "timestamp": (self.min_ago + timedelta(seconds=i)).isoformat(), }, project_id=self.project.id, ) diff --git a/tests/snuba/api/serializers/test_group.py b/tests/snuba/api/serializers/test_group.py index a8b2a3e96c84b4..2934d1a7089ad5 100644 --- a/tests/snuba/api/serializers/test_group.py +++ b/tests/snuba/api/serializers/test_group.py @@ -17,7 +17,7 @@ from sentry.notifications.types import NotificationSettingsOptionEnum from sentry.silo.base import SiloMode from sentry.testutils.cases import APITestCase, PerformanceIssueTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import assume_test_silo_mode from sentry.types.group import PriorityLevel from sentry.users.models.user_option import UserOption @@ -361,16 +361,16 @@ def test_seen_stats(self): events = [] for event_id, env, user_id, timestamp in [ - ("a" * 32, environment, 1, iso_format(self.min_ago)), - ("b" * 32, environment, 2, iso_format(self.min_ago)), - ("c" * 32, environment2, 3, iso_format(self.week_ago)), + ("a" * 32, environment, 1, self.min_ago), + ("b" * 32, environment, 2, self.min_ago), + ("c" * 32, environment2, 3, self.week_ago), ]: events.append( self.store_event( data={ "event_id": event_id, "fingerprint": ["put-me-in-group1"], - "timestamp": timestamp, + "timestamp": timestamp.isoformat(), "environment": env.name, "user": {"id": user_id}, }, @@ -390,7 +390,7 @@ def test_seen_stats(self): # should use group columns when no environments arg passed result = serialize(group, serializer=GroupSerializerSnuba(environment_ids=[])) assert result["count"] == "3" - assert iso_format(result["lastSeen"]) == iso_format(self.min_ago) + assert result["lastSeen"] == self.min_ago.replace(microsecond=0) assert result["firstSeen"] == group.first_seen # update this to something different to make sure it's being used @@ -406,8 +406,8 @@ def test_seen_stats(self): ) assert result["count"] == "3" # result is rounded down to nearest second - assert iso_format(result["lastSeen"]) == iso_format(self.min_ago) - assert iso_format(result["firstSeen"]) == iso_format(group_env.first_seen) + assert result["lastSeen"] == self.min_ago.replace(microsecond=0) + assert result["firstSeen"] == group_env.first_seen assert group_env2.first_seen is not None assert group_env2.first_seen > group_env.first_seen assert result["userCount"] == 3 @@ -421,8 +421,8 @@ def test_seen_stats(self): ), ) assert result["userCount"] == 1 - assert iso_format(result["lastSeen"]) == iso_format(self.week_ago) - assert iso_format(result["firstSeen"]) == iso_format(self.week_ago) + assert result["lastSeen"] == self.week_ago.replace(microsecond=0) + assert result["firstSeen"] == self.week_ago.replace(microsecond=0) assert result["count"] == "1" def test_get_start_from_seen_stats(self): @@ -439,7 +439,7 @@ def test_get_start_from_seen_stats(self): } ) - assert iso_format(start) == iso_format(before_now(days=expected)) + assert start.replace(microsecond=0) == before_now(days=expected).replace(microsecond=0) def test_skipped_date_timestamp_filters(self): group = self.create_group() @@ -481,7 +481,7 @@ def test_perf_seen_stats(self): proj = self.create_project() first_group_fingerprint = f"{PerformanceNPlusOneGroupType.type_id}-group1" - timestamp = timezone.now() - timedelta(days=5) + timestamp = (timezone.now() - timedelta(days=5)).replace(microsecond=0) times = 5 for _ in range(0, times): event_data = load_data( @@ -517,8 +517,8 @@ def test_perf_seen_stats(self): ) assert result["userCount"] == 2 - assert iso_format(result["lastSeen"]) == iso_format(timestamp + timedelta(minutes=2)) - assert iso_format(result["firstSeen"]) == iso_format(timestamp + timedelta(minutes=1)) + assert result["lastSeen"] == (timestamp + timedelta(minutes=2)) + assert result["firstSeen"] == (timestamp + timedelta(minutes=1)) assert result["count"] == str(times + 1) @@ -532,7 +532,9 @@ def test_profiling_seen_stats(self): environment = self.create_environment(project=proj) first_group_fingerprint = f"{ProfileFileIOGroupType.type_id}-group1" - timestamp = (timezone.now() - timedelta(days=5)).replace(hour=0, minute=0, second=0) + timestamp = (timezone.now() - timedelta(days=5)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) times = 5 for incr in range(0, times): # for user_0 - user_4, first_group @@ -566,6 +568,6 @@ def test_profiling_seen_stats(self): ) assert result["userCount"] == 6 - assert iso_format(result["lastSeen"]) == iso_format(timestamp + timedelta(minutes=5)) - assert iso_format(result["firstSeen"]) == iso_format(timestamp) + assert result["lastSeen"] == (timestamp + timedelta(minutes=5)) + assert result["firstSeen"] == timestamp assert result["count"] == str(times + 1) diff --git a/tests/snuba/api/serializers/test_group_stream.py b/tests/snuba/api/serializers/test_group_stream.py index 7b2864fae4249f..1e2501751a6cb4 100644 --- a/tests/snuba/api/serializers/test_group_stream.py +++ b/tests/snuba/api/serializers/test_group_stream.py @@ -11,7 +11,7 @@ from sentry.api.serializers.models.group_stream import StreamGroupSerializerSnuba from sentry.models.environment import Environment from sentry.testutils.cases import APITestCase, BaseMetricsTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.cache import cache from sentry.utils.hashlib import hash_values @@ -232,7 +232,7 @@ def test_session_count(self): ) data = { "fingerprint": ["meow"], - "timestamp": iso_format(timezone.now()), + "timestamp": timezone.now().isoformat(), "type": "error", "exception": [{"type": "Foo"}], } diff --git a/tests/snuba/models/test_group.py b/tests/snuba/models/test_group.py index a60d9f0ba3421d..64de81d92312d0 100644 --- a/tests/snuba/models/test_group.py +++ b/tests/snuba/models/test_group.py @@ -11,7 +11,7 @@ from sentry.issues.grouptype import PerformanceNPlusOneGroupType, ProfileFileIOGroupType from sentry.models.group import Group from sentry.testutils.cases import PerformanceIssueTestCase, SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format +from sentry.testutils.helpers.datetime import before_now, freeze_time from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import OccurrenceTestMixin @@ -41,7 +41,7 @@ def test_get_oldest_latest_for_environments(self): data={ "event_id": "a" * 32, "environment": "production", - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-1"], }, project_id=project.id, @@ -50,7 +50,7 @@ def test_get_oldest_latest_for_environments(self): data={ "event_id": "b" * 32, "environment": "production", - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-1"], }, project_id=project.id, @@ -58,7 +58,7 @@ def test_get_oldest_latest_for_environments(self): self.store_event( data={ "event_id": "c" * 32, - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group-1"], }, project_id=project.id, @@ -80,7 +80,7 @@ def test_error_issue_get_helpful_for_environments(self): event_all_helpful_params = self.store_event( data={ "event_id": "a" * 32, - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-1"], "contexts": { "replay": {"replay_id": replay_id}, @@ -98,7 +98,7 @@ def test_error_issue_get_helpful_for_environments(self): self.store_event( data={ "event_id": "b" * 32, - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-1"], "contexts": { "replay": {"replay_id": replay_id}, @@ -111,7 +111,7 @@ def test_error_issue_get_helpful_for_environments(self): event_none_helpful_params = self.store_event( data={ "event_id": "c" * 32, - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group-1"], }, project_id=project.id, @@ -134,7 +134,7 @@ def test_get_recommended_event_for_environments_retention_limit(self, mock_get_e event = self.store_event( data={ "event_id": "a" * 32, - "timestamp": iso_format(outside_retention_date), + "timestamp": outside_retention_date.isoformat(), "fingerprint": ["group-1"], "contexts": {}, "errors": [], @@ -189,7 +189,7 @@ def setUp(self): self.event_a = self.store_event( data={ "event_id": "a" * 32, - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "environment": "staging", "fingerprint": ["group-1"], "message": "Error: Division by zero", @@ -199,7 +199,7 @@ def setUp(self): self.event_b = self.store_event( data={ "event_id": "b" * 32, - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-1"], "environment": "production", "contexts": { @@ -217,7 +217,7 @@ def setUp(self): self.event_c = self.store_event( data={ "event_id": "c" * 32, - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-1"], "tags": {"organization.slug": "sentry"}, "environment": "staging", @@ -487,7 +487,7 @@ def setUp(self): project_id=self.project.id, event_id="a" * 32, event_data={ - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), "fingerprint": ["group-1"], "environment": "staging", "contexts": { @@ -500,7 +500,7 @@ def setUp(self): project_id=self.project.id, event_id="b" * 32, event_data={ - "timestamp": iso_format(before_now(minutes=2)), + "timestamp": before_now(minutes=2).isoformat(), "fingerprint": ["group-1"], "environment": "production", "contexts": { @@ -519,7 +519,7 @@ def setUp(self): project_id=self.project.id, event_id="c" * 32, event_data={ - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), "fingerprint": ["group-1"], "environment": "staging", "tags": {"organization.slug": "sentry"}, diff --git a/tests/snuba/rules/conditions/test_event_frequency.py b/tests/snuba/rules/conditions/test_event_frequency.py index 2569149d220c79..1a38a0962d58d5 100644 --- a/tests/snuba/rules/conditions/test_event_frequency.py +++ b/tests/snuba/rules/conditions/test_event_frequency.py @@ -26,7 +26,7 @@ RuleTestCase, SnubaTestCase, ) -from sentry.testutils.helpers.datetime import before_now, freeze_time, iso_format +from sentry.testutils.helpers.datetime import before_now, freeze_time from sentry.testutils.helpers.features import apply_feature_flag_on_cls from sentry.testutils.skips import requires_snuba from sentry.utils.samples import load_data @@ -72,7 +72,7 @@ def setUp(self): data={ "event_id": "a" * 32, "environment": self.environment.name, - "timestamp": iso_format(before_now(seconds=30)), + "timestamp": before_now(seconds=30).isoformat(), "fingerprint": ["group-1"], "user": {"id": uuid4().hex}, }, @@ -82,7 +82,7 @@ def setUp(self): data={ "event_id": "b" * 32, "environment": self.environment.name, - "timestamp": iso_format(before_now(seconds=12)), + "timestamp": before_now(seconds=12).isoformat(), "fingerprint": ["group-2"], "user": {"id": uuid4().hex}, }, @@ -93,7 +93,7 @@ def setUp(self): data={ "event_id": "c" * 32, "environment": self.environment2.name, - "timestamp": iso_format(before_now(seconds=12)), + "timestamp": before_now(seconds=12).isoformat(), "fingerprint": ["group-3"], "user": {"id": uuid4().hex}, }, @@ -248,7 +248,7 @@ def test_batch_query_percent_no_avg_sessions_in_interval(self): class ErrorEventMixin(SnubaTestCase): def add_event(self, data, project_id, timestamp): - data["timestamp"] = iso_format(timestamp) + data["timestamp"] = timestamp.isoformat() # Store an error event event = self.store_event( data=data, diff --git a/tests/snuba/search/test_backend.py b/tests/snuba/search/test_backend.py index 9664cb7d890ea4..2d1af8070f77ea 100644 --- a/tests/snuba/search/test_backend.py +++ b/tests/snuba/search/test_backend.py @@ -34,7 +34,7 @@ from sentry.snuba.dataset import Dataset from sentry.testutils.cases import SnubaTestCase, TestCase, TransactionTestCase from sentry.testutils.helpers import Feature, apply_feature_flag_on_cls -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.types.group import GroupSubStatus, PriorityLevel from sentry.utils import json from sentry.utils.snuba import SENTRY_SNUBA_MAP @@ -113,9 +113,9 @@ def backend(self): def setUp(self): super().setUp() - self.base_datetime = before_now(days=3) + self.base_datetime = before_now(days=3).replace(microsecond=0) - event1_timestamp = iso_format(self.base_datetime - timedelta(days=21)) + event1_timestamp = (self.base_datetime - timedelta(days=21)).isoformat() self.event1 = self.store_event( data={ "fingerprint": ["put-me-in-group1"], @@ -136,7 +136,7 @@ def setUp(self): "message": "group1", "environment": "production", "tags": {"server": "example.com", "sentry:user": "event3@example.com"}, - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, "level": "fatal", }, @@ -162,7 +162,7 @@ def setUp(self): data={ "fingerprint": ["put-me-in-group2"], "event_id": "b" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "message": "bar", "stacktrace": {"frames": [{"module": "group2"}]}, "environment": "staging", @@ -215,7 +215,7 @@ def set_up_multi_project(self): data={ "event_id": "a" * 32, "fingerprint": ["put-me-in-groupP2"], - "timestamp": iso_format(self.base_datetime - timedelta(days=21)), + "timestamp": (self.base_datetime - timedelta(days=21)).isoformat(), "message": "foo", "stacktrace": {"frames": [{"module": "group_p2"}]}, "tags": {"server": "example.com"}, @@ -235,7 +235,7 @@ def create_group_with_integration_external_issue(self, environment="production") data={ "fingerprint": ["linked_group1"], "event_id": uuid.uuid4().hex, - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "environment": environment, }, project_id=self.project.id, @@ -255,7 +255,7 @@ def create_group_with_platform_external_issue(self, environment="production"): data={ "fingerprint": ["linked_group2"], "event_id": uuid.uuid4().hex, - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "environment": environment, }, project_id=self.project.id, @@ -349,7 +349,7 @@ def test_query_timestamp(self): results = self.make_query( [self.project], environments=[self.environments["production"]], - search_filter_query=f"timestamp:>{iso_format(self.event1.datetime)} timestamp:<{iso_format(self.event3.datetime)}", + search_filter_query=f"timestamp:>{self.event1.datetime.isoformat()} timestamp:<{self.event3.datetime.isoformat()}", ) assert set(results) == {self.group1} @@ -395,7 +395,7 @@ def test_sort_with_environment(self): self.store_event( data={ "fingerprint": ["put-me-in-group2"], - "timestamp": iso_format(dt), + "timestamp": dt.isoformat(), "stacktrace": {"frames": [{"module": "group2"}]}, "environment": "production", "message": "group2", @@ -428,7 +428,7 @@ def test_status(self): data={ "fingerprint": ["put-me-in-group3"], "event_id": "c" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), }, project_id=self.project.id, ) @@ -452,7 +452,7 @@ def test_category(self): data={ "fingerprint": ["put-me-in-group3"], "event_id": "c" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), }, project_id=self.project.id, ) @@ -482,7 +482,7 @@ def test_type(self): data={ "fingerprint": ["put-me-in-group3"], "event_id": "c" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "type": PerformanceNPlusOneGroupType.type_id, }, project_id=self.project.id, @@ -499,7 +499,7 @@ def test_type(self): data={ "fingerprint": ["put-me-in-group4"], "event_id": "d" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), }, project_id=self.project.id, ) @@ -622,7 +622,7 @@ def test_search_filter_query_with_custom_trends_tag(self): self.store_event( data={ "fingerprint": ["put-me-in-group2"], - "timestamp": iso_format(self.group2.first_seen + timedelta(days=1)), + "timestamp": (self.group2.first_seen + timedelta(days=1)).isoformat(), "stacktrace": {"frames": [{"module": "group2"}]}, "message": "group2", "tags": {"trends": trends}, @@ -640,7 +640,7 @@ def test_search_filter_query_with_custom_trends_tag_and_trends_sort(self): self.store_event( data={ "fingerprint": ["put-me-in-group1"], - "timestamp": iso_format(self.group2.last_seen + timedelta(days=i)), + "timestamp": (self.group2.last_seen + timedelta(days=i)).isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, "message": "group1", "tags": {"trends": trends}, @@ -650,7 +650,7 @@ def test_search_filter_query_with_custom_trends_tag_and_trends_sort(self): self.store_event( data={ "fingerprint": ["put-me-in-group2"], - "timestamp": iso_format(self.group2.last_seen + timedelta(days=2)), + "timestamp": (self.group2.last_seen + timedelta(days=2)).isoformat(), "stacktrace": {"frames": [{"module": "group2"}]}, "message": "group2", "tags": {"trends": trends}, @@ -666,7 +666,7 @@ def test_search_tag_overlapping_with_internal_fields(self): self.store_event( data={ "fingerprint": ["put-me-in-group2"], - "timestamp": iso_format(self.group2.first_seen + timedelta(days=1)), + "timestamp": (self.group2.first_seen + timedelta(days=1)).isoformat(), "stacktrace": {"frames": [{"module": "group2"}]}, "message": "group2", "tags": {"email": "tags@example.com"}, @@ -744,7 +744,7 @@ def test_pagination_with_environment(self): self.store_event( data={ "fingerprint": ["put-me-in-group2"], - "timestamp": iso_format(dt), + "timestamp": dt.isoformat(), "environment": "production", "message": "group2", "stacktrace": {"frames": [{"module": "group2"}]}, @@ -832,7 +832,7 @@ def test_age_filter_with_environment(self): self.store_event( data={ "fingerprint": ["put-me-in-group1"], - "timestamp": iso_format(group1_first_seen + timedelta(days=1)), + "timestamp": (group1_first_seen + timedelta(days=1)).isoformat(), "message": "group1", "stacktrace": {"frames": [{"module": "group1"}]}, "environment": "development", @@ -899,7 +899,7 @@ def test_last_seen_filter_with_environment(self): self.store_event( data={ "fingerprint": ["put-me-in-group1"], - "timestamp": iso_format(self.group1.last_seen + timedelta(days=1)), + "timestamp": (self.group1.last_seen + timedelta(days=1)).isoformat(), "message": "group1", "stacktrace": {"frames": [{"module": "group1"}]}, "environment": "development", @@ -1095,7 +1095,7 @@ def test_assigned_to_me_my_teams(self): data={ "fingerprint": ["put-me-in-group-my-teams"], "event_id": "f" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "message": "baz", "environment": "staging", "tags": { @@ -1131,7 +1131,7 @@ def test_assigned_to_me_my_teams_in_syntax(self): data={ "fingerprint": ["put-me-in-group-my-teams"], "event_id": "f" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "message": "baz", "environment": "staging", "tags": { @@ -1177,7 +1177,7 @@ def test_assigned_to_in_syntax(self): data={ "fingerprint": ["put-me-in-group3"], "event_id": "c" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), }, project_id=self.project.id, ).group @@ -1229,21 +1229,21 @@ def test_assigned_or_suggested_in_syntax(self): Group.objects.all().delete() group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=180)), + "timestamp": before_now(seconds=180).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, ).group group1 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=185)), + "timestamp": before_now(seconds=185).isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, ).group group2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=190)), + "timestamp": before_now(seconds=190).isoformat(), "fingerprint": ["group-3"], }, project_id=self.project.id, @@ -1251,7 +1251,7 @@ def test_assigned_or_suggested_in_syntax(self): assigned_group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-4"], }, project_id=self.project.id, @@ -1259,7 +1259,7 @@ def test_assigned_or_suggested_in_syntax(self): assigned_to_other_group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-5"], }, project_id=self.project.id, @@ -1369,35 +1369,35 @@ def test_assigned_or_suggested_my_teams(self): Group.objects.all().delete() group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=180)), + "timestamp": before_now(seconds=180).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, ).group group1 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=185)), + "timestamp": before_now(seconds=185).isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, ).group group2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=190)), + "timestamp": before_now(seconds=190).isoformat(), "fingerprint": ["group-3"], }, project_id=self.project.id, ).group assigned_group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-4"], }, project_id=self.project.id, ).group assigned_to_other_group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-5"], }, project_id=self.project.id, @@ -1406,7 +1406,7 @@ def test_assigned_or_suggested_my_teams(self): data={ "fingerprint": ["put-me-in-group-my-teams"], "event_id": "f" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "message": "baz", "environment": "staging", "tags": { @@ -1523,35 +1523,35 @@ def test_assigned_or_suggested_my_teams_in_syntax(self): Group.objects.all().delete() group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=180)), + "timestamp": before_now(seconds=180).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, ).group group1 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=185)), + "timestamp": before_now(seconds=185).isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, ).group group2 = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=190)), + "timestamp": before_now(seconds=190).isoformat(), "fingerprint": ["group-3"], }, project_id=self.project.id, ).group assigned_group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-4"], }, project_id=self.project.id, ).group assigned_to_other_group = self.store_event( data={ - "timestamp": iso_format(before_now(seconds=195)), + "timestamp": before_now(seconds=195).isoformat(), "fingerprint": ["group-5"], }, project_id=self.project.id, @@ -1560,7 +1560,7 @@ def test_assigned_or_suggested_my_teams_in_syntax(self): data={ "fingerprint": ["put-me-in-group-my-teams"], "event_id": "f" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "message": "baz", "environment": "staging", "tags": { @@ -2127,7 +2127,7 @@ def test_wildcard(self): "message": "somet[hing]", "environment": "production", "tags": {"server": "example.net"}, - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=self.project.id, @@ -2169,7 +2169,7 @@ def test_null_tags(self): "message": "something", "environment": "production", "tags": {"server": "example.net"}, - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=self.project.id, @@ -2180,7 +2180,7 @@ def test_null_tags(self): "event_id": "5" * 32, "message": "something", "environment": "production", - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group2"}]}, }, project_id=self.project.id, @@ -2206,7 +2206,7 @@ def test_null_promoted_tags(self): "message": "something", "environment": "production", "tags": {"logger": "csp"}, - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=self.project.id, @@ -2217,7 +2217,7 @@ def test_null_promoted_tags(self): "event_id": "5" * 32, "message": "something", "environment": "production", - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group2"}]}, }, project_id=self.project.id, @@ -2429,7 +2429,7 @@ def test_message_negation(self): "fingerprint": ["put-me-in-group1"], "event_id": "2" * 32, "message": "something", - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), }, project_id=self.project.id, ) @@ -2448,7 +2448,7 @@ def test_error_main_thread_true(self): data={ "event_id": "1" * 32, "message": "something", - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "exception": { "values": [ { @@ -2489,7 +2489,7 @@ def test_error_main_thread_false(self): data={ "event_id": "2" * 32, "message": "something", - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "exception": { "values": [ { @@ -2530,7 +2530,7 @@ def test_error_main_thread_no_results(self): data={ "event_id": "3" * 32, "message": "something", - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "exception": { "values": [ { @@ -2619,7 +2619,7 @@ def test_issue_priority(self): data={ "fingerprint": ["put-me-in-group3"], "event_id": "c" * 32, - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), }, project_id=self.project.id, ) @@ -2652,7 +2652,7 @@ def test_trends_sort_old_and_new_events(self): "message": "group1", "environment": "production", "tags": {"server": "example.com", "sentry:user": "event3@example.com"}, - "timestamp": iso_format(base_datetime), + "timestamp": base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=new_project.id, @@ -2664,7 +2664,7 @@ def test_trends_sort_old_and_new_events(self): "message": "foo. Also, this message is intended to be greater than 256 characters so that we can put some unique string identifier after that point in the string. The purpose of this is in order to verify we are using snuba to search messages instead of Postgres (postgres truncates at 256 characters and clickhouse does not). santryrox.", "environment": "production", "tags": {"server": "example.com", "sentry:user": "old_event@example.com"}, - "timestamp": iso_format(base_datetime - timedelta(days=20)), + "timestamp": (base_datetime - timedelta(days=20)).isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=new_project.id, @@ -2702,7 +2702,7 @@ def test_trends_sort_v2(self): "message": "group1", "environment": "production", "tags": {"server": "example.com", "sentry:user": "event3@example.com"}, - "timestamp": iso_format(base_datetime), + "timestamp": base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=new_project.id, @@ -2714,7 +2714,7 @@ def test_trends_sort_v2(self): "message": "foo. Also, this message is intended to be greater than 256 characters so that we can put some unique string identifier after that point in the string. The purpose of this is in order to verify we are using snuba to search messages instead of Postgres (postgres truncates at 256 characters and clickhouse does not). santryrox.", "environment": "production", "tags": {"server": "example.com", "sentry:user": "old_event@example.com"}, - "timestamp": iso_format(base_datetime - timedelta(days=20)), + "timestamp": (base_datetime - timedelta(days=20)).isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=new_project.id, @@ -2747,7 +2747,7 @@ def test_trends_log_level_results(self): data={ "fingerprint": ["put-me-in-group1"], "event_id": "c" * 32, - "timestamp": iso_format(base_datetime - timedelta(hours=1)), + "timestamp": (base_datetime - timedelta(hours=1)).isoformat(), "message": "foo", "stacktrace": {"frames": [{"module": "group1"}]}, "environment": "staging", @@ -2759,7 +2759,7 @@ def test_trends_log_level_results(self): data={ "fingerprint": ["put-me-in-group2"], "event_id": "d" * 32, - "timestamp": iso_format(base_datetime), + "timestamp": base_datetime.isoformat(), "message": "bar", "stacktrace": {"frames": [{"module": "group2"}]}, "environment": "staging", @@ -2836,7 +2836,7 @@ def test_trends_has_stacktrace_results(self): data={ "event_id": "d" * 32, "message": "oh no", - "timestamp": iso_format(base_datetime - timedelta(hours=1)), + "timestamp": (base_datetime - timedelta(hours=1)).isoformat(), }, project_id=self.project.id, ) @@ -2860,7 +2860,7 @@ def test_trends_has_stacktrace_results(self): } ] }, - "timestamp": iso_format(base_datetime - timedelta(hours=1)), + "timestamp": (base_datetime - timedelta(hours=1)).isoformat(), }, project_id=self.project.id, ) @@ -2905,7 +2905,7 @@ def test_trends_event_halflife_results(self): data={ "fingerprint": ["put-me-in-group1"], "event_id": "a" * 32, - "timestamp": iso_format(base_datetime - timedelta(hours=1)), + "timestamp": (base_datetime - timedelta(hours=1)).isoformat(), "message": "foo", "stacktrace": {"frames": [{"module": "group1"}]}, "environment": "staging", @@ -2917,7 +2917,7 @@ def test_trends_event_halflife_results(self): data={ "fingerprint": ["put-me-in-group2"], "event_id": "b" * 32, - "timestamp": iso_format(base_datetime), + "timestamp": base_datetime.isoformat(), "message": "bar", "stacktrace": {"frames": [{"module": "group2"}]}, "environment": "staging", @@ -2979,7 +2979,7 @@ def test_trends_mixed_group_types(self): data={ "fingerprint": ["put-me-in-group1"], "event_id": "a" * 32, - "timestamp": iso_format(base_datetime - timedelta(hours=1)), + "timestamp": (base_datetime - timedelta(hours=1)).isoformat(), "message": "foo", "stacktrace": {"frames": [{"module": "group1"}]}, "environment": "staging", @@ -3069,8 +3069,8 @@ def setUp(self): data={ **transaction_event_data, "event_id": "a" * 32, - "timestamp": iso_format(before_now(minutes=1)), - "start_timestamp": iso_format(before_now(minutes=1, seconds=5)), + "timestamp": before_now(minutes=1).isoformat(), + "start_timestamp": (before_now(minutes=1, seconds=5)).isoformat(), "tags": {"my_tag": 1}, "fingerprint": [ f"{PerformanceRenderBlockingAssetSpanGroupType.type_id}-group1" @@ -3084,8 +3084,8 @@ def setUp(self): data={ **transaction_event_data, "event_id": "a" * 32, - "timestamp": iso_format(before_now(minutes=2)), - "start_timestamp": iso_format(before_now(minutes=2, seconds=5)), + "timestamp": before_now(minutes=2).isoformat(), + "start_timestamp": before_now(minutes=2, seconds=5).isoformat(), "tags": {"my_tag": 1}, "fingerprint": [ f"{PerformanceRenderBlockingAssetSpanGroupType.type_id}-group2" @@ -3095,7 +3095,7 @@ def setUp(self): ) self.perf_group_2 = mock_eventstream.call_args[0][2].group error_event_data = { - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "message": "bar", "environment": "staging", "tags": { @@ -3265,8 +3265,8 @@ def test_perf_issue_search_message_term_queries_postgres(self): f"{PerformanceRenderBlockingAssetSpanGroupType.type_id}-group12" ], "event_id": "e" * 32, - "timestamp": iso_format(self.base_datetime), - "start_timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), + "start_timestamp": self.base_datetime.isoformat(), "type": "transaction", "transaction": transaction_name, }, @@ -3328,8 +3328,8 @@ def test_search_message_error_and_perf_issues(self): f"{PerformanceRenderBlockingAssetSpanGroupType.type_id}-group12" ], "event_id": "e" * 32, - "timestamp": iso_format(self.base_datetime), - "start_timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), + "start_timestamp": self.base_datetime.isoformat(), "type": "transaction", "transaction": "/api/0/events", }, @@ -3346,7 +3346,7 @@ def test_search_message_error_and_perf_issues(self): "message": "Uncaught exception on api /api/0/events", "environment": "production", "tags": {"server": "example.com", "sentry:user": "event3@example.com"}, - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), "stacktrace": {"frames": [{"module": "group1"}]}, }, project_id=self.project.id, @@ -3377,7 +3377,7 @@ def test_compound_message_negation(self): "fingerprint": ["put-me-in-group1"], "event_id": "2" * 32, "message": "something", - "timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), }, project_id=self.project.id, ) @@ -3389,8 +3389,8 @@ def test_compound_message_negation(self): "contexts": {"trace": {"trace_id": "b" * 32, "span_id": "c" * 16, "op": ""}}, "fingerprint": [f"{PerformanceRenderBlockingAssetSpanGroupType.type_id}-group12"], "event_id": "e" * 32, - "timestamp": iso_format(self.base_datetime), - "start_timestamp": iso_format(self.base_datetime), + "timestamp": self.base_datetime.isoformat(), + "start_timestamp": self.base_datetime.isoformat(), "type": "transaction", "transaction": "something", }, @@ -3462,7 +3462,7 @@ def setUp(self): ) error_event_data = { - "timestamp": iso_format(self.base_datetime - timedelta(days=20)), + "timestamp": (self.base_datetime - timedelta(days=20)).isoformat(), "message": "bar", "environment": "staging", "tags": { diff --git a/tests/snuba/tagstore/test_tagstore_backend.py b/tests/snuba/tagstore/test_tagstore_backend.py index 184a28533ec2cb..060133f74bd7fe 100644 --- a/tests/snuba/tagstore/test_tagstore_backend.py +++ b/tests/snuba/tagstore/test_tagstore_backend.py @@ -25,7 +25,7 @@ from sentry.tagstore.types import GroupTagValue, TagValue from sentry.testutils.abstract import Abstract from sentry.testutils.cases import PerformanceIssueTestCase, SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils.eventuser import EventUser from sentry.utils.samples import load_data from tests.sentry.issues.test_utils import SearchIssueTestMixin @@ -72,7 +72,7 @@ def setUp(self): "platform": "python", "environment": env1, "fingerprint": ["group-1"], - "timestamp": iso_format(self.now - timedelta(seconds=1)), + "timestamp": (self.now - timedelta(seconds=1)).isoformat(), "tags": { "foo": "bar", "baz": "quux", @@ -92,7 +92,7 @@ def setUp(self): "platform": "python", "environment": env1, "fingerprint": ["group-1"], - "timestamp": iso_format(self.now - timedelta(seconds=2)), + "timestamp": (self.now - timedelta(seconds=2)).isoformat(), "tags": { "foo": "bar", "baz": "quux", @@ -112,7 +112,7 @@ def setUp(self): "platform": "python", "environment": env1, "fingerprint": ["group-2"], - "timestamp": iso_format(self.now - timedelta(seconds=2)), + "timestamp": (self.now - timedelta(seconds=2)).isoformat(), "tags": {"browser": "chrome", "sentry:user": "id:user1"}, "user": {"id": "user1"}, }, @@ -126,7 +126,7 @@ def setUp(self): "platform": "python", "environment": env2, "fingerprint": ["group-1"], - "timestamp": iso_format(self.now - timedelta(seconds=2)), + "timestamp": (self.now - timedelta(seconds=2)).isoformat(), "tags": {"foo": "bar"}, }, project_id=self.proj1.id, @@ -147,8 +147,8 @@ def perf_group_and_env(self): event_data={ **event_data, "event_id": "a" * 32, - "timestamp": iso_format(self.now - timedelta(seconds=1)), - "start_timestamp": iso_format(self.now - timedelta(seconds=1)), + "timestamp": (self.now - timedelta(seconds=1)).isoformat(), + "start_timestamp": (self.now - timedelta(seconds=1)).isoformat(), "tags": {"foo": "bar", "biz": "baz"}, "release": "releaseme", } @@ -157,8 +157,8 @@ def perf_group_and_env(self): event_data={ **event_data, "event_id": "b" * 32, - "timestamp": iso_format(self.now - timedelta(seconds=2)), - "start_timestamp": iso_format(self.now - timedelta(seconds=2)), + "timestamp": (self.now - timedelta(seconds=2)).isoformat(), + "start_timestamp": (self.now - timedelta(seconds=2)).isoformat(), "tags": {"foo": "quux"}, "release": "releaseme", } @@ -675,7 +675,7 @@ def test_get_groups_user_counts_no_environments(self): "message": "message 1", "platform": "python", "fingerprint": ["group-1"], - "timestamp": iso_format(self.now - timedelta(seconds=1)), + "timestamp": (self.now - timedelta(seconds=1)).isoformat(), "tags": { "foo": "bar", "baz": "quux", @@ -776,7 +776,7 @@ def test_get_release_tags_uses_release_project_environment(self): "platform": "python", "environment": None, "fingerprint": ["group-1"], - "timestamp": iso_format(one_day_ago), + "timestamp": one_day_ago.isoformat(), "tags": { "sentry:release": 100, }, @@ -799,7 +799,7 @@ def test_get_release_tags_uses_release_project_environment(self): "platform": "python", "environment": None, "fingerprint": ["group-1"], - "timestamp": iso_format(two_days_ago), + "timestamp": two_days_ago.isoformat(), "tags": { "sentry:release": 100, }, @@ -1148,7 +1148,7 @@ def test_get_group_tag_value_paginator_times_seen(self): "platform": "python", "environment": self.proj1env1.name, "fingerprint": ["group-1"], - "timestamp": iso_format(self.now - timedelta(seconds=2)), + "timestamp": (self.now - timedelta(seconds=2)).isoformat(), "tags": { "foo": "bar", "baz": "quux", @@ -1199,8 +1199,8 @@ def test_get_group_tag_value_paginator_times_seen_perf(self): event_data={ **event_data, "event_id": "a" * 32, - "timestamp": iso_format(self.now - timedelta(seconds=1)), - "start_timestamp": iso_format(self.now - timedelta(seconds=1)), + "timestamp": (self.now - timedelta(seconds=1)).isoformat(), + "start_timestamp": (self.now - timedelta(seconds=1)).isoformat(), "tags": {"foo": "bar", "biz": "baz"}, "release": "releaseme", "environment": env.name, @@ -1256,7 +1256,7 @@ def test_get_group_tag_value_paginator_sort_by_last_seen(self): "platform": "python", "environment": "test", "fingerprint": ["group-1"], - "timestamp": iso_format(self.now - timedelta(seconds=5)), + "timestamp": (self.now - timedelta(seconds=5)).isoformat(), "tags": { "foo": "quux", }, @@ -1274,7 +1274,7 @@ def test_get_group_tag_value_paginator_sort_by_last_seen(self): "platform": "python", "environment": "test", "fingerprint": ["group-1"], - "timestamp": iso_format(self.now), + "timestamp": self.now.isoformat(), "tags": { "foo": "quux", }, diff --git a/tests/snuba/tasks/test_unmerge.py b/tests/snuba/tasks/test_unmerge.py index 4a5a0864974f6b..4b36548c1aca10 100644 --- a/tests/snuba/tasks/test_unmerge.py +++ b/tests/snuba/tasks/test_unmerge.py @@ -30,7 +30,7 @@ unmerge, ) from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.features import with_feature from sentry.tsdb.base import TSDBModel from sentry.utils import redis @@ -69,7 +69,7 @@ def test_get_group_creation_attributes(self): "type": "default", "level": "info", "tags": {"logger": "javascript"}, - "timestamp": iso_format(now), + "timestamp": now.isoformat(), }, project_id=self.project.id, ) @@ -81,7 +81,7 @@ def test_get_group_creation_attributes(self): "type": "default", "level": "error", "tags": {"logger": "python"}, - "timestamp": iso_format(now), + "timestamp": now.isoformat(), }, project_id=self.project.id, ) @@ -93,7 +93,7 @@ def test_get_group_creation_attributes(self): "type": "default", "level": "debug", "tags": {"logger": "java"}, - "timestamp": iso_format(now), + "timestamp": now.isoformat(), }, project_id=self.project.id, ) @@ -142,7 +142,7 @@ def test_get_group_backfill_attributes(self): data={ "platform": "python", "message": "Hello from Python", - "timestamp": iso_format(now - timedelta(hours=1)), + "timestamp": (now - timedelta(hours=1)).isoformat(), "type": "default", "level": "debug", "tags": {"logger": "java"}, @@ -153,7 +153,7 @@ def test_get_group_backfill_attributes(self): data={ "platform": "java", "message": "Hello from Java", - "timestamp": iso_format(now - timedelta(hours=2)), + "timestamp": (now - timedelta(hours=2)).isoformat(), "type": "default", "level": "debug", "tags": {"logger": "java"}, @@ -205,7 +205,7 @@ def create_message_event( "user": next(user_values), "tags": tags, "fingerprint": [fingerprint], - "timestamp": iso_format(now + timedelta(seconds=i)), + "timestamp": (now + timedelta(seconds=i)).isoformat(), "environment": environment, "release": release, }, diff --git a/tests/snuba/test_snuba.py b/tests/snuba/test_snuba.py index 1fd52189349f3e..de1e122c540dc6 100644 --- a/tests/snuba/test_snuba.py +++ b/tests/snuba/test_snuba.py @@ -9,7 +9,7 @@ from snuba_sdk.column import InvalidColumnError from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.utils import snuba @@ -114,7 +114,7 @@ def test_organization_retention_larger_than_end_date(self) -> None: class BulkRawQueryTest(TestCase, SnubaTestCase): def test_simple(self) -> None: - one_min_ago = iso_format(before_now(minutes=1)) + one_min_ago = before_now(minutes=1).isoformat() event_1 = self.store_event( data={"fingerprint": ["group-1"], "message": "hello", "timestamp": one_min_ago}, project_id=self.project.id, @@ -149,7 +149,7 @@ def test_simple(self) -> None: @mock.patch("sentry.utils.snuba._bulk_snuba_query", side_effect=snuba._bulk_snuba_query) def test_cache(self, _bulk_snuba_query): - one_min_ago = iso_format(before_now(minutes=1)) + one_min_ago = before_now(minutes=1).isoformat() event_1 = self.store_event( data={"fingerprint": ["group-1"], "message": "hello", "timestamp": one_min_ago}, project_id=self.project.id, diff --git a/tests/snuba/tsdb/test_tsdb_backend.py b/tests/snuba/tsdb/test_tsdb_backend.py index 2ebdd020637299..62b1b63bf32d90 100644 --- a/tests/snuba/tsdb/test_tsdb_backend.py +++ b/tests/snuba/tsdb/test_tsdb_backend.py @@ -9,7 +9,7 @@ from sentry.models.grouprelease import GroupRelease from sentry.models.release import Release from sentry.testutils.cases import SnubaTestCase, TestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.tsdb.base import TSDBModel from sentry.tsdb.snuba import SnubaTSDB from sentry.utils.dates import to_datetime @@ -85,7 +85,7 @@ def setUp(self): "fingerprint": [["group-1"], ["group-2"]][ (r // 600) % 2 ], # Switch every 10 mins - "timestamp": iso_format(self.now + timedelta(seconds=r)), + "timestamp": (self.now + timedelta(seconds=r)).isoformat(), "tags": { "foo": "bar", "baz": "quux", @@ -142,7 +142,7 @@ def test_range_single(self): "message": "message 1", "platform": "python", "fingerprint": ["group-1"], - "timestamp": iso_format(self.now + timedelta(seconds=r)), + "timestamp": (self.now + timedelta(seconds=r)).isoformat(), "tags": { "foo": "bar", "baz": "quux", From 7faa530fc40636aa7baeab2fa22243fd0bc16990 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 20 Dec 2024 09:25:42 -0800 Subject: [PATCH 404/757] fix(query-builder): Switch to another section when the selected one no longer exists (#82385) If you rerender the same search bar with different sections, it will keep the old section selected even if it doesn't exist. This fixes that. --- .../searchQueryBuilder/index.spec.tsx | 29 +++++++++++++++++++ .../tokens/filterKeyListBox/index.tsx | 24 +++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index aad122dcb84345..9e326452d6b41c 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -520,6 +520,35 @@ describe('SearchQueryBuilder', function () { ).toBeInTheDocument(); }); + it('switches to keys menu when recent searches no longer exist', async function () { + const {rerender} = render( + + ); + + await userEvent.click(getLastInput()); + + // Recent should be selected + expect(screen.getByRole('button', {name: 'Recent'})).toHaveAttribute( + 'aria-selected', + 'true' + ); + + // Rerender without recent searches + rerender(); + + // Recent should not exist anymore + expect(screen.queryByRole('button', {name: 'Recent'})).not.toBeInTheDocument(); + // All should be selected + expect(screen.getByRole('button', {name: 'All'})).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); + it('when selecting a recent search, should reset query and call onSearch', async function () { const mockOnSearch = jest.fn(); const mockCreateRecentSearch = MockApiClient.addMockResponse({ diff --git a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx index 866218f5a064f7..7a27b7e9d9280d 100644 --- a/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filterKeyListBox/index.tsx @@ -178,6 +178,28 @@ function useHighlightFirstOptionOnSectionChange({ ]); } +// If the selected section no longer exists, switch to the first valid section +function useSwitchToValidSection({ + sections, + selectedSection, + setSelectedSection, +}: { + sections: Section[]; + selectedSection: Key | null; + setSelectedSection: (section: string) => void; +}) { + useEffect(() => { + if (!selectedSection || !sections.length) { + return; + } + + const section = sections.find(s => s.value === selectedSection); + if (!section) { + setSelectedSection(sections[0].value); + } + }, [sections, selectedSection, setSelectedSection]); +} + function FilterKeyMenuContent>({ recentFilters, selectedSection, @@ -287,6 +309,8 @@ export function FilterKeyListBox> isOpen, }); + useSwitchToValidSection({sections, selectedSection, setSelectedSection}); + const fullWidth = !query; const showDetailsPane = fullWidth && selectedSection !== RECENT_SEARCH_CATEGORY_VALUE; From 85c0661634083cace63b7c690ba1de2ad56bd913 Mon Sep 17 00:00:00 2001 From: Jodi Jang <116035587+jangjodi@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:28:08 -0800 Subject: [PATCH 405/757] fix(similarity): Make similar issues endpoint faster (#82390) Similar issues tab loads slowly when a group has a lot of group hashes Check the similar issue's group id instead of the group's hashes --- .../group_similar_issues_embeddings.py | 18 ++++++++++-------- .../test_group_similar_issues_embeddings.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/sentry/issues/endpoints/group_similar_issues_embeddings.py b/src/sentry/issues/endpoints/group_similar_issues_embeddings.py index cfc0c448c1bb8b..d8ac6226be77b2 100644 --- a/src/sentry/issues/endpoints/group_similar_issues_embeddings.py +++ b/src/sentry/issues/endpoints/group_similar_issues_embeddings.py @@ -42,24 +42,26 @@ class GroupSimilarIssuesEmbeddingsEndpoint(GroupEndpoint): "GET": ApiPublishStatus.PRIVATE, } - def get_group_hashes_for_group_id(self, group_id: int) -> set[str]: - hashes = GroupHash.objects.filter(group_id=group_id) - return {hash.hash for hash in hashes} - def get_formatted_results( self, similar_issues_data: Sequence[SeerSimilarIssueData], user: User | AnonymousUser, - group_id: int, + group: Group, ) -> Sequence[tuple[Mapping[str, Any], Mapping[str, Any]] | None]: """ Format the responses using to be used by the frontend by changing the field names and changing the cosine distances into cosine similarities. """ - hashes = self.get_group_hashes_for_group_id(group_id) group_data = {} + parent_hashes = [ + similar_issue_data.parent_hash for similar_issue_data in similar_issues_data + ] + group_hashes = GroupHash.objects.filter(project_id=group.project_id, hash__in=parent_hashes) + parent_hashes_group_ids = { + group_hash.hash: group_hash.group_id for group_hash in group_hashes + } for similar_issue_data in similar_issues_data: - if similar_issue_data.parent_hash not in hashes: + if parent_hashes_group_ids[similar_issue_data.parent_hash] != group.id: formatted_response: FormattedSimilarIssuesEmbeddingsData = { "exception": round(1 - similar_issue_data.stacktrace_distance, 4), "shouldBeGrouped": "Yes" if similar_issue_data.should_group else "No", @@ -138,6 +140,6 @@ def get(self, request: Request, group: Group) -> Response: if not results: return Response([]) - formatted_results = self.get_formatted_results(results, request.user, group.id) + formatted_results = self.get_formatted_results(results, request.user, group) return Response(formatted_results) diff --git a/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py b/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py index d7c04b3e3d3caf..a040e9ceecfde8 100644 --- a/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py +++ b/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py @@ -201,7 +201,7 @@ def test_get_formatted_results(self): formatted_results = group_similar_endpoint.get_formatted_results( similar_issues_data=[similar_issue_data_1, similar_issue_data_2], user=self.user, - group_id=self.group.id, + group=self.group, ) assert formatted_results == self.get_expected_response( [ From d552f4b6c40b43c6b4cb4555d15c37d6a5b2b4c0 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 20 Dec 2024 09:31:43 -0800 Subject: [PATCH 406/757] feat(performance): Use new search bar in web vitals pages (#82440) --- .../transactionSearchQueryBuilder.tsx | 2 +- .../transactionVitals/content.tsx | 21 ++++++------ .../transactionVitals/index.spec.tsx | 4 +++ .../performance/vitalDetail/index.spec.tsx | 21 +++++++++--- .../vitalDetail/vitalDetailContent.tsx | 32 +++++++++++++------ 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/static/app/components/performance/transactionSearchQueryBuilder.tsx b/static/app/components/performance/transactionSearchQueryBuilder.tsx index 7de519a8021e72..47a795f7e30e46 100644 --- a/static/app/components/performance/transactionSearchQueryBuilder.tsx +++ b/static/app/components/performance/transactionSearchQueryBuilder.tsx @@ -34,7 +34,7 @@ interface TransactionSearchQueryBuilderProps { filterKeyMenuWidth?: number; onSearch?: (query: string, state: CallbackSearchState) => void; placeholder?: string; - projects?: PageFilters['projects']; + projects?: PageFilters['projects'] | Readonly; trailingItems?: React.ReactNode; } diff --git a/static/app/views/performance/transactionSummary/transactionVitals/content.tsx b/static/app/views/performance/transactionSummary/transactionVitals/content.tsx index d2bb31e92410d4..308cdf09cc914d 100644 --- a/static/app/views/performance/transactionSummary/transactionVitals/content.tsx +++ b/static/app/views/performance/transactionSummary/transactionVitals/content.tsx @@ -5,13 +5,13 @@ import type {Location} from 'history'; import {Alert} from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; import {CompactSelect} from 'sentry/components/compactSelect'; -import SearchBar from 'sentry/components/events/searchBar'; import * as Layout from 'sentry/components/layouts/thirds'; import ExternalLink from 'sentry/components/links/externalLink'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; +import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; @@ -92,13 +92,14 @@ function VitalsContent(props: Props) { - + + + p.theme.breakpoints.small}) { order: 1; - grid-column: 1/5; + grid-column: 1/6; } @media (min-width: ${p => p.theme.breakpoints.xlarge}) { diff --git a/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx index 1503cdce733888..48ba077a60845b 100644 --- a/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx @@ -175,6 +175,10 @@ describe('Performance > Web Vitals', function () { url: '/organizations/org-slug/replay-count/', body: {}, }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/recent-searches/', + body: [], + }); }); afterEach(() => { diff --git a/static/app/views/performance/vitalDetail/index.spec.tsx b/static/app/views/performance/vitalDetail/index.spec.tsx index 43ab65ef900071..b99eb90388aab5 100644 --- a/static/app/views/performance/vitalDetail/index.spec.tsx +++ b/static/app/views/performance/vitalDetail/index.spec.tsx @@ -2,7 +2,13 @@ import {MetricsFieldFixture} from 'sentry-fixture/metrics'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {initializeOrg} from 'sentry-test/initializeOrg'; -import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; +import { + render, + screen, + userEvent, + waitFor, + within, +} from 'sentry-test/reactTestingLibrary'; import {textWithMarkupMatcher} from 'sentry-test/utils'; import ProjectsStore from 'sentry/stores/projectsStore'; @@ -224,7 +230,9 @@ describe('Performance > VitalDetail', function () { }); // It shows a search bar - expect(await screen.findByLabelText('Search events')).toBeInTheDocument(); + expect( + await screen.findByPlaceholderText('Search for events, users, tags, and more') + ).toBeInTheDocument(); // It shows the vital card expect( @@ -249,12 +257,17 @@ describe('Performance > VitalDetail', function () { }); // Fill out the search box, and submit it. - await userEvent.click(await screen.findByLabelText('Search events')); + await userEvent.click( + await screen.findByPlaceholderText('Search for events, users, tags, and more') + ); await userEvent.paste('user.email:uhoh*'); await userEvent.keyboard('{enter}'); // Check the navigation. - expect(browserHistory.push).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(browserHistory.push).toHaveBeenCalledTimes(1); + }); + expect(browserHistory.push).toHaveBeenCalledWith({ pathname: undefined, query: { diff --git a/static/app/views/performance/vitalDetail/vitalDetailContent.tsx b/static/app/views/performance/vitalDetail/vitalDetailContent.tsx index 1b410d8885d352..f145493aa01fe2 100644 --- a/static/app/views/performance/vitalDetail/vitalDetailContent.tsx +++ b/static/app/views/performance/vitalDetail/vitalDetailContent.tsx @@ -11,7 +11,6 @@ import {getInterval} from 'sentry/components/charts/utils'; import {CreateAlertFromViewButton} from 'sentry/components/createAlertButton'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; -import SearchBar from 'sentry/components/events/searchBar'; import * as Layout from 'sentry/components/layouts/thirds'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; @@ -20,6 +19,7 @@ import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter'; import * as TeamKeyTransactionManager from 'sentry/components/performance/teamKeyTransactionsManager'; +import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder'; import {IconCheckmark, IconClose} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -176,7 +176,7 @@ function VitalDetailContent(props: Props) { function renderContent(vital: WebVital) { const {location, organization, eventView, projects} = props; - const {fields, start, end, statsPeriod, environment, project} = eventView; + const {start, end, statsPeriod, environment, project} = eventView; const query = decodeScalar(location.query.query, ''); const orgSlug = organization.slug; @@ -197,14 +197,14 @@ function VitalDetailContent(props: Props) { - + + + p.theme.breakpoints.small}) { + order: 1; + grid-column: 1/6; + } + + @media (min-width: ${p => p.theme.breakpoints.xlarge}) { + order: initial; + grid-column: auto; + } +`; From ec4e363d61b741894715172389bf1bc6b03387c2 Mon Sep 17 00:00:00 2001 From: Bobby Carp Date: Fri, 20 Dec 2024 09:32:21 -0800 Subject: [PATCH 407/757] fix(shuffle-tests): Update the shuffle-test backend-test job definition to match definition in backend.yml (#82327) Shuffle tests are failing due to lower shard count and missing service dependencies. Sync the backend-test job with the definition in backend.yml ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- .github/workflows/shuffle-tests.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/shuffle-tests.yml b/.github/workflows/shuffle-tests.yml index 43be8dbf5ff293..4b5013a7310045 100644 --- a/.github/workflows/shuffle-tests.yml +++ b/.github/workflows/shuffle-tests.yml @@ -28,18 +28,22 @@ jobs: name: run backend tests runs-on: ubuntu-24.04 timeout-minutes: 90 + permissions: + contents: read + id-token: write strategy: # This helps not having to run multiple jobs because one fails, thus, reducing resource usage # and reducing the risk that one of many runs would turn red again (read: intermittent tests) fail-fast: false matrix: # XXX: When updating this, make sure you also update MATRIX_INSTANCE_TOTAL. - instance: [0, 1, 2, 3, 4, 5, 6] + instance: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] pg-version: ['14'] env: - # XXX: MATRIX_INSTANCE_TOTAL must be hardcoded to the length of strategy.matrix.instance. - MATRIX_INSTANCE_TOTAL: 7 + # XXX: `MATRIX_INSTANCE_TOTAL` must be hardcoded to the length of `strategy.matrix.instance`. + # If this increases, make sure to also increase `flags.backend.after_n_builds` in `codecov.yml`. + MATRIX_INSTANCE_TOTAL: 11 steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 @@ -48,7 +52,10 @@ jobs: uses: ./.github/actions/setup-sentry id: setup with: + redis_cluster: true + kafka: true snuba: true + symbolicator: true # Right now, we run so few bigtable related tests that the # overhead of running bigtable in all backend tests # is way smaller than the time it would take to run in its own job. From c6e81183b4f0289d5dbe47376834f427588644ee Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 20 Dec 2024 09:35:28 -0800 Subject: [PATCH 408/757] ref(performance): Remove feature flag check for new search bar in all tabs (#82430) --- .../components/searchQueryBuilder/index.tsx | 1 + .../transactionProfiles/index.tsx | 25 +++------ .../transactionSpans/content.tsx | 53 +++++-------------- .../transactionSpans/index.spec.tsx | 23 +++++--- .../spanSummary/content.spec.tsx | 5 ++ .../spanSummary/spanSummaryTable.tsx | 29 +++------- .../transactionTags/content.tsx | 23 +++----- .../transactionTags/index.spec.tsx | 4 ++ 8 files changed, 56 insertions(+), 107 deletions(-) diff --git a/static/app/components/searchQueryBuilder/index.tsx b/static/app/components/searchQueryBuilder/index.tsx index ff0a27b3adf4cf..27566122befad9 100644 --- a/static/app/components/searchQueryBuilder/index.tsx +++ b/static/app/components/searchQueryBuilder/index.tsx @@ -303,6 +303,7 @@ export function SearchQueryBuilder({ } ref={wrapperRef} aria-disabled={disabled} + data-test-id="search-query-builder" > - {organization.features.includes('search-query-builder-performance') ? ( - - ) : ( - - )} + diff --git a/static/app/views/performance/transactionSummary/transactionSpans/content.tsx b/static/app/views/performance/transactionSummary/transactionSpans/content.tsx index 9a7e075865b2f9..5298b71fb9721d 100644 --- a/static/app/views/performance/transactionSummary/transactionSpans/content.tsx +++ b/static/app/views/performance/transactionSummary/transactionSpans/content.tsx @@ -4,7 +4,6 @@ import type {Location} from 'history'; import omit from 'lodash/omit'; import {CompactSelect} from 'sentry/components/compactSelect'; -import SearchBar from 'sentry/components/events/searchBar'; import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; @@ -12,7 +11,6 @@ import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import Pagination from 'sentry/components/pagination'; import {SpanSearchQueryBuilder} from 'sentry/components/performance/spanSearchQueryBuilder'; -import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; @@ -20,13 +18,11 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import DiscoverQuery from 'sentry/utils/discover/discoverQuery'; import type EventView from 'sentry/utils/discover/eventView'; -import {DiscoverDatasets} from 'sentry/utils/discover/types'; import SuspectSpansQuery from 'sentry/utils/performance/suspectSpans/suspectSpansQuery'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; import {decodeScalar} from 'sentry/utils/queryString'; import useProjects from 'sentry/utils/useProjects'; import SpanMetricsTable from 'sentry/views/performance/transactionSummary/transactionSpans/spanMetricsTable'; -import {useSpanMetricsFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags'; import type {SetStateAction} from '../types'; @@ -132,22 +128,12 @@ function SpansContent(props: Props) { /> - {organization.features.includes('search-query-builder-performance') ? ( - - ) : ( - - )} + ({value: opt.field, label: opt.label}))} @@ -210,7 +196,6 @@ function SpansContent(props: Props) { // TODO: Temporary component while we make the switch to spans only. Will fully replace the old Spans tab when GA'd function SpansContentV2(props: Props) { const {location, organization, eventView, projectId, transactionName} = props; - const supportedTags = useSpanMetricsFieldSupportedTags(); const {projects} = useProjects(); const project = projects.find(p => p.id === projectId); const spansQuery = decodeScalar(location.query.spansQuery, ''); @@ -258,26 +243,12 @@ function SpansContentV2(props: Props) { - {organization.features.includes('search-query-builder-performance') ? ( - - ) : ( - - )} + Transaction Spans', function () { url: '/organizations/org-slug/spans/fields/', body: [], }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/recent-searches/', + body: [], + }); }); afterEach(function () { @@ -208,13 +212,18 @@ describe('Performance > Transaction Spans', function () { await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator')); - const searchTokens = await screen.findAllByTestId('filter-token'); - expect(searchTokens).toHaveLength(2); - expect(searchTokens[0]).toHaveTextContent('span.op:db'); - expect(searchTokens[1]).toHaveTextContent('span.action:SELECT'); - expect(await screen.findByTestId('smart-search-bar')).not.toHaveTextContent( - 'http.method:POST' - ); + const searchBar = await screen.findByTestId('search-query-builder'); + + expect( + within(searchBar).getByRole('row', {name: 'span.op:db'}) + ).toBeInTheDocument(); + expect( + within(searchBar).getByRole('row', {name: 'span.action:SELECT'}) + ).toBeInTheDocument(); + + expect( + within(searchBar).queryByRole('row', {name: 'http.method:POST'}) + ).not.toBeInTheDocument(); }); }); }); diff --git a/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/content.spec.tsx b/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/content.spec.tsx index 8f8f425ca20f8f..b7208db598e9b3 100644 --- a/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/content.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/content.spec.tsx @@ -217,6 +217,11 @@ describe('SpanSummaryPage', function () { ], }, }); + + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/recent-searches/', + body: [], + }); }); it('correctly renders the details in the header', async function () { diff --git a/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx b/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx index 6d6837dcffd017..db0b342d95959e 100644 --- a/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx @@ -2,7 +2,6 @@ import {Fragment, useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; -import SearchBar from 'sentry/components/events/searchBar'; import type {GridColumnHeader} from 'sentry/components/gridEditable'; import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; import Pagination, {type CursorHandler} from 'sentry/components/pagination'; @@ -23,7 +22,6 @@ import { type DiscoverQueryProps, useGenericDiscoverQuery, } from 'sentry/utils/discover/genericDiscoverQuery'; -import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; @@ -45,7 +43,6 @@ import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHe import {SpanDurationBar} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/spanDetailsTable'; import {SpanSummaryReferrer} from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/referrers'; import {useSpanSummarySort} from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/useSpanSummarySort'; -import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags'; import Tab from '../../tabs'; @@ -102,7 +99,6 @@ type Props = { export default function SpanSummaryTable(props: Props) { const {project} = props; const organization = useOrganization(); - const {data: supportedTags} = useSpanFieldSupportedTags(); const {spanSlug} = useParams(); const navigate = useNavigate(); const [spanOp, groupId] = spanSlug.split(':'); @@ -227,25 +223,12 @@ export default function SpanSummaryTable(props: Props) { return ( - {organization.features.includes('search-query-builder-performance') ? ( - - ) : ( - - )} + - {organization.features.includes('search-query-builder-performance') ? ( - - ) : ( - - )} + Transaction Tags', function () { url: '/organizations/org-slug/replay-count/', body: {}, }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/recent-searches/', + body: [], + }); }); afterEach(function () { From 213137dba1e16be9b44b8707fdf52608d1708586 Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Fri, 20 Dec 2024 18:37:00 +0100 Subject: [PATCH 409/757] fix(settings): fix routing for few org settings item (#82449) Fixes behavior when reloading react pages `/settings/rate-limits` and `/settings/feature-flags`. Similar to https://github.com/getsentry/sentry/pull/62996 --- src/sentry/web/urls.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/sentry/web/urls.py b/src/sentry/web/urls.py index dd5361143698c0..69cbedc6987a02 100644 --- a/src/sentry/web/urls.py +++ b/src/sentry/web/urls.py @@ -623,6 +623,11 @@ react_page_view, name="sentry-customer-domain-audit-log-settings", ), + re_path( + r"^rate-limits/", + react_page_view, + name="sentry-customer-domain-rate-limits-settings", + ), re_path( r"^relay/", react_page_view, @@ -638,6 +643,16 @@ react_page_view, name="sentry-customer-domain-integrations-settings", ), + re_path( + r"^dynamic-sampling/", + react_page_view, + name="sentry-customer-domain-dynamic-sampling-settings", + ), + re_path( + r"^feature-flags/", + react_page_view, + name="sentry-customer-domain-feature-flags-settings", + ), re_path( r"^developer-settings/", react_page_view, @@ -678,11 +693,6 @@ react_page_view, name="sentry-customer-domain-legal-settings", ), - re_path( - r"^dynamic-sampling/", - react_page_view, - name="sentry-customer-domain-dynamic-sampling-settings", - ), re_path( r"^(?P[\w_-]+)/$", react_page_view, From ecbfbcf90bae580306fc44b9210b354c2a5dc10b Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Fri, 20 Dec 2024 09:44:39 -0800 Subject: [PATCH 410/757] ref(seer grouping): Use frame tallies for excess frame check (#82414) This is the first step in simplifying the way we apply the stacktrace length filter to events sent to Seer. It puts in place the new machinery, but does not yet remove the old machinery, which will happen in a follow-up PR. In the meantime, though, even though the old code is still there, the new code should supplant it, since its check comes before the existing one - meaning anything which should get filtered should be flagged by the new check and shouldn't make it through to the existing check. I used different names for the metrics so that we can make sure that's the case before we remove the existing code. Key changes: - A new `has_too_many_contributing_frames` to be used in all the places we send events to Seer (ingest, backfill, similar issues tab), which collects a `grouping.similarity.frame_count_filter` metric, regardless of (and tagged with) the referrer. - A new ingest-specific `_has_too_many_contributing_frames` wrapper for the above, which also records a `did_call_seer` metric. - In both the backfill and similar issues tab endpoint code, getting `grouping_info` is now split into two steps, first getting the variants and then getting grouping info from the variants, so that the variants can be fed to `has_too_many_contributing_frames`. - All `get_stacktrace_string` tests whose names included some variation of "too many fames" have been renamed, so as not to confuse them with the tests of the new helper, which are added in a separate class at the bottom of the module. Note that one of the tests is marked as a skip, because in the process of doing this I found what might be a bug in our grouping logic. (TL;DR, frames which are marked `-app +group` by a stacktrace rule might not be behaving as expected... depending on what you think is expected.) In any case, for now the test is skipped, and once we resolve the stacktrace rule question, we can ether turn it back on or change it. --- src/sentry/grouping/grouping_info.py | 3 +- src/sentry/grouping/ingest/seer.py | 21 +- .../group_similar_issues_embeddings.py | 27 +- src/sentry/seer/similarity/utils.py | 60 +++++ src/sentry/tasks/embeddings_grouping/utils.py | 19 +- tests/sentry/grouping/ingest/test_seer.py | 27 +- tests/sentry/seer/similarity/test_utils.py | 235 +++++++++++++++++- .../test_backfill_seer_grouping_records.py | 13 +- 8 files changed, 361 insertions(+), 44 deletions(-) diff --git a/src/sentry/grouping/grouping_info.py b/src/sentry/grouping/grouping_info.py index 5e7ff6e9f695da..92e4d60e6bc8ac 100644 --- a/src/sentry/grouping/grouping_info.py +++ b/src/sentry/grouping/grouping_info.py @@ -1,5 +1,4 @@ import logging -from collections.abc import Mapping from typing import Any from sentry.api.exceptions import ResourceDoesNotExist @@ -89,7 +88,7 @@ def _check_for_mismatched_hashes( def get_grouping_info_from_variants( - variants: Mapping[str, BaseVariant], + variants: dict[str, BaseVariant], ) -> dict[str, dict[str, Any]]: """ Given a dictionary of variant objects, create and return a copy of the dictionary in which each diff --git a/src/sentry/grouping/ingest/seer.py b/src/sentry/grouping/ingest/seer.py index 1ee6d018a52475..2335348ef26e22 100644 --- a/src/sentry/grouping/ingest/seer.py +++ b/src/sentry/grouping/ingest/seer.py @@ -1,5 +1,4 @@ import logging -from collections.abc import Mapping from dataclasses import asdict from typing import Any @@ -22,6 +21,7 @@ event_content_has_stacktrace, filter_null_from_string, get_stacktrace_string_with_metrics, + has_too_many_contributing_frames, killswitch_enabled, record_did_call_seer_metric, ) @@ -32,7 +32,7 @@ logger = logging.getLogger("sentry.events.grouping") -def should_call_seer_for_grouping(event: Event, variants: Mapping[str, BaseVariant]) -> bool: +def should_call_seer_for_grouping(event: Event, variants: dict[str, BaseVariant]) -> bool: """ Use event content, feature flags, rate limits, killswitches, seer health, etc. to determine whether a call to Seer should be made. @@ -48,6 +48,7 @@ def should_call_seer_for_grouping(event: Event, variants: Mapping[str, BaseVaria if ( _has_customized_fingerprint(event, variants) + or _has_too_many_contributing_frames(event, variants) or killswitch_enabled(project.id, ReferrerOptions.INGEST, event) or _circuit_breaker_broken(event, project) # The rate limit check has to be last (see below) but rate-limiting aside, call this after other checks @@ -96,6 +97,14 @@ def _event_content_is_seer_eligible(event: Event) -> bool: return True +def _has_too_many_contributing_frames(event: Event, variants: dict[str, BaseVariant]) -> bool: + if has_too_many_contributing_frames(event, variants, ReferrerOptions.INGEST): + record_did_call_seer_metric(call_made=False, blocker="excess-frames") + return True + + return False + + def _project_has_similarity_grouping_enabled(project: Project) -> bool: # TODO: This is a hack to get ingest to turn on for projects as soon as they're backfilled. When # the backfill script completes, we turn on this option, enabling ingest immediately rather than @@ -116,7 +125,7 @@ def _project_has_similarity_grouping_enabled(project: Project) -> bool: # combined with some other value). To the extent to which we're then using this function to decide # whether or not to call Seer, this means that the calculations giving rise to the default part of # the value never involve Seer input. In the long run, we probably want to change that. -def _has_customized_fingerprint(event: Event, variants: Mapping[str, BaseVariant]) -> bool: +def _has_customized_fingerprint(event: Event, variants: dict[str, BaseVariant]) -> bool: fingerprint = event.data.get("fingerprint", []) if "{{ default }}" in fingerprint: @@ -190,7 +199,7 @@ def _circuit_breaker_broken(event: Event, project: Project) -> bool: return circuit_broken -def _has_empty_stacktrace_string(event: Event, variants: Mapping[str, BaseVariant]) -> bool: +def _has_empty_stacktrace_string(event: Event, variants: dict[str, BaseVariant]) -> bool: stacktrace_string = get_stacktrace_string_with_metrics( get_grouping_info_from_variants(variants), event.platform, ReferrerOptions.INGEST ) @@ -206,7 +215,7 @@ def _has_empty_stacktrace_string(event: Event, variants: Mapping[str, BaseVarian def get_seer_similar_issues( event: Event, - variants: Mapping[str, BaseVariant], + variants: dict[str, BaseVariant], num_neighbors: int = 1, ) -> tuple[dict[str, Any], GroupHash | None]: """ @@ -282,7 +291,7 @@ def get_seer_similar_issues( def maybe_check_seer_for_matching_grouphash( - event: Event, variants: Mapping[str, BaseVariant], all_grouphashes: list[GroupHash] + event: Event, variants: dict[str, BaseVariant], all_grouphashes: list[GroupHash] ) -> GroupHash | None: seer_matched_grouphash = None diff --git a/src/sentry/issues/endpoints/group_similar_issues_embeddings.py b/src/sentry/issues/endpoints/group_similar_issues_embeddings.py index d8ac6226be77b2..fb46b54b8381da 100644 --- a/src/sentry/issues/endpoints/group_similar_issues_embeddings.py +++ b/src/sentry/issues/endpoints/group_similar_issues_embeddings.py @@ -12,7 +12,7 @@ from sentry.api.base import region_silo_endpoint from sentry.api.bases.group import GroupEndpoint from sentry.api.serializers import serialize -from sentry.grouping.grouping_info import get_grouping_info +from sentry.grouping.grouping_info import get_grouping_info_from_variants from sentry.models.group import Group from sentry.models.grouphash import GroupHash from sentry.seer.similarity.similar_issues import get_similarity_data_from_seer @@ -22,6 +22,7 @@ TooManyOnlySystemFramesException, event_content_has_stacktrace, get_stacktrace_string, + has_too_many_contributing_frames, killswitch_enabled, ) from sentry.users.models.user import User @@ -83,16 +84,22 @@ def get(self, request: Request, group: Group) -> Response: latest_event = group.get_latest_event() stacktrace_string = "" + if latest_event and event_content_has_stacktrace(latest_event): - grouping_info = get_grouping_info(None, project=group.project, event=latest_event) - try: - stacktrace_string = get_stacktrace_string( - grouping_info, platform=latest_event.platform - ) - except TooManyOnlySystemFramesException: - pass - except Exception: - logger.exception("Unexpected exception in stacktrace string formatting") + variants = latest_event.get_grouping_variants(normalize_stacktraces=True) + + if not has_too_many_contributing_frames( + latest_event, variants, ReferrerOptions.SIMILAR_ISSUES_TAB + ): + grouping_info = get_grouping_info_from_variants(variants) + try: + stacktrace_string = get_stacktrace_string( + grouping_info, platform=latest_event.platform + ) + except TooManyOnlySystemFramesException: + pass + except Exception: + logger.exception("Unexpected exception in stacktrace string formatting") if not stacktrace_string or not latest_event: return Response([]) # No exception, stacktrace or in-app frames, or event diff --git a/src/sentry/seer/similarity/utils.py b/src/sentry/seer/similarity/utils.py index dd8af36a7286e5..49c5b0c417d610 100644 --- a/src/sentry/seer/similarity/utils.py +++ b/src/sentry/seer/similarity/utils.py @@ -5,6 +5,8 @@ from sentry import options from sentry.eventstore.models import Event, GroupEvent +from sentry.grouping.api import get_contributing_variant_and_component +from sentry.grouping.variants import BaseVariant, ComponentVariant from sentry.killswitches import killswitch_matches_context from sentry.models.project import Project from sentry.utils import metrics @@ -316,6 +318,64 @@ def record_did_call_seer_metric(*, call_made: bool, blocker: str) -> None: ) +def has_too_many_contributing_frames( + event: Event | GroupEvent, + variants: dict[str, BaseVariant], + referrer: ReferrerOptions, +) -> bool: + platform = event.platform + shared_tags = {"referrer": referrer.value, "platform": platform} + + contributing_variant, contributing_component = get_contributing_variant_and_component(variants) + + # Ideally we're calling this function after we already know the event both has a stacktrace and + # is using it for grouping (in which case none of the below conditions should apply), but still + # worth checking that we have enough information to answer the question just in case + if ( + # Fingerprint, checksum, fallback variants + not isinstance(contributing_variant, ComponentVariant) + # Security violations, log-message-based grouping + or contributing_variant.variant_name == "default" + # Any ComponentVariant will have this, but this reassures mypy + or not contributing_component + # Exception-message-based grouping + or not hasattr(contributing_component, "frame_counts") + ): + # We don't bother to collect a metric on this outcome, because we shouldn't have called the + # function in the first place + return False + + # Certain platforms were backfilled before we added this filter, so to keep new events matching + # with the existing data, we turn off the filter for them (instead their stacktraces will be + # truncated) + if platform in EVENT_PLATFORMS_BYPASSING_FRAME_COUNT_CHECK: + metrics.incr( + "grouping.similarity.frame_count_filter", + sample_rate=options.get("seer.similarity.metrics_sample_rate"), + tags={**shared_tags, "outcome": "bypass"}, + ) + return False + + stacktrace_type = "in_app" if contributing_variant.variant_name == "app" else "system" + key = f"{stacktrace_type}_contributing_frames" + shared_tags["stacktrace_type"] = stacktrace_type + + if contributing_component.frame_counts[key] > MAX_FRAME_COUNT: + metrics.incr( + "grouping.similarity.frame_count_filter", + sample_rate=options.get("seer.similarity.metrics_sample_rate"), + tags={**shared_tags, "outcome": "block"}, + ) + return True + + metrics.incr( + "grouping.similarity.frame_count_filter", + sample_rate=options.get("seer.similarity.metrics_sample_rate"), + tags={**shared_tags, "outcome": "pass"}, + ) + return False + + def killswitch_enabled( project_id: int | None, referrer: ReferrerOptions, diff --git a/src/sentry/tasks/embeddings_grouping/utils.py b/src/sentry/tasks/embeddings_grouping/utils.py index b585ed828e11e9..84f121ff440ec2 100644 --- a/src/sentry/tasks/embeddings_grouping/utils.py +++ b/src/sentry/tasks/embeddings_grouping/utils.py @@ -13,7 +13,7 @@ from sentry import nodestore, options from sentry.conf.server import SEER_SIMILARITY_MODEL_VERSION from sentry.eventstore.models import Event -from sentry.grouping.grouping_info import get_grouping_info +from sentry.grouping.grouping_info import get_grouping_info_from_variants from sentry.issues.grouptype import ErrorGroupType from sentry.models.group import Group, GroupStatus from sentry.models.project import Project @@ -35,6 +35,7 @@ event_content_has_stacktrace, filter_null_from_string, get_stacktrace_string_with_metrics, + has_too_many_contributing_frames, ) from sentry.snuba.dataset import Dataset from sentry.snuba.referrer import Referrer @@ -356,11 +357,17 @@ def get_events_from_nodestore( bulk_event_ids = set() for group_id, event in nodestore_events.items(): event._project_cache = project - if event and event.data and event_content_has_stacktrace(event): - grouping_info = get_grouping_info(None, project=project, event=event) - stacktrace_string = get_stacktrace_string_with_metrics( - grouping_info, event.platform, ReferrerOptions.BACKFILL - ) + stacktrace_string = None + + if event and event_content_has_stacktrace(event): + variants = event.get_grouping_variants(normalize_stacktraces=True) + + if not has_too_many_contributing_frames(event, variants, ReferrerOptions.BACKFILL): + grouping_info = get_grouping_info_from_variants(variants) + stacktrace_string = get_stacktrace_string_with_metrics( + grouping_info, event.platform, ReferrerOptions.BACKFILL + ) + if not stacktrace_string: invalid_event_group_ids.append(group_id) continue diff --git a/tests/sentry/grouping/ingest/test_seer.py b/tests/sentry/grouping/ingest/test_seer.py index 739a967843cc5e..c57a2dd306ad60 100644 --- a/tests/sentry/grouping/ingest/test_seer.py +++ b/tests/sentry/grouping/ingest/test_seer.py @@ -178,6 +178,16 @@ def test_obeys_customized_fingerprint_check(self) -> None: is expected_result ), f'Case with fingerprint {event.data["fingerprint"]} failed.' + def test_obeys_excessive_frame_check(self) -> None: + self.project.update_option("sentry:similarity_backfill_completed", int(time())) + + for frame_check_result, expected_result in [(True, False), (False, True)]: + with patch( + "sentry.grouping.ingest.seer._has_too_many_contributing_frames", + return_value=frame_check_result, + ): + assert should_call_seer_for_grouping(self.event, self.variants) is expected_result + @patch("sentry.grouping.ingest.seer.record_did_call_seer_metric") def test_obeys_empty_stacktrace_string_check(self, mock_record_did_call_seer: Mock) -> None: self.project.update_option("sentry:similarity_backfill_completed", int(time())) @@ -492,10 +502,10 @@ def test_valid_maybe_check_seer_for_matching_group_hash( } ) - @patch("sentry.seer.similarity.utils.record_did_call_seer_metric") + @patch("sentry.grouping.ingest.seer.record_did_call_seer_metric") @patch("sentry.grouping.ingest.seer.get_seer_similar_issues") @patch("sentry.seer.similarity.utils.metrics") - def test_too_many_only_system_frames_maybe_check_seer_for_matching_group_hash( + def test_too_many_frames_maybe_check_seer_for_matching_group_hash( self, mock_metrics: MagicMock, mock_get_similar_issues: MagicMock, @@ -543,16 +553,21 @@ def test_too_many_only_system_frames_maybe_check_seer_for_matching_group_hash( sample_rate = options.get("seer.similarity.metrics_sample_rate") mock_metrics.incr.assert_any_call( - "grouping.similarity.over_threshold_only_system_frames", + "grouping.similarity.frame_count_filter", sample_rate=sample_rate, - tags={"platform": "java", "referrer": "ingest"}, + tags={ + "platform": "java", + "referrer": "ingest", + "stacktrace_type": "system", + "outcome": "block", + }, ) - mock_record_did_call_seer.assert_any_call(call_made=False, blocker="over-threshold-frames") + mock_record_did_call_seer.assert_any_call(call_made=False, blocker="excess-frames") mock_get_similar_issues.assert_not_called() @patch("sentry.grouping.ingest.seer.get_similarity_data_from_seer", return_value=[]) - def test_too_many_only_system_frames_maybe_check_seer_for_matching_group_hash_invalid_platform( + def test_too_many_frames_maybe_check_seer_for_matching_group_hash_bypassed_platform( self, mock_get_similarity_data: MagicMock ) -> None: self.project.update_option("sentry:similarity_backfill_completed", int(time())) diff --git a/tests/sentry/seer/similarity/test_utils.py b/tests/sentry/seer/similarity/test_utils.py index ad374795fb7228..21fe2ec54426d4 100644 --- a/tests/sentry/seer/similarity/test_utils.py +++ b/tests/sentry/seer/similarity/test_utils.py @@ -5,6 +5,9 @@ import pytest +from sentry.eventstore.models import Event +from sentry.grouping.api import get_contributing_variant_and_component +from sentry.grouping.variants import CustomFingerprintVariant from sentry.seer.similarity.utils import ( BASE64_ENCODED_PREFIXES, MAX_FRAME_COUNT, @@ -14,6 +17,7 @@ filter_null_from_string, get_stacktrace_string, get_stacktrace_string_with_metrics, + has_too_many_contributing_frames, ) from sentry.testutils.cases import TestCase @@ -532,7 +536,7 @@ def test_chained(self): ) assert stacktrace_str == expected_stacktrace_str - def test_chained_too_many_frames(self): + def test_chained_stacktrace_truncation(self): data_chained_exception = copy.deepcopy(self.CHAINED_APP_DATA) data_chained_exception["app"]["component"]["values"][0]["values"] = [ self.create_exception( @@ -578,7 +582,7 @@ def test_chained_too_many_frames(self): ) assert stacktrace_str == expected - def test_chained_too_many_frames_all_minified_js(self): + def test_chained_stacktrace_truncation_all_minified_js(self): data_chained_exception = copy.deepcopy(self.CHAINED_APP_DATA) data_chained_exception["app"]["component"]["values"][0]["values"] = [ self.create_exception( @@ -630,7 +634,7 @@ def test_chained_too_many_frames_all_minified_js(self): ) assert stacktrace_str == expected - def test_chained_too_many_frames_minified_js_frame_limit(self): + def test_chained_stacktrace_truncation_minified_js_frame_limit_is_lower(self): """Test that we restrict fully-minified stacktraces to 20 frames, and all other stacktraces to 30 frames.""" for minified_frames, expected_frame_count in [("all", 20), ("some", 30), ("none", 30)]: data_chained_exception = copy.deepcopy(self.CHAINED_APP_DATA) @@ -672,7 +676,7 @@ def test_chained_too_many_frames_minified_js_frame_limit(self): == expected_frame_count ) - def test_chained_too_many_exceptions(self): + def test_chained_exception_limit(self): """Test that we restrict number of chained exceptions to MAX_FRAME_COUNT.""" data_chained_exception = copy.deepcopy(self.CHAINED_APP_DATA) data_chained_exception["app"]["component"]["values"][0]["values"] = [ @@ -713,7 +717,7 @@ def test_no_app_no_system(self): stacktrace_str = get_stacktrace_string(data) assert stacktrace_str == "" - def test_too_many_system_frames_single_exception(self): + def test_stacktrace_length_filter_single_exception(self): data_system = copy.deepcopy(self.BASE_APP_DATA) data_system["system"] = data_system.pop("app") data_system["system"]["component"]["values"][0]["values"][0][ @@ -723,7 +727,7 @@ def test_too_many_system_frames_single_exception(self): with pytest.raises(TooManyOnlySystemFramesException): get_stacktrace_string(data_system, platform="java") - def test_too_many_system_frames_single_exception_invalid_platform(self): + def test_stacktrace_length_filter_single_exception_invalid_platform(self): data_system = copy.deepcopy(self.BASE_APP_DATA) data_system["system"] = data_system.pop("app") data_system["system"]["component"]["values"][0]["values"][0][ @@ -733,7 +737,7 @@ def test_too_many_system_frames_single_exception_invalid_platform(self): stacktrace_string = get_stacktrace_string(data_system, "python") assert stacktrace_string is not None and stacktrace_string != "" - def test_too_many_system_frames_chained_exception(self): + def test_stacktrace_length_filter_chained_exception(self): data_system = copy.deepcopy(self.CHAINED_APP_DATA) data_system["system"] = data_system.pop("app") # Split MAX_FRAME_COUNT across the two exceptions @@ -747,7 +751,7 @@ def test_too_many_system_frames_chained_exception(self): with pytest.raises(TooManyOnlySystemFramesException): get_stacktrace_string(data_system, platform="java") - def test_too_many_system_frames_chained_exception_invalid_platform(self): + def test_stacktrace_length_filter_chained_exception_invalid_platform(self): data_system = copy.deepcopy(self.CHAINED_APP_DATA) data_system["system"] = data_system.pop("app") # Split MAX_FRAME_COUNT across the two exceptions @@ -761,7 +765,7 @@ def test_too_many_system_frames_chained_exception_invalid_platform(self): stacktrace_string = get_stacktrace_string(data_system, "python") assert stacktrace_string is not None and stacktrace_string != "" - def test_too_many_in_app_contributing_frames(self): + def test_stacktrace_truncation_uses_in_app_contributing_frames(self): """ Check that when there are over MAX_FRAME_COUNT contributing frames, the last MAX_FRAME_COUNT is included. @@ -794,7 +798,7 @@ def test_too_many_in_app_contributing_frames(self): assert ("test = " + str(i) + "!") in stacktrace_str assert num_frames == MAX_FRAME_COUNT - def test_too_many_frames_minified_js_frame_limit(self): + def test_stacktrace_truncation_minified_js_frame_limit_is_lower(self): """Test that we restrict fully-minified stacktraces to 20 frames, and all other stacktraces to 30 frames.""" for minified_frames, expected_frame_count in [("all", 20), ("some", 30), ("none", 30)]: data_frames = copy.deepcopy(self.BASE_APP_DATA) @@ -875,3 +879,214 @@ class SeerUtilsTest(TestCase): def test_filter_null_from_string(self): string_with_null = 'String with null \x00, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" is null' assert filter_null_from_string(string_with_null) == 'String with null , "" is null' + + +class HasTooManyFramesTest(TestCase): + def setUp(self): + # The `in_app` and `contributes` values of these frames will be determined by the project + # stacktrace rules we'll add below + self.contributing_system_frame = { + "function": "handleRequest", + "filename": "/node_modules/express/router.js", + "context_line": "return handler(request);", + } + self.non_contributing_system_frame = { + "function": "runApp", + "filename": "/node_modules/express/app.js", + "context_line": "return server.serve(port);", + } + self.contributing_in_app_frame = { + "function": "playFetch", + "filename": "/dogApp/dogpark.js", + "context_line": "raise FailedToFetchError('Charlie didn't bring the ball back');", + } + self.non_contributing_in_app_frame = { + "function": "recordMetrics", + "filename": "/dogApp/metrics.js", + "context_line": "return withMetrics(handler, metricName, tags);", + } + self.exception_value = { + "type": "FailedToFetchError", + "value": "Charlie didn't bring the ball back", + } + self.event = Event( + event_id="12312012041520130908201311212012", + project_id=self.project.id, + data={ + "title": "FailedToFetchError('Charlie didn't bring the ball back')", + "exception": {"values": [self.exception_value]}, + }, + ) + self.project.update_option( + "sentry:grouping_enhancements", + "\n".join( + [ + "stack.function:runApp -app -group", + "stack.function:handleRequest -app +group", + "stack.function:recordMetrics +app -group", + "stack.function:playFetch +app +group", + ] + ), + ) + + def test_single_exception_simple(self): + for stacktrace_length, expected_result in [ + (MAX_FRAME_COUNT - 1, False), + (MAX_FRAME_COUNT + 1, True), + ]: + self.event.data["platform"] = "java" + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": [self.contributing_in_app_frame] * stacktrace_length + } + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is expected_result + ) + + def test_single_exception_bypassed_platform(self): + # Regardless of the number of frames, we never flag it as being too long + for stacktrace_length, expected_result in [ + (MAX_FRAME_COUNT - 1, False), + (MAX_FRAME_COUNT + 1, False), + ]: + self.event.data["platform"] = "python" + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": [self.contributing_in_app_frame] * stacktrace_length + } + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is expected_result + ) + + def test_chained_exception_simple(self): + for total_frames, expected_result in [ + (MAX_FRAME_COUNT - 2, False), + (MAX_FRAME_COUNT + 2, True), + ]: + self.event.data["platform"] = "java" + self.event.data["exception"]["values"] = [ + {**self.exception_value}, + {**self.exception_value}, + ] + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": [self.contributing_in_app_frame] * (total_frames // 2) + } + self.event.data["exception"]["values"][1]["stacktrace"] = { + "frames": [self.contributing_in_app_frame] * (total_frames // 2) + } + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is expected_result + ) + + def test_chained_exception_bypassed_platform(self): + # Regardless of the number of frames, we never flag it as being too long + for total_frames, expected_result in [ + (MAX_FRAME_COUNT - 2, False), + (MAX_FRAME_COUNT + 2, False), + ]: + self.event.data["platform"] = "python" + self.event.data["exception"]["values"] = [ + {**self.exception_value}, + {**self.exception_value}, + ] + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": [self.contributing_in_app_frame] * (total_frames // 2) + } + self.event.data["exception"]["values"][1]["stacktrace"] = { + "frames": [self.contributing_in_app_frame] * (total_frames // 2) + } + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is expected_result + ) + + def test_ignores_non_contributing_frames(self): + self.event.data["platform"] = "java" + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": ( + # Taken together, there are too many frames + [self.contributing_in_app_frame] * (MAX_FRAME_COUNT - 1) + + [self.non_contributing_in_app_frame] * 2 + ) + } + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is False # Not flagged as too many because only contributing frames are counted + ) + + def test_prefers_app_frames(self): + self.event.data["platform"] = "java" + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": ( + [self.contributing_in_app_frame] * (MAX_FRAME_COUNT - 1) # Under the limit + + [self.contributing_system_frame] * (MAX_FRAME_COUNT + 1) # Over the limit + ) + } + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is False # Not flagged as too many because only in-app frames are counted + ) + + @pytest.mark.skip(reason="wonky behavior with -app +group rules") + def test_uses_app_or_system_variants(self): + for frame, expected_variant_name in [ + (self.contributing_in_app_frame, "app"), + (self.contributing_system_frame, "system"), + ]: + self.event.data["platform"] = "java" + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": [frame] * (MAX_FRAME_COUNT + 1) + } + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + + contributing_variant, _ = get_contributing_variant_and_component(variants) + assert contributing_variant.variant_name == expected_variant_name + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is True + ) + + def test_ignores_events_not_grouped_on_stacktrace(self): + self.event.data["platform"] = "java" + self.event.data["exception"]["values"][0]["stacktrace"] = { + "frames": ([self.contributing_system_frame] * (MAX_FRAME_COUNT + 1)) # Over the limit + } + self.event.data["fingerprint"] = ["dogs_are_great"] + + # `normalize_stacktraces=True` forces the custom stacktrace enhancements to run + variants = self.event.get_grouping_variants(normalize_stacktraces=True) + contributing_variant, _ = get_contributing_variant_and_component(variants) + assert isinstance(contributing_variant, CustomFingerprintVariant) + + assert ( + has_too_many_contributing_frames(self.event, variants, ReferrerOptions.INGEST) + is False # Not flagged as too many because it's grouped by fingerprint + ) diff --git a/tests/sentry/tasks/test_backfill_seer_grouping_records.py b/tests/sentry/tasks/test_backfill_seer_grouping_records.py index 6f8535e616c5f5..389dadbe8a0364 100644 --- a/tests/sentry/tasks/test_backfill_seer_grouping_records.py +++ b/tests/sentry/tasks/test_backfill_seer_grouping_records.py @@ -359,7 +359,7 @@ def test_lookup_group_data_stacktrace_bulk_no_stacktrace_exception(self): @patch("sentry.seer.similarity.utils.metrics") def test_lookup_group_data_stacktrace_bulk_invalid_stacktrace_exception(self, mock_metrics): """ - Test that if a group has over MAX_FRAME_COUNT only system frames, its data is not included in + Test that if a group has over MAX_FRAME_COUNT frames, its data is not included in the bulk lookup result """ # Use 2 events @@ -367,7 +367,7 @@ def test_lookup_group_data_stacktrace_bulk_invalid_stacktrace_exception(self, mo group_ids = [row["group_id"] for row in rows] for group_id in group_ids: hashes.update({group_id: self.group_hashes[group_id]}) - # Create one event where the stacktrace has over MAX_FRAME_COUNT system only frames + # Create one event where the stacktrace has over MAX_FRAME_COUNT frames exception = copy.deepcopy(EXCEPTION) exception["values"][0]["stacktrace"]["frames"] = [ { @@ -414,9 +414,14 @@ def test_lookup_group_data_stacktrace_bulk_invalid_stacktrace_exception(self, mo sample_rate = options.get("seer.similarity.metrics_sample_rate") mock_metrics.incr.assert_called_with( - "grouping.similarity.over_threshold_only_system_frames", + "grouping.similarity.frame_count_filter", sample_rate=sample_rate, - tags={"platform": "java", "referrer": "backfill"}, + tags={ + "platform": "java", + "referrer": "backfill", + "stacktrace_type": "system", + "outcome": "block", + }, ) def test_lookup_group_data_stacktrace_bulk_with_fallback_success(self): From 0d6dcbb3b5d2f05320d9ccd5da4fa1ddcbf276e4 Mon Sep 17 00:00:00 2001 From: Dora Date: Fri, 20 Dec 2024 09:46:31 -0800 Subject: [PATCH 411/757] fix(issue-trace): change linking mechanism (#82418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before hover: Screenshot 2024-12-20 at 1 48 54 AM After hover: Screenshot 2024-12-20 at 1 48 50 AM --------- Co-authored-by: Abdullah Khan --- .../interfaces/performance/eventTraceView.tsx | 15 ++++++++++++--- .../newTraceDetails/traceDrawer/traceDrawer.tsx | 3 +++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/static/app/components/events/interfaces/performance/eventTraceView.tsx b/static/app/components/events/interfaces/performance/eventTraceView.tsx index 979e8f790932ce..ce2e3e7700d94a 100644 --- a/static/app/components/events/interfaces/performance/eventTraceView.tsx +++ b/static/app/components/events/interfaces/performance/eventTraceView.tsx @@ -7,7 +7,6 @@ import Link from 'sentry/components/links/link'; import {generateTraceTarget} from 'sentry/components/quickTrace/utils'; import {IconOpen} from 'sentry/icons'; import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; import {type Group, IssueCategory} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; @@ -207,9 +206,19 @@ const IssuesTraceOverlayContainer = styled('div')` z-index: 10; a { + display: none; position: absolute; - top: ${space(1)}; - right: ${space(1)}; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &:hover { + background-color: rgba(128, 128, 128, 0.4); + + a { + display: block; + } } `; diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx index 49b1093c8771a6..bbc08f9db58604 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx @@ -642,6 +642,8 @@ const StyledIconClose = styled(IconClose)` const CloseButton = styled(Button)` font-size: ${p => p.theme.fontSizeSmall}; color: ${p => p.theme.subText}; + height: 100%; + border-bottom: 2px solid transparent; &:hover { color: ${p => p.theme.textColor}; } @@ -791,6 +793,7 @@ const TabLayoutControlItem = styled('li')` position: relative; z-index: 10; background-color: ${p => p.theme.background}; + height: 100%; `; const Tab = styled('li')` From 8f769d5920c4c87b33f7dab01598878656389855 Mon Sep 17 00:00:00 2001 From: Colleen O'Rourke Date: Fri, 20 Dec 2024 09:47:06 -0800 Subject: [PATCH 412/757] feat(ACI): Remove DataConditions condition field (#82403) Follow up to https://github.com/getsentry/sentry/pull/82336 and the final piece to removing this column. --- migrations_lockfile.txt | 2 +- .../0019_drop_dataconditions_condition.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/sentry/workflow_engine/migrations/0019_drop_dataconditions_condition.py diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index e9fb7d22ebadef..0ec7da63fda40d 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -23,4 +23,4 @@ tempest: 0001_create_tempest_credentials_model uptime: 0021_drop_region_table_col -workflow_engine: 0018_rm_data_condition_condition +workflow_engine: 0019_drop_dataconditions_condition diff --git a/src/sentry/workflow_engine/migrations/0019_drop_dataconditions_condition.py b/src/sentry/workflow_engine/migrations/0019_drop_dataconditions_condition.py new file mode 100644 index 00000000000000..9c96936d1ec154 --- /dev/null +++ b/src/sentry/workflow_engine/migrations/0019_drop_dataconditions_condition.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.4 on 2024-12-19 19:56 + +from sentry.new_migrations.migrations import CheckedMigration +from sentry.new_migrations.monkey.fields import SafeRemoveField +from sentry.new_migrations.monkey.state import DeletionAction + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("workflow_engine", "0018_rm_data_condition_condition"), + ] + + operations = [ + SafeRemoveField( + model_name="datacondition", + name="condition", + deletion_action=DeletionAction.DELETE, + ), + ] From 19ba6e7b2dd457ff6b03a1cc5f3f86ef7b446efb Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:51:39 -0500 Subject: [PATCH 413/757] feat(dashboards): Add `AreaChartWidget` (#81962) Adds an `AreaChartWidget`. Just like `LineChartWidget` but doesn't support as many features for now! This creates a whole bunch of code duplication that I'll resolve over time. Closes https://github.com/getsentry/sentry/issues/81836 --- .../areaChartWidget/areaChartWidget.spec.tsx | 88 +++++++ .../areaChartWidget.stories.tsx | 234 ++++++++++++++++++ .../areaChartWidget/areaChartWidget.tsx | 78 ++++++ .../areaChartWidgetVisualization.tsx | 216 ++++++++++++++++ .../sampleLatencyTimeSeries.json | 205 +++++++++++++++ .../sampleSpanDurationTimeSeries.json | 205 +++++++++++++++ .../formatTooltipValue.spec.tsx | 0 .../formatTooltipValue.tsx | 0 .../formatYAxisValue.spec.tsx | 0 .../formatYAxisValue.tsx | 0 .../shiftTimeserieToNow.tsx | 2 +- .../lineChartWidget.stories.tsx | 2 +- .../lineChartWidgetVisualization.tsx | 4 +- 13 files changed, 1030 insertions(+), 4 deletions(-) create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json rename static/app/views/dashboards/widgets/{lineChartWidget => common}/formatTooltipValue.spec.tsx (100%) rename static/app/views/dashboards/widgets/{lineChartWidget => common}/formatTooltipValue.tsx (100%) rename static/app/views/dashboards/widgets/{lineChartWidget => common}/formatYAxisValue.spec.tsx (100%) rename static/app/views/dashboards/widgets/{lineChartWidget => common}/formatYAxisValue.tsx (100%) rename static/app/views/dashboards/widgets/{lineChartWidget => common}/shiftTimeserieToNow.tsx (91%) diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx new file mode 100644 index 00000000000000..b17340231ad56a --- /dev/null +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx @@ -0,0 +1,88 @@ +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {AreaChartWidget} from './areaChartWidget'; +import sampleLatencyTimeSeries from './sampleLatencyTimeSeries.json'; +import sampleSpanDurationTimeSeries from './sampleSpanDurationTimeSeries.json'; + +describe('AreaChartWidget', () => { + describe('Layout', () => { + it('Renders', () => { + render( + + ); + }); + }); + + describe('Visualization', () => { + it('Explains missing data', () => { + render(); + + expect(screen.getByText('No Data')).toBeInTheDocument(); + }); + }); + + describe('State', () => { + it('Shows a loading placeholder', () => { + render(); + + expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); + }); + + it('Loading state takes precedence over error state', () => { + render( + + ); + + expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); + }); + + it('Shows an error message', () => { + render(); + + expect(screen.getByText('Error: Uh oh')).toBeInTheDocument(); + }); + + it('Shows a retry button', async () => { + const onRetry = jest.fn(); + + render(); + + await userEvent.click(screen.getByRole('button', {name: 'Retry'})); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it('Hides other actions if there is an error and a retry handler', () => { + const onRetry = jest.fn(); + + render( + + ); + + expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument(); + expect( + screen.queryByRole('link', {name: 'Open in Discover'}) + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx new file mode 100644 index 00000000000000..744d6f7d0e2793 --- /dev/null +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx @@ -0,0 +1,234 @@ +import {Fragment} from 'react'; +import {useTheme} from '@emotion/react'; +import styled from '@emotion/styled'; +import moment from 'moment-timezone'; + +import JSXNode from 'sentry/components/stories/jsxNode'; +import SideBySide from 'sentry/components/stories/sideBySide'; +import SizingWindow from 'sentry/components/stories/sizingWindow'; +import storyBook from 'sentry/stories/storyBook'; +import type {DateString} from 'sentry/types/core'; +import usePageFilters from 'sentry/utils/usePageFilters'; + +import type {Release, TimeseriesData} from '../common/types'; + +import {AreaChartWidget} from './areaChartWidget'; +import sampleLatencyTimeSeries from './sampleLatencyTimeSeries.json'; +import sampleSpanDurationTimeSeries from './sampleSpanDurationTimeSeries.json'; + +export default storyBook(AreaChartWidget, story => { + story('Getting Started', () => { + return ( + +

+ is a Dashboard Widget Component. It displays + a timeseries chart with multiple timeseries, and the timeseries are stacked. + Each timeseries is shown using a solid block of color. This chart is used to + visualize multiple timeseries that represent parts of something. For example, a + chart that shows time spent in the app broken down by component. In all other + ways, it behaves like , though it doesn't + support features like "Previous Period Data". +

+

+ NOTE: This chart is not appropriate for showing a single timeseries! + You should use instead. +

+
+ ); + }); + + story('Visualization', () => { + const {selection} = usePageFilters(); + const {datetime} = selection; + const {start, end} = datetime; + + const latencyTimeSeries = toTimeSeriesSelection( + sampleLatencyTimeSeries as unknown as TimeseriesData, + start, + end + ); + + const spanDurationTimeSeries = toTimeSeriesSelection( + sampleSpanDurationTimeSeries as unknown as TimeseriesData, + start, + end + ); + + return ( + +

+ The visualization of a stacked area chart. It + has some bells and whistles including automatic axes labels, and a hover + tooltip. Like other widgets, it automatically fills the parent element. +

+ + + +
+ ); + }); + + story('State', () => { + return ( + +

+ supports the usual loading and error states. + The loading state shows a spinner. The error state shows a message, and an + optional "Retry" button. +

+ + + + + + + + + + + + + {}} + /> + + +
+ ); + }); + + story('Colors', () => { + const theme = useTheme(); + + return ( + +

+ You can control the color of each timeseries by setting the color{' '} + attribute to a string that contains a valid hex color code. +

+ + + + +
+ ); + }); + + story('Releases', () => { + const releases = [ + { + version: 'ui@0.1.2', + timestamp: sampleLatencyTimeSeries.data.at(2)?.timestamp, + }, + { + version: 'ui@0.1.3', + timestamp: sampleLatencyTimeSeries.data.at(20)?.timestamp, + }, + ].filter(hasTimestamp); + + return ( + +

+ supports the releases prop. If + passed in, the widget will plot every release as a vertical line that overlays + the chart data. Clicking on a release line will open the release details page. +

+ + + + +
+ ); + }); +}); + +const MediumWidget = styled('div')` + width: 420px; + height: 250px; +`; + +const SmallWidget = styled('div')` + width: 360px; + height: 160px; +`; + +const SmallSizingWindow = styled(SizingWindow)` + width: 50%; + height: 300px; +`; + +function toTimeSeriesSelection( + timeSeries: TimeseriesData, + start: DateString | null, + end: DateString | null +): TimeseriesData { + return { + ...timeSeries, + data: timeSeries.data.filter(datum => { + if (start && moment(datum.timestamp).isBefore(moment.utc(start))) { + return false; + } + + if (end && moment(datum.timestamp).isAfter(moment.utc(end))) { + return false; + } + + return true; + }), + }; +} + +function hasTimestamp(release: Partial): release is Release { + return Boolean(release?.timestamp); +} diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx new file mode 100644 index 00000000000000..71d3b76c4f4826 --- /dev/null +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx @@ -0,0 +1,78 @@ +import styled from '@emotion/styled'; + +import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import {defined} from 'sentry/utils'; +import { + AreaChartWidgetVisualization, + type AreaChartWidgetVisualizationProps, +} from 'sentry/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization'; +import { + WidgetFrame, + type WidgetFrameProps, +} from 'sentry/views/dashboards/widgets/common/widgetFrame'; + +import {MISSING_DATA_MESSAGE, X_GUTTER, Y_GUTTER} from '../common/settings'; +import type {StateProps} from '../common/types'; + +export interface AreaChartWidgetProps + extends StateProps, + Omit, + Partial {} + +export function AreaChartWidget(props: AreaChartWidgetProps) { + const {timeseries} = props; + + if (props.isLoading) { + return ( + + + + + + + ); + } + + let parsingError: string | undefined = undefined; + + if (!defined(timeseries)) { + parsingError = MISSING_DATA_MESSAGE; + } + + const error = props.error ?? parsingError; + + return ( + + {defined(timeseries) && ( + + + + )} + + ); +} + +const AreaChartWrapper = styled('div')` + flex-grow: 1; + padding: 0 ${X_GUTTER} ${Y_GUTTER} ${X_GUTTER}; +`; + +const LoadingPlaceholder = styled('div')` + position: absolute; + inset: 0; + + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx new file mode 100644 index 00000000000000..8c33bc93879f6f --- /dev/null +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx @@ -0,0 +1,216 @@ +import {useRef} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useTheme} from '@emotion/react'; +import type { + TooltipFormatterCallback, + TopLevelFormatterParams, +} from 'echarts/types/dist/shared'; +import type EChartsReactCore from 'echarts-for-react/lib/core'; + +import BaseChart from 'sentry/components/charts/baseChart'; +import {getFormatter} from 'sentry/components/charts/components/tooltip'; +import AreaSeries from 'sentry/components/charts/series/areaSeries'; +import LineSeries from 'sentry/components/charts/series/lineSeries'; +import {useChartZoom} from 'sentry/components/charts/useChartZoom'; +import {isChartHovered} from 'sentry/components/charts/utils'; +import type {Series} from 'sentry/types/echarts'; +import {defined} from 'sentry/utils'; +import normalizeUrl from 'sentry/utils/url/normalizeUrl'; +import useOrganization from 'sentry/utils/useOrganization'; +import usePageFilters from 'sentry/utils/usePageFilters'; + +import {useWidgetSyncContext} from '../../contexts/widgetSyncContext'; +import {formatTooltipValue} from '../common/formatTooltipValue'; +import {formatYAxisValue} from '../common/formatYAxisValue'; +import {ReleaseSeries} from '../common/releaseSeries'; +import type {Meta, Release, TimeseriesData} from '../common/types'; + +export interface AreaChartWidgetVisualizationProps { + timeseries: TimeseriesData[]; + meta?: Meta; + releases?: Release[]; +} + +export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualizationProps) { + const chartRef = useRef(null); + const {register: registerWithWidgetSyncContext} = useWidgetSyncContext(); + + const pageFilters = usePageFilters(); + const {start, end, period, utc} = pageFilters.selection.datetime; + const {meta} = props; + + const theme = useTheme(); + const organization = useOrganization(); + const navigate = useNavigate(); + + let releaseSeries: Series | undefined = undefined; + if (props.releases) { + const onClick = (release: Release) => { + navigate( + normalizeUrl({ + pathname: `/organizations/${ + organization.slug + }/releases/${encodeURIComponent(release.version)}/`, + }) + ); + }; + + releaseSeries = ReleaseSeries(theme, props.releases, onClick, utc ?? false); + } + + const chartZoomProps = useChartZoom({ + saveOnZoom: true, + }); + + // TODO: There's a TypeScript indexing error here. This _could_ in theory be + // `undefined`. We need to guard against this in the parent component, and + // show an error. + const firstSeries = props.timeseries[0]; + + // TODO: Raise error if attempting to plot series of different types or units + const firstSeriesField = firstSeries?.field; + const type = meta?.fields?.[firstSeriesField] ?? 'number'; + const unit = meta?.units?.[firstSeriesField] ?? undefined; + + const formatter: TooltipFormatterCallback = ( + params, + asyncTicket + ) => { + // Only show the tooltip of the current chart. Otherwise, all tooltips + // in the chart group appear. + if (!isChartHovered(chartRef?.current)) { + return ''; + } + + let deDupedParams = params; + + if (Array.isArray(params)) { + // We split each series into a complete and incomplete series, and they + // have the same name. The two series overlap at one point on the chart, + // to create a continuous line. This code prevents both series from + // showing up on the tooltip + const uniqueSeries = new Set(); + + deDupedParams = params.filter(param => { + // Filter null values from tooltip + if (param.value[1] === null) { + return false; + } + + if (uniqueSeries.has(param.seriesName)) { + return false; + } + + uniqueSeries.add(param.seriesName); + return true; + }); + } + + return getFormatter({ + isGroupedByDate: true, + showTimeInTooltip: true, + valueFormatter: value => { + return formatTooltipValue(value, type, unit); + }, + truncate: true, + utc: utc ?? false, + })(deDupedParams, asyncTicket); + }; + + let visibleSeriesCount = props.timeseries.length; + if (releaseSeries) { + visibleSeriesCount += 1; + } + + const showLegend = visibleSeriesCount > 1; + + return ( + { + chartRef.current = e; + + if (e?.getEchartsInstance) { + registerWithWidgetSyncContext(e.getEchartsInstance()); + } + }} + autoHeightResize + series={[ + ...props.timeseries.map(timeserie => { + return AreaSeries({ + name: timeserie.field, + color: timeserie.color, + stack: 'area', + animation: false, + areaStyle: { + color: timeserie.color, + opacity: 1.0, + }, + data: timeserie.data.map(datum => { + return [datum.timestamp, datum.value]; + }), + }); + }), + releaseSeries && + LineSeries({ + ...releaseSeries, + name: releaseSeries.seriesName, + data: [], + }), + ].filter(defined)} + grid={{ + left: 0, + top: showLegend ? 25 : 10, + right: 4, + bottom: 0, + containLabel: true, + }} + legend={ + showLegend + ? { + top: 0, + left: 0, + } + : undefined + } + tooltip={{ + trigger: 'axis', + axisPointer: { + type: 'cross', + }, + formatter, + }} + xAxis={{ + axisLabel: { + padding: [0, 10, 0, 10], + width: 60, + }, + splitNumber: 0, + }} + yAxis={{ + axisLabel: { + formatter(value: number) { + return formatYAxisValue(value, type, unit); + }, + }, + axisPointer: { + type: 'line', + snap: false, + lineStyle: { + type: 'solid', + width: 0.5, + }, + label: { + show: false, + }, + }, + }} + {...chartZoomProps} + isGroupedByDate + useMultilineDate + start={start ? new Date(start) : undefined} + end={end ? new Date(end) : undefined} + period={period} + utc={utc ?? undefined} + /> + ); +} diff --git a/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json b/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json new file mode 100644 index 00000000000000..a6fd6ffaee59e2 --- /dev/null +++ b/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json @@ -0,0 +1,205 @@ +{ + "field": "avg(latency)", + "data": [ + { + "timestamp": "2024-12-09T22:00:00Z", + "value": 15.797387473297285 + }, + { + "timestamp": "2024-12-09T22:30:00Z", + "value": 6.098785205112891 + }, + { + "timestamp": "2024-12-09T23:00:00Z", + "value": 10.023521061981072 + }, + { + "timestamp": "2024-12-09T23:30:00Z", + "value": 6.303244308149576 + }, + { + "timestamp": "2024-12-10T00:00:00Z", + "value": 11.352363343199878 + }, + { + "timestamp": "2024-12-10T00:30:00Z", + "value": 4.845476325747717 + }, + { + "timestamp": "2024-12-10T01:00:00Z", + "value": 11.251521855070287 + }, + { + "timestamp": "2024-12-10T01:30:00Z", + "value": 4.425860645618586 + }, + { + "timestamp": "2024-12-10T02:00:00Z", + "value": 9.989218687466126 + }, + { + "timestamp": "2024-12-10T02:30:00Z", + "value": 4.025772756170272 + }, + { + "timestamp": "2024-12-10T03:00:00Z", + "value": 9.17541579241641 + }, + { + "timestamp": "2024-12-10T03:30:00Z", + "value": 4.2380217489924865 + }, + { + "timestamp": "2024-12-10T04:00:00Z", + "value": 8.027566877217474 + }, + { + "timestamp": "2024-12-10T04:30:00Z", + "value": 5.295137636393576 + }, + { + "timestamp": "2024-12-10T05:00:00Z", + "value": 8.656494836655359 + }, + { + "timestamp": "2024-12-10T05:30:00Z", + "value": 5.475662654003712 + }, + { + "timestamp": "2024-12-10T06:00:00Z", + "value": 9.793632841698757 + }, + { + "timestamp": "2024-12-10T06:30:00Z", + "value": 5.591866790430932 + }, + { + "timestamp": "2024-12-10T07:00:00Z", + "value": 9.187478698931766 + }, + { + "timestamp": "2024-12-10T07:30:00Z", + "value": 3.779925204205954 + }, + { + "timestamp": "2024-12-10T08:00:00Z", + "value": 10.315176397597504 + }, + { + "timestamp": "2024-12-10T08:30:00Z", + "value": 5.101678176894567 + }, + { + "timestamp": "2024-12-10T09:00:00Z", + "value": 10.405819741300553 + }, + { + "timestamp": "2024-12-10T09:30:00Z", + "value": 5.157673142672812 + }, + { + "timestamp": "2024-12-10T10:00:00Z", + "value": 13.454678120116997 + }, + { + "timestamp": "2024-12-10T10:30:00Z", + "value": 5.633257152519796 + }, + { + "timestamp": "2024-12-10T11:00:00Z", + "value": 12.38331484233643 + }, + { + "timestamp": "2024-12-10T11:30:00Z", + "value": 6.7168414807525245 + }, + { + "timestamp": "2024-12-10T12:00:00Z", + "value": 10.82394699109634 + }, + { + "timestamp": "2024-12-10T12:30:00Z", + "value": 6.649313439563898 + }, + { + "timestamp": "2024-12-10T13:00:00Z", + "value": 10.957238674362912 + }, + { + "timestamp": "2024-12-10T13:30:00Z", + "value": 7.891612896606848 + }, + { + "timestamp": "2024-12-10T14:00:00Z", + "value": 9.83684972309017 + }, + { + "timestamp": "2024-12-10T14:30:00Z", + "value": 4.472633330313572 + }, + { + "timestamp": "2024-12-10T15:00:00Z", + "value": 13.886404361521333 + }, + { + "timestamp": "2024-12-10T15:30:00Z", + "value": 9.212753388080626 + }, + { + "timestamp": "2024-12-10T16:00:00Z", + "value": 12.213060522650725 + }, + { + "timestamp": "2024-12-10T16:30:00Z", + "value": 5.308362659314872 + }, + { + "timestamp": "2024-12-10T17:00:00Z", + "value": 12.167955277129803 + }, + { + "timestamp": "2024-12-10T17:30:00Z", + "value": 5.95843470061378 + }, + { + "timestamp": "2024-12-10T18:00:00Z", + "value": 11.623431372484815 + }, + { + "timestamp": "2024-12-10T18:30:00Z", + "value": 7.133246343117738 + }, + { + "timestamp": "2024-12-10T19:00:00Z", + "value": 9.91670999332601 + }, + { + "timestamp": "2024-12-10T19:30:00Z", + "value": 8.220551566043556 + }, + { + "timestamp": "2024-12-10T20:00:00Z", + "value": 10.644324473372116 + }, + { + "timestamp": "2024-12-10T20:30:00Z", + "value": 6.380262999155083 + }, + { + "timestamp": "2024-12-10T21:00:00Z", + "value": 11.611059631291011 + }, + { + "timestamp": "2024-12-10T21:30:00Z", + "value": 4.830427051517974 + }, + { + "timestamp": "2024-12-10T22:00:00Z", + "value": 14.018863061817425 + }, + { + "timestamp": "2024-12-10T22:30:00Z", + "value": 0 + } + ] +} diff --git a/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json b/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json new file mode 100644 index 00000000000000..815c94d3eeac81 --- /dev/null +++ b/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json @@ -0,0 +1,205 @@ +{ + "field": "avg(span.duration)", + "data": [ + { + "timestamp": "2024-12-09T22:00:00Z", + "value": 97.16045076146938 + }, + { + "timestamp": "2024-12-09T22:30:00Z", + "value": 73.81276498986948 + }, + { + "timestamp": "2024-12-09T23:00:00Z", + "value": 83.65629482432625 + }, + { + "timestamp": "2024-12-09T23:30:00Z", + "value": 77.4773209216508 + }, + { + "timestamp": "2024-12-10T00:00:00Z", + "value": 88.87920889091058 + }, + { + "timestamp": "2024-12-10T00:30:00Z", + "value": 74.81035769529551 + }, + { + "timestamp": "2024-12-10T01:00:00Z", + "value": 74.18713473976618 + }, + { + "timestamp": "2024-12-10T01:30:00Z", + "value": 75.51014337607224 + }, + { + "timestamp": "2024-12-10T02:00:00Z", + "value": 71.75338738667213 + }, + { + "timestamp": "2024-12-10T02:30:00Z", + "value": 71.44267608528102 + }, + { + "timestamp": "2024-12-10T03:00:00Z", + "value": 73.23258991739772 + }, + { + "timestamp": "2024-12-10T03:30:00Z", + "value": 73.62710321499654 + }, + { + "timestamp": "2024-12-10T04:00:00Z", + "value": 73.24679865212109 + }, + { + "timestamp": "2024-12-10T04:30:00Z", + "value": 71.00435116898733 + }, + { + "timestamp": "2024-12-10T05:00:00Z", + "value": 78.74461664047831 + }, + { + "timestamp": "2024-12-10T05:30:00Z", + "value": 72.30733243572487 + }, + { + "timestamp": "2024-12-10T06:00:00Z", + "value": 78.42600782679243 + }, + { + "timestamp": "2024-12-10T06:30:00Z", + "value": 74.29718208848486 + }, + { + "timestamp": "2024-12-10T07:00:00Z", + "value": 77.86389655145648 + }, + { + "timestamp": "2024-12-10T07:30:00Z", + "value": 74.11896494459268 + }, + { + "timestamp": "2024-12-10T08:00:00Z", + "value": 77.83106835851203 + }, + { + "timestamp": "2024-12-10T08:30:00Z", + "value": 80.87383050251273 + }, + { + "timestamp": "2024-12-10T09:00:00Z", + "value": 85.53912058349381 + }, + { + "timestamp": "2024-12-10T09:30:00Z", + "value": 82.16649885587427 + }, + { + "timestamp": "2024-12-10T10:00:00Z", + "value": 96.46710756423539 + }, + { + "timestamp": "2024-12-10T10:30:00Z", + "value": 90.2296730934056 + }, + { + "timestamp": "2024-12-10T11:00:00Z", + "value": 86.49672620288618 + }, + { + "timestamp": "2024-12-10T11:30:00Z", + "value": 81.76636615572134 + }, + { + "timestamp": "2024-12-10T12:00:00Z", + "value": 91.30479930843254 + }, + { + "timestamp": "2024-12-10T12:30:00Z", + "value": 86.5261930482867 + }, + { + "timestamp": "2024-12-10T13:00:00Z", + "value": 88.58528428072619 + }, + { + "timestamp": "2024-12-10T13:30:00Z", + "value": 84.57326040990161 + }, + { + "timestamp": "2024-12-10T14:00:00Z", + "value": 82.43636182893853 + }, + { + "timestamp": "2024-12-10T14:30:00Z", + "value": 90.33512756892627 + }, + { + "timestamp": "2024-12-10T15:00:00Z", + "value": 103.27654137156406 + }, + { + "timestamp": "2024-12-10T15:30:00Z", + "value": 127.10951181976695 + }, + { + "timestamp": "2024-12-10T16:00:00Z", + "value": 95.72899826334971 + }, + { + "timestamp": "2024-12-10T16:30:00Z", + "value": 84.04312923323451 + }, + { + "timestamp": "2024-12-10T17:00:00Z", + "value": 85.59506151608197 + }, + { + "timestamp": "2024-12-10T17:30:00Z", + "value": 82.71849476828766 + }, + { + "timestamp": "2024-12-10T18:00:00Z", + "value": 89.57229292359843 + }, + { + "timestamp": "2024-12-10T18:30:00Z", + "value": 81.22558140363343 + }, + { + "timestamp": "2024-12-10T19:00:00Z", + "value": 86.8826705009411 + }, + { + "timestamp": "2024-12-10T19:30:00Z", + "value": 79.4383370343256 + }, + { + "timestamp": "2024-12-10T20:00:00Z", + "value": 82.63652211651058 + }, + { + "timestamp": "2024-12-10T20:30:00Z", + "value": 77.87329880924307 + }, + { + "timestamp": "2024-12-10T21:00:00Z", + "value": 82.0189272882679 + }, + { + "timestamp": "2024-12-10T21:30:00Z", + "value": 80.03621684743777 + }, + { + "timestamp": "2024-12-10T22:00:00Z", + "value": 80.69735445090794 + }, + { + "timestamp": "2024-12-10T22:30:00Z", + "value": 0 + } + ] +} diff --git a/static/app/views/dashboards/widgets/lineChartWidget/formatTooltipValue.spec.tsx b/static/app/views/dashboards/widgets/common/formatTooltipValue.spec.tsx similarity index 100% rename from static/app/views/dashboards/widgets/lineChartWidget/formatTooltipValue.spec.tsx rename to static/app/views/dashboards/widgets/common/formatTooltipValue.spec.tsx diff --git a/static/app/views/dashboards/widgets/lineChartWidget/formatTooltipValue.tsx b/static/app/views/dashboards/widgets/common/formatTooltipValue.tsx similarity index 100% rename from static/app/views/dashboards/widgets/lineChartWidget/formatTooltipValue.tsx rename to static/app/views/dashboards/widgets/common/formatTooltipValue.tsx diff --git a/static/app/views/dashboards/widgets/lineChartWidget/formatYAxisValue.spec.tsx b/static/app/views/dashboards/widgets/common/formatYAxisValue.spec.tsx similarity index 100% rename from static/app/views/dashboards/widgets/lineChartWidget/formatYAxisValue.spec.tsx rename to static/app/views/dashboards/widgets/common/formatYAxisValue.spec.tsx diff --git a/static/app/views/dashboards/widgets/lineChartWidget/formatYAxisValue.tsx b/static/app/views/dashboards/widgets/common/formatYAxisValue.tsx similarity index 100% rename from static/app/views/dashboards/widgets/lineChartWidget/formatYAxisValue.tsx rename to static/app/views/dashboards/widgets/common/formatYAxisValue.tsx diff --git a/static/app/views/dashboards/widgets/lineChartWidget/shiftTimeserieToNow.tsx b/static/app/views/dashboards/widgets/common/shiftTimeserieToNow.tsx similarity index 91% rename from static/app/views/dashboards/widgets/lineChartWidget/shiftTimeserieToNow.tsx rename to static/app/views/dashboards/widgets/common/shiftTimeserieToNow.tsx index 2c484d1e2cbce1..abb7552e88a93f 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/shiftTimeserieToNow.tsx +++ b/static/app/views/dashboards/widgets/common/shiftTimeserieToNow.tsx @@ -1,4 +1,4 @@ -import type {TimeseriesData} from '../common/types'; +import type {TimeseriesData} from './types'; export function shiftTimeserieToNow(timeserie: TimeseriesData): TimeseriesData { const currentTimestamp = new Date().getTime(); diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx index d3cbb4653a4454..e69d13cb5e4654 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx @@ -10,12 +10,12 @@ import storyBook from 'sentry/stories/storyBook'; import type {DateString} from 'sentry/types/core'; import usePageFilters from 'sentry/utils/usePageFilters'; +import {shiftTimeserieToNow} from '../common/shiftTimeserieToNow'; import type {Release, TimeseriesData} from '../common/types'; import {LineChartWidget} from './lineChartWidget'; import sampleDurationTimeSeries from './sampleDurationTimeSeries.json'; import sampleThroughputTimeSeries from './sampleThroughputTimeSeries.json'; -import {shiftTimeserieToNow} from './shiftTimeserieToNow'; const sampleDurationTimeSeries2 = { ...sampleDurationTimeSeries, diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx index fa2d6ada8df637..8a870d4554cdd4 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx @@ -19,11 +19,11 @@ import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {useWidgetSyncContext} from '../../contexts/widgetSyncContext'; +import {formatTooltipValue} from '../common/formatTooltipValue'; +import {formatYAxisValue} from '../common/formatYAxisValue'; import {ReleaseSeries} from '../common/releaseSeries'; import type {Meta, Release, TimeseriesData} from '../common/types'; -import {formatTooltipValue} from './formatTooltipValue'; -import {formatYAxisValue} from './formatYAxisValue'; import {splitSeriesIntoCompleteAndIncomplete} from './splitSeriesIntoCompleteAndIncomplete'; export interface LineChartWidgetVisualizationProps { From 87f51d3b51558a1f79eddeced5560badbb6e043e Mon Sep 17 00:00:00 2001 From: Lyn Nagara <1779792+lynnagara@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:52:54 -0800 Subject: [PATCH 414/757] support routing stale messages to lowpri topic (#82322) the sentry consumer configuration now supports an optional `stale_topic`. if passed, all invalid message rejected with reason "stale" get routed to the stale topic. raising stale message exceptions is configured for the transactions consumer --- src/sentry/conf/types/kafka_definition.py | 2 + src/sentry/consumers/__init__.py | 42 +++--- src/sentry/consumers/dlq.py | 154 ++++++++++++++++++++++ src/sentry/runner/commands/run.py | 6 + tests/sentry/consumers/__init__.py | 0 tests/sentry/consumers/test_dlq.py | 73 ++++++++++ 6 files changed, 258 insertions(+), 19 deletions(-) create mode 100644 src/sentry/consumers/dlq.py create mode 100644 tests/sentry/consumers/__init__.py create mode 100644 tests/sentry/consumers/test_dlq.py diff --git a/src/sentry/conf/types/kafka_definition.py b/src/sentry/conf/types/kafka_definition.py index 59ae1228343494..f0013025c3de45 100644 --- a/src/sentry/conf/types/kafka_definition.py +++ b/src/sentry/conf/types/kafka_definition.py @@ -86,6 +86,8 @@ class ConsumerDefinition(TypedDict, total=False): dlq_max_invalid_ratio: float | None dlq_max_consecutive_count: int | None + stale_topic: Topic + def validate_consumer_definition(consumer_definition: ConsumerDefinition) -> None: if "dlq_topic" not in consumer_definition and ( diff --git a/src/sentry/consumers/__init__.py b/src/sentry/consumers/__init__.py index 32ff8744842363..6076fc5c35e12a 100644 --- a/src/sentry/consumers/__init__.py +++ b/src/sentry/consumers/__init__.py @@ -6,11 +6,10 @@ import click from arroyo.backends.abstract import Consumer -from arroyo.backends.kafka import KafkaProducer from arroyo.backends.kafka.configuration import build_kafka_consumer_configuration from arroyo.backends.kafka.consumer import KafkaConsumer from arroyo.commit import ONCE_PER_SECOND -from arroyo.dlq import DlqLimit, DlqPolicy, KafkaDlqProducer +from arroyo.dlq import DlqLimit, DlqPolicy from arroyo.processing.processor import StreamProcessor from arroyo.processing.strategies import Healthcheck from arroyo.processing.strategies.abstract import ProcessingStrategy, ProcessingStrategyFactory @@ -22,11 +21,12 @@ Topic, validate_consumer_definition, ) +from sentry.consumers.dlq import DlqStaleMessagesStrategyFactoryWrapper, maybe_build_dlq_producer from sentry.consumers.validate_schema import ValidateSchema from sentry.eventstream.types import EventStreamEventType from sentry.ingest.types import ConsumerType from sentry.utils.imports import import_string -from sentry.utils.kafka_config import get_kafka_producer_cluster_options, get_topic_definition +from sentry.utils.kafka_config import get_topic_definition logger = logging.getLogger(__name__) @@ -371,6 +371,7 @@ def ingest_transactions_options() -> list[click.Option]: "strategy_factory": "sentry.ingest.consumer.factory.IngestTransactionsStrategyFactory", "click_options": ingest_transactions_options(), "dlq_topic": Topic.INGEST_TRANSACTIONS_DLQ, + "stale_topic": Topic.INGEST_TRANSACTIONS_DLQ, }, "ingest-metrics": { "topic": Topic.INGEST_METRICS, @@ -469,6 +470,8 @@ def get_stream_processor( synchronize_commit_group: str | None = None, healthcheck_file_path: str | None = None, enable_dlq: bool = True, + # If set, messages above this age will be rerouted to the stale topic if one is configured + stale_threshold_sec: int | None = None, enforce_schema: bool = False, group_instance_id: str | None = None, ) -> StreamProcessor: @@ -578,37 +581,38 @@ def build_consumer_config(group_id: str): consumer_topic.value, enforce_schema, strategy_factory ) + if stale_threshold_sec: + strategy_factory = DlqStaleMessagesStrategyFactoryWrapper( + stale_threshold_sec, strategy_factory + ) + if healthcheck_file_path is not None: strategy_factory = HealthcheckStrategyFactoryWrapper( healthcheck_file_path, strategy_factory ) if enable_dlq and consumer_definition.get("dlq_topic"): - try: - dlq_topic = consumer_definition["dlq_topic"] - except KeyError as e: - raise click.BadParameter( - f"Cannot enable DLQ for consumer: {consumer_name}, no DLQ topic has been defined for it" - ) from e - try: - dlq_topic_defn = get_topic_definition(dlq_topic) - cluster_setting = dlq_topic_defn["cluster"] - except ValueError as e: - raise click.BadParameter( - f"Cannot enable DLQ for consumer: {consumer_name}, DLQ topic {dlq_topic} is not configured in this environment" - ) from e + dlq_topic = consumer_definition["dlq_topic"] + else: + dlq_topic = None + + if stale_threshold_sec and consumer_definition.get("stale_topic"): + stale_topic = consumer_definition["stale_topic"] + else: + stale_topic = None - producer_config = get_kafka_producer_cluster_options(cluster_setting) - dlq_producer = KafkaProducer(producer_config) + dlq_producer = maybe_build_dlq_producer(dlq_topic=dlq_topic, stale_topic=stale_topic) + if dlq_producer: dlq_policy = DlqPolicy( - KafkaDlqProducer(dlq_producer, ArroyoTopic(dlq_topic_defn["real_topic_name"])), + dlq_producer, DlqLimit( max_invalid_ratio=consumer_definition.get("dlq_max_invalid_ratio"), max_consecutive_count=consumer_definition.get("dlq_max_consecutive_count"), ), None, ) + else: dlq_policy = None diff --git a/src/sentry/consumers/dlq.py b/src/sentry/consumers/dlq.py new file mode 100644 index 00000000000000..b445817b175704 --- /dev/null +++ b/src/sentry/consumers/dlq.py @@ -0,0 +1,154 @@ +import logging +import time +from collections.abc import Mapping, MutableMapping +from concurrent.futures import Future +from datetime import datetime, timedelta, timezone +from enum import Enum + +from arroyo.backends.kafka import KafkaPayload, KafkaProducer +from arroyo.dlq import InvalidMessage, KafkaDlqProducer +from arroyo.processing.strategies.abstract import ProcessingStrategy, ProcessingStrategyFactory +from arroyo.types import FILTERED_PAYLOAD, BrokerValue, Commit, FilteredPayload, Message, Partition +from arroyo.types import Topic as ArroyoTopic +from arroyo.types import Value + +from sentry.conf.types.kafka_definition import Topic +from sentry.utils.kafka_config import get_kafka_producer_cluster_options, get_topic_definition + +logger = logging.getLogger(__name__) + + +class RejectReason(Enum): + STALE = "stale" + INVALID = "invalid" + + +class MultipleDestinationDlqProducer(KafkaDlqProducer): + """ + Produces to either the DLQ or stale message topic depending on the reason. + """ + + def __init__( + self, + producers: Mapping[RejectReason, KafkaDlqProducer | None], + ) -> None: + self.producers = producers + + def produce( + self, + value: BrokerValue[KafkaPayload], + reason: str | None = None, + ) -> Future[BrokerValue[KafkaPayload]]: + + reject_reason = RejectReason(reason) if reason else RejectReason.INVALID + producer = self.producers.get(reject_reason) + + if producer: + return producer.produce(value) + else: + # No DLQ producer configured for the reason. + logger.error("No DLQ producer configured for reason %s", reason) + future: Future[BrokerValue[KafkaPayload]] = Future() + future.set_running_or_notify_cancel() + future.set_result(value) + return future + + +def _get_dlq_producer(topic: Topic | None) -> KafkaDlqProducer | None: + if topic is None: + return None + + topic_defn = get_topic_definition(topic) + config = get_kafka_producer_cluster_options(topic_defn["cluster"]) + real_topic = topic_defn["real_topic_name"] + return KafkaDlqProducer(KafkaProducer(config), ArroyoTopic(real_topic)) + + +def maybe_build_dlq_producer( + dlq_topic: Topic | None, + stale_topic: Topic | None, +) -> MultipleDestinationDlqProducer | None: + if dlq_topic is None and stale_topic is None: + return None + + producers = { + RejectReason.INVALID: _get_dlq_producer(dlq_topic), + RejectReason.STALE: _get_dlq_producer(stale_topic), + } + + return MultipleDestinationDlqProducer(producers) + + +class DlqStaleMessages(ProcessingStrategy[KafkaPayload]): + def __init__( + self, + stale_threshold_sec: int, + next_step: ProcessingStrategy[KafkaPayload | FilteredPayload], + ) -> None: + self.stale_threshold_sec = stale_threshold_sec + self.next_step = next_step + + # A filtered message is created so we commit periodically if all are stale. + self.last_forwarded_offsets = time.time() + self.offsets_to_forward: MutableMapping[Partition, int] = {} + + def submit(self, message: Message[KafkaPayload]) -> None: + min_accepted_timestamp = datetime.now(timezone.utc) - timedelta( + seconds=self.stale_threshold_sec + ) + + if isinstance(message.value, BrokerValue): + if message.value.timestamp < min_accepted_timestamp: + self.offsets_to_forward[message.value.partition] = message.value.next_offset + raise InvalidMessage( + message.value.partition, message.value.offset, reason=RejectReason.STALE.value + ) + + # If we get a valid message for a partition later, don't emit a filtered message for it + if self.offsets_to_forward: + for partition in message.committable: + self.offsets_to_forward.pop(partition) + + self.next_step.submit(message) + + def poll(self) -> None: + self.next_step.poll() + + # Ensure we commit frequently even if all messages are invalid + if self.offsets_to_forward: + if time.time() > self.last_forwarded_offsets + 1: + filtered_message = Message(Value(FILTERED_PAYLOAD, self.offsets_to_forward)) + self.next_step.submit(filtered_message) + self.offsets_to_forward = {} + self.last_forwarded_offsets = time.time() + + def join(self, timeout: float | None = None) -> None: + self.next_step.join(timeout) + + def close(self) -> None: + self.next_step.close() + + def terminate(self) -> None: + self.next_step.terminate() + + +class DlqStaleMessagesStrategyFactoryWrapper(ProcessingStrategyFactory[KafkaPayload]): + """ + Wrapper used to dlq a message with a stale timestamp before it is passed to + the rest of the pipeline. The InvalidMessage is raised with a + "stale" reason so it can be routed to a separate stale topic. + """ + + def __init__( + self, + stale_threshold_sec: int, + inner: ProcessingStrategyFactory[KafkaPayload | FilteredPayload], + ) -> None: + self.stale_threshold_sec = stale_threshold_sec + self.inner = inner + + def create_with_partitions( + self, commit: Commit, partitions: Mapping[Partition, int] + ) -> ProcessingStrategy[KafkaPayload]: + rv = self.inner.create_with_partitions(commit, partitions) + return DlqStaleMessages(self.stale_threshold_sec, rv) diff --git a/src/sentry/runner/commands/run.py b/src/sentry/runner/commands/run.py index cabd65661eca99..75679d41fc50d2 100644 --- a/src/sentry/runner/commands/run.py +++ b/src/sentry/runner/commands/run.py @@ -416,6 +416,11 @@ def cron(**options: Any) -> None: is_flag=True, default=True, ) +@click.option( + "--stale-threshold-sec", + type=click.IntRange(min=300), + help="Routes stale messages to stale topic if provided. This feature is currently being tested, do not pass in production yet.", +) @click.option( "--log-level", type=click.Choice(["debug", "info", "warning", "error", "critical"], case_sensitive=False), @@ -500,6 +505,7 @@ def dev_consumer(consumer_names: tuple[str, ...]) -> None: synchronize_commit_group=None, synchronize_commit_log_topic=None, enable_dlq=False, + stale_threshold_sec=None, healthcheck_file_path=None, enforce_schema=True, ) diff --git a/tests/sentry/consumers/__init__.py b/tests/sentry/consumers/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/sentry/consumers/test_dlq.py b/tests/sentry/consumers/test_dlq.py new file mode 100644 index 00000000000000..c8b274f744263e --- /dev/null +++ b/tests/sentry/consumers/test_dlq.py @@ -0,0 +1,73 @@ +import time +from datetime import datetime, timedelta, timezone +from unittest.mock import Mock + +import msgpack +import pytest +from arroyo.backends.kafka import KafkaPayload +from arroyo.dlq import InvalidMessage +from arroyo.types import BrokerValue, Message, Partition, Topic + +from sentry.consumers.dlq import DlqStaleMessagesStrategyFactoryWrapper +from sentry.testutils.pytest.fixtures import django_db_all + + +def make_message( + payload: bytes, partition: Partition, offset: int, timestamp: datetime | None = None +) -> Message: + return Message( + BrokerValue( + KafkaPayload(None, payload, []), + partition, + offset, + timestamp if timestamp else datetime.now(), + ) + ) + + +@pytest.mark.parametrize("stale_threshold_sec", [300]) +@django_db_all +def test_dlq_stale_messages(factories, stale_threshold_sec) -> None: + # Tests messages that have gotten stale (default longer than 5 minutes) + + organization = factories.create_organization() + project = factories.create_project(organization=organization) + + empty_event_payload = msgpack.packb( + { + "type": "event", + "project_id": project.id, + "payload": b"{}", + "start_time": int(time.time()), + "event_id": "aaa", + } + ) + + partition = Partition(Topic("topic"), 0) + offset = 10 + inner_factory_mock = Mock() + inner_strategy_mock = Mock() + inner_factory_mock.create_with_partitions = Mock(return_value=inner_strategy_mock) + factory = DlqStaleMessagesStrategyFactoryWrapper( + stale_threshold_sec=stale_threshold_sec, + inner=inner_factory_mock, + ) + strategy = factory.create_with_partitions(Mock(), Mock()) + + for time_diff in range(10, 0, -1): + message = make_message( + empty_event_payload, + partition, + offset - time_diff, + timestamp=datetime.now(timezone.utc) - timedelta(minutes=time_diff), + ) + if time_diff < 5: + strategy.submit(message) + else: + with pytest.raises(InvalidMessage) as exc_info: + strategy.submit(message) + + assert exc_info.value.partition == partition + assert exc_info.value.offset == offset - time_diff + + assert inner_strategy_mock.submit.call_count == 4 From ca528a6ad4f06dc25777341a76ee858ffb4ef03a Mon Sep 17 00:00:00 2001 From: Abdullah Khan <60121741+Abdkhan14@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:08:26 -0500 Subject: [PATCH 415/757] feat(new-trace): Fixing scroll on trace drawer (#82475) Co-authored-by: Abdullah Khan --- .../performance/newTraceDetails/traceDrawer/details/styles.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx index 7a309bc382a7d1..5761397441829b 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx @@ -74,7 +74,7 @@ const BodyContainer = styled('div')<{hasNewTraceUi?: boolean}>` flex-direction: column; gap: ${p => (p.hasNewTraceUi ? 0 : space(2))}; padding: ${p => (p.hasNewTraceUi ? `${space(0.5)} ${space(2)}` : space(1))}; - height: 100%; + height: calc(100% - 52px); overflow: auto; ${DataSection} { From e8371a7e81a05fdae24eec4d5ed042a536697cbf Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Fri, 20 Dec 2024 19:12:28 +0100 Subject: [PATCH 416/757] fix(flags): separate permission class (#82463) --- src/sentry/api/bases/organization.py | 8 ++++++++ src/sentry/flags/endpoints/secrets.py | 9 ++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/sentry/api/bases/organization.py b/src/sentry/api/bases/organization.py index 1866b60c6316f1..4e5d2e632d0abc 100644 --- a/src/sentry/api/bases/organization.py +++ b/src/sentry/api/bases/organization.py @@ -230,6 +230,14 @@ class OrganizationMetricsPermission(OrganizationPermission): } +class OrganizationFlagWebHookSigningSecretPermission(OrganizationPermission): + scope_map = { + "GET": ["org:read", "org:write", "org:admin"], + "POST": ["org:read", "org:write", "org:admin"], + "DELETE": ["org:write", "org:admin"], + } + + class ControlSiloOrganizationEndpoint(Endpoint): """ A base class for endpoints that use an organization scoping but lives in the control silo diff --git a/src/sentry/flags/endpoints/secrets.py b/src/sentry/flags/endpoints/secrets.py index 5e6ab64340a712..41e2626864e827 100644 --- a/src/sentry/flags/endpoints/secrets.py +++ b/src/sentry/flags/endpoints/secrets.py @@ -11,7 +11,10 @@ from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint -from sentry.api.bases.organization import OrganizationEndpoint, OrgAuthTokenPermission +from sentry.api.bases.organization import ( + OrganizationEndpoint, + OrganizationFlagWebHookSigningSecretPermission, +) from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import Serializer, register, serialize from sentry.flags.models import FlagWebHookSigningSecretModel @@ -46,7 +49,7 @@ class FlagWebhookSigningSecretValidator(serializers.Serializer): @region_silo_endpoint class OrganizationFlagsWebHookSigningSecretsEndpoint(OrganizationEndpoint): owner = ApiOwner.REPLAY - permission_classes = (OrgAuthTokenPermission,) + permission_classes = (OrganizationFlagWebHookSigningSecretPermission,) publish_status = { "GET": ApiPublishStatus.PRIVATE, "POST": ApiPublishStatus.PRIVATE, @@ -95,7 +98,7 @@ def post(self, request: Request, organization: Organization) -> Response: @region_silo_endpoint class OrganizationFlagsWebHookSigningSecretEndpoint(OrganizationEndpoint): owner = ApiOwner.REPLAY - permission_classes = (OrgAuthTokenPermission,) + permission_classes = (OrganizationFlagWebHookSigningSecretPermission,) publish_status = {"DELETE": ApiPublishStatus.PRIVATE} def delete( From 0203fa4d5e33fa3ae2c57dcbcc8cd96a56240e6a Mon Sep 17 00:00:00 2001 From: Michelle Fu <83109586+mifu67@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:41:25 -0800 Subject: [PATCH 417/757] ref(workflow_engine): remove remaining references to condition in validators (#82438) Refactor the validator checks for the `condition` field, which was removed, so that they now validate the `type` field. --- src/sentry/incidents/endpoints/validators.py | 1 - .../workflow_engine/endpoints/validators.py | 22 +++----- .../endpoints/test_project_detector_index.py | 2 +- .../endpoints/test_validators.py | 50 ++++++++++++------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/sentry/incidents/endpoints/validators.py b/src/sentry/incidents/endpoints/validators.py index 382484a21e361c..de938853f8b568 100644 --- a/src/sentry/incidents/endpoints/validators.py +++ b/src/sentry/incidents/endpoints/validators.py @@ -89,7 +89,6 @@ class MetricAlertComparisonConditionValidator(NumericComparisonConditionValidato supported_conditions = frozenset((Condition.GREATER, Condition.LESS)) supported_results = frozenset((DetectorPriorityLevel.HIGH, DetectorPriorityLevel.MEDIUM)) - type = "metric_alert" class MetricAlertsDetectorValidator(BaseGroupTypeDetectorValidator): diff --git a/src/sentry/workflow_engine/endpoints/validators.py b/src/sentry/workflow_engine/endpoints/validators.py index 718e9d173c6860..4a1829a7f971c6 100644 --- a/src/sentry/workflow_engine/endpoints/validators.py +++ b/src/sentry/workflow_engine/endpoints/validators.py @@ -37,7 +37,7 @@ def create(self) -> T: class BaseDataConditionValidator(CamelSnakeSerializer): - condition = serializers.CharField( + type = serializers.CharField( required=True, max_length=200, help_text="Condition used to compare data value to the stored comparison value", @@ -51,14 +51,8 @@ def comparison(self) -> Field: def result(self) -> Field: raise NotImplementedError - @property - def type(self) -> str: - # TODO: This should probably at least be an enum - raise NotImplementedError - def validate(self, attrs): attrs = super().validate(attrs) - attrs["type"] = self.type return attrs @@ -84,15 +78,15 @@ def supported_conditions(self) -> frozenset[Condition]: def supported_results(self) -> frozenset[DetectorPriorityLevel]: raise NotImplementedError - def validate_condition(self, value: str) -> Condition: + def validate_type(self, value: str) -> Condition: try: - condition = Condition(value) + type = Condition(value) except ValueError: - condition = None + type = None - if condition not in self.supported_conditions: - raise serializers.ValidationError(f"Unsupported condition {value}") - return condition + if type not in self.supported_conditions: + raise serializers.ValidationError(f"Unsupported type {value}") + return type def validate_result(self, value: str) -> DetectorPriorityLevel: try: @@ -162,7 +156,7 @@ def create(self, validated_data): DataCondition.objects.create( comparison=condition["comparison"], condition_result=condition["result"], - type=condition["condition"], + type=condition["type"], condition_group=condition_group, ) detector = Detector.objects.create( diff --git a/tests/sentry/workflow_engine/endpoints/test_project_detector_index.py b/tests/sentry/workflow_engine/endpoints/test_project_detector_index.py index 87e1ad00cb1862..a76306169d5244 100644 --- a/tests/sentry/workflow_engine/endpoints/test_project_detector_index.py +++ b/tests/sentry/workflow_engine/endpoints/test_project_detector_index.py @@ -70,7 +70,7 @@ def setUp(self): }, "data_conditions": [ { - "condition": "gt", + "type": Condition.GREATER, "comparison": 100, "result": DetectorPriorityLevel.HIGH, } diff --git a/tests/sentry/workflow_engine/endpoints/test_validators.py b/tests/sentry/workflow_engine/endpoints/test_validators.py index 0442394eb9733f..5e8ac74cce059c 100644 --- a/tests/sentry/workflow_engine/endpoints/test_validators.py +++ b/tests/sentry/workflow_engine/endpoints/test_validators.py @@ -54,15 +54,15 @@ def supported_results(self): def test_validate_condition_valid(self): validator = self.validator_class() - assert validator.validate_condition("gt") == Condition.GREATER + assert validator.validate_type("gt") == Condition.GREATER def test_validate_condition_invalid(self): validator = self.validator_class() with pytest.raises( ValidationError, - match="[ErrorDetail(string='Unsupported condition invalid_condition', code='invalid')]", + match="[ErrorDetail(string='Unsupported type invalid_condition', code='invalid')]", ): - validator.validate_condition("invalid_condition") + validator.validate_type("invalid_condition") def test_validate_result_valid(self): validator = self.validator_class() @@ -135,29 +135,32 @@ def test_validate_group_type_incompatible(self): class MetricAlertComparisonConditionValidatorTest(TestCase): def test(self): validator = MetricAlertComparisonConditionValidator( - data={"condition": "gt", "comparison": 100, "result": DetectorPriorityLevel.HIGH} + data={ + "type": Condition.GREATER, + "comparison": 100, + "result": DetectorPriorityLevel.HIGH, + } ) assert validator.is_valid() assert validator.validated_data == { "comparison": 100.0, - "condition": Condition.GREATER, "result": DetectorPriorityLevel.HIGH, - "type": "metric_alert", + "type": Condition.GREATER, } def test_invalid_condition(self): validator = MetricAlertComparisonConditionValidator( - data={"condition": "invalid", "comparison": 100, "result": DetectorPriorityLevel.HIGH} + data={"type": "invalid", "comparison": 100, "result": DetectorPriorityLevel.HIGH} ) assert not validator.is_valid() - assert validator.errors.get("condition") == [ - ErrorDetail(string="Unsupported condition invalid", code="invalid") + assert validator.errors.get("type") == [ + ErrorDetail(string="Unsupported type invalid", code="invalid") ] def test_invalid_comparison(self): validator = MetricAlertComparisonConditionValidator( data={ - "condition": "gt", + "type": Condition.GREATER, "comparison": "not_a_number", "result": DetectorPriorityLevel.HIGH, } @@ -169,7 +172,7 @@ def test_invalid_comparison(self): def test_invalid_result(self): validator = MetricAlertComparisonConditionValidator( - data={"condition": "gt", "comparison": 100, "result": 25} + data={"type": Condition.GREATER, "comparison": 100, "result": 25} ) assert not validator.is_valid() assert validator.errors.get("result") == [ @@ -195,7 +198,7 @@ def setUp(self): }, "data_conditions": [ { - "condition": "gte", + "type": Condition.GREATER_OR_EQUAL, "comparison": 100, "result": DetectorPriorityLevel.HIGH, } @@ -287,7 +290,7 @@ def setUp(self): }, "data_conditions": [ { - "condition": "gt", + "type": Condition.GREATER, "comparison": 100, "result": DetectorPriorityLevel.HIGH, } @@ -342,7 +345,7 @@ def test_create_with_valid_data(self, mock_audit): conditions = list(DataCondition.objects.filter(condition_group=condition_group)) assert len(conditions) == 1 condition = conditions[0] - assert condition.type == "gt" + assert condition.type == Condition.GREATER assert condition.comparison == 100 assert condition.condition_result == DetectorPriorityLevel.HIGH @@ -367,9 +370,21 @@ def test_too_many_conditions(self): data = { **self.valid_data, "data_conditions": [ - {"condition": "gt", "comparison": 100, "result": DetectorPriorityLevel.HIGH}, - {"condition": "gt", "comparison": 200, "result": DetectorPriorityLevel.HIGH}, - {"condition": "gt", "comparison": 300, "result": DetectorPriorityLevel.HIGH}, + { + "type": Condition.GREATER, + "comparison": 100, + "result": DetectorPriorityLevel.HIGH, + }, + { + "type": Condition.GREATER, + "comparison": 200, + "result": DetectorPriorityLevel.HIGH, + }, + { + "type": Condition.GREATER, + "comparison": 300, + "result": DetectorPriorityLevel.HIGH, + }, ], } validator = MetricAlertsDetectorValidator(data=data, context=self.context) @@ -433,7 +448,6 @@ def test_validate_adds_creator_and_type(self): class MockDataConditionValidator(NumericComparisonConditionValidator): supported_conditions = frozenset([Condition.GREATER_OR_EQUAL, Condition.LESS_OR_EQUAL]) supported_results = frozenset([DetectorPriorityLevel.HIGH, DetectorPriorityLevel.LOW]) - type = "test" class MockDetectorValidator(BaseGroupTypeDetectorValidator): From 6a22b214b05bdd2a33d4b0a1a855c1a34ebb04a9 Mon Sep 17 00:00:00 2001 From: Harshitha Durai <76853136+harshithadurai@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:45:48 -0500 Subject: [PATCH 418/757] feat(dashboards): enable sorting by column in table view (#82239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable sorting by column in dashboards table view. - This PR adds sorting by `name`, `date created`, and `owner` - Added sort by `Name (Z-A)` to dropdown to maintain consistency b/w grid and table view - Sorting by `owner` column makes your dashboards show up on top (i.e. `myDashboards` in the sort dropdown) - The plan is to remove the sort dropdown for the table view in the future Screenshot 2024-12-17 at 1 03 30 PM --- .../dashboards/manage/dashboardTable.tsx | 44 +++++++++++++++++++ static/app/views/dashboards/manage/index.tsx | 1 + 2 files changed, 45 insertions(+) diff --git a/static/app/views/dashboards/manage/dashboardTable.tsx b/static/app/views/dashboards/manage/dashboardTable.tsx index 6366f3b1e22cef..8608652460cf6b 100644 --- a/static/app/views/dashboards/manage/dashboardTable.tsx +++ b/static/app/views/dashboards/manage/dashboardTable.tsx @@ -21,6 +21,7 @@ import GridEditable, { COL_WIDTH_UNDEFINED, type GridColumnOrder, } from 'sentry/components/gridEditable'; +import SortLink from 'sentry/components/gridEditable/sortLink'; import Link from 'sentry/components/links/link'; import TimeSince from 'sentry/components/timeSince'; import {IconCopy, IconDelete, IconStar} from 'sentry/icons'; @@ -28,6 +29,7 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {decodeScalar} from 'sentry/utils/queryString'; import withApi from 'sentry/utils/withApi'; import EditAccessSelector from 'sentry/views/dashboards/editAccessSelector'; import type { @@ -56,6 +58,12 @@ enum ResponseKeys { FAVORITE = 'isFavorited', } +const SortKeys = { + title: {asc: 'title', desc: '-title'}, + dateCreated: {asc: 'dateCreated', desc: '-dateCreated'}, + createdBy: {asc: 'mydashboards', desc: 'mydashboards'}, +}; + type FavoriteButtonProps = { api: Client; dashboardId: string; @@ -159,6 +167,41 @@ function DashboardTable({ : {}), }; + function renderHeadCell(column: GridColumnOrder) { + if (column.key in SortKeys) { + const urlSort = decodeScalar(location.query.sort, 'mydashboards'); + const isCurrentSort = + urlSort === SortKeys[column.key].asc || urlSort === SortKeys[column.key].desc; + const sortDirection = + !isCurrentSort || column.key === 'createdBy' + ? undefined + : urlSort.startsWith('-') + ? 'desc' + : 'asc'; + + return ( + { + const newSort = isCurrentSort + ? sortDirection === 'asc' + ? SortKeys[column.key].desc + : SortKeys[column.key].asc + : SortKeys[column.key].asc; + return { + ...location, + query: {...location.query, sort: newSort}, + }; + }} + /> + ); + } + return column.name; + } + const renderBodyCell = ( column: GridColumnOrder, dataRow: DashboardListItem @@ -289,6 +332,7 @@ function DashboardTable({ columnSortBy={[]} grid={{ renderBodyCell, + renderHeadCell: column => renderHeadCell(column), // favorite column renderPrependColumns: (isHeader: boolean, dataRow?: any) => { if (!organization.features.includes('dashboards-favourite')) { diff --git a/static/app/views/dashboards/manage/index.tsx b/static/app/views/dashboards/manage/index.tsx index 3da5da508983a7..444d8956342e8a 100644 --- a/static/app/views/dashboards/manage/index.tsx +++ b/static/app/views/dashboards/manage/index.tsx @@ -60,6 +60,7 @@ import TemplateCard from './templateCard'; const SORT_OPTIONS: SelectValue[] = [ {label: t('My Dashboards'), value: 'mydashboards'}, {label: t('Dashboard Name (A-Z)'), value: 'title'}, + {label: t('Dashboard Name (Z-A)'), value: '-title'}, {label: t('Date Created (Newest)'), value: '-dateCreated'}, {label: t('Date Created (Oldest)'), value: 'dateCreated'}, {label: t('Most Popular'), value: 'mostPopular'}, From ca2376404f985bcd473221c446eb07296c2b43d3 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:59:22 -0500 Subject: [PATCH 419/757] ref: remove calls to iso_format in testutils (#82461) --- src/sentry/integrations/github/client.py | 2 +- src/sentry/testutils/cases.py | 6 +++--- src/sentry/testutils/fixtures.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sentry/integrations/github/client.py b/src/sentry/integrations/github/client.py index 98c189d50bbbfd..5db8d605024778 100644 --- a/src/sentry/integrations/github/client.py +++ b/src/sentry/integrations/github/client.py @@ -141,7 +141,7 @@ def _get_token(self, prepared_request: PreparedRequest) -> str | None: access_token: str | None = self.integration.metadata.get("access_token") expires_at: str | None = self.integration.metadata.get("expires_at") is_expired = ( - bool(expires_at) and datetime.strptime(cast(str, expires_at), "%Y-%m-%dT%H:%M:%S") < now + bool(expires_at) and datetime.fromisoformat(expires_at).replace(tzinfo=None) < now ) should_refresh = not access_token or not expires_at or is_expired diff --git a/src/sentry/testutils/cases.py b/src/sentry/testutils/cases.py index c8d30c3b3a71a3..dee1be41de057b 100644 --- a/src/sentry/testutils/cases.py +++ b/src/sentry/testutils/cases.py @@ -132,7 +132,7 @@ from sentry.snuba.metrics.naming_layer.public import TransactionMetricKey from sentry.tagstore.snuba.backend import SnubaTagStorage from sentry.testutils.factories import get_fixture_path -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.helpers.notifications import TEST_ISSUE_OCCURRENCE from sentry.testutils.helpers.slack import install_slack from sentry.testutils.pytest.selenium import Browser @@ -2126,7 +2126,7 @@ def create_event(self, timestamp, fingerprint=None, user=None): data = { "event_id": event_id, "fingerprint": [fingerprint], - "timestamp": iso_format(timestamp), + "timestamp": timestamp.isoformat(), "type": "error", # This is necessary because event type error should not exist without # an exception being in the payload @@ -3388,7 +3388,7 @@ def load_default(self) -> Event: start, _ = self.get_start_end_from_day_ago(1000) return self.store_event( { - "timestamp": iso_format(start), + "timestamp": start.isoformat(), "contexts": { "trace": { "type": "trace", diff --git a/src/sentry/testutils/fixtures.py b/src/sentry/testutils/fixtures.py index 8496eded5216ea..6c6b2c535c750a 100644 --- a/src/sentry/testutils/fixtures.py +++ b/src/sentry/testutils/fixtures.py @@ -29,7 +29,7 @@ from sentry.snuba.models import QuerySubscription from sentry.tempest.models import TempestCredentials from sentry.testutils.factories import Factories -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import assume_test_silo_mode # XXX(dcramer): this is a compatibility layer to transition to pytest-based fixtures @@ -104,7 +104,7 @@ def event(self): data={ "event_id": "a" * 32, "message": "\u3053\u3093\u306b\u3061\u306f", - "timestamp": iso_format(before_now(seconds=1)), + "timestamp": before_now(seconds=1).isoformat(), }, project_id=self.project.id, ) @@ -129,7 +129,7 @@ def integration(self): external_id="github:1", metadata={ "access_token": "xxxxx-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx", - "expires_at": iso_format(timezone.now() + timedelta(days=14)), + "expires_at": (timezone.now() + timedelta(days=14)).isoformat(), }, ) integration.add_organization(self.organization, self.user) From 557ada4ff55df4ffe5a11754cff21a2db62f7edb Mon Sep 17 00:00:00 2001 From: Matt Duncan <14761+mrduncan@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:26:50 -0800 Subject: [PATCH 420/757] chore(issues): Opt in a few more endpoint tests to stronger types (#82382) `tests.sentry.issues.endpoints.test_organization_group_index` is the only remaining before we can just fully opt in `tests.sentry.issues.endpoints.*`. --- pyproject.toml | 5 + .../issues/endpoints/test_group_events.py | 5 +- .../endpoints/test_group_notes_details.py | 18 +-- .../endpoints/test_group_participants.py | 9 +- .../test_group_similar_issues_embeddings.py | 140 +++++------------- .../endpoints/test_shared_group_details.py | 14 +- 6 files changed, 70 insertions(+), 121 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ccbfb2f66028b9..258cc7d056c922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -511,8 +511,12 @@ module = [ "tests.sentry.issues.endpoints.test_group_activities", "tests.sentry.issues.endpoints.test_group_details", "tests.sentry.issues.endpoints.test_group_event_details", + "tests.sentry.issues.endpoints.test_group_events", "tests.sentry.issues.endpoints.test_group_hashes", "tests.sentry.issues.endpoints.test_group_notes", + "tests.sentry.issues.endpoints.test_group_notes_details", + "tests.sentry.issues.endpoints.test_group_participants", + "tests.sentry.issues.endpoints.test_group_similar_issues_embeddings", "tests.sentry.issues.endpoints.test_group_tombstone", "tests.sentry.issues.endpoints.test_group_tombstone_details", "tests.sentry.issues.endpoints.test_organization_group_search_views", @@ -521,6 +525,7 @@ module = [ "tests.sentry.issues.endpoints.test_project_group_stats", "tests.sentry.issues.endpoints.test_project_stacktrace_link", "tests.sentry.issues.endpoints.test_related_issues", + "tests.sentry.issues.endpoints.test_shared_group_details", "tests.sentry.issues.endpoints.test_source_map_debug", "tests.sentry.issues.endpoints.test_team_groups_old", "tests.sentry.issues.test_attributes", diff --git a/tests/sentry/issues/endpoints/test_group_events.py b/tests/sentry/issues/endpoints/test_group_events.py index 7a6962efad9d79..ed6cc997366f50 100644 --- a/tests/sentry/issues/endpoints/test_group_events.py +++ b/tests/sentry/issues/endpoints/test_group_events.py @@ -1,6 +1,7 @@ from datetime import timedelta from django.utils import timezone +from rest_framework.response import Response from sentry.issues.grouptype import ProfileFileIOGroupType from sentry.testutils.cases import APITestCase, PerformanceIssueTestCase, SnubaTestCase @@ -15,10 +16,10 @@ def setUp(self) -> None: self.min_ago = before_now(minutes=1) self.two_min_ago = before_now(minutes=2) - def do_request(self, url: str): + def do_request(self, url: str) -> Response: return self.client.get(url, format="json") - def _parse_links(self, header): + def _parse_links(self, header: str) -> dict[str | None, dict[str, str | None]]: # links come in {url: {...attrs}}, but we need {rel: {...attrs}} links = {} for url, attrs in parse_link_header(header).items(): diff --git a/tests/sentry/issues/endpoints/test_group_notes_details.py b/tests/sentry/issues/endpoints/test_group_notes_details.py index 4fc9aad57fa92f..c9edd470a46764 100644 --- a/tests/sentry/issues/endpoints/test_group_notes_details.py +++ b/tests/sentry/issues/endpoints/test_group_notes_details.py @@ -1,5 +1,5 @@ from functools import cached_property -from unittest.mock import patch +from unittest.mock import MagicMock, patch import responses @@ -16,7 +16,7 @@ class GroupNotesDetailsTest(APITestCase): - def setUp(self): + def setUp(self) -> None: super().setUp() self.activity.data["external_id"] = "123" self.activity.save() @@ -43,10 +43,10 @@ def setUp(self): ) @cached_property - def url(self): + def url(self) -> str: return f"/api/0/issues/{self.group.id}/comments/{self.activity.id}/" - def test_delete(self): + def test_delete(self) -> None: self.login_as(user=self.user) url = self.url @@ -59,7 +59,7 @@ def test_delete(self): assert Group.objects.get(id=self.group.id).num_comments == 0 - def test_delete_comment_and_subscription(self): + def test_delete_comment_and_subscription(self) -> None: """Test that if a user deletes their comment on an issue, we delete the subscription too""" self.login_as(user=self.user) event = self.store_event(data={}, project_id=self.project.id) @@ -91,7 +91,7 @@ def test_delete_comment_and_subscription(self): reason=GroupSubscriptionReason.comment, ).exists() - def test_delete_multiple_comments(self): + def test_delete_multiple_comments(self) -> None: """Test that if a user has commented multiple times on an issue and deletes one, we don't remove the subscription""" self.login_as(user=self.user) event = self.store_event(data={}, project_id=self.project.id) @@ -130,7 +130,7 @@ def test_delete_multiple_comments(self): @patch("sentry.integrations.mixins.issues.IssueBasicIntegration.update_comment") @responses.activate - def test_put(self, mock_update_comment): + def test_put(self, mock_update_comment: MagicMock) -> None: self.login_as(user=self.user) url = self.url @@ -154,7 +154,7 @@ def test_put(self, mock_update_comment): assert mock_update_comment.call_args[0][2] == activity @responses.activate - def test_put_ignore_mentions(self): + def test_put_ignore_mentions(self) -> None: GroupLink.objects.filter(group_id=self.group.id).delete() self.login_as(user=self.user) @@ -179,7 +179,7 @@ def test_put_ignore_mentions(self): } @patch("sentry.integrations.mixins.issues.IssueBasicIntegration.update_comment") - def test_put_no_external_id(self, mock_update_comment): + def test_put_no_external_id(self, mock_update_comment: MagicMock) -> None: del self.activity.data["external_id"] self.activity.save() self.login_as(user=self.user) diff --git a/tests/sentry/issues/endpoints/test_group_participants.py b/tests/sentry/issues/endpoints/test_group_participants.py index ff72ea1569a313..c80877c8b52415 100644 --- a/tests/sentry/issues/endpoints/test_group_participants.py +++ b/tests/sentry/issues/endpoints/test_group_participants.py @@ -1,13 +1,16 @@ +from collections.abc import Callable + +from sentry.models.group import Group from sentry.models.groupsubscription import GroupSubscription from sentry.testutils.cases import APITestCase class GroupParticipantsTest(APITestCase): - def setUp(self): + def setUp(self) -> None: super().setUp() self.login_as(self.user) - def _get_path_functions(self): + def _get_path_functions(self) -> tuple[Callable[[Group], str], Callable[[Group], str]]: # The urls for group participants are supported both with an org slug and without. # We test both as long as we support both. # Because removing old urls takes time and consideration of the cost of breaking lingering references, a @@ -17,7 +20,7 @@ def _get_path_functions(self): lambda group: f"/api/0/organizations/{self.organization.slug}/issues/{group.id}/participants/", ) - def test_simple(self): + def test_simple(self) -> None: group = self.create_group() GroupSubscription.objects.create( user_id=self.user.id, group=group, project=group.project, is_active=True diff --git a/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py b/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py index a040e9ceecfde8..ac20568cfd9ec2 100644 --- a/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py +++ b/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py @@ -43,7 +43,7 @@ class GroupSimilarIssuesEmbeddingsTest(APITestCase): - def setUp(self): + def setUp(self) -> None: super().setUp() self.login_as(self.user) self.org = self.create_organization(owner=self.user) @@ -81,82 +81,6 @@ def setUp(self): data={"message": "Dogs are great!"}, project_id=self.project ) - def create_exception( - self, exception_type_str="Exception", exception_value="it broke", frames=None - ): - frames = frames or [] - return { - "id": "exception", - "name": "exception", - "contributes": True, - "hint": None, - "values": [ - { - "id": "stacktrace", - "name": "stack-trace", - "contributes": True, - "hint": None, - "values": frames, - }, - { - "id": "type", - "name": None, - "contributes": True, - "hint": None, - "values": [exception_type_str], - }, - { - "id": "value", - "name": None, - "contributes": False, - "hint": None, - "values": [exception_value], - }, - ], - } - - def create_frames( - self, - num_frames, - contributes=True, - start_index=1, - context_line_factory=lambda i: f"test = {i}!", - ): - frames = [] - for i in range(start_index, start_index + num_frames): - frames.append( - { - "id": "frame", - "name": None, - "contributes": contributes, - "hint": None, - "values": [ - { - "id": "filename", - "name": None, - "contributes": contributes, - "hint": None, - "values": ["hello.py"], - }, - { - "id": "function", - "name": None, - "contributes": contributes, - "hint": None, - "values": ["hello_there"], - }, - { - "id": "context-line", - "name": None, - "contributes": contributes, - "hint": None, - "values": [context_line_factory(i)], - }, - ], - } - ) - return frames - def get_expected_response( self, group_ids: Sequence[int], @@ -179,7 +103,7 @@ def get_expected_response( ) return response - def test_get_formatted_results(self): + def test_get_formatted_results(self) -> None: event_from_second_similar_group = save_new_event( {"message": "Adopt don't shop"}, self.project ) @@ -215,7 +139,12 @@ def test_get_formatted_results(self): @mock.patch("sentry.seer.similarity.similar_issues.metrics.incr") @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") @mock.patch("sentry.issues.endpoints.group_similar_issues_embeddings.logger") - def test_simple(self, mock_logger, mock_seer_request, mock_metrics_incr): + def test_simple( + self, + mock_logger: mock.MagicMock, + mock_seer_request: mock.MagicMock, + mock_metrics_incr: mock.MagicMock, + ) -> None: seer_return_value: SimilarIssuesEmbeddingsResponse = { "responses": [ { @@ -271,7 +200,7 @@ def test_simple(self, mock_logger, mock_seer_request, mock_metrics_incr): ) @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") - def test_simple_threads(self, mock_seer_request): + def test_simple_threads(self, mock_seer_request: mock.MagicMock) -> None: event = self.store_event(data=EVENT_WITH_THREADS_STACKTRACE, project_id=self.project) data = { "parent_hash": self.similar_event.get_primary_hash(), @@ -293,7 +222,7 @@ def test_simple_threads(self, mock_seer_request): @mock.patch("sentry.analytics.record") @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") - def test_multiple(self, mock_seer_request, mock_record): + def test_multiple(self, mock_seer_request: mock.MagicMock, mock_record: mock.MagicMock) -> None: over_threshold_group_event = save_new_event({"message": "Maisey is silly"}, self.project) under_threshold_group_event = save_new_event({"message": "Charlie is goofy"}, self.project) @@ -347,7 +276,7 @@ def test_multiple(self, mock_seer_request, mock_record): ) @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") - def test_parent_hash_in_group_hashes(self, mock_seer_request): + def test_parent_hash_in_group_hashes(self, mock_seer_request: mock.MagicMock) -> None: """ Test that the request group's hashes are filtered out of the returned similar parent hashes """ @@ -377,7 +306,12 @@ def test_parent_hash_in_group_hashes(self, mock_seer_request): @mock.patch("sentry.seer.similarity.similar_issues.metrics.incr") @mock.patch("sentry.seer.similarity.similar_issues.logger") @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") - def test_incomplete_return_data(self, mock_seer_request, mock_logger, mock_metrics_incr): + def test_incomplete_return_data( + self, + mock_seer_request: mock.MagicMock, + mock_logger: mock.MagicMock, + mock_metrics_incr: mock.MagicMock, + ) -> None: # Two suggested groups, one with valid data, one missing parent hash. We should log the # second and return the first. seer_return_value: Any = { @@ -438,11 +372,11 @@ def test_incomplete_return_data(self, mock_seer_request, mock_logger, mock_metri @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") def test_nonexistent_grouphash( self, - mock_seer_similarity_request, - mock_logger, - mock_metrics_incr, - mock_seer_deletion_request, - ): + mock_seer_similarity_request: mock.MagicMock, + mock_logger: mock.MagicMock, + mock_metrics_incr: mock.MagicMock, + mock_seer_deletion_request: mock.MagicMock, + ) -> None: """ The seer API can return grouphashes that do not exist if their groups have been deleted/merged. Test info about these groups is not returned. @@ -499,11 +433,11 @@ def test_nonexistent_grouphash( @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") def test_grouphash_with_no_group( self, - mock_seer_similarity_request, - mock_logger, - mock_metrics_incr, - mock_seer_deletion_request, - ): + mock_seer_similarity_request: mock.MagicMock, + mock_logger: mock.MagicMock, + mock_metrics_incr: mock.MagicMock, + mock_seer_deletion_request: mock.MagicMock, + ) -> None: """ The seer API can return groups that do not exist if they have been deleted/merged. Test that these groups are not returned. @@ -549,7 +483,9 @@ def test_grouphash_with_no_group( @mock.patch("sentry.analytics.record") @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") - def test_empty_seer_return(self, mock_seer_request, mock_record): + def test_empty_seer_return( + self, mock_seer_request: mock.MagicMock, mock_record: mock.MagicMock + ) -> None: mock_seer_request.return_value = HTTPResponse([], status=200) response = self.client.get(self.path) assert response.data == [] @@ -564,7 +500,7 @@ def test_empty_seer_return(self, mock_seer_request, mock_record): user_id=self.user.id, ) - def test_no_contributing_exception(self): + def test_no_contributing_exception(self) -> None: data_no_contributing_exception = { "fingerprint": ["message"], "message": "Message", @@ -604,7 +540,7 @@ def test_no_contributing_exception(self): assert response.data == [] - def test_no_exception(self): + def test_no_exception(self) -> None: event_no_exception = self.store_event(data={}, project_id=self.project) group_no_exception = event_no_exception.group assert group_no_exception @@ -616,7 +552,7 @@ def test_no_exception(self): assert response.data == [] @mock.patch("sentry.models.group.Group.get_latest_event") - def test_no_latest_event(self, mock_get_latest_event): + def test_no_latest_event(self, mock_get_latest_event: mock.MagicMock) -> None: mock_get_latest_event.return_value = None response = self.client.get( @@ -627,7 +563,7 @@ def test_no_latest_event(self, mock_get_latest_event): assert response.data == [] @mock.patch("sentry.issues.endpoints.group_similar_issues_embeddings.get_stacktrace_string") - def test_no_stacktrace_string(self, mock_get_stacktrace_string): + def test_no_stacktrace_string(self, mock_get_stacktrace_string: mock.MagicMock) -> None: mock_get_stacktrace_string.return_value = "" response = self.client.get( @@ -638,7 +574,7 @@ def test_no_stacktrace_string(self, mock_get_stacktrace_string): assert response.data == [] @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") - def test_no_optional_params(self, mock_seer_request): + def test_no_optional_params(self, mock_seer_request: mock.MagicMock) -> None: """ Test that optional parameters, k, threshold, and read_only can not be included. """ @@ -739,7 +675,7 @@ def test_no_optional_params(self, mock_seer_request): ) @mock.patch("sentry.seer.similarity.similar_issues.seer_grouping_connection_pool.urlopen") - def test_obeys_useReranking_query_param(self, mock_seer_request): + def test_obeys_useReranking_query_param(self, mock_seer_request: mock.MagicMock) -> None: for incoming_value, outgoing_value in [("true", True), ("false", False)]: self.client.get(self.path, data={"useReranking": incoming_value}) @@ -749,7 +685,7 @@ def test_obeys_useReranking_query_param(self, mock_seer_request): mock_seer_request.reset_mock() - def test_too_many_system_frames(self): + def test_too_many_system_frames(self) -> None: type = "FailedToFetchError" value = "Charlie didn't bring the ball back" context_line = f"raise {type}('{value}')" @@ -782,7 +718,7 @@ def test_too_many_system_frames(self): ) assert response.data == [] - def test_no_filename_or_module(self): + def test_no_filename_or_module(self) -> None: type = "FailedToFetchError" value = "Charlie didn't bring the ball back" context_line = f"raise {type}('{value}')" diff --git a/tests/sentry/issues/endpoints/test_shared_group_details.py b/tests/sentry/issues/endpoints/test_shared_group_details.py index 759d939d38887a..28d60834eecc38 100644 --- a/tests/sentry/issues/endpoints/test_shared_group_details.py +++ b/tests/sentry/issues/endpoints/test_shared_group_details.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from sentry.models.groupshare import GroupShare from sentry.testutils.cases import APITestCase from sentry.testutils.helpers.datetime import before_now @@ -7,7 +9,9 @@ class SharedGroupDetailsTest(APITestCase): - def _get_path_functions(self): + def _get_path_functions( + self, + ) -> tuple[Callable[[str], str], Callable[[str], str], Callable[[str], str]]: # The urls for shared group details are supported both with an org slug and without. # We test both as long as we support both. # Because removing old urls takes time and consideration of the cost of breaking lingering references, a @@ -18,7 +22,7 @@ def _get_path_functions(self): lambda share_id: f"/api/0/organizations/{self.organization.id}/shared/issues/{share_id}/", ) - def test_simple(self): + def test_simple(self) -> None: self.login_as(user=self.user) min_ago = before_now(minutes=1).isoformat() @@ -44,7 +48,7 @@ def test_simple(self): assert response.data["project"]["slug"] == group.project.slug assert response.data["project"]["organization"]["slug"] == group.organization.slug - def test_does_not_leak_assigned_to(self): + def test_does_not_leak_assigned_to(self) -> None: self.login_as(user=self.user) min_ago = before_now(minutes=1).isoformat() @@ -71,7 +75,7 @@ def test_does_not_leak_assigned_to(self): assert response.data["project"]["organization"]["slug"] == group.organization.slug assert "assignedTo" not in response.data - def test_feature_disabled(self): + def test_feature_disabled(self) -> None: self.login_as(user=self.user) group = self.create_group() @@ -93,7 +97,7 @@ def test_feature_disabled(self): assert response.status_code == 404 - def test_permalink(self): + def test_permalink(self) -> None: group = self.create_group() share_id = group.get_share_id() From 0f44d1b883c4a2bc2160a16da07bc5c88efcccf9 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 20 Dec 2024 14:36:53 -0500 Subject: [PATCH 421/757] fix(eap): Numeric attribute filtering in snql eap (#82472) RPC isn't fully stable yet so fix this in SnQL first. --- .../search/events/builder/spans_indexed.py | 7 ++++ .../test_organization_events_span_indexed.py | 32 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/sentry/search/events/builder/spans_indexed.py b/src/sentry/search/events/builder/spans_indexed.py index 79a82d81f46a1e..3e603f55cf5802 100644 --- a/src/sentry/search/events/builder/spans_indexed.py +++ b/src/sentry/search/events/builder/spans_indexed.py @@ -71,6 +71,13 @@ class SpansEAPQueryBuilder(BaseQueryBuilder): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + def get_field_type(self, field: str) -> str | None: + tag_match = constants.TYPED_TAG_KEY_RE.search(field) + field_type = tag_match.group("type") if tag_match else None + if field_type == "number": + return "number" + return super().get_field_type(field) + def resolve_field(self, raw_field: str, alias: bool = False) -> Column: # try the typed regex first if len(raw_field) > constants.MAX_TAG_KEY_LENGTH: diff --git a/tests/snuba/api/endpoints/test_organization_events_span_indexed.py b/tests/snuba/api/endpoints/test_organization_events_span_indexed.py index c7f29b341e5c07..e44b63e32c6372 100644 --- a/tests/snuba/api/endpoints/test_organization_events_span_indexed.py +++ b/tests/snuba/api/endpoints/test_organization_events_span_indexed.py @@ -1474,8 +1474,36 @@ def test_span_data_fields_http_resource(self): }, } - def test_other_category_span(self): - super().test_other_category_span() + def test_filtering_numeric_attr(self): + span_1 = self.create_span( + {"description": "foo"}, + measurements={"foo": {"value": 30}}, + start_ts=self.ten_mins_ago, + ) + span_2 = self.create_span( + {"description": "foo"}, + measurements={"foo": {"value": 10}}, + start_ts=self.ten_mins_ago, + ) + self.store_spans([span_1, span_2], is_eap=self.is_eap) + + response = self.do_request( + { + "field": ["tags[foo,number]"], + "query": "span.duration:>=0 tags[foo,number]:>20", + "project": self.project.id, + "dataset": self.dataset, + } + ) + + assert response.status_code == 200, response.content + assert response.data["data"] == [ + { + "id": span_1["span_id"], + "project.name": self.project.slug, + "tags[foo,number]": 30, + }, + ] class OrganizationEventsEAPRPCSpanEndpointTest(OrganizationEventsEAPSpanEndpointTest): From bac442e1a8522ee67500c5dfab6c62b60821b642 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:41:12 -0500 Subject: [PATCH 422/757] feat(dashboards): Pass `LineChart` series meta alongside the data (#82047) Instead of a separate `meta` prop, just pass the meta alongside the data. This is easier and it matches how the server returns data. --- .../contexts/widgetSyncContext.stories.tsx | 16 ----- .../views/dashboards/widgets/common/types.tsx | 1 + .../lineChartWidget/lineChartWidget.spec.tsx | 8 --- .../lineChartWidget.stories.tsx | 58 ++++++++----------- .../lineChartWidget/lineChartWidget.tsx | 1 - .../lineChartWidgetVisualization.tsx | 8 +-- .../sampleDurationTimeSeries.json | 8 +++ .../sampleThroughputTimeSeries.json | 8 +++ 8 files changed, 44 insertions(+), 64 deletions(-) diff --git a/static/app/views/dashboards/contexts/widgetSyncContext.stories.tsx b/static/app/views/dashboards/contexts/widgetSyncContext.stories.tsx index 28c4b8a73421a5..e3951f4bdd7531 100644 --- a/static/app/views/dashboards/contexts/widgetSyncContext.stories.tsx +++ b/static/app/views/dashboards/contexts/widgetSyncContext.stories.tsx @@ -35,14 +35,6 @@ export default storyBook('WidgetSyncContext', story => { {visible && ( @@ -50,14 +42,6 @@ export default storyBook('WidgetSyncContext', story => { )} diff --git a/static/app/views/dashboards/widgets/common/types.tsx b/static/app/views/dashboards/widgets/common/types.tsx index 4b5959990400b4..f85d06f7ffd19b 100644 --- a/static/app/views/dashboards/widgets/common/types.tsx +++ b/static/app/views/dashboards/widgets/common/types.tsx @@ -17,6 +17,7 @@ export type TimeseriesData = { data: TimeSeriesItem[]; field: string; color?: string; + meta?: Meta; }; export type ErrorProp = Error | string; diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.spec.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.spec.tsx index 64fcbe832b9e32..0ddf8090e667e3 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.spec.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.spec.tsx @@ -11,14 +11,6 @@ describe('LineChartWidget', () => { title="eps()" description="Number of events per second" timeseries={[sampleDurationTimeSeries]} - meta={{ - fields: { - 'eps()': 'rate', - }, - units: { - 'eps()': '1/second', - }, - }} /> ); }); diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx index e69d13cb5e4654..2789dc918cabb6 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx @@ -26,6 +26,14 @@ const sampleDurationTimeSeries2 = { value: datum.value * 0.3 + 30 * Math.random(), }; }), + meta: { + fields: { + 'p50(span.duration)': 'duration', + }, + units: { + 'p50(span.duration)': 'millisecond', + }, + }, }; export default storyBook(LineChartWidget, story => { @@ -76,14 +84,6 @@ export default storyBook(LineChartWidget, story => { title="eps()" description="Number of events per second" timeseries={[throughputTimeSeries]} - meta={{ - fields: { - 'eps()': 'rate', - }, - units: { - 'eps()': '1/second', - }, - }} /> @@ -103,16 +103,6 @@ export default storyBook(LineChartWidget, story => { shiftTimeserieToNow(durationTimeSeries1), shiftTimeserieToNow(durationTimeSeries2), ]} - meta={{ - fields: { - 'p99(span.duration)': 'duration', - 'p50(span.duration)': 'duration', - }, - units: { - 'p99(span.duration)': 'millisecond', - 'p50(span.duration)': 'millisecond', - }, - }} /> @@ -172,17 +162,17 @@ export default storyBook(LineChartWidget, story => { { ...sampleThroughputTimeSeries, field: 'error_rate()', + meta: { + fields: { + 'error_rate()': 'rate', + }, + units: { + 'error_rate()': '1/second', + }, + }, color: theme.error, } as unknown as TimeseriesData, ]} - meta={{ - fields: { - 'error_rate()': 'rate', - }, - units: { - 'error_rate()': '1/second', - }, - }} />
@@ -216,17 +206,17 @@ export default storyBook(LineChartWidget, story => { { ...sampleThroughputTimeSeries, field: 'error_rate()', + meta: { + fields: { + 'error_rate()': 'rate', + }, + units: { + 'error_rate()': '1/second', + }, + }, } as unknown as TimeseriesData, ]} releases={releases} - meta={{ - fields: { - 'error_rate()': 'rate', - }, - units: { - 'error_rate()': '1/second', - }, - }} /> diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx index 4936caf8aabbae..e7197ba47d9510 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx @@ -60,7 +60,6 @@ export function LineChartWidget(props: LineChartWidgetProps) { diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx index 8a870d4554cdd4..d99f870851ded0 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx @@ -22,14 +22,13 @@ import {useWidgetSyncContext} from '../../contexts/widgetSyncContext'; import {formatTooltipValue} from '../common/formatTooltipValue'; import {formatYAxisValue} from '../common/formatYAxisValue'; import {ReleaseSeries} from '../common/releaseSeries'; -import type {Meta, Release, TimeseriesData} from '../common/types'; +import type {Release, TimeseriesData} from '../common/types'; import {splitSeriesIntoCompleteAndIncomplete} from './splitSeriesIntoCompleteAndIncomplete'; export interface LineChartWidgetVisualizationProps { timeseries: TimeseriesData[]; dataCompletenessDelay?: number; - meta?: Meta; releases?: Release[]; } @@ -39,7 +38,6 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization const pageFilters = usePageFilters(); const {start, end, period, utc} = pageFilters.selection.datetime; - const {meta} = props; const dataCompletenessDelay = props.dataCompletenessDelay ?? 0; @@ -95,8 +93,8 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization // TODO: Raise error if attempting to plot series of different types or units const firstSeriesField = firstSeries?.field; - const type = meta?.fields?.[firstSeriesField] ?? 'number'; - const unit = meta?.units?.[firstSeriesField] ?? undefined; + const type = firstSeries?.meta?.fields?.[firstSeriesField] ?? 'number'; + const unit = firstSeries?.meta?.units?.[firstSeriesField] ?? undefined; const formatter: TooltipFormatterCallback = ( params, diff --git a/static/app/views/dashboards/widgets/lineChartWidget/sampleDurationTimeSeries.json b/static/app/views/dashboards/widgets/lineChartWidget/sampleDurationTimeSeries.json index 9a17607b405dca..65ac173fc8b976 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/sampleDurationTimeSeries.json +++ b/static/app/views/dashboards/widgets/lineChartWidget/sampleDurationTimeSeries.json @@ -1,5 +1,13 @@ { "field": "p99(span.duration)", + "meta": { + "fields": { + "p99(span.duration)": "duration" + }, + "units": { + "p99(span.duration)": "millisecond" + } + }, "data": [ { "value": 163.26759544018776, diff --git a/static/app/views/dashboards/widgets/lineChartWidget/sampleThroughputTimeSeries.json b/static/app/views/dashboards/widgets/lineChartWidget/sampleThroughputTimeSeries.json index fcda062750ee1b..eea4cac77dc6f8 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/sampleThroughputTimeSeries.json +++ b/static/app/views/dashboards/widgets/lineChartWidget/sampleThroughputTimeSeries.json @@ -1,5 +1,13 @@ { "field": "eps()", + "meta": { + "fields": { + "eps()": "rate" + }, + "units": { + "eps()": "1/second" + } + }, "data": [ { "value": 7456.966666666666, From 526a0594d1d2268f9860414e916927910b349c5b Mon Sep 17 00:00:00 2001 From: Colleen O'Rourke Date: Fri, 20 Dec 2024 11:42:45 -0800 Subject: [PATCH 423/757] feat(alerts): ACI dual write alert rule helpers (#82400) A smaller chunk of https://github.com/getsentry/sentry/pull/81953 that creates the helper methods to migrate the `AlertRule` and not the `AlertRuleTrigger` or `AlertRuleTriggerAction` just yet. --- .../migration_helpers/alert_rule.py | 156 ++++++++++++++++++ .../test_migrate_alert_rule.py | 83 ++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/sentry/workflow_engine/migration_helpers/alert_rule.py create mode 100644 tests/sentry/workflow_engine/migration_helpers/test_migrate_alert_rule.py diff --git a/src/sentry/workflow_engine/migration_helpers/alert_rule.py b/src/sentry/workflow_engine/migration_helpers/alert_rule.py new file mode 100644 index 00000000000000..2cf652b0afb5f8 --- /dev/null +++ b/src/sentry/workflow_engine/migration_helpers/alert_rule.py @@ -0,0 +1,156 @@ +from sentry.incidents.grouptype import MetricAlertFire +from sentry.incidents.models.alert_rule import AlertRule +from sentry.snuba.models import QuerySubscription, SnubaQuery +from sentry.users.services.user import RpcUser +from sentry.workflow_engine.models import ( + AlertRuleDetector, + AlertRuleWorkflow, + DataConditionGroup, + DataSource, + Detector, + DetectorState, + DetectorWorkflow, + Workflow, + WorkflowDataConditionGroup, +) +from sentry.workflow_engine.types import DetectorPriorityLevel + + +def create_metric_alert_lookup_tables( + alert_rule: AlertRule, + detector: Detector, + workflow: Workflow, + data_source: DataSource, + data_condition_group: DataConditionGroup, +) -> tuple[AlertRuleDetector, AlertRuleWorkflow, DetectorWorkflow, WorkflowDataConditionGroup]: + alert_rule_detector = AlertRuleDetector.objects.create(alert_rule=alert_rule, detector=detector) + alert_rule_workflow = AlertRuleWorkflow.objects.create(alert_rule=alert_rule, workflow=workflow) + detector_workflow = DetectorWorkflow.objects.create(detector=detector, workflow=workflow) + workflow_data_condition_group = WorkflowDataConditionGroup.objects.create( + condition_group=data_condition_group, workflow=workflow + ) + return ( + alert_rule_detector, + alert_rule_workflow, + detector_workflow, + workflow_data_condition_group, + ) + + +def create_data_source( + organization_id: int, snuba_query: SnubaQuery | None = None +) -> DataSource | None: + if not snuba_query: + return None + + try: + query_subscription = QuerySubscription.objects.get(snuba_query=snuba_query.id) + except QuerySubscription.DoesNotExist: + return None + + return DataSource.objects.create( + organization_id=organization_id, + query_id=query_subscription.id, + type="snuba_query_subscription", + ) + + +def create_data_condition_group(organization_id: int) -> DataConditionGroup: + return DataConditionGroup.objects.create( + logic_type=DataConditionGroup.Type.ANY, + organization_id=organization_id, + ) + + +def create_workflow( + name: str, + organization_id: int, + data_condition_group: DataConditionGroup, + user: RpcUser | None = None, +) -> Workflow: + return Workflow.objects.create( + name=name, + organization_id=organization_id, + when_condition_group=data_condition_group, + enabled=True, + created_by_id=user.id if user else None, + ) + + +def create_detector( + alert_rule: AlertRule, + project_id: int, + data_condition_group: DataConditionGroup, + user: RpcUser | None = None, +) -> Detector: + return Detector.objects.create( + project_id=project_id, + enabled=True, + created_by_id=user.id if user else None, + name=alert_rule.name, + workflow_condition_group=data_condition_group, + type=MetricAlertFire.slug, + description=alert_rule.description, + owner_user_id=alert_rule.user_id, + owner_team=alert_rule.team, + config={ # TODO create a schema + "threshold_period": alert_rule.threshold_period, + "sensitivity": alert_rule.sensitivity, + "seasonality": alert_rule.seasonality, + "comparison_delta": alert_rule.comparison_delta, + }, + ) + + +def migrate_alert_rule( + alert_rule: AlertRule, + user: RpcUser | None = None, +) -> ( + tuple[ + DataSource, + DataConditionGroup, + Workflow, + Detector, + DetectorState, + AlertRuleDetector, + AlertRuleWorkflow, + DetectorWorkflow, + WorkflowDataConditionGroup, + ] + | None +): + organization_id = alert_rule.organization_id + project = alert_rule.projects.first() + if not project: + return None + + data_source = create_data_source(organization_id, alert_rule.snuba_query) + if not data_source: + return None + + data_condition_group = create_data_condition_group(organization_id) + workflow = create_workflow(alert_rule.name, organization_id, data_condition_group, user) + detector = create_detector(alert_rule, project.id, data_condition_group, user) + + data_source.detectors.set([detector]) + detector_state = DetectorState.objects.create( + detector=detector, + active=False, + state=DetectorPriorityLevel.OK, + ) + alert_rule_detector, alert_rule_workflow, detector_workflow, workflow_data_condition_group = ( + create_metric_alert_lookup_tables( + alert_rule, detector, workflow, data_source, data_condition_group + ) + ) + return ( + data_source, + data_condition_group, + workflow, + detector, + detector_state, + alert_rule_detector, + alert_rule_workflow, + detector_workflow, + workflow_data_condition_group, + ) diff --git a/tests/sentry/workflow_engine/migration_helpers/test_migrate_alert_rule.py b/tests/sentry/workflow_engine/migration_helpers/test_migrate_alert_rule.py new file mode 100644 index 00000000000000..28509c1c63a679 --- /dev/null +++ b/tests/sentry/workflow_engine/migration_helpers/test_migrate_alert_rule.py @@ -0,0 +1,83 @@ +from sentry.incidents.grouptype import MetricAlertFire +from sentry.snuba.models import QuerySubscription +from sentry.testutils.cases import APITestCase +from sentry.users.services.user.service import user_service +from sentry.workflow_engine.migration_helpers.alert_rule import migrate_alert_rule +from sentry.workflow_engine.models import ( + AlertRuleDetector, + AlertRuleWorkflow, + DataSource, + DataSourceDetector, + Detector, + DetectorState, + DetectorWorkflow, + Workflow, + WorkflowDataConditionGroup, +) +from sentry.workflow_engine.types import DetectorPriorityLevel + + +class AlertRuleMigrationHelpersTest(APITestCase): + def setUp(self): + self.metric_alert = self.create_alert_rule() + self.rpc_user = user_service.get_user(user_id=self.user.id) + + def test_create_metric_alert(self): + """ + Test that when we call the helper methods we create all the ACI models correctly for an alert rule + """ + migrate_alert_rule(self.metric_alert, self.rpc_user) + + alert_rule_workflow = AlertRuleWorkflow.objects.get(alert_rule=self.metric_alert) + alert_rule_detector = AlertRuleDetector.objects.get(alert_rule=self.metric_alert) + + workflow = Workflow.objects.get(id=alert_rule_workflow.workflow.id) + assert workflow.name == self.metric_alert.name + assert self.metric_alert.organization + assert workflow.organization_id == self.metric_alert.organization.id + detector = Detector.objects.get(id=alert_rule_detector.detector.id) + assert detector.name == self.metric_alert.name + assert detector.project_id == self.project.id + assert detector.enabled is True + assert detector.description == self.metric_alert.description + assert detector.owner_user_id == self.metric_alert.user_id + assert detector.owner_team == self.metric_alert.team + assert detector.type == MetricAlertFire.slug + assert detector.config == { + "threshold_period": self.metric_alert.threshold_period, + "sensitivity": None, + "seasonality": None, + "comparison_delta": None, + } + + detector_workflow = DetectorWorkflow.objects.get(detector=detector) + assert detector_workflow.workflow == workflow + + workflow_data_condition_group = WorkflowDataConditionGroup.objects.get(workflow=workflow) + assert workflow_data_condition_group.condition_group == workflow.when_condition_group + + assert self.metric_alert.snuba_query + query_subscription = QuerySubscription.objects.get( + snuba_query=self.metric_alert.snuba_query.id + ) + data_source = DataSource.objects.get( + organization_id=self.metric_alert.organization_id, query_id=query_subscription.id + ) + assert data_source.type == "snuba_query_subscription" + detector_state = DetectorState.objects.get(detector=detector) + assert detector_state.active is False + assert detector_state.state == str(DetectorPriorityLevel.OK.value) + + data_source_detector = DataSourceDetector.objects.get(data_source=data_source) + assert data_source_detector.detector == detector + + def test_create_metric_alert_no_data_source(self): + """ + Test that when we return None and don't create any ACI models if the data source can't be created + """ + self.metric_alert.update(snuba_query=None) + migrated = migrate_alert_rule(self.metric_alert, self.rpc_user) + assert migrated is None + assert len(DataSource.objects.all()) == 0 + assert not AlertRuleWorkflow.objects.filter(alert_rule=self.metric_alert).exists() + assert not AlertRuleDetector.objects.filter(alert_rule=self.metric_alert).exists() From fd99887982f76d67d6957f6acc06b8da6f8a91de Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 20 Dec 2024 22:55:36 +0300 Subject: [PATCH 424/757] fix(web): Add react_config context on auth pages take 2 (#82480) Follow up to #82396 and should fix getsentry/self-hosted#3473 --- src/sentry/web/frontend/auth_login.py | 15 +---- .../web/frontend/auth_organization_login.py | 10 ++- src/sentry/web/frontend/oauth_authorize.py | 3 +- tests/sentry/web/frontend/test_react_page.py | 5 +- tests/sentry/web/test_api.py | 64 ++----------------- 5 files changed, 15 insertions(+), 82 deletions(-) diff --git a/src/sentry/web/frontend/auth_login.py b/src/sentry/web/frontend/auth_login.py index 302f1e7d69ff3a..cf19698091d73c 100644 --- a/src/sentry/web/frontend/auth_login.py +++ b/src/sentry/web/frontend/auth_login.py @@ -432,7 +432,7 @@ def get_ratelimited_login_form( ] metrics.incr("login.attempt", instance="rate_limited", skip_internal=True, sample_rate=1.0) - context = { + context = self.get_default_context(request=request) | { "op": "login", "login_form": login_form, "referrer": request.GET.get("referrer"), @@ -527,9 +527,7 @@ def get_default_context(self, request: Request, **kwargs) -> dict: default_context = { "server_hostname": get_server_hostname(), "login_form": None, - "organization": kwargs.pop( - "organization", None - ), # NOTE: not utilized in basic login page (only org login) + "organization": organization, # NOTE: not utilized in basic login page (only org login) "register_form": None, "CAN_REGISTER": False, "react_config": get_client_config(request, self.active_organization), @@ -704,18 +702,11 @@ def handle_basic_auth(self, request: Request, **kwargs) -> HttpResponseBase: "login.attempt", instance="failure", skip_internal=True, sample_rate=1.0 ) - context = { + context = self.get_default_context(request=request, organization=organization) | { "op": op or "login", - "server_hostname": get_server_hostname(), "login_form": login_form, - "organization": organization, "register_form": register_form, "CAN_REGISTER": can_register, - "join_request_link": self.get_join_request_link( - organization=organization, request=request - ), - "show_login_banner": settings.SHOW_LOGIN_BANNER, - "referrer": request.GET.get("referrer"), } context.update(additional_context.run_callbacks(request)) diff --git a/src/sentry/web/frontend/auth_organization_login.py b/src/sentry/web/frontend/auth_organization_login.py index 3e2de690770ea6..18af32980bcdc5 100644 --- a/src/sentry/web/frontend/auth_organization_login.py +++ b/src/sentry/web/frontend/auth_organization_login.py @@ -23,14 +23,15 @@ def respond_login(self, request: Request, context, *args, **kwargs) -> HttpRespo return self.respond("sentry/organization-login.html", context) def handle_sso(self, request: Request, organization: RpcOrganization, auth_provider): - referrer = request.GET.get("referrer") if request.method == "POST": helper = AuthHelper( request=request, organization=organization, auth_provider=auth_provider, flow=AuthHelper.FLOW_LOGIN, - referrer=referrer, # TODO: get referrer from the form submit - not the query parms + referrer=request.GET.get( + "referrer" + ), # TODO: get referrer from the form submit - not the query parms ) if request.POST.get("init"): @@ -47,13 +48,10 @@ def handle_sso(self, request: Request, organization: RpcOrganization, auth_provi provider = auth_provider.get_provider() - context = { - "CAN_REGISTER": False, - "organization": organization, + context = self.get_default_context(request, organization=organization) | { "provider_key": provider.key, "provider_name": provider.name, "authenticated": request.user.is_authenticated, - "referrer": referrer, } return self.respond("sentry/organization-login.html", context) diff --git a/src/sentry/web/frontend/oauth_authorize.py b/src/sentry/web/frontend/oauth_authorize.py index e9ed3e3543fc99..163b2f973440c6 100644 --- a/src/sentry/web/frontend/oauth_authorize.py +++ b/src/sentry/web/frontend/oauth_authorize.py @@ -233,13 +233,14 @@ def get(self, request: HttpRequest, **kwargs) -> HttpResponseBase: # If application is not org level we should not show organizations to choose from at all organization_options = [] - context = { + context = self.get_default_context(request) | { "user": request.user, "application": application, "scopes": scopes, "permissions": permissions, "organization_options": organization_options, } + return self.respond("sentry/oauth-authorize.html", context) def post(self, request: HttpRequest, **kwargs) -> HttpResponseBase: diff --git a/tests/sentry/web/frontend/test_react_page.py b/tests/sentry/web/frontend/test_react_page.py index d9f25f12b561e9..bd294f459075e1 100644 --- a/tests/sentry/web/frontend/test_react_page.py +++ b/tests/sentry/web/frontend/test_react_page.py @@ -296,14 +296,11 @@ def _run_customer_domain_elevated_privileges(self, is_superuser: bool, is_staff: assert response.redirect_chain == [ (f"http://{other_org.slug}.testserver/issues/", 302) ] + assert self.client.session["activeorg"] == other_org.slug else: assert response.redirect_chain == [ (f"http://{other_org.slug}.testserver/auth/login/{other_org.slug}/", 302) ] - - if is_superuser or is_staff: - assert self.client.session["activeorg"] == other_org.slug - else: assert "activeorg" not in self.client.session # Accessing org without customer domain as superuser and/or staff. diff --git a/tests/sentry/web/test_api.py b/tests/sentry/web/test_api.py index f1285d02730fdb..3b14b44003b357 100644 --- a/tests/sentry/web/test_api.py +++ b/tests/sentry/web/test_api.py @@ -8,7 +8,6 @@ from sentry import options from sentry.api.utils import generate_region_url from sentry.auth import superuser -from sentry.conf.types.sentry_config import SentryMode from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion from sentry.deletions.tasks.scheduled import run_deletion from sentry.models.apitoken import ApiToken @@ -78,61 +77,10 @@ class RobotsTxtTest(TestCase): def path(self): return reverse("sentry-robots-txt") - def test_robots_self_hosted(self): - with override_settings(SENTRY_MODE=SentryMode.SELF_HOSTED): - resp = self.client.get(self.path) - assert resp.status_code == 200 - assert resp["Content-Type"] == "text/plain" - assert ( - resp.content - == b"""\ -User-agent: * -Disallow: / -""" - ) - - def test_robots_saas(self): - with override_settings(SENTRY_MODE=SentryMode.SAAS): - resp = self.client.get(self.path) - assert resp.status_code == 200 - assert resp["Content-Type"] == "text/plain" - # This is sentry.io/robots.txt. - assert ( - resp.content - == b"""\ -User-agent: * -Disallow: /api/ -Allow: /api/*/store/ -Allow: / - -Sitemap: https://sentry.io/sitemap-index.xml -""" - ) - - # SaaS customer domains should disallow all. - resp = self.client.get(self.path, HTTP_HOST="foo.testserver") - assert resp.status_code == 200 - assert resp["Content-Type"] == "text/plain" - assert ( - resp.content - == b"""\ -User-agent: * -Disallow: / -""" - ) - - def test_robots_single_tenant(self): - with override_settings(SENTRY_MODE=SentryMode.SINGLE_TENANT): - resp = self.client.get(self.path) - assert resp.status_code == 200 - assert resp["Content-Type"] == "text/plain" - assert ( - resp.content - == b"""\ -User-agent: * -Disallow: / -""" - ) + def test_robots(self): + resp = self.client.get(self.path) + assert resp.status_code == 200 + assert resp["Content-Type"] == "text/plain" @region_silo_test(regions=create_test_regions("us", "eu"), include_monolith_run=True) @@ -301,13 +249,11 @@ def _run_test_with_privileges(self, is_superuser: bool, is_staff: bool): assert response.redirect_chain == [ (f"http://{other_org.slug}.testserver/issues/", 302) ] + assert self.client.session["activeorg"] == other_org.slug else: assert response.redirect_chain == [ (f"http://{other_org.slug}.testserver/auth/login/{other_org.slug}/", 302) ] - if is_superuser or is_staff: - assert self.client.session["activeorg"] == other_org.slug - else: assert "activeorg" not in self.client.session # lastOrganization is set From 2ecf5fd7cbdfb65c5e9dace25059fc38c0d6bcd1 Mon Sep 17 00:00:00 2001 From: Leander Rodrigues Date: Fri, 20 Dec 2024 14:56:36 -0500 Subject: [PATCH 425/757] fix(group-events): Fix typo and error text (#82490) Correct `it's` to `its` and change the text altogether if it's not a known reserved keyword for the `event_id`. --- src/sentry/issues/endpoints/group_event_details.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sentry/issues/endpoints/group_event_details.py b/src/sentry/issues/endpoints/group_event_details.py index 0ba40af6994d0c..a4fa6a945680ba 100644 --- a/src/sentry/issues/endpoints/group_event_details.py +++ b/src/sentry/issues/endpoints/group_event_details.py @@ -225,12 +225,12 @@ def get(self, request: Request, group: Group, event_id: str) -> Response: event = event.for_group(event.group) if event is None: - return Response( - { - "detail": "Event not found. The event ID may be incorrect, or it's age exceeded the retention period." - }, - status=404, + error_text = ( + "Event not found. The event ID may be incorrect, or its age exceeded the retention period." + if event_id not in {"recommended", "latest", "oldest"} + else "No matching event found. Try changing the environments, date range, or query." ) + return Response({"detail": error_text}, status=404) collapse = request.GET.getlist("collapse", []) if "stacktraceOnly" in collapse: From fe35f16abbb3258dd934d4bf3262b5dd3ae4ee01 Mon Sep 17 00:00:00 2001 From: Cathy Teng <70817427+cathteng@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:00:31 -0800 Subject: [PATCH 426/757] ref(aci): pass WorkflowJob into process_workflows (#82489) --- src/sentry/tasks/post_process.py | 14 +++++- .../handlers/action/notification.py | 5 +- .../handlers/condition/__init__.py | 3 ++ .../condition/group_event_handlers.py | 13 ++--- .../condition/group_state_handlers.py | 27 +++++++++++ src/sentry/workflow_engine/models/action.py | 7 ++- .../workflow_engine/models/data_condition.py | 2 + src/sentry/workflow_engine/models/workflow.py | 6 +-- .../workflow_engine/processors/action.py | 6 +-- .../workflow_engine/processors/detector.py | 6 +-- .../workflow_engine/processors/workflow.py | 18 +++---- src/sentry/workflow_engine/types.py | 17 ++++++- .../workflow_engine/models/test_workflow.py | 8 ++-- .../workflow_engine/processors/test_action.py | 8 ++-- .../processors/test_workflow.py | 47 +++++++++++++++---- 15 files changed, 136 insertions(+), 51 deletions(-) create mode 100644 src/sentry/workflow_engine/handlers/condition/group_state_handlers.py diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index 0eb44ffeea7aa5..b5cb83206e0336 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -40,6 +40,7 @@ from sentry.utils.sdk import bind_organization_context, set_current_event_project from sentry.utils.sdk_crashes.sdk_crash_detection_config import build_sdk_crash_detection_configs from sentry.utils.services import build_instance_from_options_of_type +from sentry.workflow_engine.types import WorkflowJob if TYPE_CHECKING: from sentry.eventstore.models import Event, GroupEvent @@ -1002,10 +1003,19 @@ def process_workflow_engine(job: PostProcessJob) -> None: # If the flag is enabled, use the code below from sentry.workflow_engine.processors.workflow import process_workflows - evt = job["event"] + # PostProcessJob event is optional, WorkflowJob event is required + if "event" not in job: + logger.error("Missing event to create WorkflowJob", extra={"job": job}) + return + + try: + workflow_job = WorkflowJob({**job}) # type: ignore[typeddict-item] + except Exception: + logger.exception("Could not create WorkflowJob", extra={"job": job}) + return with sentry_sdk.start_span(op="tasks.post_process_group.workflow_engine.process_workflow"): - process_workflows(evt) + process_workflows(workflow_job) def process_rules(job: PostProcessJob) -> None: diff --git a/src/sentry/workflow_engine/handlers/action/notification.py b/src/sentry/workflow_engine/handlers/action/notification.py index c9637f32466442..91b8bfcc96e718 100644 --- a/src/sentry/workflow_engine/handlers/action/notification.py +++ b/src/sentry/workflow_engine/handlers/action/notification.py @@ -1,7 +1,6 @@ -from sentry.eventstore.models import GroupEvent from sentry.workflow_engine.models import Action, Detector from sentry.workflow_engine.registry import action_handler_registry -from sentry.workflow_engine.types import ActionHandler +from sentry.workflow_engine.types import ActionHandler, WorkflowJob # TODO - Enable once the PR to allow for multiple of the same funcs is merged @@ -10,7 +9,7 @@ class NotificationActionHandler(ActionHandler): @staticmethod def execute( - evt: GroupEvent, + job: WorkflowJob, action: Action, detector: Detector, ) -> None: diff --git a/src/sentry/workflow_engine/handlers/condition/__init__.py b/src/sentry/workflow_engine/handlers/condition/__init__.py index 578500d97efdb1..7a98d09c4ad7be 100644 --- a/src/sentry/workflow_engine/handlers/condition/__init__.py +++ b/src/sentry/workflow_engine/handlers/condition/__init__.py @@ -1,9 +1,12 @@ __all__ = [ "EventCreatedByDetectorConditionHandler", "EventSeenCountConditionHandler", + "ReappearedEventConditionHandler", + "RegressedEventConditionHandler", ] from .group_event_handlers import ( EventCreatedByDetectorConditionHandler, EventSeenCountConditionHandler, ) +from .group_state_handlers import ReappearedEventConditionHandler, RegressedEventConditionHandler diff --git a/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py b/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py index ee299180400a65..2bc9422c544595 100644 --- a/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py +++ b/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py @@ -1,15 +1,15 @@ from typing import Any -from sentry.eventstore.models import GroupEvent from sentry.workflow_engine.models.data_condition import Condition from sentry.workflow_engine.registry import condition_handler_registry -from sentry.workflow_engine.types import DataConditionHandler +from sentry.workflow_engine.types import DataConditionHandler, WorkflowJob @condition_handler_registry.register(Condition.EVENT_CREATED_BY_DETECTOR) -class EventCreatedByDetectorConditionHandler(DataConditionHandler[GroupEvent]): +class EventCreatedByDetectorConditionHandler(DataConditionHandler[WorkflowJob]): @staticmethod - def evaluate_value(event: GroupEvent, comparison: Any) -> bool: + def evaluate_value(job: WorkflowJob, comparison: Any) -> bool: + event = job["event"] if event.occurrence is None or event.occurrence.evidence_data is None: return False @@ -17,7 +17,8 @@ def evaluate_value(event: GroupEvent, comparison: Any) -> bool: @condition_handler_registry.register(Condition.EVENT_SEEN_COUNT) -class EventSeenCountConditionHandler(DataConditionHandler[GroupEvent]): +class EventSeenCountConditionHandler(DataConditionHandler[WorkflowJob]): @staticmethod - def evaluate_value(event: GroupEvent, comparison: Any) -> bool: + def evaluate_value(job: WorkflowJob, comparison: Any) -> bool: + event = job["event"] return event.group.times_seen == comparison diff --git a/src/sentry/workflow_engine/handlers/condition/group_state_handlers.py b/src/sentry/workflow_engine/handlers/condition/group_state_handlers.py new file mode 100644 index 00000000000000..3c34b052844928 --- /dev/null +++ b/src/sentry/workflow_engine/handlers/condition/group_state_handlers.py @@ -0,0 +1,27 @@ +from typing import Any + +from sentry.workflow_engine.models.data_condition import Condition +from sentry.workflow_engine.registry import condition_handler_registry +from sentry.workflow_engine.types import DataConditionHandler, WorkflowJob + + +@condition_handler_registry.register(Condition.REGRESSED_EVENT) +class RegressedEventConditionHandler(DataConditionHandler[WorkflowJob]): + @staticmethod + def evaluate_value(job: WorkflowJob, comparison: Any) -> bool: + state = job.get("group_state", None) + if state is None: + return False + + return state["is_regression"] == comparison + + +@condition_handler_registry.register(Condition.REAPPEARED_EVENT) +class ReappearedEventConditionHandler(DataConditionHandler[WorkflowJob]): + @staticmethod + def evaluate_value(job: WorkflowJob, comparison: Any) -> bool: + has_reappeared = job.get("has_reappeared", None) + if has_reappeared is None: + return False + + return has_reappeared == comparison diff --git a/src/sentry/workflow_engine/models/action.py b/src/sentry/workflow_engine/models/action.py index d6b90a7fa8846d..efc7a1e3dabd9f 100644 --- a/src/sentry/workflow_engine/models/action.py +++ b/src/sentry/workflow_engine/models/action.py @@ -7,10 +7,9 @@ from sentry.backup.scopes import RelocationScope from sentry.db.models import DefaultFieldsModel, region_silo_model, sane_repr from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey -from sentry.eventstore.models import GroupEvent from sentry.notifications.models.notificationaction import ActionTarget from sentry.workflow_engine.registry import action_handler_registry -from sentry.workflow_engine.types import ActionHandler +from sentry.workflow_engine.types import ActionHandler, WorkflowJob if TYPE_CHECKING: from sentry.workflow_engine.models import Detector @@ -72,7 +71,7 @@ def get_handler(self) -> ActionHandler: action_type = Action.Type(self.type) return action_handler_registry.get(action_type) - def trigger(self, evt: GroupEvent, detector: Detector) -> None: + def trigger(self, job: WorkflowJob, detector: Detector) -> None: # get the handler for the action type handler = self.get_handler() - handler.execute(evt, self, detector) + handler.execute(job, self, detector) diff --git a/src/sentry/workflow_engine/models/data_condition.py b/src/sentry/workflow_engine/models/data_condition.py index 27a36d70ddcb40..8180723466faac 100644 --- a/src/sentry/workflow_engine/models/data_condition.py +++ b/src/sentry/workflow_engine/models/data_condition.py @@ -22,6 +22,8 @@ class Condition(models.TextChoices): NOT_EQUAL = "ne" EVENT_CREATED_BY_DETECTOR = "event_created_by_detector" EVENT_SEEN_COUNT = "event_seen_count" + REGRESSED_EVENT = "regressed_event" + REAPPEARED_EVENT = "reappeared_event" condition_ops = { diff --git a/src/sentry/workflow_engine/models/workflow.py b/src/sentry/workflow_engine/models/workflow.py index 829fe66f596273..4c25fabc175b26 100644 --- a/src/sentry/workflow_engine/models/workflow.py +++ b/src/sentry/workflow_engine/models/workflow.py @@ -8,9 +8,9 @@ from sentry.backup.scopes import RelocationScope from sentry.db.models import DefaultFieldsModel, FlexibleForeignKey, region_silo_model, sane_repr from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey -from sentry.eventstore.models import GroupEvent from sentry.models.owner_base import OwnerModel from sentry.workflow_engine.processors.data_condition_group import evaluate_condition_group +from sentry.workflow_engine.types import WorkflowJob from .json_config import JSONConfigBase @@ -53,7 +53,7 @@ class Meta: ) ] - def evaluate_trigger_conditions(self, evt: GroupEvent) -> bool: + def evaluate_trigger_conditions(self, job: WorkflowJob) -> bool: """ Evaluate the conditions for the workflow trigger and return if the evaluation was successful. If there aren't any workflow trigger conditions, the workflow is considered triggered. @@ -61,7 +61,7 @@ def evaluate_trigger_conditions(self, evt: GroupEvent) -> bool: if self.when_condition_group is None: return True - evaluation, _ = evaluate_condition_group(self.when_condition_group, evt) + evaluation, _ = evaluate_condition_group(self.when_condition_group, job) return evaluation diff --git a/src/sentry/workflow_engine/processors/action.py b/src/sentry/workflow_engine/processors/action.py index 0e57ee44441aea..bf03015bf37444 100644 --- a/src/sentry/workflow_engine/processors/action.py +++ b/src/sentry/workflow_engine/processors/action.py @@ -1,11 +1,11 @@ from sentry.db.models.manager.base_query_set import BaseQuerySet -from sentry.eventstore.models import GroupEvent from sentry.workflow_engine.models import Action, DataConditionGroup, Workflow from sentry.workflow_engine.processors.data_condition_group import evaluate_condition_group +from sentry.workflow_engine.types import WorkflowJob def evaluate_workflow_action_filters( - workflows: set[Workflow], evt: GroupEvent + workflows: set[Workflow], job: WorkflowJob ) -> BaseQuerySet[Action]: filtered_action_groups: set[DataConditionGroup] = set() @@ -17,7 +17,7 @@ def evaluate_workflow_action_filters( ).distinct() for action_condition in action_conditions: - evaluation, result = evaluate_condition_group(action_condition, evt) + evaluation, result = evaluate_condition_group(action_condition, job) if evaluation: filtered_action_groups.add(action_condition) diff --git a/src/sentry/workflow_engine/processors/detector.py b/src/sentry/workflow_engine/processors/detector.py index ba7c6e9718cacd..b0a038fdbcb5c6 100644 --- a/src/sentry/workflow_engine/processors/detector.py +++ b/src/sentry/workflow_engine/processors/detector.py @@ -2,19 +2,19 @@ import logging -from sentry.eventstore.models import GroupEvent from sentry.issues.grouptype import ErrorGroupType from sentry.issues.issue_occurrence import IssueOccurrence from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka from sentry.workflow_engine.handlers.detector import DetectorEvaluationResult from sentry.workflow_engine.models import DataPacket, Detector -from sentry.workflow_engine.types import DetectorGroupKey +from sentry.workflow_engine.types import DetectorGroupKey, WorkflowJob logger = logging.getLogger(__name__) # TODO - cache these by evt.group_id? :thinking: -def get_detector_by_event(evt: GroupEvent) -> Detector: +def get_detector_by_event(job: WorkflowJob) -> Detector: + evt = job["event"] issue_occurrence = evt.occurrence if issue_occurrence is None: diff --git a/src/sentry/workflow_engine/processors/workflow.py b/src/sentry/workflow_engine/processors/workflow.py index effc18173780a6..be2fe0f88e4e92 100644 --- a/src/sentry/workflow_engine/processors/workflow.py +++ b/src/sentry/workflow_engine/processors/workflow.py @@ -2,26 +2,26 @@ import sentry_sdk -from sentry.eventstore.models import GroupEvent from sentry.utils import metrics from sentry.workflow_engine.models import Detector, Workflow from sentry.workflow_engine.processors.action import evaluate_workflow_action_filters from sentry.workflow_engine.processors.detector import get_detector_by_event +from sentry.workflow_engine.types import WorkflowJob logger = logging.getLogger(__name__) -def evaluate_workflow_triggers(workflows: set[Workflow], evt: GroupEvent) -> set[Workflow]: +def evaluate_workflow_triggers(workflows: set[Workflow], job: WorkflowJob) -> set[Workflow]: triggered_workflows: set[Workflow] = set() for workflow in workflows: - if workflow.evaluate_trigger_conditions(evt): + if workflow.evaluate_trigger_conditions(job): triggered_workflows.add(workflow) return triggered_workflows -def process_workflows(evt: GroupEvent) -> set[Workflow]: +def process_workflows(job: WorkflowJob) -> set[Workflow]: """ This method will get the detector based on the event, and then gather the associated workflows. Next, it will evaluate the "when" (or trigger) conditions for each workflow, if the conditions are met, @@ -31,19 +31,19 @@ def process_workflows(evt: GroupEvent) -> set[Workflow]: """ # Check to see if the GroupEvent has an issue occurrence try: - detector = get_detector_by_event(evt) + detector = get_detector_by_event(job) except Detector.DoesNotExist: metrics.incr("workflow_engine.process_workflows.error") - logger.exception("Detector not found for event", extra={"event_id": evt.event_id}) + logger.exception("Detector not found for event", extra={"event_id": job["event"].event_id}) return set() # Get the workflows, evaluate the when_condition_group, finally evaluate the actions for workflows that are triggered workflows = set(Workflow.objects.filter(detectorworkflow__detector_id=detector.id).distinct()) - triggered_workflows = evaluate_workflow_triggers(workflows, evt) - actions = evaluate_workflow_action_filters(triggered_workflows, evt) + triggered_workflows = evaluate_workflow_triggers(workflows, job) + actions = evaluate_workflow_action_filters(triggered_workflows, job) with sentry_sdk.start_span(op="workflow_engine.process_workflows.trigger_actions"): for action in actions: - action.trigger(evt, detector) + action.trigger(job, detector) return triggered_workflows diff --git a/src/sentry/workflow_engine/types.py b/src/sentry/workflow_engine/types.py index ce87257a13ad5c..b8add37097e204 100644 --- a/src/sentry/workflow_engine/types.py +++ b/src/sentry/workflow_engine/types.py @@ -1,12 +1,13 @@ from __future__ import annotations from enum import IntEnum -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar from sentry.types.group import PriorityLevel if TYPE_CHECKING: from sentry.eventstore.models import GroupEvent + from sentry.eventstream.base import GroupState from sentry.workflow_engine.models import Action, Detector T = TypeVar("T") @@ -28,9 +29,21 @@ class DetectorPriorityLevel(IntEnum): ProcessedDataConditionResult = tuple[bool, list[DataConditionResult]] +class EventJob(TypedDict): + event: GroupEvent + + +class WorkflowJob(EventJob, total=False): + group_state: GroupState + is_reprocessed: bool + has_reappeared: bool + has_alert: bool + has_escalated: bool + + class ActionHandler: @staticmethod - def execute(group_event: GroupEvent, action: Action, detector: Detector) -> None: + def execute(job: WorkflowJob, action: Action, detector: Detector) -> None: raise NotImplementedError diff --git a/tests/sentry/workflow_engine/models/test_workflow.py b/tests/sentry/workflow_engine/models/test_workflow.py index c00edd89a02432..8a9d1e841a4b9c 100644 --- a/tests/sentry/workflow_engine/models/test_workflow.py +++ b/tests/sentry/workflow_engine/models/test_workflow.py @@ -1,3 +1,4 @@ +from sentry.workflow_engine.types import WorkflowJob from tests.sentry.workflow_engine.test_base import BaseWorkflowTest @@ -8,21 +9,22 @@ def setUp(self): ) self.data_condition = self.data_condition_group.conditions.first() self.group, self.event, self.group_event = self.create_group_event() + self.job = WorkflowJob({"event": self.group_event}) def test_evaluate_trigger_conditions__condition_new_event__True(self): - evaluation = self.workflow.evaluate_trigger_conditions(self.group_event) + evaluation = self.workflow.evaluate_trigger_conditions(self.job) assert evaluation is True def test_evaluate_trigger_conditions__condition_new_event__False(self): # Update event to have been seen before self.group_event.group.times_seen = 5 - evaluation = self.workflow.evaluate_trigger_conditions(self.group_event) + evaluation = self.workflow.evaluate_trigger_conditions(self.job) assert evaluation is False def test_evaluate_trigger_conditions__no_conditions(self): self.workflow.when_condition_group = None self.workflow.save() - evaluation = self.workflow.evaluate_trigger_conditions(self.group_event) + evaluation = self.workflow.evaluate_trigger_conditions(self.job) assert evaluation is True diff --git a/tests/sentry/workflow_engine/processors/test_action.py b/tests/sentry/workflow_engine/processors/test_action.py index 7bc44d77abc61c..72534ccad5fedb 100644 --- a/tests/sentry/workflow_engine/processors/test_action.py +++ b/tests/sentry/workflow_engine/processors/test_action.py @@ -1,5 +1,6 @@ from sentry.workflow_engine.models.data_condition import Condition from sentry.workflow_engine.processors.action import evaluate_workflow_action_filters +from sentry.workflow_engine.types import WorkflowJob from tests.sentry.workflow_engine.test_base import BaseWorkflowTest @@ -17,9 +18,10 @@ def setUp(self): self.group, self.event, self.group_event = self.create_group_event( occurrence=self.build_occurrence(evidence_data={"detector_id": self.detector.id}) ) + self.job = WorkflowJob({"event": self.group_event}) def test_basic__no_filter(self): - triggered_actions = evaluate_workflow_action_filters({self.workflow}, self.group_event) + triggered_actions = evaluate_workflow_action_filters({self.workflow}, self.job) assert set(triggered_actions) == {self.action} def test_basic__with_filter__passes(self): @@ -30,7 +32,7 @@ def test_basic__with_filter__passes(self): condition_result=True, ) - triggered_actions = evaluate_workflow_action_filters({self.workflow}, self.group_event) + triggered_actions = evaluate_workflow_action_filters({self.workflow}, self.job) assert set(triggered_actions) == {self.action} def test_basic__with_filter__filtered(self): @@ -41,5 +43,5 @@ def test_basic__with_filter__filtered(self): comparison=self.detector.id + 1, ) - triggered_actions = evaluate_workflow_action_filters({self.workflow}, self.group_event) + triggered_actions = evaluate_workflow_action_filters({self.workflow}, self.job) assert not triggered_actions diff --git a/tests/sentry/workflow_engine/processors/test_workflow.py b/tests/sentry/workflow_engine/processors/test_workflow.py index 7f75494c571252..9c1e39848324d3 100644 --- a/tests/sentry/workflow_engine/processors/test_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_workflow.py @@ -1,9 +1,11 @@ from unittest import mock +from sentry.eventstream.base import GroupState from sentry.issues.grouptype import ErrorGroupType from sentry.workflow_engine.models import DataConditionGroup from sentry.workflow_engine.models.data_condition import Condition from sentry.workflow_engine.processors.workflow import evaluate_workflow_triggers, process_workflows +from sentry.workflow_engine.types import WorkflowJob from tests.sentry.workflow_engine.test_base import BaseWorkflowTest @@ -25,24 +27,50 @@ def setUp(self): ) self.group, self.event, self.group_event = self.create_group_event() + self.job = WorkflowJob( + { + "event": self.group_event, + "group_state": GroupState( + id=1, is_new=False, is_regression=True, is_new_group_environment=False + ), + } + ) def test_error_event(self): - triggered_workflows = process_workflows(self.group_event) + triggered_workflows = process_workflows(self.job) assert triggered_workflows == {self.error_workflow} def test_issue_occurrence_event(self): issue_occurrence = self.build_occurrence(evidence_data={"detector_id": self.detector.id}) self.group_event.occurrence = issue_occurrence - triggered_workflows = process_workflows(self.group_event) + triggered_workflows = process_workflows(self.job) assert triggered_workflows == {self.workflow} + def test_regressed_event(self): + dcg = self.create_data_condition_group() + self.create_data_condition( + type=Condition.REGRESSED_EVENT, + comparison=True, + condition_result=True, + condition_group=dcg, + ) + + workflow = self.create_workflow(when_condition_group=dcg) + self.create_detector_workflow( + detector=self.error_detector, + workflow=workflow, + ) + + triggered_workflows = process_workflows(self.job) + assert triggered_workflows == {self.error_workflow, workflow} + def test_no_detector(self): self.group_event.occurrence = self.build_occurrence(evidence_data={}) with mock.patch("sentry.workflow_engine.processors.workflow.logger") as mock_logger: with mock.patch("sentry.workflow_engine.processors.workflow.metrics") as mock_metrics: - triggered_workflows = process_workflows(self.group_event) + triggered_workflows = process_workflows(self.job) assert not triggered_workflows @@ -66,13 +94,14 @@ def setUp(self): self.group, self.event, self.group_event = self.create_group_event( occurrence=occurrence, ) + self.job = WorkflowJob({"event": self.group_event}) def test_workflow_trigger(self): - triggered_workflows = evaluate_workflow_triggers({self.workflow}, self.group_event) + triggered_workflows = evaluate_workflow_triggers({self.workflow}, self.job) assert triggered_workflows == {self.workflow} def test_no_workflow_trigger(self): - triggered_workflows = evaluate_workflow_triggers(set(), self.group_event) + triggered_workflows = evaluate_workflow_triggers(set(), self.job) assert not triggered_workflows def test_workflow_many_filters(self): @@ -86,7 +115,7 @@ def test_workflow_many_filters(self): condition_result=75, ) - triggered_workflows = evaluate_workflow_triggers({self.workflow}, self.group_event) + triggered_workflows = evaluate_workflow_triggers({self.workflow}, self.job) assert triggered_workflows == {self.workflow} def test_workflow_filterd_out(self): @@ -99,13 +128,11 @@ def test_workflow_filterd_out(self): comparison=self.detector.id + 1, ) - triggered_workflows = evaluate_workflow_triggers({self.workflow}, self.group_event) + triggered_workflows = evaluate_workflow_triggers({self.workflow}, self.job) assert not triggered_workflows def test_many_workflows(self): workflow_two, _, _, _ = self.create_detector_and_workflow(name_prefix="two") - triggered_workflows = evaluate_workflow_triggers( - {self.workflow, workflow_two}, self.group_event - ) + triggered_workflows = evaluate_workflow_triggers({self.workflow, workflow_two}, self.job) assert triggered_workflows == {self.workflow, workflow_two} From 8d5d0b5bfcd20f34a3b21ff5f10ec896ce421848 Mon Sep 17 00:00:00 2001 From: Gabe Villalobos Date: Fri, 20 Dec 2024 12:01:47 -0800 Subject: [PATCH 427/757] fix(ecosystem): Track metrics for issue detail ticket creation (#82436) --- .../endpoints/group_integration_details.py | 26 +++++-- .../project_management/metrics.py | 4 +- .../test_group_integration_details.py | 67 ++++++++++++++++--- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/sentry/api/endpoints/group_integration_details.py b/src/sentry/api/endpoints/group_integration_details.py index 7663f366a458c0..fdc22c9e9e9601 100644 --- a/src/sentry/api/endpoints/group_integration_details.py +++ b/src/sentry/api/endpoints/group_integration_details.py @@ -266,12 +266,26 @@ def post(self, request: Request, group, integration_id) -> Response: ) installation = integration.get_installation(organization_id=organization_id) - try: - data = installation.create_issue(request.data) - except IntegrationFormError as exc: - return Response(exc.field_errors, status=400) - except IntegrationError as e: - return Response({"non_field_errors": [str(e)]}, status=400) + + with ProjectManagementEvent( + action_type=ProjectManagementActionType.CREATE_EXTERNAL_ISSUE_VIA_ISSUE_DETAIL, + integration=integration, + ).capture() as lifecycle: + lifecycle.add_extras( + { + "provider": integration.provider, + "integration_id": integration.id, + } + ) + + try: + data = installation.create_issue(request.data) + except IntegrationFormError as exc: + lifecycle.record_halt(exc) + return Response(exc.field_errors, status=400) + except IntegrationError as e: + lifecycle.record_failure(e) + return Response({"non_field_errors": [str(e)]}, status=400) external_issue_key = installation.make_external_key(data) external_issue, created = ExternalIssue.objects.get_or_create( diff --git a/src/sentry/integrations/project_management/metrics.py b/src/sentry/integrations/project_management/metrics.py index 782bb9c2f4021d..09c113140409c4 100644 --- a/src/sentry/integrations/project_management/metrics.py +++ b/src/sentry/integrations/project_management/metrics.py @@ -15,9 +15,7 @@ class ProjectManagementActionType(StrEnum): OUTBOUND_STATUS_SYNC = "outbound_status_sync" INBOUND_STATUS_SYNC = "inbound_status_sync" LINK_EXTERNAL_ISSUE = "link_external_issue" - - def __str__(self): - return self.value.lower() + CREATE_EXTERNAL_ISSUE_VIA_ISSUE_DETAIL = "create_external_issue_via_issue_detail" class ProjectManagementHaltReason(StrEnum): diff --git a/tests/sentry/api/endpoints/test_group_integration_details.py b/tests/sentry/api/endpoints/test_group_integration_details.py index 2f71f06150e443..202ce02a3594fb 100644 --- a/tests/sentry/api/endpoints/test_group_integration_details.py +++ b/tests/sentry/api/endpoints/test_group_integration_details.py @@ -1,3 +1,4 @@ +from typing import Any from unittest import mock from django.db.utils import IntegrityError @@ -45,6 +46,18 @@ def setUp(self): ) self.group = self.event.group + def assert_metric_recorded( + self, mock_metric_method, expected_exc: type[Exception], exc_args: Any | None = None + ): + + assert mock_metric_method.call_count == 1 + mock_metric_method.assert_called_with(mock.ANY) + call_arg = mock_metric_method.call_args_list[0][0][0] + assert isinstance(call_arg, expected_exc) + + if exc_args: + assert call_arg.args == (exc_args,) + def test_simple_get_link(self): self.login_as(user=self.user) org = self.organization @@ -317,21 +330,18 @@ def test_put_group_after_link_raises_exception( response = self.client.put(path, data={"externalIssue": "APP-123"}) assert response.status_code == 400 - mock_record_halt.assert_called_once_with(mock.ANY) - - call_arg = mock_record_halt.call_args_list[0][0][0] - assert isinstance(call_arg, IntegrationFormError) - assert call_arg.field_errors == {"foo": "Invalid foo provided"} + self.assert_metric_recorded( + mock_record_halt, IntegrationFormError, str({"foo": "Invalid foo provided"}) + ) # Test with IntegrationError mock_after_link_issue.side_effect = raise_integration_error response = self.client.put(path, data={"externalIssue": "APP-123"}) assert response.status_code == 400 - mock_record_failure.assert_called_once_with(mock.ANY) - call_arg = mock_record_failure.call_args_list[0][0][0] - assert isinstance(call_arg, IntegrationError) - assert call_arg.args == ("The whole operation was invalid",) + self.assert_metric_recorded( + mock_record_failure, IntegrationError, "The whole operation was invalid" + ) def test_put_feature_disabled(self): self.login_as(user=self.user) @@ -353,7 +363,8 @@ def test_put_feature_disabled(self): assert response.status_code == 400 assert response.data["detail"] == "Your organization does not have access to this feature." - def test_simple_post(self): + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") + def test_simple_post(self, mock_record_event): self.login_as(user=self.user) org = self.organization group = self.create_group() @@ -403,6 +414,42 @@ def test_simple_post(self): "new": True, } + mock_record_event.assert_called_with(EventLifecycleOutcome.SUCCESS, None) + + @mock.patch.object(ExampleIntegration, "create_issue") + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_halt") + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_failure") + def test_post_raises_issue_creation_exception( + self, mock_record_failure, mock_record_halt, mock_create_issue + ): + self.login_as(user=self.user) + org = self.organization + group = self.create_group() + integration = self.create_integration( + organization=org, provider="example", name="Example", external_id="example:1" + ) + + path = f"/api/0/issues/{group.id}/integrations/{integration.id}/" + with self.feature("organizations:integrations-issue-basic"): + mock_create_issue.side_effect = raise_integration_error + response = self.client.post(path, data={}) + assert response.status_code == 400 + + assert mock_record_failure.call_count == 1 + + self.assert_metric_recorded( + mock_record_failure, IntegrationError, "The whole operation was invalid" + ) + + mock_create_issue.side_effect = raise_integration_form_error + + response = self.client.post(path, data={}) + assert response.status_code == 400 + + self.assert_metric_recorded( + mock_record_halt, IntegrationFormError, str({"foo": "Invalid foo provided"}) + ) + def test_post_feature_disabled(self): self.login_as(user=self.user) org = self.organization From 8f92c83f07189305eb004312d98f8cb645337c65 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:39:09 -0500 Subject: [PATCH 428/757] ref(insights): Split out `getAxisMaxForPercentageSeries` (#82493) Moves this helper into its own file. --- .../getAxisMaxForPercentageSeries.spec.tsx | 46 ++++++++++++++++++ .../utils/getAxisMaxForPercentageSeries.tsx | 16 +++++++ .../charts/responseRateChart.spec.tsx | 48 ------------------- .../components/charts/responseRateChart.tsx | 16 +------ 4 files changed, 63 insertions(+), 63 deletions(-) create mode 100644 static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.spec.tsx create mode 100644 static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.tsx delete mode 100644 static/app/views/insights/http/components/charts/responseRateChart.spec.tsx diff --git a/static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.spec.tsx b/static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.spec.tsx new file mode 100644 index 00000000000000..106a5de80bb40a --- /dev/null +++ b/static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.spec.tsx @@ -0,0 +1,46 @@ +import type {Series} from 'sentry/types/echarts'; +import {getAxisMaxForPercentageSeries} from 'sentry/views/insights/common/utils/getAxisMaxForPercentageSeries'; + +describe('getAxisMaxForPercentageSeries', function () { + it('Returns nearest significant digit for small series', function () { + expect(getAxisMaxForPercentageSeries([HTTP_5XX_SERIES])).toBeCloseTo(0.0001); + }); + + it('Returns 1 for larger series', function () { + expect(getAxisMaxForPercentageSeries([HTTP_2XX_SERIES])).toBeCloseTo(1); + }); + + it('Takes all series into account', function () { + expect(getAxisMaxForPercentageSeries([HTTP_2XX_SERIES, HTTP_5XX_SERIES])).toBeCloseTo( + 1 + ); + }); +}); + +const HTTP_2XX_SERIES: Series = { + seriesName: '5XX', + data: [ + { + value: 0.9812, + name: '2024-03-12T13:30:00-04:00', + }, + { + value: 0.9992, + name: '2024-03-12T14:00:00-04:00', + }, + ], +}; + +const HTTP_5XX_SERIES: Series = { + seriesName: '5XX', + data: [ + { + value: 0.00006713689346852019, + name: '2024-03-12T13:30:00-04:00', + }, + { + value: 0.000041208717375685543, + name: '2024-03-12T14:00:00-04:00', + }, + ], +}; diff --git a/static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.tsx b/static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.tsx new file mode 100644 index 00000000000000..ca9c94fac255e7 --- /dev/null +++ b/static/app/views/insights/common/utils/getAxisMaxForPercentageSeries.tsx @@ -0,0 +1,16 @@ +import type {Series} from 'sentry/types/echarts'; + +/** + * Given a set of `Series` objects that contain percentage data (i.e., every item in `data` has a `value` between 0 and 1) return an appropriate max value. + * + * e.g., for series with very low values (like 5xx rates), it rounds to the nearest significant digit. For other cases, it limits it to 100 + */ +export function getAxisMaxForPercentageSeries(series: Series[]): number { + const maxValue = Math.max( + ...series.map(serie => Math.max(...serie.data.map(datum => datum.value))) + ); + + const maxNumberOfDecimalPlaces = Math.ceil(Math.min(0, Math.log10(maxValue))); + + return Math.pow(10, maxNumberOfDecimalPlaces); +} diff --git a/static/app/views/insights/http/components/charts/responseRateChart.spec.tsx b/static/app/views/insights/http/components/charts/responseRateChart.spec.tsx deleted file mode 100644 index 423aff6f1f67d3..00000000000000 --- a/static/app/views/insights/http/components/charts/responseRateChart.spec.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type {Series} from 'sentry/types/echarts'; -import {getAxisMaxForPercentageSeries} from 'sentry/views/insights/http/components/charts/responseRateChart'; - -describe('ResponseRateChart', function () { - describe('getAxisMaxForPercentageSeries', function () { - it('Returns nearest significant digit for small series', function () { - expect(getAxisMaxForPercentageSeries([HTTP_5XX_SERIES])).toBeCloseTo(0.0001); - }); - - it('Returns 1 for larger series', function () { - expect(getAxisMaxForPercentageSeries([HTTP_2XX_SERIES])).toBeCloseTo(1); - }); - - it('Takes all series into account', function () { - expect( - getAxisMaxForPercentageSeries([HTTP_2XX_SERIES, HTTP_5XX_SERIES]) - ).toBeCloseTo(1); - }); - }); -}); - -const HTTP_2XX_SERIES: Series = { - seriesName: '5XX', - data: [ - { - value: 0.9812, - name: '2024-03-12T13:30:00-04:00', - }, - { - value: 0.9992, - name: '2024-03-12T14:00:00-04:00', - }, - ], -}; - -const HTTP_5XX_SERIES: Series = { - seriesName: '5XX', - data: [ - { - value: 0.00006713689346852019, - name: '2024-03-12T13:30:00-04:00', - }, - { - value: 0.000041208717375685543, - name: '2024-03-12T14:00:00-04:00', - }, - ], -}; diff --git a/static/app/views/insights/http/components/charts/responseRateChart.tsx b/static/app/views/insights/http/components/charts/responseRateChart.tsx index a5fd79dc09d754..0cfdf610aeda52 100644 --- a/static/app/views/insights/http/components/charts/responseRateChart.tsx +++ b/static/app/views/insights/http/components/charts/responseRateChart.tsx @@ -7,6 +7,7 @@ import { } from 'sentry/views/insights/colors'; import Chart, {ChartType} from 'sentry/views/insights/common/components/chart'; import ChartPanel from 'sentry/views/insights/common/components/chartPanel'; +import {getAxisMaxForPercentageSeries} from 'sentry/views/insights/common/utils/getAxisMaxForPercentageSeries'; import {DataTitles} from 'sentry/views/insights/common/views/spans/types'; import {CHART_HEIGHT} from 'sentry/views/insights/http/settings'; @@ -46,18 +47,3 @@ export function ResponseRateChart({series, isLoading, error}: Props) { ); } - -/** - * Given a set of `Series` objects that contain percentage data (i.e., every item in `data` has a `value` between 0 and 1) return an appropriate max value. - * - * e.g., for series with very low values (like 5xx rates), it rounds to the nearest significant digit. For other cases, it limits it to 100 - */ -export function getAxisMaxForPercentageSeries(series: Series[]): number { - const maxValue = Math.max( - ...series.map(serie => Math.max(...serie.data.map(datum => datum.value))) - ); - - const maxNumberOfDecimalPlaces = Math.ceil(Math.min(0, Math.log10(maxValue))); - - return Math.pow(10, maxNumberOfDecimalPlaces); -} From 28ef2bd768c08da2a1a8615e432fec5f9bc03def Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Fri, 20 Dec 2024 13:13:38 -0800 Subject: [PATCH 429/757] chore(various): Fix linter warnings (#82494) This fixes a number of small issues my linter has been yelling at me about, which I've come across in the last handful of weeks - things like unused variables, shadowing built-ins, etc. No behavior changes just tiny refactors to clear out some noise. --- src/sentry/event_manager.py | 4 +- src/sentry/grouping/enhancer/__init__.py | 2 +- src/sentry/grouping/enhancer/actions.py | 4 +- .../grouping/fingerprinting/__init__.py | 13 +--- src/sentry/grouping/strategies/legacy.py | 4 +- src/sentry/grouping/strategies/newstyle.py | 5 +- src/sentry/utils/tag_normalization.py | 2 +- .../event_manager/test_event_manager.py | 6 +- tests/sentry/grouping/ingest/test_seer.py | 76 +++++++++---------- .../sentry/grouping/test_parameterization.py | 10 +-- .../test_group_similar_issues_embeddings.py | 20 ++--- .../tasks/test_detect_broken_monitor_envs.py | 2 +- 12 files changed, 69 insertions(+), 79 deletions(-) diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index bc932821517af3..790433a11f895a 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -2376,7 +2376,7 @@ def save_attachment( return from sentry import ratelimits as ratelimiter - is_limited, num_requests, reset_time = ratelimiter.backend.is_limited_with_value( + is_limited, _, _ = ratelimiter.backend.is_limited_with_value( key="event_attachment.save_per_sec", limit=options.get("sentry.save-event-attachments.project-per-sec-limit"), project=project, @@ -2384,7 +2384,7 @@ def save_attachment( ) rate_limit_tag = "per_sec" if not is_limited: - is_limited, num_requests, reset_time = ratelimiter.backend.is_limited_with_value( + is_limited, _, _ = ratelimiter.backend.is_limited_with_value( key="event_attachment.save_5_min", limit=options.get("sentry.save-event-attachments.project-per-5-minute-limit"), project=project, diff --git a/src/sentry/grouping/enhancer/__init__.py b/src/sentry/grouping/enhancer/__init__.py index 123c84907c3d61..1966ab111f6f66 100644 --- a/src/sentry/grouping/enhancer/__init__.py +++ b/src/sentry/grouping/enhancer/__init__.py @@ -265,7 +265,7 @@ def loads(cls, data) -> Enhancements: @classmethod @sentry_sdk.tracing.trace - def from_config_string(self, s, bases=None, id=None) -> Enhancements: + def from_config_string(cls, s, bases=None, id=None) -> Enhancements: rust_enhancements = parse_rust_enhancements("config_string", s) rules = parse_enhancements(s) diff --git a/src/sentry/grouping/enhancer/actions.py b/src/sentry/grouping/enhancer/actions.py index 10911971a0b23f..988a514b2fd576 100644 --- a/src/sentry/grouping/enhancer/actions.py +++ b/src/sentry/grouping/enhancer/actions.py @@ -56,8 +56,8 @@ def is_updater(self) -> bool: def _from_config_structure(cls, val, version: int): if isinstance(val, list): return VarAction(val[0], val[1]) - flag, range = REVERSE_ACTION_FLAGS[val >> ACTION_BITSIZE] - return FlagAction(ACTIONS[val & 0xF], flag, range) + flag, range_direction = REVERSE_ACTION_FLAGS[val >> ACTION_BITSIZE] + return FlagAction(ACTIONS[val & 0xF], flag, range_direction) class FlagAction(EnhancementAction): diff --git a/src/sentry/grouping/fingerprinting/__init__.py b/src/sentry/grouping/fingerprinting/__init__.py index 05c8e8f68d0dfa..003083263266b8 100644 --- a/src/sentry/grouping/fingerprinting/__init__.py +++ b/src/sentry/grouping/fingerprinting/__init__.py @@ -452,20 +452,13 @@ def _positive_match(self, values: dict[str, Any]) -> bool: value = values.get(self.key) if value is None: return False - elif self.key == "package": + elif self.key in ["package", "release"]: if self._positive_path_match(value): return True - elif self.key == "family": + elif self.key in ["family", "sdk"]: flags = self.pattern.split(",") if "all" in flags or value in flags: return True - elif self.key == "sdk": - flags = self.pattern.split(",") - if "all" in flags or value in flags: - return True - elif self.key == "release": - if self._positive_path_match(value): - return True elif self.key == "app": ref_val = bool_from_string(self.pattern) if ref_val is not None and ref_val == value: @@ -591,7 +584,7 @@ def visit_fingerprinting_rules( in_header = True for child in children: if isinstance(child, str): - if in_header and child[:2] == "##": + if in_header and child.startswith("##"): changelog.append(child[2:].rstrip()) else: in_header = False diff --git a/src/sentry/grouping/strategies/legacy.py b/src/sentry/grouping/strategies/legacy.py index 4a3e60d4e12a72..d1cd5232fef73c 100644 --- a/src/sentry/grouping/strategies/legacy.py +++ b/src/sentry/grouping/strategies/legacy.py @@ -111,9 +111,9 @@ def is_recursion_legacy(frame1: Frame, frame2: Frame) -> bool: def remove_module_outliers_legacy(module: str, platform: str) -> tuple[str, str | None]: """Remove things that augment the module but really should not.""" if platform == "java": - if module[:35] == "sun.reflect.GeneratedMethodAccessor": + if module.startswith("sun.reflect.GeneratedMethodAccessor"): return "sun.reflect.GeneratedMethodAccessor", "removed reflection marker" - if module[:44] == "jdk.internal.reflect.GeneratedMethodAccessor": + if module.startswith("jdk.internal.reflect.GeneratedMethodAccessor"): return "jdk.internal.reflect.GeneratedMethodAccessor", "removed reflection marker" old_module = module module = _java_reflect_enhancer_re.sub(r"\1", module) diff --git a/src/sentry/grouping/strategies/newstyle.py b/src/sentry/grouping/strategies/newstyle.py index 35176ca06b536b..d32174de0bf638 100644 --- a/src/sentry/grouping/strategies/newstyle.py +++ b/src/sentry/grouping/strategies/newstyle.py @@ -152,7 +152,6 @@ def get_filename_component( new_filename = _java_assist_enhancer_re.sub(r"\1", filename) if new_filename != filename: filename_component.update(values=[new_filename], hint="cleaned javassist parts") - filename = new_filename return filename_component @@ -176,11 +175,11 @@ def get_module_component( elif platform == "java": if "$$Lambda$" in module: module_component.update(contributes=False, hint="ignored java lambda") - if module[:35] == "sun.reflect.GeneratedMethodAccessor": + if module.startswith("sun.reflect.GeneratedMethodAccessor"): module_component.update( values=["sun.reflect.GeneratedMethodAccessor"], hint="removed reflection marker" ) - elif module[:44] == "jdk.internal.reflect.GeneratedMethodAccessor": + elif module.startswith("jdk.internal.reflect.GeneratedMethodAccessor"): module_component.update( values=["jdk.internal.reflect.GeneratedMethodAccessor"], hint="removed reflection marker", diff --git a/src/sentry/utils/tag_normalization.py b/src/sentry/utils/tag_normalization.py index 09724112f99bcb..bce6efa1b9047d 100644 --- a/src/sentry/utils/tag_normalization.py +++ b/src/sentry/utils/tag_normalization.py @@ -92,7 +92,7 @@ def normalize_sdk_tag(tag: str) -> str: # collapse tags other than JavaScript / Native to their top-level SDK - if not tag.split(".")[1] in {"javascript", "native"}: + if tag.split(".")[1] not in {"javascript", "native"}: tag = ".".join(tag.split(".", 2)[0:2]) if tag.split(".")[1] == "native": diff --git a/tests/sentry/event_manager/test_event_manager.py b/tests/sentry/event_manager/test_event_manager.py index 29960e70dd7f4d..78c2fa907b1528 100644 --- a/tests/sentry/event_manager/test_event_manager.py +++ b/tests/sentry/event_manager/test_event_manager.py @@ -1121,7 +1121,7 @@ def test_group_release_no_env(self) -> None: ).exists() # ensure we're not erroring on second creation - event = self.make_release_event("1.0", project_id) + self.make_release_event("1.0", project_id) def test_group_release_with_env(self) -> None: manager = EventManager(make_event(release="1.0", environment="prod", event_id="a" * 32)) @@ -1891,9 +1891,7 @@ def test_throws_when_matches_discarded_hash(self) -> None: with self.feature("organizations:event-attachments"): with self.tasks(): with pytest.raises(HashDiscarded): - event = manager.save( - self.project.id, cache_key=cache_key, has_attachments=True - ) + manager.save(self.project.id, cache_key=cache_key, has_attachments=True) assert mock_track_outcome.call_count == 3 diff --git a/tests/sentry/grouping/ingest/test_seer.py b/tests/sentry/grouping/ingest/test_seer.py index c57a2dd306ad60..e140991c3d5b19 100644 --- a/tests/sentry/grouping/ingest/test_seer.py +++ b/tests/sentry/grouping/ingest/test_seer.py @@ -240,19 +240,19 @@ def setUp(self) -> None: @patch("sentry.grouping.ingest.seer.get_similarity_data_from_seer", return_value=[]) def test_sends_expected_data_to_seer(self, mock_get_similarity_data: MagicMock) -> None: - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" new_event = Event( project_id=self.project.id, event_id="12312012112120120908201304152013", data={ - "title": f"{type}('{value}')", + "title": f"{error_type}('{error_value}')", "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { @@ -275,7 +275,7 @@ def test_sends_expected_data_to_seer(self, mock_get_similarity_data: MagicMock) "event_id": new_event.event_id, "hash": new_event.get_primary_hash(), "project_id": self.project.id, - "stacktrace": f'{type}: {value}\n File "dogpark.py", function play_fetch\n {context_line}', + "stacktrace": f'{error_type}: {error_value}\n File "dogpark.py", function play_fetch\n {context_line}', "exception_type": "FailedToFetchError", "k": 1, "referrer": "ingest", @@ -353,19 +353,19 @@ def test_returns_no_grouphash_and_empty_metadata_if_empty_stacktrace( def test_too_many_frames( self, mock_metrics: Mock, mock_record_did_call_seer: MagicMock ) -> None: - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" new_event = Event( project_id=self.project.id, event_id="22312012112120120908201304152013", data={ - "title": f"{type}('{value}')", + "title": f"{error_type}('{error_value}')", "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { @@ -401,19 +401,19 @@ def test_too_many_frames( @patch("sentry.seer.similarity.utils.record_did_call_seer_metric") def test_too_many_frames_allowed_platform(self, mock_record_did_call_seer: MagicMock) -> None: - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" new_event = Event( project_id=self.project.id, event_id="22312012112120120908201304152013", data={ - "title": f"{type}('{value}')", + "title": f"{error_type}('{error_value}')", "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { @@ -453,19 +453,19 @@ def test_valid_maybe_check_seer_for_matching_group_hash( ) -> None: self.project.update_option("sentry:similarity_backfill_completed", int(time())) - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" new_event = Event( project_id=self.project.id, event_id="12312012112120120908201304152013", data={ - "title": f"{type}('{value}')", + "title": f"{error_type}('{error_value}')", "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { @@ -494,7 +494,7 @@ def test_valid_maybe_check_seer_for_matching_group_hash( "event_id": new_event.event_id, "hash": new_event.get_primary_hash(), "project_id": self.project.id, - "stacktrace": f'{type}: {value}\n File "dogpark.py", function play_fetch\n {context_line}', + "stacktrace": f'{error_type}: {error_value}\n File "dogpark.py", function play_fetch\n {context_line}', "exception_type": "FailedToFetchError", "k": 1, "referrer": "ingest", @@ -513,19 +513,19 @@ def test_too_many_frames_maybe_check_seer_for_matching_group_hash( ) -> None: self.project.update_option("sentry:similarity_backfill_completed", int(time())) - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" new_event = Event( project_id=self.project.id, event_id="22312012112120120908201304152013", data={ - "title": f"{type}('{value}')", + "title": f"{error_type}('{error_value}')", "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { @@ -572,19 +572,19 @@ def test_too_many_frames_maybe_check_seer_for_matching_group_hash_bypassed_platf ) -> None: self.project.update_option("sentry:similarity_backfill_completed", int(time())) - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" new_event = Event( project_id=self.project.id, event_id="22312012112120120908201304152013", data={ - "title": f"{type}('{value}')", + "title": f"{error_type}('{error_value}')", "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { diff --git a/tests/sentry/grouping/test_parameterization.py b/tests/sentry/grouping/test_parameterization.py index 8c8d094368a8fa..05b128439a58ed 100644 --- a/tests/sentry/grouping/test_parameterization.py +++ b/tests/sentry/grouping/test_parameterization.py @@ -241,8 +241,8 @@ def test_parameterize_regex_experiment(): regex_pattern_keys=(), experiments=(FooExperiment,), ) - input = "blah foobarbaz fooooo" - normalized = parameterizer.parameterize_all(input) + input_str = "blah foobarbaz fooooo" + normalized = parameterizer.parameterize_all(input_str) assert normalized == "blah barbaz ooo" assert len(parameterizer.get_successful_experiments()) == 1 assert parameterizer.get_successful_experiments()[0] == FooExperiment @@ -261,9 +261,9 @@ def test_parameterize_regex_experiment_cached_compiled(): regex_pattern_keys=(), experiments=(FooExperiment,), ) - input = "blah foobarbaz fooooo" - _ = parameterizer.parameterize_all(input) - _ = parameterizer.parameterize_all(input) + input_str = "blah foobarbaz fooooo" + _ = parameterizer.parameterize_all(input_str) + _ = parameterizer.parameterize_all(input_str) mocked_pattern.assert_called_once() diff --git a/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py b/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py index ac20568cfd9ec2..742b7f783736c5 100644 --- a/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py +++ b/tests/sentry/issues/endpoints/test_group_similar_issues_embeddings.py @@ -686,15 +686,15 @@ def test_obeys_useReranking_query_param(self, mock_seer_request: mock.MagicMock) mock_seer_request.reset_mock() def test_too_many_system_frames(self) -> None: - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" error_data = { "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { @@ -719,15 +719,15 @@ def test_too_many_system_frames(self) -> None: assert response.data == [] def test_no_filename_or_module(self) -> None: - type = "FailedToFetchError" - value = "Charlie didn't bring the ball back" - context_line = f"raise {type}('{value}')" + error_type = "FailedToFetchError" + error_value = "Charlie didn't bring the ball back" + context_line = f"raise {error_type}('{error_value}')" error_data = { "exception": { "values": [ { - "type": type, - "value": value, + "type": error_type, + "value": error_value, "stacktrace": { "frames": [ { diff --git a/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py b/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py index d861f8c5ce875d..368a7ab6fe473b 100644 --- a/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py +++ b/tests/sentry/monitors/tasks/test_detect_broken_monitor_envs.py @@ -130,7 +130,7 @@ def test_does_not_create_broken_detection_insufficient_duration(self): grouphash=hash_from_values([uuid.uuid4()]), ) - for i in range(4): + for _ in range(4): MonitorCheckIn.objects.create( monitor=monitor, monitor_environment=monitor_environment, From 50688caa9a402ac7dc1f40b92374a1d241cb634a Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:21:03 -0500 Subject: [PATCH 430/757] ref: strptime -> fromisoformat in tests (#82488) --- .../sentry/api/endpoints/test_custom_rules.py | 14 +++--- .../api/endpoints/test_group_ai_autofix.py | 4 +- .../issues/test_escalating_issues_alg.py | 50 +++++++++---------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/tests/sentry/api/endpoints/test_custom_rules.py b/tests/sentry/api/endpoints/test_custom_rules.py index 1c7ba791b57ed8..6c3d42ae031456 100644 --- a/tests/sentry/api/endpoints/test_custom_rules.py +++ b/tests/sentry/api/endpoints/test_custom_rules.py @@ -10,7 +10,7 @@ UnsupportedSearchQueryReason, get_rule_condition, ) -from sentry.models.dynamicsampling import CUSTOM_RULE_DATE_FORMAT, CustomDynamicSamplingRule +from sentry.models.dynamicsampling import CustomDynamicSamplingRule from sentry.testutils.cases import APITestCase, TestCase @@ -206,8 +206,8 @@ def test_create(self): data = resp.data - start_date = datetime.strptime(data["startDate"], CUSTOM_RULE_DATE_FORMAT) - end_date = datetime.strptime(data["endDate"], CUSTOM_RULE_DATE_FORMAT) + start_date = datetime.fromisoformat(data["startDate"]) + end_date = datetime.fromisoformat(data["endDate"]) assert end_date - start_date == timedelta(days=2) projects = data["projects"] assert projects == [self.project.id] @@ -260,8 +260,8 @@ def test_updates_existing(self): data = resp.data rule_id = data["ruleId"] - start_date = datetime.strptime(data["startDate"], CUSTOM_RULE_DATE_FORMAT) - end_date = datetime.strptime(data["endDate"], CUSTOM_RULE_DATE_FORMAT) + start_date = datetime.fromisoformat(data["startDate"]) + end_date = datetime.fromisoformat(data["endDate"]) assert end_date - start_date == timedelta(days=2) request_data = { @@ -275,8 +275,8 @@ def test_updates_existing(self): assert resp.status_code == 200 data = resp.data - start_date = datetime.strptime(data["startDate"], CUSTOM_RULE_DATE_FORMAT) - end_date = datetime.strptime(data["endDate"], CUSTOM_RULE_DATE_FORMAT) + start_date = datetime.fromisoformat(data["startDate"]) + end_date = datetime.fromisoformat(data["endDate"]) assert end_date - start_date >= timedelta(days=2) projects = data["projects"] diff --git a/tests/sentry/api/endpoints/test_group_ai_autofix.py b/tests/sentry/api/endpoints/test_group_ai_autofix.py index 915274955569d3..568c5e13dbc92f 100644 --- a/tests/sentry/api/endpoints/test_group_ai_autofix.py +++ b/tests/sentry/api/endpoints/test_group_ai_autofix.py @@ -29,7 +29,7 @@ def test_ai_autofix_get_endpoint_with_autofix(self, mock_get_autofix_state): mock_get_autofix_state.return_value = AutofixState( run_id=123, request={"project_id": 456, "issue": {"id": 789}}, - updated_at=datetime.strptime("2023-07-18T12:00:00Z", "%Y-%m-%dT%H:%M:%SZ"), + updated_at=datetime.fromisoformat("2023-07-18T12:00:00Z"), status=AutofixStatus.PROCESSING, ) @@ -64,7 +64,7 @@ def test_ai_autofix_get_endpoint_repositories( mock_get_autofix_state.return_value = AutofixState( run_id=123, request={"project_id": 456, "issue": {"id": 789}}, - updated_at=datetime.strptime("2023-07-18T12:00:00Z", "%Y-%m-%dT%H:%M:%SZ"), + updated_at=datetime.fromisoformat("2023-07-18T12:00:00Z"), status=AutofixStatus.PROCESSING, ) diff --git a/tests/sentry/issues/test_escalating_issues_alg.py b/tests/sentry/issues/test_escalating_issues_alg.py index 80087b5aaf12e0..800e293f758308 100644 --- a/tests/sentry/issues/test_escalating_issues_alg.py +++ b/tests/sentry/issues/test_escalating_issues_alg.py @@ -1,12 +1,10 @@ from datetime import datetime -from typing import Any from sentry.issues.escalating_issues_alg import generate_issue_forecast -from sentry.tasks.weekly_escalating_forecast import GroupCount -START_TIME = datetime.strptime("2022-07-27T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%f%z") +START_TIME = datetime.fromisoformat("2022-07-27T00:00:00+00:00") -SEVEN_DAY_INPUT_INTERVALS: list[Any] = [ +SEVEN_DAY_INPUT_INTERVALS = [ "2022-07-20T00:00:00+00:00", "2022-07-20T01:00:00+00:00", "2022-07-20T02:00:00+00:00", @@ -177,7 +175,7 @@ "2022-07-26T23:00:00+00:00", ] -SIX_DAY_INPUT_INTERVALS: list[Any] = [ +SIX_DAY_INPUT_INTERVALS = [ "2022-07-21T00:00:00+00:00", "2022-07-21T01:00:00+00:00", "2022-07-21T02:00:00+00:00", @@ -324,7 +322,7 @@ "2022-07-26T23:00:00+00:00", ] -SEVEN_DAY_ERROR_EVENTS: list[Any] = [ +SEVEN_DAY_ERROR_EVENTS = [ 74, 532, 670, @@ -497,10 +495,10 @@ def test_spike_case() -> None: - start_time = datetime.strptime("2022-07-27T00:00:00+00:00", "%Y-%m-%dT%H:%M:%S%f%z") - data: GroupCount = {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": SEVEN_DAY_ERROR_EVENTS} - - ceilings_list = generate_issue_forecast(data, start_time) + start_time = datetime.fromisoformat("2022-07-27T00:00:00+00:00") + ceilings_list = generate_issue_forecast( + {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": SEVEN_DAY_ERROR_EVENTS}, start_time + ) ceilings = [x["forecasted_value"] for x in ceilings_list] assert ceilings == [6987] * 14, "Ceilings are incorrect" @@ -583,20 +581,18 @@ def test_bursty_case() -> None: 7627, ] + [0] * 95 - data: GroupCount = {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": error_events} - - ceilings_list = generate_issue_forecast(data, START_TIME) + ceilings_list = generate_issue_forecast( + {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": error_events}, START_TIME + ) ceilings = [x["forecasted_value"] for x in ceilings_list] assert ceilings == [16580] * 14, "Ceilings are incorrect" def test_empty_input() -> None: - error_events: list[int] = [] - - data: GroupCount = {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": error_events} - - ceilings_list = generate_issue_forecast(data, START_TIME) + ceilings_list = generate_issue_forecast( + {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": []}, START_TIME + ) ceilings = [x["forecasted_value"] for x in ceilings_list] assert ceilings == [], "Empty Input" @@ -655,9 +651,9 @@ def test_less_than_week_data() -> None: 7627, ] + [0] * 95 - data: GroupCount = {"intervals": SIX_DAY_INPUT_INTERVALS, "data": error_events} - - ceilings_list = generate_issue_forecast(data, START_TIME) + ceilings_list = generate_issue_forecast( + {"intervals": SIX_DAY_INPUT_INTERVALS, "data": error_events}, START_TIME + ) ceilings = [x["forecasted_value"] for x in ceilings_list] assert ceilings == [82900] * 14, "Ceilings are incorrect" @@ -666,18 +662,18 @@ def test_less_than_week_data() -> None: def test_low_freq_events() -> None: error_events = [6] * 168 - data: GroupCount = {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": error_events} - - ceilings_list = generate_issue_forecast(data, START_TIME) + ceilings_list = generate_issue_forecast( + {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": error_events}, START_TIME + ) ceilings = [x["forecasted_value"] for x in ceilings_list] assert ceilings == [36] * 14, "Ceilings are incorrect" def test_output() -> None: - data: GroupCount = {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": SEVEN_DAY_ERROR_EVENTS} - - ceilings_list = generate_issue_forecast(data, START_TIME) + ceilings_list = generate_issue_forecast( + {"intervals": SEVEN_DAY_INPUT_INTERVALS, "data": SEVEN_DAY_ERROR_EVENTS}, START_TIME + ) assert ceilings_list == [ {"forecasted_date": "2022-07-27", "forecasted_value": 6987}, From e2ca54d325b57752b78e0693617f07f0fa7827f3 Mon Sep 17 00:00:00 2001 From: Michael Sun <55160142+MichaelSun48@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:34:47 -0500 Subject: [PATCH 431/757] ref(issue-views): Overhaul issue views state and logic to a new context (#82429) This PR makes a major code refactor to the Issue Views family of components. No functionality should be broken or otherwise altered. The purpose of this refactor was to move lots of reused code and duplicated state into one unified context, `IssueViews.tsx`. This allows the displayed components to be much much cleaner and easier to understand while making it easier to add new functionality in the future. The two primary things it does are: 1. **Creates a new context, `IssueViews`, that extends the old `Tabs` context. This new context now contains the views and temporary tabs state** 2. **Delegates almost all tab alteration logic to a `useReducer` within the `IssueViews` context** --- .../draggableTabs/draggableTabList.tsx | 4 +- static/app/components/tabs/index.tsx | 2 +- .../app/views/issueList/customViewsHeader.tsx | 262 ++------- .../groupSearchViewTabs/draggableTabBar.tsx | 384 +++--------- .../groupSearchViewTabs/issueViews.tsx | 547 ++++++++++++++++++ 5 files changed, 698 insertions(+), 501 deletions(-) create mode 100644 static/app/views/issueList/groupSearchViewTabs/issueViews.tsx diff --git a/static/app/components/draggableTabs/draggableTabList.tsx b/static/app/components/draggableTabs/draggableTabList.tsx index b672cedb645fec..31df02af17428e 100644 --- a/static/app/components/draggableTabs/draggableTabList.tsx +++ b/static/app/components/draggableTabs/draggableTabList.tsx @@ -23,7 +23,6 @@ import {motion, Reorder} from 'framer-motion'; import {Button} from 'sentry/components/button'; import {CompactSelect} from 'sentry/components/compactSelect'; import DropdownButton from 'sentry/components/dropdownButton'; -import {TabsContext} from 'sentry/components/tabs'; import {type BaseTabProps, Tab} from 'sentry/components/tabs/tab'; import {IconAdd, IconEllipsis} from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -34,6 +33,7 @@ import {useDimensions} from 'sentry/utils/useDimensions'; import {useDimensionsMultiple} from 'sentry/utils/useDimensionsMultiple'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; +import {IssueViewsContext} from 'sentry/views/issueList/groupSearchViewTabs/issueViews'; import type {DraggableTabListItemProps} from './item'; import {Item} from './item'; @@ -273,7 +273,7 @@ function BaseDraggableTabList({ }: BaseDraggableTabListProps) { const navigate = useNavigate(); const [hoveringKey, setHoveringKey] = useState(null); - const {rootProps, setTabListState} = useContext(TabsContext); + const {rootProps, setTabListState} = useContext(IssueViewsContext); const organization = useOrganization(); const { value, diff --git a/static/app/components/tabs/index.tsx b/static/app/components/tabs/index.tsx index cbe934be1b99af..d196786b5c5277 100644 --- a/static/app/components/tabs/index.tsx +++ b/static/app/components/tabs/index.tsx @@ -47,7 +47,7 @@ export interface TabsProps value?: T; } -interface TabContext { +export interface TabContext { rootProps: Omit, 'children' | 'className'>; setTabListState: (state: TabListState) => void; tabListState?: TabListState; diff --git a/static/app/views/issueList/customViewsHeader.tsx b/static/app/views/issueList/customViewsHeader.tsx index 47994421a681d1..bccbb96cc1ef01 100644 --- a/static/app/views/issueList/customViewsHeader.tsx +++ b/static/app/views/issueList/customViewsHeader.tsx @@ -1,6 +1,5 @@ -import {useContext, useEffect, useMemo, useState} from 'react'; +import {useContext, useEffect, useMemo} from 'react'; import styled from '@emotion/styled'; -import debounce from 'lodash/debounce'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; @@ -9,7 +8,6 @@ import GlobalEventProcessingAlert from 'sentry/components/globalEventProcessingA import * as Layout from 'sentry/components/layouts/thirds'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip'; -import {Tabs, TabsContext} from 'sentry/components/tabs'; import {IconMegaphone, IconPause, IconPlay} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -17,19 +15,18 @@ import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; -import {useEffectAfterFirstRender} from 'sentry/utils/useEffectAfterFirstRender'; import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; +import {DraggableTabBar} from 'sentry/views/issueList/groupSearchViewTabs/draggableTabBar'; import { - DraggableTabBar, - type Tab, -} from 'sentry/views/issueList/groupSearchViewTabs/draggableTabBar'; -import {useUpdateGroupSearchViews} from 'sentry/views/issueList/mutations/useUpdateGroupSearchViews'; + type IssueView, + IssueViews, + IssueViewsContext, +} from 'sentry/views/issueList/groupSearchViewTabs/issueViews'; import {useFetchGroupSearchViews} from 'sentry/views/issueList/queries/useFetchGroupSearchViews'; -import type {UpdateGroupSearchViewPayload} from 'sentry/views/issueList/types'; import {NewTabContext} from 'sentry/views/issueList/utils/newTabContext'; import {IssueSortOptions} from './utils'; @@ -45,7 +42,6 @@ type CustomViewsIssueListHeaderProps = { type CustomViewsIssueListHeaderTabsContentProps = { organization: Organization; router: InjectedRouter; - views: UpdateGroupSearchViewPayload[]; }; function CustomViewsIssueListHeader({ @@ -59,6 +55,8 @@ function CustomViewsIssueListHeader({ selectedProjectIds.includes(Number(id)) ); + const {newViewActive} = useContext(NewTabContext); + const {data: groupSearchViews} = useFetchGroupSearchViews({ orgSlug: props.organization.slug, }); @@ -67,8 +65,6 @@ function CustomViewsIssueListHeader({ ? t('Pause real-time updates') : t('Enable real-time updates'); - const {newViewActive} = useContext(NewTabContext); - const openForm = useFeedbackForm(); const hasNewLayout = props.organization.features.includes('issue-stream-table-layout'); @@ -127,9 +123,28 @@ function CustomViewsIssueListHeader({ {groupSearchViews ? ( - - - + { + const tabId = id ?? `default${index.toString()}`; + + return { + id: tabId, + key: tabId, + label: name, + query: viewQuery, + querySort: viewQuerySort, + isCommitted: true, + }; + } + )} + > + + ) : (
)} @@ -140,19 +155,19 @@ function CustomViewsIssueListHeader({ function CustomViewsIssueListHeaderTabsContent({ organization, router, - views, }: CustomViewsIssueListHeaderTabsContentProps) { - // TODO(msun): Possible replace navigate with useSearchParams() in the future? const navigate = useNavigate(); const location = useLocation(); - const {setNewViewActive, newViewActive} = useContext(NewTabContext); const pageFilters = usePageFilters(); + const {newViewActive, setNewViewActive} = useContext(NewTabContext); + const {tabListState, state, dispatch} = useContext(IssueViewsContext); + + const {views: draggableTabs} = state; + // TODO(msun): Use the location from useLocation instead of props router in the future const {cursor: _cursor, page: _page, ...queryParams} = router?.location.query; - const {query, sort, viewId, project, environment} = queryParams; - const queryParamsWithPageFilters = useMemo(() => { return { ...queryParams, @@ -169,74 +184,6 @@ function CustomViewsIssueListHeaderTabsContent({ queryParams, ]); - const [draggableTabs, setDraggableTabs] = useState( - views.map(({id, name, query: viewQuery, querySort: viewQuerySort}, index): Tab => { - const tabId = id ?? `default${index.toString()}`; - - return { - id: tabId, - key: tabId, - label: name, - query: viewQuery, - querySort: viewQuerySort, - unsavedChanges: undefined, - isCommitted: true, - }; - }) - ); - - const getInitialTabKey = () => { - if (viewId && draggableTabs.find(tab => tab.id === viewId)) { - return draggableTabs.find(tab => tab.id === viewId)!.key; - } - if (query) { - return TEMPORARY_TAB_KEY; - } - return draggableTabs[0].key; - }; - - const {tabListState} = useContext(TabsContext); - - // TODO: Try to remove this state if possible - const [tempTab, setTempTab] = useState( - getInitialTabKey() === TEMPORARY_TAB_KEY && query - ? { - id: TEMPORARY_TAB_KEY, - key: TEMPORARY_TAB_KEY, - label: t('Unsaved'), - query, - querySort: sort ?? IssueSortOptions.DATE, - isCommitted: true, - } - : undefined - ); - - const {mutate: updateViews} = useUpdateGroupSearchViews(); - - const debounceUpdateViews = useMemo( - () => - debounce((newTabs: Tab[]) => { - if (newTabs) { - updateViews({ - orgSlug: organization.slug, - groupSearchViews: newTabs - .filter(tab => tab.isCommitted) - .map(tab => ({ - // Do not send over an ID if it's a temporary or default tab so that - // the backend will save these and generate permanent Ids for them - ...(tab.id[0] !== '_' && !tab.id.startsWith('default') - ? {id: tab.id} - : {}), - name: tab.label, - query: tab.query, - querySort: tab.querySort, - })), - }); - } - }, 500), - [organization.slug, updateViews] - ); - // This insane useEffect ensures that the correct tab is selected when the url updates useEffect(() => { // If no query, sort, or viewId is present, set the first tab as the selected tab, update query accordingly @@ -275,30 +222,14 @@ function CustomViewsIssueListHeaderTabsContent({ ) { // If there were no unsaved changes before, or the existing unsaved changes // don't match the new query and/or sort, update the unsaved changes - setDraggableTabs( - draggableTabs.map(tab => - tab.key === selectedTab.key - ? { - ...tab, - unsavedChanges: newUnsavedChanges, - } - : tab - ) - ); + dispatch({ + type: 'UPDATE_UNSAVED_CHANGES', + unsavedChanges: newUnsavedChanges, + }); } else if (!newUnsavedChanges && selectedTab.unsavedChanges) { // If there are no longer unsaved changes but there were before, remove them - setDraggableTabs( - draggableTabs.map(tab => - tab.key === selectedTab.key - ? { - ...tab, - unsavedChanges: undefined, - } - : tab - ) - ); + dispatch({type: 'UPDATE_UNSAVED_CHANGES', unsavedChanges: undefined}); } - if (!tabListState?.selectionManager.isSelected(selectedTab.key)) { navigate( normalizeUrl({ @@ -336,15 +267,7 @@ function CustomViewsIssueListHeaderTabsContent({ } if (query) { if (!tabListState?.selectionManager.isSelected(TEMPORARY_TAB_KEY)) { - tabListState?.setSelectedKey(TEMPORARY_TAB_KEY); - setTempTab({ - id: TEMPORARY_TAB_KEY, - key: TEMPORARY_TAB_KEY, - label: t('Unsaved'), - query, - querySort: sort ?? IssueSortOptions.DATE, - isCommitted: true, - }); + dispatch({type: 'SET_TEMP_VIEW', query, sort}); navigate( normalizeUrl({ ...location, @@ -355,6 +278,7 @@ function CustomViewsIssueListHeaderTabsContent({ }), {replace: true} ); + tabListState?.setSelectedKey(TEMPORARY_TAB_KEY); return; } } @@ -369,64 +293,10 @@ function CustomViewsIssueListHeaderTabsContent({ queryParamsWithPageFilters, draggableTabs, organization, + dispatch, ]); - // Update local tabs when new views are received from mutation request - useEffectAfterFirstRender(() => { - const newlyCreatedViews = views.filter( - view => !draggableTabs.find(tab => tab.id === view.id) - ); - const currentView = draggableTabs.find(tab => tab.id === viewId); - - setDraggableTabs(oldDraggableTabs => { - const assignedIds = new Set(); - return oldDraggableTabs.map(tab => { - // Temp viewIds are prefixed with '_' - if (tab.id && tab.id[0] === '_') { - const matchingView = newlyCreatedViews.find( - view => - view.id && - !assignedIds.has(view.id) && - tab.query === view.query && - tab.querySort === view.querySort && - tab.label === view.name - ); - if (matchingView?.id) { - assignedIds.add(matchingView.id); - return { - ...tab, - id: matchingView.id, - }; - } - } - return tab; - }); - }); - - if (viewId?.startsWith('_') && currentView) { - const matchingView = newlyCreatedViews.find( - view => - view.id && - currentView.query === view.query && - currentView.querySort === view.querySort && - currentView.label === view.name - ); - if (matchingView?.id) { - navigate( - normalizeUrl({ - ...location, - query: { - ...queryParamsWithPageFilters, - viewId: matchingView.id, - }, - }), - {replace: true} - ); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [views]); - + // This useEffect ensures the "new view" page is displayed/hidden correctly useEffect(() => { if (viewId?.startsWith('_')) { if (draggableTabs.find(tab => tab.id === viewId)?.isCommitted) { @@ -437,17 +307,12 @@ function CustomViewsIssueListHeaderTabsContent({ // and persist the query if (newViewActive && query !== '') { setNewViewActive(false); - const updatedTabs: Tab[] = draggableTabs.map(tab => - tab.id === viewId - ? { - ...tab, - unsavedChanges: [query, sort ?? IssueSortOptions.DATE], - isCommitted: true, - } - : tab - ); - setDraggableTabs(updatedTabs); - debounceUpdateViews(updatedTabs); + dispatch({ + type: 'UPDATE_UNSAVED_CHANGES', + unsavedChanges: [query, sort ?? IssueSortOptions.DATE], + isCommitted: true, + syncViews: true, + }); trackAnalytics('issue_views.add_view.custom_query_saved', { organization, query, @@ -461,29 +326,22 @@ function CustomViewsIssueListHeaderTabsContent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [viewId, query]); + const initialTabKey = + viewId && draggableTabs.find(tab => tab.id === viewId) + ? draggableTabs.find(tab => tab.id === viewId)!.key + : query + ? TEMPORARY_TAB_KEY + : draggableTabs[0].key; + return ( - debounceUpdateViews(newTabs)} - onSave={debounceUpdateViews} - onSaveTempView={debounceUpdateViews} - router={router} - /> + // TODO(msun): look into possibly folding the DraggableTabBar component into this component + ); } export default CustomViewsIssueListHeader; -const StyledTabs = styled(Tabs)` +const StyledIssueViews = styled(IssueViews)` grid-column: 1 / -1; `; diff --git a/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx b/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx index 454e5008fccec0..3a7318b6d45df8 100644 --- a/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx +++ b/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx @@ -12,11 +12,8 @@ import { } from 'sentry/components/draggableTabs/draggableTabList'; import type {DraggableTabListItemProps} from 'sentry/components/draggableTabs/item'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; -import {TabsContext} from 'sentry/components/tabs'; import {t} from 'sentry/locale'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; -import {defined} from 'sentry/utils'; -import {trackAnalytics} from 'sentry/utils/analytics'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; import {useHotkeys} from 'sentry/utils/useHotkeys'; import {useLocation} from 'sentry/utils/useLocation'; @@ -24,98 +21,21 @@ import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import {DraggableTabMenuButton} from 'sentry/views/issueList/groupSearchViewTabs/draggableTabMenuButton'; import EditableTabTitle from 'sentry/views/issueList/groupSearchViewTabs/editableTabTitle'; +import { + type IssueView, + IssueViewsContext, +} from 'sentry/views/issueList/groupSearchViewTabs/issueViews'; import {IssueSortOptions} from 'sentry/views/issueList/utils'; import {NewTabContext, type NewView} from 'sentry/views/issueList/utils/newTabContext'; -export interface Tab { - id: string; - /** - * False for tabs that were added view the "Add View" button, but - * have not been edited in any way. Only tabs with isCommitted=true - * will be saved to the backend. - */ - isCommitted: boolean; - key: string; - label: string; - query: string; - querySort: IssueSortOptions; - content?: React.ReactNode; - unsavedChanges?: [string, IssueSortOptions]; -} - export interface DraggableTabBarProps { initialTabKey: string; - orgSlug: string; router: InjectedRouter; - setTabs: (tabs: Tab[]) => void; - setTempTab: (tab: Tab | undefined) => void; - tabs: Tab[]; - /** - * Callback function to be called when user clicks the `Add View` button. - */ - onAddView?: (newTabs: Tab[]) => void; - /** - * Callback function to be called when user clicks the `Delete` button. - * Note: The `Delete` button only appears for persistent views - */ - onDelete?: (newTabs: Tab[]) => void; - /** - * Callback function to be called when user clicks on the `Discard Changes` button. - * Note: The `Discard Changes` button only appears for persistent views when `isChanged=true` - */ - onDiscard?: () => void; - /** - * Callback function to be called when user clicks on the `Discard` button for temporary views. - * Note: The `Discard` button only appears for temporary views - */ - onDiscardTempView?: () => void; - /** - * Callback function to be called when user clicks the 'Duplicate' button. - * Note: The `Duplicate` button only appears for persistent views - */ - onDuplicate?: (newTabs: Tab[]) => void; - /** - * Callback function to be called when the user reorders the tabs. Returns the - * new order of the tabs along with their props. - */ - onReorder?: (newTabs: Tab[]) => void; - /** - * Callback function to be called when user clicks the 'Save' button. - * Note: The `Save` button only appears for persistent views when `isChanged=true` - */ - onSave?: (newTabs: Tab[]) => void; - /** - * Callback function to be called when user clicks the 'Save View' button for temporary views. - */ - onSaveTempView?: (newTabs: Tab[]) => void; - /** - * Callback function to be called when user renames a tab. - * Note: The `Rename` button only appears for persistent views - */ - onTabRenamed?: (newTabs: Tab[], newLabel: string) => void; - tempTab?: Tab; } export const generateTempViewId = () => `_${Math.random().toString().substring(2, 7)}`; -export function DraggableTabBar({ - initialTabKey, - tabs, - setTabs, - tempTab, - setTempTab, - orgSlug, - router, - onReorder, - onAddView, - onDelete, - onDiscard, - onDuplicate, - onTabRenamed, - onSave, - onDiscardTempView, - onSaveTempView, -}: DraggableTabBarProps) { +export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) { // TODO: Extract this to a separate component encompassing Tab.Item in the future const [editingTabKey, setEditingTabKey] = useState(null); @@ -126,42 +46,9 @@ export function DraggableTabBar({ const {cursor: _cursor, page: _page, ...queryParams} = router?.location?.query ?? {}; const {viewId} = queryParams; - const {tabListState} = useContext(TabsContext); const {setNewViewActive, setOnNewViewsSaved} = useContext(NewTabContext); - - const handleOnReorder = (newOrder: Node[]) => { - const newTabs: Tab[] = newOrder - .map(node => { - const foundTab = tabs.find(tab => tab.key === node.key); - return foundTab?.key === node.key ? foundTab : null; - }) - .filter(defined); - setTabs(newTabs); - trackAnalytics('issue_views.reordered_views', { - organization, - }); - }; - - const handleOnSaveChanges = useCallback(() => { - const originalTab = tabs.find(tab => tab.key === tabListState?.selectedKey); - if (originalTab) { - const newTabs: Tab[] = tabs.map(tab => { - return tab.key === tabListState?.selectedKey && tab.unsavedChanges - ? { - ...tab, - query: tab.unsavedChanges[0], - querySort: tab.unsavedChanges[1], - unsavedChanges: undefined, - } - : tab; - }); - setTabs(newTabs); - onSave?.(newTabs); - trackAnalytics('issue_views.saved_changes', { - organization, - }); - } - }, [onSave, organization, setTabs, tabListState?.selectedKey, tabs]); + const {tabListState, state, dispatch} = useContext(IssueViewsContext); + const {views: tabs, tempView: tempTab} = state; useHotkeys( [ @@ -170,174 +57,49 @@ export function DraggableTabBar({ includeInputs: true, callback: () => { if (tabs.find(tab => tab.key === tabListState?.selectedKey)?.unsavedChanges) { - handleOnSaveChanges(); + dispatch({type: 'SAVE_CHANGES', syncViews: true}); addSuccessMessage(t('Changes saved to view')); } }, }, ], - [handleOnSaveChanges, tabListState?.selectedKey, tabs] + [dispatch, tabListState?.selectedKey, tabs] ); - const handleOnDiscardChanges = () => { - const originalTab = tabs.find(tab => tab.key === tabListState?.selectedKey); - if (originalTab) { - setTabs( - tabs.map(tab => { - return tab.key === tabListState?.selectedKey - ? {...tab, unsavedChanges: undefined} - : tab; - }) - ); - navigate({ - ...location, - query: { - ...queryParams, - query: originalTab.query, - sort: originalTab.querySort, - ...(originalTab.id ? {viewId: originalTab.id} : {}), - }, - }); - onDiscard?.(); - trackAnalytics('issue_views.discarded_changes', { - organization, - }); - } - }; - - const handleOnTabRenamed = (newLabel: string, tabKey: string) => { - const renamedTab = tabs.find(tb => tb.key === tabKey); - if (renamedTab && newLabel !== renamedTab.label) { - const newTabs = tabs.map(tab => - tab.key === renamedTab.key ? {...tab, label: newLabel, isCommitted: true} : tab - ); - setTabs(newTabs); - onTabRenamed?.(newTabs, newLabel); - trackAnalytics('issue_views.renamed_view', { - organization, - }); + const handleDuplicateView = () => { + const newViewId = generateTempViewId(); + const duplicatedTab = state.views.find( + view => view.key === tabListState?.selectedKey + ); + if (!duplicatedTab) { + return; } - }; - - const handleOnDuplicate = () => { - const idx = tabs.findIndex(tb => tb.key === tabListState?.selectedKey); - if (idx !== -1) { - const tempId = generateTempViewId(); - const duplicatedTab = tabs[idx]; - const newTabs: Tab[] = [ - ...tabs.slice(0, idx + 1), - { - ...duplicatedTab, - id: tempId, - key: tempId, - label: `${duplicatedTab.label} (Copy)`, - isCommitted: true, - }, - ...tabs.slice(idx + 1), - ]; - navigate({ - ...location, - query: { - ...queryParams, - query: duplicatedTab.query, - sort: duplicatedTab.querySort, - viewId: tempId, - }, - }); - setTabs(newTabs); - tabListState?.setSelectedKey(tempId); - onDuplicate?.(newTabs); - trackAnalytics('issue_views.duplicated_view', { - organization, - }); - } - }; - - const handleOnDelete = () => { - if (tabs.length > 1) { - const newTabs = tabs.filter(tb => tb.key !== tabListState?.selectedKey); - setTabs(newTabs); - tabListState?.setSelectedKey(newTabs[0].key); - onDelete?.(newTabs); - trackAnalytics('issue_views.deleted_view', { - organization, - }); - } - }; - - const handleOnSaveTempView = () => { - if (tempTab) { - const tempId = generateTempViewId(); - const newTab: Tab = { - id: tempId, - key: tempId, - label: 'New View', - query: tempTab.query, - querySort: tempTab.querySort, - isCommitted: true, - }; - const newTabs = [...tabs, newTab]; - navigate( - { - ...location, - query: { - ...queryParams, - query: tempTab.query, - querySort: tempTab.querySort, - viewId: tempId, - }, - }, - {replace: true} - ); - setTabs(newTabs); - setTempTab(undefined); - tabListState?.setSelectedKey(tempId); - onSaveTempView?.(newTabs); - trackAnalytics('issue_views.temp_view_saved', { - organization, - }); - } - }; - - const handleOnDiscardTempView = () => { - tabListState?.setSelectedKey(tabs[0].key); - setTempTab(undefined); - onDiscardTempView?.(); - trackAnalytics('issue_views.temp_view_discarded', { - organization, + dispatch({type: 'DUPLICATE_VIEW', newViewId, syncViews: true}); + navigate({ + ...location, + query: { + ...queryParams, + query: duplicatedTab.query, + sort: duplicatedTab.querySort, + viewId: newViewId, + }, }); }; - const handleCreateNewView = () => { - // Triggers the add view flow page - setNewViewActive(true); - const tempId = generateTempViewId(); - const currentTab = tabs.find(tab => tab.key === tabListState?.selectedKey); - if (currentTab) { - const newTabs: Tab[] = [ - ...tabs, - { - id: tempId, - key: tempId, - label: 'New View', - query: '', - querySort: IssueSortOptions.DATE, - isCommitted: false, - }, - ]; + const handleDiscardChanges = () => { + dispatch({type: 'DISCARD_CHANGES'}); + const originalTab = state.views.find(view => view.key === tabListState?.selectedKey); + if (originalTab) { + // TODO(msun): Move navigate logic to IssueViewsContext navigate({ ...location, query: { ...queryParams, - query: '', - viewId: tempId, + query: originalTab.query, + sort: originalTab.querySort, + viewId: originalTab.id, }, }); - setTabs(newTabs); - tabListState?.setSelectedKey(tempId); - trackAnalytics('issue_views.add_view.clicked', { - organization, - }); } }; @@ -350,9 +112,9 @@ export function DraggableTabBar({ } setNewViewActive(false); const {label, query, saveQueryToView} = newViews[0]; - const remainingNewViews: Tab[] = newViews.slice(1)?.map(view => { + const remainingNewViews: IssueView[] = newViews.slice(1)?.map(view => { const newId = generateTempViewId(); - const viewToTab: Tab = { + const viewToTab: IssueView = { id: newId, key: newId, label: view.label, @@ -365,7 +127,7 @@ export function DraggableTabBar({ }; return viewToTab; }); - let updatedTabs: Tab[] = tabs.map(tab => { + let updatedTabs: IssueView[] = tabs.map(tab => { if (tab.key === viewId) { return { ...tab, @@ -383,7 +145,7 @@ export function DraggableTabBar({ updatedTabs = [...updatedTabs, ...remainingNewViews]; } - setTabs(updatedTabs); + dispatch({type: 'SET_VIEWS', views: updatedTabs, syncViews: true}); navigate( { ...location, @@ -395,37 +157,57 @@ export function DraggableTabBar({ }, {replace: true} ); - onAddView?.(updatedTabs); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [location, navigate, onAddView, setNewViewActive, setTabs, tabs, viewId] + [location, navigate, setNewViewActive, tabs, viewId] ); + const handleCreateNewView = () => { + const tempId = generateTempViewId(); + dispatch({type: 'CREATE_NEW_VIEW', tempId}); + tabListState?.setSelectedKey(tempId); + navigate({ + ...location, + query: { + ...queryParams, + query: '', + viewId: tempId, + }, + }); + }; + + const handleDeleteView = (tab: IssueView) => { + dispatch({type: 'DELETE_VIEW', syncViews: true}); + // Including this logic in the dispatch call breaks the tests for some reason + // so we're doing it here instead + tabListState?.setSelectedKey(tabs.filter(tb => tb.key !== tab.key)[0].key); + }; + useEffect(() => { setOnNewViewsSaved(handleNewViewsSaved); }, [setOnNewViewsSaved, handleNewViewsSaved]); - const makeMenuOptions = (tab: Tab): MenuItemProps[] => { + const makeMenuOptions = (tab: IssueView): MenuItemProps[] => { if (tab.key === TEMPORARY_TAB_KEY) { return makeTempViewMenuOptions({ - onSaveTempView: handleOnSaveTempView, - onDiscardTempView: handleOnDiscardTempView, + onSaveTempView: () => dispatch({type: 'SAVE_TEMP_VIEW', syncViews: true}), + onDiscardTempView: () => dispatch({type: 'DISCARD_TEMP_VIEW'}), }); } if (tab.unsavedChanges) { return makeUnsavedChangesMenuOptions({ onRename: () => setEditingTabKey(tab.key), - onDuplicate: handleOnDuplicate, - onDelete: tabs.length > 1 ? handleOnDelete : undefined, - onSave: handleOnSaveChanges, - onDiscard: handleOnDiscardChanges, + onDuplicate: handleDuplicateView, + onDelete: tabs.length > 1 ? () => handleDeleteView(tab) : undefined, + onSave: () => dispatch({type: 'SAVE_CHANGES', syncViews: true}), + onDiscard: handleDiscardChanges, }); } return makeDefaultMenuOptions({ onRename: () => setEditingTabKey(tab.key), - onDuplicate: handleOnDuplicate, - onDelete: tabs.length > 1 ? handleOnDelete : undefined, + onDuplicate: handleDuplicateView, + onDelete: tabs.length > 1 ? () => handleDeleteView(tab) : undefined, }); }; @@ -433,8 +215,13 @@ export function DraggableTabBar({ return ( onReorder?.(tabs)} + onReorder={(newOrder: Node[]) => + dispatch({ + type: 'REORDER_TABS', + newKeyOrder: newOrder.map(node => node.key.toString()), + }) + } + onReorderComplete={() => dispatch({type: 'SYNC_VIEWS_TO_BACKEND'})} defaultSelectedKey={initialTabKey} onAddView={handleCreateNewView} orientation="horizontal" @@ -452,7 +239,7 @@ export function DraggableTabBar({ sort: tab.unsavedChanges?.[1] ?? tab.querySort, viewId: tab.id !== TEMPORARY_TAB_KEY ? tab.id : undefined, }, - pathname: `/organizations/${orgSlug}/issues/`, + pathname: `/organizations/${organization.slug}/issues/`, })} disabled={tab.key === editingTabKey} > @@ -461,7 +248,9 @@ export function DraggableTabBar({ label={tab.label} isEditing={editingTabKey === tab.key} setIsEditing={isEditing => setEditingTabKey(isEditing ? tab.key : null)} - onChange={newLabel => handleOnTabRenamed(newLabel.trim(), tab.key)} + onChange={newLabel => + dispatch({type: 'RENAME_TAB', newLabel: newLabel.trim(), syncViews: true}) + } isSelected={ (tabListState && tabListState?.selectedKey === tab.key) || (!tabListState && tab.key === initialTabKey) @@ -516,12 +305,15 @@ const makeDefaultMenuOptions = ({ }, ]; if (onDelete) { - menuOptions.push({ - key: 'delete-tab', - label: t('Delete'), - priority: 'danger', - onAction: onDelete, - }); + return [ + ...menuOptions, + { + key: 'delete-tab', + label: t('Delete'), + priority: 'danger', + onAction: onDelete, + }, + ]; } return menuOptions; }; diff --git a/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx b/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx new file mode 100644 index 00000000000000..965a1e6c7ab717 --- /dev/null +++ b/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx @@ -0,0 +1,547 @@ +import type {Dispatch, Reducer} from 'react'; +import {createContext, useCallback, useMemo, useReducer, useState} from 'react'; +import styled from '@emotion/styled'; +import type {TabListState} from '@react-stately/tabs'; +import type {Orientation} from '@react-types/shared'; +import debounce from 'lodash/debounce'; + +import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; +import type {TabContext, TabsProps} from 'sentry/components/tabs'; +import {tabsShouldForwardProp} from 'sentry/components/tabs/utils'; +import {t} from 'sentry/locale'; +import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; +import {defined} from 'sentry/utils'; +import normalizeUrl from 'sentry/utils/url/normalizeUrl'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import useOrganization from 'sentry/utils/useOrganization'; +import usePageFilters from 'sentry/utils/usePageFilters'; +import {generateTempViewId} from 'sentry/views/issueList/groupSearchViewTabs/draggableTabBar'; +import {useUpdateGroupSearchViews} from 'sentry/views/issueList/mutations/useUpdateGroupSearchViews'; +import type { + GroupSearchView, + UpdateGroupSearchViewPayload, +} from 'sentry/views/issueList/types'; +import {IssueSortOptions} from 'sentry/views/issueList/utils'; + +const TEMPORARY_TAB_KEY = 'temporary-tab'; + +export interface IssueView { + id: string; + /** + * False for tabs that were added view the "Add View" button, but + * have not been edited in any way. Only tabs with isCommitted=true + * will be saved to the backend. + */ + isCommitted: boolean; + key: string; + label: string; + query: string; + querySort: IssueSortOptions; + content?: React.ReactNode; + unsavedChanges?: [string, IssueSortOptions]; +} + +type BaseIssueViewsAction = { + /** If true, the new views state created by the action will be synced to the backend */ + syncViews?: boolean; +}; + +type ReorderTabsAction = { + newKeyOrder: string[]; + type: 'REORDER_TABS'; +} & BaseIssueViewsAction; + +type SaveChangesAction = { + type: 'SAVE_CHANGES'; +} & BaseIssueViewsAction; + +type DiscardChangesAction = { + type: 'DISCARD_CHANGES'; +} & BaseIssueViewsAction; + +type RenameTabAction = { + newLabel: string; + type: 'RENAME_TAB'; +} & BaseIssueViewsAction; + +type DuplicateViewAction = { + newViewId: string; + type: 'DUPLICATE_VIEW'; +} & BaseIssueViewsAction; + +type DeleteViewAction = { + type: 'DELETE_VIEW'; +} & BaseIssueViewsAction; + +type CreateNewViewAction = { + tempId: string; + type: 'CREATE_NEW_VIEW'; +} & BaseIssueViewsAction; + +type SetTempViewAction = { + query: string; + sort: IssueSortOptions; + type: 'SET_TEMP_VIEW'; +} & BaseIssueViewsAction; + +type DiscardTempViewAction = { + type: 'DISCARD_TEMP_VIEW'; +} & BaseIssueViewsAction; + +type SaveTempViewAction = { + type: 'SAVE_TEMP_VIEW'; +} & BaseIssueViewsAction; + +type UpdateUnsavedChangesAction = { + type: 'UPDATE_UNSAVED_CHANGES'; + unsavedChanges: [string, IssueSortOptions] | undefined; + isCommitted?: boolean; +} & BaseIssueViewsAction; + +type UpdateViewIdsAction = { + newViews: UpdateGroupSearchViewPayload[]; + type: 'UPDATE_VIEW_IDS'; +} & BaseIssueViewsAction; + +type SetViewsAction = { + type: 'SET_VIEWS'; + views: IssueView[]; +} & BaseIssueViewsAction; + +type SyncViewsToBackendAction = { + /** Syncs the current views state to the backend. Does not make any changes to the views state. */ + type: 'SYNC_VIEWS_TO_BACKEND'; +}; + +export type IssueViewsActions = + | ReorderTabsAction + | SaveChangesAction + | DiscardChangesAction + | RenameTabAction + | DuplicateViewAction + | DeleteViewAction + | CreateNewViewAction + | SetTempViewAction + | DiscardTempViewAction + | SaveTempViewAction + | UpdateUnsavedChangesAction + | UpdateViewIdsAction + | SetViewsAction + | SyncViewsToBackendAction; + +export interface IssueViewsState { + views: IssueView[]; + tempView?: IssueView; +} + +export interface IssueViewsContextType extends TabContext { + dispatch: Dispatch; + state: IssueViewsState; +} + +export const IssueViewsContext = createContext({ + rootProps: {orientation: 'horizontal'}, + setTabListState: () => {}, + // Issue Views specific state + dispatch: () => {}, + state: {views: []}, +}); + +function reorderTabs(state: IssueViewsState, action: ReorderTabsAction) { + const newTabs: IssueView[] = action.newKeyOrder + .map(key => { + const foundTab = state.views.find(tab => tab.key === key); + return foundTab?.key === key ? foundTab : null; + }) + .filter(defined); + return {...state, views: newTabs}; +} + +function saveChanges(state: IssueViewsState, tabListState: TabListState) { + const originalTab = state.views.find(tab => tab.key === tabListState?.selectedKey); + if (originalTab) { + const newViews = state.views.map(tab => { + return tab.key === tabListState?.selectedKey && tab.unsavedChanges + ? { + ...tab, + query: tab.unsavedChanges[0], + querySort: tab.unsavedChanges[1], + unsavedChanges: undefined, + } + : tab; + }); + return {...state, views: newViews}; + } + return state; +} + +function discardChanges(state: IssueViewsState, tabListState: TabListState) { + const originalTab = state.views.find(tab => tab.key === tabListState?.selectedKey); + if (originalTab) { + const newViews = state.views.map(tab => { + return tab.key === tabListState?.selectedKey + ? {...tab, unsavedChanges: undefined} + : tab; + }); + return {...state, views: newViews}; + } + return state; +} + +function renameView( + state: IssueViewsState, + action: RenameTabAction, + tabListState: TabListState +) { + const renamedTab = state.views.find(tab => tab.key === tabListState?.selectedKey); + if (renamedTab && action.newLabel !== renamedTab.label) { + const newViews = state.views.map(tab => + tab.key === renamedTab.key + ? {...tab, label: action.newLabel, isCommitted: true} + : tab + ); + return {...state, views: newViews}; + } + return state; +} + +function duplicateView( + state: IssueViewsState, + action: DuplicateViewAction, + tabListState: TabListState +) { + const idx = state.views.findIndex(tb => tb.key === tabListState?.selectedKey); + if (idx !== -1) { + const duplicatedTab = state.views[idx]; + const newTabs: IssueView[] = [ + ...state.views.slice(0, idx + 1), + { + ...duplicatedTab, + id: action.newViewId, + key: action.newViewId, + label: `${duplicatedTab.label} (Copy)`, + isCommitted: true, + }, + ...state.views.slice(idx + 1), + ]; + return {...state, views: newTabs}; + } + return state; +} + +function deleteView(state: IssueViewsState, tabListState: TabListState) { + const newViews = state.views.filter(tab => tab.key !== tabListState?.selectedKey); + return {...state, views: newViews}; +} + +function createNewView(state: IssueViewsState, action: CreateNewViewAction) { + const newTabs: IssueView[] = [ + ...state.views, + { + id: action.tempId, + key: action.tempId, + label: 'New View', + query: '', + querySort: IssueSortOptions.DATE, + isCommitted: false, + }, + ]; + return {...state, views: newTabs}; +} + +function setTempView(state: IssueViewsState, action: SetTempViewAction) { + const tempView: IssueView = { + id: TEMPORARY_TAB_KEY, + key: TEMPORARY_TAB_KEY, + label: t('Unsaved'), + query: action.query, + querySort: action.sort ?? IssueSortOptions.DATE, + isCommitted: true, + }; + return {...state, tempView}; +} + +function discardTempView(state: IssueViewsState, tabListState: TabListState) { + tabListState?.setSelectedKey(state.views[0].key); + return {...state, tempView: undefined}; +} + +function saveTempView(state: IssueViewsState, tabListState: TabListState) { + if (state.tempView) { + const tempId = generateTempViewId(); + const newTab: IssueView = { + id: tempId, + key: tempId, + label: 'New View', + query: state.tempView?.query, + querySort: state.tempView?.querySort, + isCommitted: true, + }; + tabListState?.setSelectedKey(tempId); + return {...state, views: [...state.views, newTab], tempView: undefined}; + } + return state; +} + +function updateUnsavedChanges( + state: IssueViewsState, + action: UpdateUnsavedChangesAction, + tabListState: TabListState +) { + return { + ...state, + views: state.views.map(tab => + tab.key === tabListState?.selectedKey + ? { + ...tab, + unsavedChanges: action.unsavedChanges, + isCommitted: action.isCommitted ?? tab.isCommitted, + } + : tab + ), + }; +} + +function updateViewIds(state: IssueViewsState, action: UpdateViewIdsAction) { + const assignedIds = new Set(); + const updatedViews = state.views.map(tab => { + if (tab.id && tab.id[0] === '_') { + const matchingView = action.newViews.find( + view => + view.id && + !assignedIds.has(view.id) && + tab.query === view.query && + tab.querySort === view.querySort && + tab.label === view.name + ); + if (matchingView?.id) { + assignedIds.add(matchingView.id); + return {...tab, id: matchingView.id}; + } + } + return tab; + }); + return {...state, views: updatedViews}; +} + +function setViews(state: IssueViewsState, action: SetViewsAction) { + return {...state, views: action.views}; +} + +interface IssueViewsStateProviderProps extends Omit, 'children'> { + children: React.ReactNode; + initialViews: IssueView[]; + // TODO(msun): Replace router with useLocation() / useUrlParams() / useSearchParams() in the future + router: InjectedRouter; +} + +export function IssueViewsStateProvider({ + children, + initialViews, + router, + ...props +}: IssueViewsStateProviderProps) { + const navigate = useNavigate(); + const pageFilters = usePageFilters(); + const organization = useOrganization(); + const [tabListState, setTabListState] = useState>(); + const {className: _className, ...restProps} = props; + + const {cursor: _cursor, page: _page, ...queryParams} = router?.location.query; + const {query, sort, viewId, project, environment} = queryParams; + + const queryParamsWithPageFilters = useMemo(() => { + return { + ...queryParams, + project: project ?? pageFilters.selection.projects, + environment: environment ?? pageFilters.selection.environments, + ...normalizeDateTimeParams(pageFilters.selection.datetime), + }; + }, [ + environment, + pageFilters.selection.datetime, + pageFilters.selection.environments, + pageFilters.selection.projects, + project, + queryParams, + ]); + + // This function is fired upon receiving new views from the backend - it replaces any previously + // generated temporary view ids with the permanent view ids from the backend + const replaceWithPersistantViewIds = (views: GroupSearchView[]) => { + const newlyCreatedViews = views.filter( + view => !state.views.find(tab => tab.id === view.id) + ); + if (newlyCreatedViews.length > 0) { + dispatch({type: 'UPDATE_VIEW_IDS', newViews: newlyCreatedViews}); + const currentView = state.views.find(tab => tab.id === viewId); + + if (viewId?.startsWith('_') && currentView) { + const matchingView = newlyCreatedViews.find( + view => + view.id && + currentView.query === view.query && + currentView.querySort === view.querySort + ); + if (matchingView?.id) { + navigate( + normalizeUrl({ + ...location, + query: { + ...queryParamsWithPageFilters, + viewId: matchingView.id, + }, + }), + {replace: true} + ); + } + } + } + return; + }; + + const {mutate: updateViews} = useUpdateGroupSearchViews({ + onSuccess: replaceWithPersistantViewIds, + }); + + const debounceUpdateViews = useMemo( + () => + debounce((newTabs: IssueView[]) => { + if (newTabs) { + updateViews({ + orgSlug: organization.slug, + groupSearchViews: newTabs + .filter(tab => tab.isCommitted) + .map(tab => ({ + // Do not send over an ID if it's a temporary or default tab so that + // the backend will save these and generate permanent Ids for them + ...(tab.id[0] !== '_' && !tab.id.startsWith('default') + ? {id: tab.id} + : {}), + name: tab.label, + query: tab.query, + querySort: tab.querySort, + })), + }); + } + }, 500), + [organization.slug, updateViews] + ); + + const reducer: Reducer = useCallback( + (state, action): IssueViewsState => { + if (!tabListState) { + return state; + } + switch (action.type) { + case 'REORDER_TABS': + return reorderTabs(state, action); + case 'SAVE_CHANGES': + return saveChanges(state, tabListState); + case 'DISCARD_CHANGES': + return discardChanges(state, tabListState); + case 'RENAME_TAB': + return renameView(state, action, tabListState); + case 'DUPLICATE_VIEW': + return duplicateView(state, action, tabListState); + case 'DELETE_VIEW': + return deleteView(state, tabListState); + case 'CREATE_NEW_VIEW': + return createNewView(state, action); + case 'SET_TEMP_VIEW': + return setTempView(state, action); + case 'DISCARD_TEMP_VIEW': + return discardTempView(state, tabListState); + case 'SAVE_TEMP_VIEW': + return saveTempView(state, tabListState); + case 'UPDATE_UNSAVED_CHANGES': + return updateUnsavedChanges(state, action, tabListState); + case 'UPDATE_VIEW_IDS': + return updateViewIds(state, action); + case 'SET_VIEWS': + return setViews(state, action); + case 'SYNC_VIEWS_TO_BACKEND': + return state; + default: + return state; + } + }, + [tabListState] + ); + + const sortOption = + sort && Object.values(IssueSortOptions).includes(sort.toString() as IssueSortOptions) + ? (sort.toString() as IssueSortOptions) + : IssueSortOptions.DATE; + + const initialTempView: IssueView | undefined = + query && (!viewId || !initialViews.find(tab => tab.id === viewId)) + ? { + id: TEMPORARY_TAB_KEY, + key: TEMPORARY_TAB_KEY, + label: t('Unsaved'), + query: query.toString(), + querySort: sortOption, + isCommitted: true, + } + : undefined; + + const [state, dispatch] = useReducer(reducer, { + views: initialViews, + tempView: initialTempView, + }); + + const dispatchWrapper = (action: IssueViewsActions) => { + const newState = reducer(state, action); + dispatch(action); + if (action.type === 'SYNC_VIEWS_TO_BACKEND' || action.syncViews) { + debounceUpdateViews(newState.views); + } + }; + + return ( + + {children} + + ); +} + +export function IssueViews({ + orientation = 'horizontal', + className, + children, + initialViews, + router, + ...props +}: TabsProps & Omit) { + return ( + + + {children} + + + ); +} + +const TabsWrap = styled('div', {shouldForwardProp: tabsShouldForwardProp})<{ + orientation: Orientation; +}>` + display: flex; + flex-direction: ${p => (p.orientation === 'horizontal' ? 'column' : 'row')}; + flex-grow: 1; + + ${p => + p.orientation === 'vertical' && + ` + height: 100%; + align-items: stretch; + `}; +`; From 87c2e6f9cb82faad1751ab878a3973499e4a8624 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:42:18 -0500 Subject: [PATCH 432/757] ref(tsc): convert dashboardWidgetQuerySelectorModal to FC (#82466) --- .../dashboardWidgetQuerySelectorModal.tsx | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/static/app/components/modals/dashboardWidgetQuerySelectorModal.tsx b/static/app/components/modals/dashboardWidgetQuerySelectorModal.tsx index 5b725103ca7671..d1ece1c1f00853 100644 --- a/static/app/components/modals/dashboardWidgetQuerySelectorModal.tsx +++ b/static/app/components/modals/dashboardWidgetQuerySelectorModal.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment} from 'react'; +import {Fragment} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; @@ -31,9 +31,10 @@ type Props = ModalRenderProps & selection: PageFilters; }; -class DashboardWidgetQuerySelectorModal extends Component { - renderQueries() { - const {organization, widget, selection, isMetricsData} = this.props; +function DashboardWidgetQuerySelectorModal(props: Props) { + const {organization, widget, selection, isMetricsData, Body, Header} = props; + + const renderQueries = () => { const querySearchBars = widget.queries.map((query, index) => { const discoverLocation = getWidgetDiscoverUrl( { @@ -72,26 +73,23 @@ class DashboardWidgetQuerySelectorModal extends Component { ); }); return querySearchBars; - } + }; - render() { - const {Body, Header, widget} = this.props; - return ( - -
-

{widget.title}

-
- -

- {t( - 'Multiple queries were used to create this widget visualization. Which query would you like to view in Discover?' - )} -

- {this.renderQueries()} - -
- ); - } + return ( + +
+

{widget.title}

+
+ +

+ {t( + 'Multiple queries were used to create this widget visualization. Which query would you like to view in Discover?' + )} +

+ {renderQueries()} + +
+ ); } const StyledInput = styled(Input)` From 1b162a0fb443132cf13c5b4a427d9c92daff7335 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:42:30 -0500 Subject: [PATCH 433/757] ref(tsc): convert teamAccessRequestModal to FC (#82470) --- .../modals/teamAccessRequestModal.tsx | 69 +++++++------------ 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/static/app/components/modals/teamAccessRequestModal.tsx b/static/app/components/modals/teamAccessRequestModal.tsx index 0c78b751a3481e..5baa228a9eb590 100644 --- a/static/app/components/modals/teamAccessRequestModal.tsx +++ b/static/app/components/modals/teamAccessRequestModal.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment} from 'react'; +import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; @@ -21,22 +21,12 @@ export interface CreateTeamAccessRequestModalProps teamId: string; } -type State = { - createBusy: boolean; -}; +function CreateTeamAccessRequestModal(props: CreateTeamAccessRequestModalProps) { + const [createBusy, setCreateBusy] = useState(false); + const {api, memberId, orgId, teamId, closeModal, Body, Footer} = props; -class CreateTeamAccessRequestModal extends Component< - CreateTeamAccessRequestModalProps, - State -> { - state: State = { - createBusy: false, - }; - - handleClick = async () => { - const {api, memberId, orgId, teamId, closeModal} = this.props; - - this.setState({createBusy: true}); + const handleClick = async () => { + setCreateBusy(true); try { await api.requestPromise( @@ -49,37 +39,28 @@ class CreateTeamAccessRequestModal extends Component< } catch (err) { addErrorMessage(t('Unable to send team request')); } - this.setState({createBusy: false}); + setCreateBusy(false); closeModal(); }; - render() { - const {Body, Footer, closeModal, teamId} = this.props; - - return ( - - - {tct( - 'You do not have permission to add members to the #[team] team, but we will send a request to your organization admins for approval.', - {team: teamId} - )} - -
- - - - -
-
- ); - } + return ( + + + {tct( + 'You do not have permission to add members to the #[team] team, but we will send a request to your organization admins for approval.', + {team: teamId} + )} + +
+ + + + +
+
+ ); } const ButtonGroup = styled('div')` From 32371819967b9ca39f10aefcfb0da086c80e4141 Mon Sep 17 00:00:00 2001 From: Neo Huang <126607112+kneeyo1@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:53:44 -0800 Subject: [PATCH 434/757] fix timezone normalization (#82496) This fixes the timezone normalization (both offset-naive and offset-aware timestamps). Also added some test cases --------- Co-authored-by: Lyn Nagara <1779792+lynnagara@users.noreply.github.com> --- src/sentry/consumers/dlq.py | 8 ++++- tests/sentry/consumers/test_dlq.py | 47 +++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/sentry/consumers/dlq.py b/src/sentry/consumers/dlq.py index b445817b175704..ab021f26c1e50d 100644 --- a/src/sentry/consumers/dlq.py +++ b/src/sentry/consumers/dlq.py @@ -98,7 +98,13 @@ def submit(self, message: Message[KafkaPayload]) -> None: ) if isinstance(message.value, BrokerValue): - if message.value.timestamp < min_accepted_timestamp: + # Normalize the message timezone to be UTC + if message.value.timestamp.tzinfo is None: + message_timestamp = message.value.timestamp.replace(tzinfo=timezone.utc) + else: + message_timestamp = message.value.timestamp + + if message_timestamp < min_accepted_timestamp: self.offsets_to_forward[message.value.partition] = message.value.next_offset raise InvalidMessage( message.value.partition, message.value.offset, reason=RejectReason.STALE.value diff --git a/tests/sentry/consumers/test_dlq.py b/tests/sentry/consumers/test_dlq.py index c8b274f744263e..257f479534410f 100644 --- a/tests/sentry/consumers/test_dlq.py +++ b/tests/sentry/consumers/test_dlq.py @@ -1,5 +1,6 @@ import time from datetime import datetime, timedelta, timezone +from typing import cast from unittest.mock import Mock import msgpack @@ -12,23 +13,20 @@ from sentry.testutils.pytest.fixtures import django_db_all -def make_message( - payload: bytes, partition: Partition, offset: int, timestamp: datetime | None = None -) -> Message: +def make_message(payload: bytes, partition: Partition, offset: int, timestamp: datetime) -> Message: return Message( BrokerValue( KafkaPayload(None, payload, []), partition, offset, - timestamp if timestamp else datetime.now(), + timestamp, ) ) @pytest.mark.parametrize("stale_threshold_sec", [300]) @django_db_all -def test_dlq_stale_messages(factories, stale_threshold_sec) -> None: - # Tests messages that have gotten stale (default longer than 5 minutes) +def test_dlq_stale_messages_timestamps(factories, stale_threshold_sec) -> None: organization = factories.create_organization() project = factories.create_project(organization=organization) @@ -54,20 +52,41 @@ def test_dlq_stale_messages(factories, stale_threshold_sec) -> None: ) strategy = factory.create_with_partitions(Mock(), Mock()) - for time_diff in range(10, 0, -1): + test_cases = [ + { + "timestamp": datetime.now() - timedelta(seconds=stale_threshold_sec - 60), + "should_raise": False, + }, + { + "timestamp": datetime.now() - timedelta(seconds=stale_threshold_sec + 60), + "should_raise": True, + }, + { + "timestamp": datetime.now(timezone.utc) - timedelta(seconds=stale_threshold_sec + 60), + "should_raise": True, + }, + { + "timestamp": datetime.now(timezone.utc) - timedelta(seconds=stale_threshold_sec - 60), + "should_raise": False, + }, + ] + + for idx, case in enumerate(test_cases): message = make_message( empty_event_payload, partition, - offset - time_diff, - timestamp=datetime.now(timezone.utc) - timedelta(minutes=time_diff), + offset + idx, + timestamp=cast(datetime, case["timestamp"]), ) - if time_diff < 5: - strategy.submit(message) - else: + + if case["should_raise"]: with pytest.raises(InvalidMessage) as exc_info: strategy.submit(message) assert exc_info.value.partition == partition - assert exc_info.value.offset == offset - time_diff + assert exc_info.value.offset == offset + idx + else: + strategy.submit(message) - assert inner_strategy_mock.submit.call_count == 4 + valid_messages = sum(1 for case in test_cases if not case["should_raise"]) + assert inner_strategy_mock.submit.call_count == valid_messages From 5739b5318dc05c5f3e665471df85d272d8342d68 Mon Sep 17 00:00:00 2001 From: Christinarlong <60594860+Christinarlong@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:49:47 -0800 Subject: [PATCH 435/757] chore(sentry apps): Introduce new error types for sentry apps (#82507) --- src/sentry/sentry_apps/utils/errors.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/sentry/sentry_apps/utils/errors.py diff --git a/src/sentry/sentry_apps/utils/errors.py b/src/sentry/sentry_apps/utils/errors.py new file mode 100644 index 00000000000000..af43d04c00b01d --- /dev/null +++ b/src/sentry/sentry_apps/utils/errors.py @@ -0,0 +1,35 @@ +from enum import Enum + + +class SentryAppErrorType(Enum): + CLIENT = "client" + INTEGRATOR = "integrator" + SENTRY = "sentry" + + +# Represents a user/client error that occured during a Sentry App process +class SentryAppError(Exception): + error_type = SentryAppErrorType.CLIENT + status_code = 400 + + def __init__( + self, + error: Exception | None = None, + status_code: int | None = None, + ) -> None: + if status_code: + self.status_code = status_code + + +# Represents an error caused by a 3p integrator during a Sentry App process +class SentryAppIntegratorError(Exception): + error_type = SentryAppErrorType.INTEGRATOR + status_code = 400 + + def __init__( + self, + error: Exception | None = None, + status_code: int | None = None, + ) -> None: + if status_code: + self.status_code = status_code From 5bd1b24a0efa5a8d7e21f4317307c091ea221348 Mon Sep 17 00:00:00 2001 From: Michael Sun <55160142+MichaelSun48@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:55:43 -0500 Subject: [PATCH 436/757] chore(issue-views): Add analytics back to tab actions (#82504) I deleted some of the analytics calls in [this refactoring change](https://github.com/getsentry/sentry/pull/82429) and forgot to add them back. This PR adds them back. I found what the old analytics keys were [here](https://github.com/getsentry/sentry/blob/7cb3a89c7a0737ea5c53ab0a1e85b03c7d8dd4ee/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx) --- .../groupSearchViewTabs/issueViews.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx b/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx index 965a1e6c7ab717..d9e477533878b1 100644 --- a/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx +++ b/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx @@ -11,6 +11,7 @@ import {tabsShouldForwardProp} from 'sentry/components/tabs/utils'; import {t} from 'sentry/locale'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import {defined} from 'sentry/utils'; +import {trackAnalytics} from 'sentry/utils/analytics'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; @@ -129,6 +130,18 @@ export type IssueViewsActions = | SetViewsAction | SyncViewsToBackendAction; +const ACTION_ANALYTICS_MAP: Partial> = { + REORDER_TABS: 'issue_views.reordered_views', + SAVE_CHANGES: 'issue_views.saved_changes', + DISCARD_CHANGES: 'issue_views.discarded_changes', + RENAME_TAB: 'issue_views.renamed_view', + DUPLICATE_VIEW: 'issue_views.duplicated_view', + DELETE_VIEW: 'issue_views.deleted_view', + SAVE_TEMP_VIEW: 'issue_views.temp_view_saved', + DISCARD_TEMP_VIEW: 'issue_views.temp_view_discarded', + CREATE_NEW_VIEW: 'issue_views.add_view.clicked', +}; + export interface IssueViewsState { views: IssueView[]; tempView?: IssueView; @@ -494,9 +507,17 @@ export function IssueViewsStateProvider({ const dispatchWrapper = (action: IssueViewsActions) => { const newState = reducer(state, action); dispatch(action); + if (action.type === 'SYNC_VIEWS_TO_BACKEND' || action.syncViews) { debounceUpdateViews(newState.views); } + + const actionAnalyticsKey = ACTION_ANALYTICS_MAP[action.type]; + if (actionAnalyticsKey) { + trackAnalytics(actionAnalyticsKey, { + organization, + }); + } }; return ( From d2decf3c0601c29b6d5e18742556e66b22e1ce01 Mon Sep 17 00:00:00 2001 From: Lyn Nagara <1779792+lynnagara@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:41:00 -0800 Subject: [PATCH 437/757] fix: fixes KeyError when running with stale topic dlq (#82512) --- src/sentry/consumers/dlq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/consumers/dlq.py b/src/sentry/consumers/dlq.py index ab021f26c1e50d..43b4b8c3dcd12b 100644 --- a/src/sentry/consumers/dlq.py +++ b/src/sentry/consumers/dlq.py @@ -113,7 +113,7 @@ def submit(self, message: Message[KafkaPayload]) -> None: # If we get a valid message for a partition later, don't emit a filtered message for it if self.offsets_to_forward: for partition in message.committable: - self.offsets_to_forward.pop(partition) + self.offsets_to_forward.pop(partition, None) self.next_step.submit(message) From 9f30cbed6be321b4c590903506b7d484d43efa07 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sat, 21 Dec 2024 13:06:48 +0000 Subject: [PATCH 438/757] release: 24.12.1 --- CHANGES | 32 ++++++++++++++++++++++++++++++++ setup.cfg | 2 +- src/sentry/conf/server.py | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index a4e0464f27a085..0655a554ced755 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,35 @@ +24.12.1 +------- + +### Various fixes & improvements + +- fix: fixes KeyError when running with stale topic dlq (#82512) by @lynnagara +- chore(issue-views): Add analytics back to tab actions (#82504) by @MichaelSun48 +- chore(sentry apps): Introduce new error types for sentry apps (#82507) by @Christinarlong +- fix timezone normalization (#82496) by @kneeyo1 +- ref(tsc): convert teamAccessRequestModal to FC (#82470) by @michellewzhang +- ref(tsc): convert dashboardWidgetQuerySelectorModal to FC (#82466) by @michellewzhang +- ref(issue-views): Overhaul issue views state and logic to a new context (#82429) by @MichaelSun48 +- ref: strptime -> fromisoformat in tests (#82488) by @asottile-sentry +- chore(various): Fix linter warnings (#82494) by @lobsterkatie +- ref(insights): Split out `getAxisMaxForPercentageSeries` (#82493) by @gggritso +- fix(ecosystem): Track metrics for issue detail ticket creation (#82436) by @GabeVillalobos +- ref(aci): pass WorkflowJob into process_workflows (#82489) by @cathteng +- fix(group-events): Fix typo and error text (#82490) by @leeandher +- fix(web): Add react_config context on auth pages take 2 (#82480) by @BYK +- feat(alerts): ACI dual write alert rule helpers (#82400) by @ceorourke +- feat(dashboards): Pass `LineChart` series meta alongside the data (#82047) by @gggritso +- fix(eap): Numeric attribute filtering in snql eap (#82472) by @Zylphrex +- chore(issues): Opt in a few more endpoint tests to stronger types (#82382) by @mrduncan +- ref: remove calls to iso_format in testutils (#82461) by @asottile-sentry +- feat(dashboards): enable sorting by column in table view (#82239) by @harshithadurai +- ref(workflow_engine): remove remaining references to condition in validators (#82438) by @mifu67 +- fix(flags): separate permission class (#82463) by @oioki +- feat(new-trace): Fixing scroll on trace drawer (#82475) by @Abdkhan14 +- support routing stale messages to lowpri topic (#82322) by @lynnagara + +_Plus 240 more_ + 24.12.0 ------- diff --git a/setup.cfg b/setup.cfg index c7b83b9787a533..74308d4a224008 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sentry -version = 25.1.0.dev0 +version = 24.12.1 description = A realtime logging and aggregation server. long_description = file: README.md long_description_content_type = text/markdown diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 12b8e31b5ac385..ca5144bbd0527c 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -2532,7 +2532,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: SENTRY_SELF_HOSTED_ERRORS_ONLY = False # only referenced in getsentry to provide the stable beacon version # updated with scripts/bump-version.sh -SELF_HOSTED_STABLE_VERSION = "24.12.0" +SELF_HOSTED_STABLE_VERSION = "24.12.1" # Whether we should look at X-Forwarded-For header or not # when checking REMOTE_ADDR ip addresses From 7f803c257fc06e2e1304868219966b9326a73bc0 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sat, 21 Dec 2024 19:57:03 +0000 Subject: [PATCH 439/757] meta: Bump new development version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 74308d4a224008..c7b83b9787a533 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sentry -version = 24.12.1 +version = 25.1.0.dev0 description = A realtime logging and aggregation server. long_description = file: README.md long_description_content_type = text/markdown From d139b578ed0ecfeca23dd69d4d98e39384ad4047 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 23 Dec 2024 16:56:54 +0300 Subject: [PATCH 440/757] fix(web): Add react_config context on auth pages take 3 (#82518) Follow up to #82480 and #82396. Should fix the final complaints, minor issues mentioned in getsentry/self-hosted#3473 --- src/sentry/web/frontend/twofactor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry/web/frontend/twofactor.py b/src/sentry/web/frontend/twofactor.py index 2b846c900332cf..a07c041b988ca8 100644 --- a/src/sentry/web/frontend/twofactor.py +++ b/src/sentry/web/frontend/twofactor.py @@ -18,6 +18,7 @@ from sentry.utils.email import MessageBuilder from sentry.utils.geo import geo_by_addr from sentry.utils.http import absolute_uri +from sentry.web.client_config import get_client_config from sentry.web.forms.accounts import TwoFactorForm from sentry.web.frontend.base import BaseView, control_silo_view from sentry.web.helpers import render_to_response @@ -249,6 +250,7 @@ def handle(self, request: HttpRequest) -> HttpResponse: "interface": interface, "other_interfaces": self.get_other_interfaces(interface, interfaces), "activation": activation, + "react_config": get_client_config(request, self.active_organization), }, request, status=200, From 1dcf3aceef681e6a94ace32a092f249da26ab5cf Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 23 Dec 2024 06:47:47 -0800 Subject: [PATCH 441/757] ref(replay): Make root in VideoReplay* classes non-nullable (#82517) The class is only instantiated from one spot, and there's a guard to make sure root is not-null: https://github.com/getsentry/sentry/blob/2c797560316c1666a0aca53b6eaa9f90bd3cd6e0/static/app/components/replays/replayContext.tsx#L394-L396 Lets lean into that and enforce it. --- static/app/components/replays/videoReplayer.tsx | 8 ++------ .../components/replays/videoReplayerWithInteractions.tsx | 8 +++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/static/app/components/replays/videoReplayer.tsx b/static/app/components/replays/videoReplayer.tsx index b2894d200d7258..0cf3678bd84353 100644 --- a/static/app/components/replays/videoReplayer.tsx +++ b/static/app/components/replays/videoReplayer.tsx @@ -3,8 +3,6 @@ import type {ClipWindow, VideoEvent} from 'sentry/utils/replays/types'; import {findVideoSegmentIndex} from './utils'; -type RootElem = HTMLDivElement | null; - // The number of segments to load on either side of the requested segment (around 15 seconds) // Also the number of segments we load initially const PRELOAD_BUFFER = 3; @@ -19,7 +17,7 @@ interface VideoReplayerOptions { onBuffer: (isBuffering: boolean) => void; onFinished: () => void; onLoaded: (event: any) => void; - root: RootElem; + root: HTMLDivElement; start: number; videoApiPrefix: string; clipWindow?: ClipWindow; @@ -92,9 +90,7 @@ export class VideoReplayer { this.config = config; this.wrapper = document.createElement('div'); - if (root) { - root.appendChild(this.wrapper); - } + root.appendChild(this.wrapper); this._trackList = this._attachments.map(({timestamp}, i) => [timestamp, i]); diff --git a/static/app/components/replays/videoReplayerWithInteractions.tsx b/static/app/components/replays/videoReplayerWithInteractions.tsx index ebccbacb409ff7..5dead848709361 100644 --- a/static/app/components/replays/videoReplayerWithInteractions.tsx +++ b/static/app/components/replays/videoReplayerWithInteractions.tsx @@ -6,8 +6,6 @@ import type {VideoReplayerConfig} from 'sentry/components/replays/videoReplayer' import {VideoReplayer} from 'sentry/components/replays/videoReplayer'; import type {ClipWindow, RecordingFrame, VideoEvent} from 'sentry/utils/replays/types'; -type RootElem = HTMLDivElement | null; - interface VideoReplayerWithInteractionsOptions { context: {sdkName: string | undefined | null; sdkVersion: string | undefined | null}; durationMs: number; @@ -15,7 +13,7 @@ interface VideoReplayerWithInteractionsOptions { onBuffer: (isBuffering: boolean) => void; onFinished: () => void; onLoaded: (event: any) => void; - root: RootElem; + root: HTMLDivElement; speed: number; start: number; theme: Theme; @@ -67,7 +65,7 @@ export class VideoReplayerWithInteractions { config: this.config, }); - root?.classList.add('video-replayer'); + root.classList.add('video-replayer'); const grouped = Object.groupBy(touchEvents, (t: any) => t.data.pointerId); Object.values(grouped).forEach(t => { @@ -86,7 +84,7 @@ export class VideoReplayerWithInteractions { }); this.replayer = new Replayer(eventsWithSnapshots, { - root: root as Element, + root, blockClass: 'sentry-block', mouseTail: { duration: 0.75 * 1000, From 819b6fbc4b43f836798b1dd5dc67021d2546ac15 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 23 Dec 2024 07:03:47 -0800 Subject: [PATCH 442/757] ref(replay): Move some project code into useReplayProjectSlug (#82516) Extract some code that converts a projectId into a projectSlug. I'm extracting it because it's something to be reused between `components/replays/replayPlayer.tsx` and `components/replays/player/replayPlayer.tsx`. Also, relying on the ProjectStore as this does is something that'll need to be refactored in the future because loading the ProjectStore is slow, so it's nice to separate this out. --- .../app/utils/replays/hooks/useReplayData.tsx | 10 ++-------- .../replays/hooks/useReplayProjectSlug.tsx | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 static/app/utils/replays/hooks/useReplayProjectSlug.tsx diff --git a/static/app/utils/replays/hooks/useReplayData.tsx b/static/app/utils/replays/hooks/useReplayData.tsx index 05d15ed0ed20c6..021b9b1ca64ad8 100644 --- a/static/app/utils/replays/hooks/useReplayData.tsx +++ b/static/app/utils/replays/hooks/useReplayData.tsx @@ -7,9 +7,9 @@ import {DiscoverDatasets} from 'sentry/utils/discover/types'; import parseLinkHeader from 'sentry/utils/parseLinkHeader'; import type {ApiQueryKey} from 'sentry/utils/queryClient'; import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient'; +import {useReplayProjectSlug} from 'sentry/utils/replays/hooks/useReplayProjectSlug'; import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils'; import type RequestError from 'sentry/utils/requestError/requestError'; -import useProjects from 'sentry/utils/useProjects'; import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types'; type Options = { @@ -77,7 +77,6 @@ function useReplayData({ segmentsPerPage = 100, }: Options): Result { const hasFetchedAttachments = useRef(false); - const projects = useProjects(); const queryClient = useQueryClient(); // Fetch every field of the replay. The TS type definition lists every field @@ -98,12 +97,7 @@ function useReplayData({ [replayData?.data] ); - const projectSlug = useMemo(() => { - if (!replayRecord) { - return null; - } - return projects.projects.find(p => p.id === replayRecord.project_id)?.slug ?? null; - }, [replayRecord, projects.projects]); + const projectSlug = useReplayProjectSlug({replayRecord}); const getAttachmentsQueryKey = useCallback( ({cursor, per_page}): ApiQueryKey => { diff --git a/static/app/utils/replays/hooks/useReplayProjectSlug.tsx b/static/app/utils/replays/hooks/useReplayProjectSlug.tsx new file mode 100644 index 00000000000000..c35a40f2b0321a --- /dev/null +++ b/static/app/utils/replays/hooks/useReplayProjectSlug.tsx @@ -0,0 +1,18 @@ +import {useMemo} from 'react'; + +import useProjects from 'sentry/utils/useProjects'; +import type {ReplayRecord} from 'sentry/views/replays/types'; + +interface Props { + replayRecord: ReplayRecord | undefined; +} + +export function useReplayProjectSlug({replayRecord}: Props) { + const projects = useProjects(); + return useMemo(() => { + if (!replayRecord) { + return null; + } + return projects.projects.find(p => p.id === replayRecord.project_id)?.slug ?? null; + }, [replayRecord, projects]); +} From e839f8d625577672adf4c760df6216d1531500bf Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 23 Dec 2024 07:10:25 -0800 Subject: [PATCH 443/757] ref(replay): Refactor video-replay related css (#82514) This moves two bits of mobile-replay video-replay CSS closer to where it's needed. first, i moved the `.video-replayer` class down from [videoReplayerWithInteractions.tsx](https://github.com/getsentry/sentry/compare/ryan953/ref-video-replayer-css?expand=1#diff-fddd507998d34043d72bbf4f1aef9e995a8cfea4ce3903d460102fb8faf815c2) and into [videoReplayer.tsx](https://github.com/getsentry/sentry/compare/ryan953/ref-video-replayer-css?expand=1#diff-e351f1b474cf18c091f79bc29e785970cbbb5da2d4a623b5b4e7e9272dfd9670) with the new name `.video-replayer-wrapper`. Then i updated the selectors to use sibling selections. So if the video wrapper exists it'll kick in. We want the `opacity` rule to kick in no matter the use-case, so that stays inside styles.tsx. But the absolute positioning is only for the older `static/app/components/replays/replayPlayer.tsx` and not the newer `static/app/components/replays/player/replayPlayer.tsx`. The newer one, when it gets support for video replays, will use `` instead of `postition:absolute;` like this. --- static/app/components/replays/player/styles.tsx | 17 +++-------------- static/app/components/replays/replayPlayer.tsx | 8 ++++++++ static/app/components/replays/videoReplayer.tsx | 1 + .../replays/videoReplayerWithInteractions.tsx | 2 -- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/static/app/components/replays/player/styles.tsx b/static/app/components/replays/player/styles.tsx index 02a32c09c7ffa0..29c64ccf695428 100644 --- a/static/app/components/replays/player/styles.tsx +++ b/static/app/components/replays/player/styles.tsx @@ -19,6 +19,9 @@ export const baseReplayerCss = css` border: none; background: white; } + .video-replayer-wrapper + .replayer-wrapper > iframe { + opacity: 0; + } &[data-inspectable='true'] .replayer-wrapper > iframe { /* Set pointer-events to make it easier to right-click & inspect */ @@ -111,18 +114,4 @@ export const sentryReplayerCss = (theme: Theme) => css` height: 10px; } } - - /* Correctly positions the canvas for video replays and shows the purple "mousetails" */ - &.video-replayer { - .replayer-wrapper { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - .replayer-wrapper > iframe { - opacity: 0; - } - } `; diff --git a/static/app/components/replays/replayPlayer.tsx b/static/app/components/replays/replayPlayer.tsx index 9b3160f52b3f58..408b93060583e9 100644 --- a/static/app/components/replays/replayPlayer.tsx +++ b/static/app/components/replays/replayPlayer.tsx @@ -217,6 +217,14 @@ const SentryPlayerRoot = styled(BasePlayerRoot)` ${baseReplayerCss} /* Sentry-specific styles for the player */ ${p => sentryReplayerCss(p.theme)} + + .video-replayer-wrapper + .replayer-wrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } `; const Overlay = styled('div')` diff --git a/static/app/components/replays/videoReplayer.tsx b/static/app/components/replays/videoReplayer.tsx index 0cf3678bd84353..1583fc9172238d 100644 --- a/static/app/components/replays/videoReplayer.tsx +++ b/static/app/components/replays/videoReplayer.tsx @@ -90,6 +90,7 @@ export class VideoReplayer { this.config = config; this.wrapper = document.createElement('div'); + this.wrapper.className = 'video-replayer-wrapper'; root.appendChild(this.wrapper); this._trackList = this._attachments.map(({timestamp}, i) => [timestamp, i]); diff --git a/static/app/components/replays/videoReplayerWithInteractions.tsx b/static/app/components/replays/videoReplayerWithInteractions.tsx index 5dead848709361..114327c70033a3 100644 --- a/static/app/components/replays/videoReplayerWithInteractions.tsx +++ b/static/app/components/replays/videoReplayerWithInteractions.tsx @@ -65,8 +65,6 @@ export class VideoReplayerWithInteractions { config: this.config, }); - root.classList.add('video-replayer'); - const grouped = Object.groupBy(touchEvents, (t: any) => t.data.pointerId); Object.values(grouped).forEach(t => { if (t?.length !== 2) { From e583a3d9eb967fee5f39f0ae48b020d8d9c23bff Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:11:24 -0500 Subject: [PATCH 444/757] fix(alerts): Fix open in explore date range (#82506) When rendering alert previews on a 7d time window, the alerts UI actually renders using 9998m instead. Updates the `open in explore` button for eap alerts to map 9998m back to 7d in explore instead. --- static/app/views/alerts/rules/utils.spec.tsx | 17 +++++++++++++++++ static/app/views/alerts/rules/utils.tsx | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/static/app/views/alerts/rules/utils.spec.tsx b/static/app/views/alerts/rules/utils.spec.tsx index 66751ef2034561..145784807f9139 100644 --- a/static/app/views/alerts/rules/utils.spec.tsx +++ b/static/app/views/alerts/rules/utils.spec.tsx @@ -21,4 +21,21 @@ describe('getExploreUrl', () => { '/organizations/slug/traces/?dataset=spansRpc&environment=prod&interval=30m&project=1&query=span.op%3Ahttp.client&statsPeriod=7d&visualize=%7B%22chartType%22%3A1%2C%22yAxes%22%3A%5B%22p75%28span.duration%29%22%5D%7D' ); }); + it('should return the correct url for 9998m', () => { + const rule = MetricRuleFixture(); + rule.dataset = Dataset.EVENTS_ANALYTICS_PLATFORM; + rule.timeWindow = TimeWindow.THIRTY_MINUTES; + rule.aggregate = 'p75(span.duration)'; + rule.query = 'span.op:http.client'; + rule.environment = 'prod'; + const url = getAlertRuleExploreUrl({ + rule, + orgSlug: 'slug', + period: '9998m', + projectId: '1', + }); + expect(url).toEqual( + '/organizations/slug/traces/?dataset=spansRpc&environment=prod&interval=30m&project=1&query=span.op%3Ahttp.client&statsPeriod=7d&visualize=%7B%22chartType%22%3A1%2C%22yAxes%22%3A%5B%22p75%28span.duration%29%22%5D%7D' + ); + }); }); diff --git a/static/app/views/alerts/rules/utils.tsx b/static/app/views/alerts/rules/utils.tsx index 92e5114fb07828..200003f383c9f7 100644 --- a/static/app/views/alerts/rules/utils.tsx +++ b/static/app/views/alerts/rules/utils.tsx @@ -137,7 +137,7 @@ export function getAlertRuleExploreUrl({ orgSlug, selection: { datetime: { - period, + period: period === '9998m' ? '7d' : period, start: null, end: null, utc: null, From 79a7368c0f58f1cd2508167ae17cd21c29dd5218 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Mon, 23 Dec 2024 10:37:46 -0500 Subject: [PATCH 445/757] ref(insights): Simplify isModuleEnabled (#82483) There's no need for the undefined check here. The types completely guard against the possibility of an undefined result. --- static/app/views/insights/pages/utils.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/static/app/views/insights/pages/utils.ts b/static/app/views/insights/pages/utils.ts index 40555de420d78f..76dd92626f6d9f 100644 --- a/static/app/views/insights/pages/utils.ts +++ b/static/app/views/insights/pages/utils.ts @@ -7,13 +7,8 @@ import { } from 'sentry/views/insights/settings'; import type {ModuleName} from 'sentry/views/insights/types'; -export const isModuleEnabled = (module: ModuleName, organization: Organization) => { - const moduleFeatures: string[] | undefined = MODULE_FEATURE_MAP[module]; - if (!moduleFeatures) { - return false; - } - return moduleFeatures.every(feature => organization.features.includes(feature)); -}; +export const isModuleEnabled = (module: ModuleName, organization: Organization) => + MODULE_FEATURE_MAP[module].every(f => organization.features.includes(f)); export const isModuleHidden = (module: ModuleName, organization: Organization) => MODULE_HIDDEN_WHEN_FEAUTRE_DISABLED.includes(module) && From 8ac1303135a00127a3ffd383d19410d765480805 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Mon, 23 Dec 2024 10:37:58 -0500 Subject: [PATCH 446/757] ref(insights): Correct crons description (#82482) --- static/app/views/insights/crons/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/insights/crons/settings.ts b/static/app/views/insights/crons/settings.ts index 452664b90658dd..3807da623c7739 100644 --- a/static/app/views/insights/crons/settings.ts +++ b/static/app/views/insights/crons/settings.ts @@ -7,7 +7,7 @@ export const DATA_TYPE_PLURAL = t('Cron Check-Ins'); export const BASE_URL = 'crons'; export const MODULE_DESCRIPTION = t( - 'Monitor cron jobs for failures, timeouts, and missed runs.' + 'Scheduled monitors that check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.' ); export const MODULE_DOC_LINK = 'https://docs.sentry.io/product/crons/'; From 2fd9e369a249d0792b304cd8b59434758fe76a1f Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:40:01 -0500 Subject: [PATCH 447/757] ref(tsc): convert skipConfirm to FC (#82471) --- .../onboardingWizard/skipConfirm.tsx | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/static/app/components/onboardingWizard/skipConfirm.tsx b/static/app/components/onboardingWizard/skipConfirm.tsx index 4820c9fe8c4b5a..bda36f8973d3df 100644 --- a/static/app/components/onboardingWizard/skipConfirm.tsx +++ b/static/app/components/onboardingWizard/skipConfirm.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment} from 'react'; +import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import {Button, LinkButton} from 'sentry/components/button'; @@ -13,39 +13,30 @@ type Props = { onSkip: () => void; }; -type State = { - showConfirmation: boolean; -}; - -class SkipConfirm extends Component { - state: State = { - showConfirmation: false, - }; +function SkipConfirm(props: Props) { + const [showConfirmation, setShowConfirmation] = useState(false); + const {onSkip, children} = props; - toggleConfirm = (e: React.MouseEvent) => { + const toggleConfirm = (e: React.MouseEvent) => { e.stopPropagation(); - this.setState(state => ({showConfirmation: !state.showConfirmation})); + setShowConfirmation(!showConfirmation); }; - handleSkip = (e: React.MouseEvent) => { + const handleSkip = (e: React.MouseEvent) => { e.stopPropagation(); - this.props.onSkip(); + onSkip(); }; - render() { - const {children} = this.props; - - return ( - - {children({skip: this.toggleConfirm})} - - - ); - } + return ( + + {children({skip: toggleConfirm})} + + + ); } export default SkipConfirm; From d6540d1ec3b45e7e4196d8272d41f0b69fdd143d Mon Sep 17 00:00:00 2001 From: Cathy Teng <70817427+cathteng@users.noreply.github.com> Date: Mon, 23 Dec 2024 07:44:52 -0800 Subject: [PATCH 448/757] feat(aci): registry for dual writing issue alert conditions (#82511) --- .../handlers/condition/__init__.py | 4 +- .../condition/group_state_handlers.py | 4 +- .../issue_alert_conditions.py | 41 ++++++++++ .../workflow_engine/models/data_condition.py | 2 +- .../handlers/condition/__init__.py | 0 .../handlers/condition/test_base.py | 42 ++++++++++ .../condition/test_group_state_handlers.py | 79 +++++++++++++++++++ .../processors/test_workflow.py | 2 +- 8 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py create mode 100644 tests/sentry/workflow_engine/handlers/condition/__init__.py create mode 100644 tests/sentry/workflow_engine/handlers/condition/test_base.py create mode 100644 tests/sentry/workflow_engine/handlers/condition/test_group_state_handlers.py diff --git a/src/sentry/workflow_engine/handlers/condition/__init__.py b/src/sentry/workflow_engine/handlers/condition/__init__.py index 7a98d09c4ad7be..2168a6ac24cd87 100644 --- a/src/sentry/workflow_engine/handlers/condition/__init__.py +++ b/src/sentry/workflow_engine/handlers/condition/__init__.py @@ -2,11 +2,11 @@ "EventCreatedByDetectorConditionHandler", "EventSeenCountConditionHandler", "ReappearedEventConditionHandler", - "RegressedEventConditionHandler", + "RegressionEventConditionHandler", ] from .group_event_handlers import ( EventCreatedByDetectorConditionHandler, EventSeenCountConditionHandler, ) -from .group_state_handlers import ReappearedEventConditionHandler, RegressedEventConditionHandler +from .group_state_handlers import ReappearedEventConditionHandler, RegressionEventConditionHandler diff --git a/src/sentry/workflow_engine/handlers/condition/group_state_handlers.py b/src/sentry/workflow_engine/handlers/condition/group_state_handlers.py index 3c34b052844928..e3dd951b7341a0 100644 --- a/src/sentry/workflow_engine/handlers/condition/group_state_handlers.py +++ b/src/sentry/workflow_engine/handlers/condition/group_state_handlers.py @@ -5,8 +5,8 @@ from sentry.workflow_engine.types import DataConditionHandler, WorkflowJob -@condition_handler_registry.register(Condition.REGRESSED_EVENT) -class RegressedEventConditionHandler(DataConditionHandler[WorkflowJob]): +@condition_handler_registry.register(Condition.REGRESSION_EVENT) +class RegressionEventConditionHandler(DataConditionHandler[WorkflowJob]): @staticmethod def evaluate_value(job: WorkflowJob, comparison: Any) -> bool: state = job.get("group_state", None) diff --git a/src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py b/src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py new file mode 100644 index 00000000000000..36dfb5007c1abd --- /dev/null +++ b/src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py @@ -0,0 +1,41 @@ +from collections.abc import Callable +from typing import Any + +from sentry.rules.conditions.reappeared_event import ReappearedEventCondition +from sentry.rules.conditions.regression_event import RegressionEventCondition +from sentry.utils.registry import Registry +from sentry.workflow_engine.models.data_condition import Condition, DataCondition +from sentry.workflow_engine.models.data_condition_group import DataConditionGroup + +data_condition_translator_registry = Registry[ + Callable[[dict[str, Any], DataConditionGroup], DataCondition] +]() + + +def translate_to_data_condition(data: dict[str, Any], dcg: DataConditionGroup): + translator = data_condition_translator_registry.get(data["id"]) + return translator(data, dcg) + + +@data_condition_translator_registry.register(ReappearedEventCondition.id) +def create_reappeared_event_data_condition( + data: dict[str, Any], dcg: DataConditionGroup +) -> DataCondition: + return DataCondition.objects.create( + type=Condition.REAPPEARED_EVENT, + comparison=True, + condition_result=True, + condition_group=dcg, + ) + + +@data_condition_translator_registry.register(RegressionEventCondition.id) +def create_regressed_event_data_condition( + data: dict[str, Any], dcg: DataConditionGroup +) -> DataCondition: + return DataCondition.objects.create( + type=Condition.REGRESSION_EVENT, + comparison=True, + condition_result=True, + condition_group=dcg, + ) diff --git a/src/sentry/workflow_engine/models/data_condition.py b/src/sentry/workflow_engine/models/data_condition.py index 8180723466faac..e9ef394cff23a1 100644 --- a/src/sentry/workflow_engine/models/data_condition.py +++ b/src/sentry/workflow_engine/models/data_condition.py @@ -22,7 +22,7 @@ class Condition(models.TextChoices): NOT_EQUAL = "ne" EVENT_CREATED_BY_DETECTOR = "event_created_by_detector" EVENT_SEEN_COUNT = "event_seen_count" - REGRESSED_EVENT = "regressed_event" + REGRESSION_EVENT = "regression_event" REAPPEARED_EVENT = "reappeared_event" diff --git a/tests/sentry/workflow_engine/handlers/condition/__init__.py b/tests/sentry/workflow_engine/handlers/condition/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tests/sentry/workflow_engine/handlers/condition/test_base.py b/tests/sentry/workflow_engine/handlers/condition/test_base.py new file mode 100644 index 00000000000000..51e56bbcf46624 --- /dev/null +++ b/tests/sentry/workflow_engine/handlers/condition/test_base.py @@ -0,0 +1,42 @@ +from typing import Any + +from sentry.rules.base import RuleBase +from sentry.workflow_engine.migration_helpers.issue_alert_conditions import ( + translate_to_data_condition as dual_write_condition, +) +from sentry.workflow_engine.models.data_condition import Condition, DataCondition +from sentry.workflow_engine.models.data_condition_group import DataConditionGroup +from sentry.workflow_engine.types import WorkflowJob +from tests.sentry.workflow_engine.test_base import BaseWorkflowTest + + +class ConditionTestCase(BaseWorkflowTest): + def setUp(self): + self.group, self.event, self.group_event = self.create_group_event() + + @property + def condition(self) -> Condition: + raise NotImplementedError + + @property + def rule_cls(self) -> type[RuleBase]: + # for mapping purposes, can delete later + raise NotImplementedError + + @property + def payload(self) -> dict[str, Any]: + # for dual write, can delete later + raise NotImplementedError + + def translate_to_data_condition( + self, data: dict[str, Any], dcg: DataConditionGroup + ) -> DataCondition: + return dual_write_condition(data, dcg) + + def assert_passes(self, data_condition: DataCondition, job: WorkflowJob) -> None: + assert data_condition.evaluate_value(job) == data_condition.get_condition_result() + + def assert_does_not_pass(self, data_condition: DataCondition, job: WorkflowJob) -> None: + assert data_condition.evaluate_value(job) != data_condition.get_condition_result() + + # TODO: activity diff --git a/tests/sentry/workflow_engine/handlers/condition/test_group_state_handlers.py b/tests/sentry/workflow_engine/handlers/condition/test_group_state_handlers.py new file mode 100644 index 00000000000000..2e59282bbf595a --- /dev/null +++ b/tests/sentry/workflow_engine/handlers/condition/test_group_state_handlers.py @@ -0,0 +1,79 @@ +from sentry.eventstream.base import GroupState +from sentry.rules.conditions.reappeared_event import ReappearedEventCondition +from sentry.rules.conditions.regression_event import RegressionEventCondition +from sentry.workflow_engine.models.data_condition import Condition +from sentry.workflow_engine.types import WorkflowJob +from tests.sentry.workflow_engine.handlers.condition.test_base import ConditionTestCase + + +class TestReappearedEventCondition(ConditionTestCase): + condition = Condition.REAPPEARED_EVENT + rule_cls = ReappearedEventCondition + payload = {"id": ReappearedEventCondition.id} + + def test(self): + job = WorkflowJob( + { + "event": self.group_event, + "has_reappeared": True, + } + ) + dc = self.create_data_condition( + type=self.condition, + comparison=True, + condition_result=True, + ) + + self.assert_passes(dc, job) + + job["has_reappeared"] = False + self.assert_does_not_pass(dc, job) + + def test_dual_write(self): + dcg = self.create_data_condition_group() + dc = self.translate_to_data_condition(self.payload, dcg) + + assert dc.type == self.condition + assert dc.comparison is True + assert dc.condition_result is True + assert dc.condition_group == dcg + + +class TestRegressionEventCondition(ConditionTestCase): + condition = Condition.REGRESSION_EVENT + rule_cls = RegressionEventCondition + payload = {"id": RegressionEventCondition.id} + + def test(self): + job = WorkflowJob( + { + "event": self.group_event, + "group_state": GroupState( + { + "id": 1, + "is_regression": True, + "is_new": False, + "is_new_group_environment": False, + } + ), + } + ) + dc = self.create_data_condition( + type=self.condition, + comparison=True, + condition_result=True, + ) + + self.assert_passes(dc, job) + + job["group_state"]["is_regression"] = False + self.assert_does_not_pass(dc, job) + + def test_dual_write(self): + dcg = self.create_data_condition_group() + dc = self.translate_to_data_condition(self.payload, dcg) + + assert dc.type == self.condition + assert dc.comparison is True + assert dc.condition_result is True + assert dc.condition_group == dcg diff --git a/tests/sentry/workflow_engine/processors/test_workflow.py b/tests/sentry/workflow_engine/processors/test_workflow.py index 9c1e39848324d3..a41c976a022598 100644 --- a/tests/sentry/workflow_engine/processors/test_workflow.py +++ b/tests/sentry/workflow_engine/processors/test_workflow.py @@ -50,7 +50,7 @@ def test_issue_occurrence_event(self): def test_regressed_event(self): dcg = self.create_data_condition_group() self.create_data_condition( - type=Condition.REGRESSED_EVENT, + type=Condition.REGRESSION_EVENT, comparison=True, condition_result=True, condition_group=dcg, From 4b25ba28ab9d98e52eaa82a779bb3e5622600390 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 23 Dec 2024 07:57:29 -0800 Subject: [PATCH 449/757] ref(replay): Refactor replay for mobile touch start/end parity check (#82515) Instead of doing this check inside the `VideoReplayerWithInteractions` constructor i just moved it up one level to the react component that creates that new instance, and wrapped it into a hook because react. It was previously guarded by: ``` if (root === null || isFetching) { return null; } // check if this is a video replay and if we can use the video (wrapper) replayer if (!isVideoReplay || !videoEvents || !startTimestampMs) { return null; } ``` and now we're just guarding the check with: ``` replay: isFetching ? null : replay ... if (!replay || !replay.getVideoEvents()) { return; } ``` But it'll run at the same time as before: once whenever the replay is fully loaded. --- .../app/components/replays/replayContext.tsx | 8 ++--- .../replays/videoReplayerWithInteractions.tsx | 21 ------------ .../playback/hooks/useTouchEventsCheck.tsx | 32 +++++++++++++++++++ 3 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 static/app/utils/replays/playback/hooks/useTouchEventsCheck.tsx diff --git a/static/app/components/replays/replayContext.tsx b/static/app/components/replays/replayContext.tsx index 25b64b29411833..9f53e0a5f74343 100644 --- a/static/app/components/replays/replayContext.tsx +++ b/static/app/components/replays/replayContext.tsx @@ -7,6 +7,7 @@ import {VideoReplayerWithInteractions} from 'sentry/components/replays/videoRepl import {trackAnalytics} from 'sentry/utils/analytics'; import clamp from 'sentry/utils/number/clamp'; import type useInitialOffsetMs from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs'; +import useTouchEventsCheck from 'sentry/utils/replays/playback/hooks/useTouchEventsCheck'; import {useReplayPrefs} from 'sentry/utils/replays/playback/providers/replayPreferencesContext'; import {ReplayCurrentTimeContextProvider} from 'sentry/utils/replays/playback/providers/useCurrentHoverTime'; import type ReplayReader from 'sentry/utils/replays/replayReader'; @@ -389,6 +390,8 @@ export function Provider({ ] ); + useTouchEventsCheck({replay: isFetching ? null : replay}); + const initVideoRoot = useCallback( (root: RootElem) => { if (root === null || isFetching) { @@ -429,13 +432,8 @@ export function Provider({ // rrweb specific theme, eventsWithSnapshots: replay?.getRRWebFramesWithSnapshots() ?? [], - touchEvents: replay?.getRRwebTouchEvents() ?? [], // common to both root, - context: { - sdkName: replay?.getReplay().sdk.name, - sdkVersion: replay?.getReplay().sdk.version, - }, }); // `.current` is marked as readonly, but it's safe to set the value from // inside a `useEffect` hook. diff --git a/static/app/components/replays/videoReplayerWithInteractions.tsx b/static/app/components/replays/videoReplayerWithInteractions.tsx index 114327c70033a3..58bb814a63701c 100644 --- a/static/app/components/replays/videoReplayerWithInteractions.tsx +++ b/static/app/components/replays/videoReplayerWithInteractions.tsx @@ -1,5 +1,4 @@ import type {Theme} from '@emotion/react'; -import * as Sentry from '@sentry/react'; import {Replayer} from '@sentry-internal/rrweb'; import type {VideoReplayerConfig} from 'sentry/components/replays/videoReplayer'; @@ -7,7 +6,6 @@ import {VideoReplayer} from 'sentry/components/replays/videoReplayer'; import type {ClipWindow, RecordingFrame, VideoEvent} from 'sentry/utils/replays/types'; interface VideoReplayerWithInteractionsOptions { - context: {sdkName: string | undefined | null; sdkVersion: string | undefined | null}; durationMs: number; eventsWithSnapshots: RecordingFrame[]; onBuffer: (isBuffering: boolean) => void; @@ -17,7 +15,6 @@ interface VideoReplayerWithInteractionsOptions { speed: number; start: number; theme: Theme; - touchEvents: RecordingFrame[]; videoApiPrefix: string; videoEvents: VideoEvent[]; clipWindow?: ClipWindow; @@ -35,7 +32,6 @@ export class VideoReplayerWithInteractions { constructor({ videoEvents, eventsWithSnapshots, - touchEvents, root, start, videoApiPrefix, @@ -46,7 +42,6 @@ export class VideoReplayerWithInteractions { durationMs, theme, speed, - context, }: VideoReplayerWithInteractionsOptions) { this.config = { skipInactive: false, @@ -65,22 +60,6 @@ export class VideoReplayerWithInteractions { config: this.config, }); - const grouped = Object.groupBy(touchEvents, (t: any) => t.data.pointerId); - Object.values(grouped).forEach(t => { - if (t?.length !== 2) { - Sentry.captureMessage( - 'Mobile replay has mismatching touch start and end events', - { - tags: { - sdk_name: context.sdkName, - sdk_version: context.sdkVersion, - touch_event_type: typeof t, - }, - } - ); - } - }); - this.replayer = new Replayer(eventsWithSnapshots, { root, blockClass: 'sentry-block', diff --git a/static/app/utils/replays/playback/hooks/useTouchEventsCheck.tsx b/static/app/utils/replays/playback/hooks/useTouchEventsCheck.tsx new file mode 100644 index 00000000000000..f49bc159978691 --- /dev/null +++ b/static/app/utils/replays/playback/hooks/useTouchEventsCheck.tsx @@ -0,0 +1,32 @@ +import {useEffect} from 'react'; +import * as Sentry from '@sentry/react'; + +import type ReplayReader from 'sentry/utils/replays/replayReader'; + +interface Props { + replay: ReplayReader | null; +} + +export default function useTouchEventsCheck({replay}: Props) { + useEffect(() => { + if (!replay || !replay.getVideoEvents()) { + return; + } + const touchEvents = replay.getRRwebTouchEvents() ?? []; + const grouped = Object.groupBy(touchEvents, (t: any) => t.data.pointerId); + Object.values(grouped).forEach(t => { + if (t?.length !== 2) { + Sentry.captureMessage( + 'Mobile replay has mismatching touch start and end events', + { + tags: { + sdk_name: replay.getReplay().sdk.name, + sdk_version: replay.getReplay().sdk.version, + touch_event_type: typeof t, + }, + } + ); + } + }); + }, [replay]); +} From 27f8b06b0d18bb5351ec52be1e1a941fced31f7d Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:11:12 -0500 Subject: [PATCH 450/757] ref: remove remaining references to iso_format(...) (#82526) --- src/sentry/testutils/helpers/datetime.py | 6 +----- tests/acceptance/test_issue_tag_values.py | 4 ++-- tests/acceptance/test_organization_dashboards.py | 4 ++-- tests/acceptance/test_organization_events_v2.py | 14 +++++++------- .../test_organization_global_selection_header.py | 6 +++--- tests/acceptance/test_organization_group_index.py | 6 +++--- tests/acceptance/test_performance_summary.py | 6 +++--- tests/acceptance/test_performance_trace_detail.py | 6 +++--- tests/acceptance/test_performance_trends.py | 6 +++--- tests/acceptance/test_project_tags_settings.py | 4 ++-- tests/acceptance/test_shared_issue.py | 4 ++-- 11 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/sentry/testutils/helpers/datetime.py b/src/sentry/testutils/helpers/datetime.py index da1af95d8c5c51..a24d8ec8a075e6 100644 --- a/src/sentry/testutils/helpers/datetime.py +++ b/src/sentry/testutils/helpers/datetime.py @@ -5,11 +5,7 @@ import time_machine -__all__ = ["iso_format", "before_now", "timestamp_format"] - - -def iso_format(date: datetime) -> str: - return date.isoformat()[:19] +__all__ = ["before_now", "timestamp_format"] def before_now(**kwargs: float) -> datetime: diff --git a/tests/acceptance/test_issue_tag_values.py b/tests/acceptance/test_issue_tag_values.py index 5952066b42a768..731cbe80baa869 100644 --- a/tests/acceptance/test_issue_tag_values.py +++ b/tests/acceptance/test_issue_tag_values.py @@ -1,6 +1,6 @@ from fixtures.page_objects.issue_details import IssueDetailsPage from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test from sentry.utils.samples import load_data @@ -24,7 +24,7 @@ def setUp(self): def create_issue(self): event_data = load_data("javascript") - event_data["timestamp"] = iso_format(before_now(minutes=1)) + event_data["timestamp"] = before_now(minutes=1).isoformat() event_data["tags"] = {"url": "http://example.org/path?key=value"} return self.store_event(data=event_data, project_id=self.project.id) diff --git a/tests/acceptance/test_organization_dashboards.py b/tests/acceptance/test_organization_dashboards.py index 4be11043fcb38f..8986cc42ec841f 100644 --- a/tests/acceptance/test_organization_dashboards.py +++ b/tests/acceptance/test_organization_dashboards.py @@ -20,7 +20,7 @@ DashboardWidgetTypes, ) from sentry.testutils.cases import AcceptanceTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test FEATURE_NAMES = [ @@ -41,7 +41,7 @@ class OrganizationDashboardsAcceptanceTest(AcceptanceTestCase): def setUp(self): super().setUp() - min_ago = iso_format(before_now(minutes=1)) + min_ago = before_now(minutes=1).isoformat() self.store_event( data={"event_id": "a" * 32, "message": "oh no", "timestamp": min_ago}, project_id=self.project.id, diff --git a/tests/acceptance/test_organization_events_v2.py b/tests/acceptance/test_organization_events_v2.py index ec7dca227fb658..064529cd9c2f83 100644 --- a/tests/acceptance/test_organization_events_v2.py +++ b/tests/acceptance/test_organization_events_v2.py @@ -9,7 +9,7 @@ from sentry.discover.models import DiscoverSavedQuery from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format, timestamp_format +from sentry.testutils.helpers.datetime import before_now, timestamp_format from sentry.testutils.silo import no_silo_test from sentry.utils.samples import load_data @@ -183,8 +183,8 @@ def test_all_events_query_empty_state(self): def test_all_events_query(self, mock_now): now = before_now() mock_now.return_value = now - five_mins_ago = iso_format(now - timedelta(minutes=5)) - ten_mins_ago = iso_format(now - timedelta(minutes=10)) + five_mins_ago = (now - timedelta(minutes=5)).isoformat() + ten_mins_ago = (now - timedelta(minutes=10)).isoformat() self.store_event( data={ "event_id": "a" * 32, @@ -238,7 +238,7 @@ def test_errors_query_empty_state(self): def test_errors_query(self, mock_now): now = before_now() mock_now.return_value = now - ten_mins_ago = iso_format(now - timedelta(minutes=10)) + ten_mins_ago = (now - timedelta(minutes=10)).isoformat() self.store_event( data={ "event_id": "a" * 32, @@ -306,7 +306,7 @@ def test_transactions_query(self, mock_now): def test_event_detail_view_from_all_events(self, mock_now): now = before_now() mock_now.return_value = now - ten_mins_ago = iso_format(now - timedelta(minutes=10)) + ten_mins_ago = (now - timedelta(minutes=10)).isoformat() event_data = load_data("python") event_data.update( @@ -346,7 +346,7 @@ def test_event_detail_view_from_errors_view(self, mock_now): event_data = load_data("javascript") event_data.update( { - "timestamp": iso_format(now - timedelta(minutes=5)), + "timestamp": (now - timedelta(minutes=5)).isoformat(), "event_id": "d" * 32, "fingerprint": ["group-1"], } @@ -646,7 +646,7 @@ def test_duplicate_query(self): def test_drilldown_result(self, mock_now): now = before_now() mock_now.return_value = now - ten_mins_ago = iso_format(now - timedelta(minutes=10)) + ten_mins_ago = (now - timedelta(minutes=10)).isoformat() events = ( ("a" * 32, "oh no", "group-1"), ("b" * 32, "oh no", "group-1"), diff --git a/tests/acceptance/test_organization_global_selection_header.py b/tests/acceptance/test_organization_global_selection_header.py index e712c340a1d56a..bd59d2841a6243 100644 --- a/tests/acceptance/test_organization_global_selection_header.py +++ b/tests/acceptance/test_organization_global_selection_header.py @@ -7,7 +7,7 @@ from fixtures.page_objects.issue_details import IssueDetailsPage from fixtures.page_objects.issue_list import IssueListPage from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test event_time = before_now(days=3) @@ -51,7 +51,7 @@ def create_issues(self): data={ "event_id": "a" * 32, "message": "oh no", - "timestamp": iso_format(event_time), + "timestamp": event_time.isoformat(), "fingerprint": ["group-1"], }, project_id=self.project_1.id, @@ -60,7 +60,7 @@ def create_issues(self): data={ "event_id": "b" * 32, "message": "oh snap", - "timestamp": iso_format(event_time), + "timestamp": event_time.isoformat(), "fingerprint": ["group-2"], "environment": "prod", }, diff --git a/tests/acceptance/test_organization_group_index.py b/tests/acceptance/test_organization_group_index.py index fca1fd2e02ffec..24958b38cb2204 100644 --- a/tests/acceptance/test_organization_group_index.py +++ b/tests/acceptance/test_organization_group_index.py @@ -8,7 +8,7 @@ from sentry.models.group import GroupStatus from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test event_time = before_now(days=3) @@ -36,7 +36,7 @@ def create_issues(self): data={ "event_id": "a" * 32, "message": "oh no", - "timestamp": iso_format(event_time - timedelta(hours=1)), + "timestamp": (event_time - timedelta(hours=1)).isoformat(), "fingerprint": ["group-1"], }, project_id=self.project.id, @@ -46,7 +46,7 @@ def create_issues(self): data={ "event_id": "b" * 32, "message": "oh snap", - "timestamp": iso_format(event_time), + "timestamp": event_time.isoformat(), "fingerprint": ["group-2"], }, project_id=self.project.id, diff --git a/tests/acceptance/test_performance_summary.py b/tests/acceptance/test_performance_summary.py index 2b40d6df60c34d..eb22e6335b9391 100644 --- a/tests/acceptance/test_performance_summary.py +++ b/tests/acceptance/test_performance_summary.py @@ -4,7 +4,7 @@ from fixtures.page_objects.transaction_summary import TransactionSummaryPage from sentry.models.assistant import AssistantActivity from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test from sentry.utils.samples import load_data @@ -52,7 +52,7 @@ def test_with_data(self, mock_now): "transaction": "/country_by_code/", "message": "This is bad", "event_id": "b" * 32, - "timestamp": iso_format(before_now(minutes=1)), + "timestamp": before_now(minutes=1).isoformat(), }, project_id=self.project.id, ) @@ -195,7 +195,7 @@ def test_transaction_threshold_modal(self, mock_now): "transaction": "/country_by_code/", "message": "This is bad", "event_id": "b" * 32, - "timestamp": iso_format(before_now(minutes=3)), + "timestamp": before_now(minutes=3).isoformat(), }, project_id=self.project.id, ) diff --git a/tests/acceptance/test_performance_trace_detail.py b/tests/acceptance/test_performance_trace_detail.py index e0ae6743a68597..9e509dd890a954 100644 --- a/tests/acceptance/test_performance_trace_detail.py +++ b/tests/acceptance/test_performance_trace_detail.py @@ -3,7 +3,7 @@ from uuid import uuid4 from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test from sentry.utils.samples import load_data @@ -183,8 +183,8 @@ def path(self): return "/organizations/{}/performance/trace/{}/?pageStart={}&pageEnd={}".format( self.org.slug, self.trace_id, - iso_format(before_now(days=1).replace(hour=9, minute=0, second=0, microsecond=0)), - iso_format(before_now(days=1).replace(hour=11, minute=0, second=0, microsecond=0)), + before_now(days=1).replace(hour=9, minute=0, second=0, microsecond=0).isoformat(), + before_now(days=1).replace(hour=11, minute=0, second=0, microsecond=0).isoformat(), ) @patch("django.utils.timezone.now") diff --git a/tests/acceptance/test_performance_trends.py b/tests/acceptance/test_performance_trends.py index 7329a9a499deaf..2d16aec4e54cb5 100644 --- a/tests/acceptance/test_performance_trends.py +++ b/tests/acceptance/test_performance_trends.py @@ -6,7 +6,7 @@ from fixtures.page_objects.base import BasePage from sentry.models.project import Project from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test from sentry.utils.samples import load_data @@ -28,8 +28,8 @@ def make_trend( { "transaction": name, "event_id": f"{index:02x}".rjust(32, "0"), - "start_timestamp": iso_format(before_now(minutes=minutes, seconds=duration)), - "timestamp": iso_format(before_now(minutes=minutes)), + "start_timestamp": before_now(minutes=minutes, seconds=duration).isoformat(), + "timestamp": before_now(minutes=minutes).isoformat(), } ) self.store_event(data=event, project_id=self.project.id) diff --git a/tests/acceptance/test_project_tags_settings.py b/tests/acceptance/test_project_tags_settings.py index 7131519c8e02c1..1bbc8cdf48eed2 100644 --- a/tests/acceptance/test_project_tags_settings.py +++ b/tests/acceptance/test_project_tags_settings.py @@ -2,7 +2,7 @@ from unittest.mock import patch from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test event_time = before_now(days=3) @@ -29,7 +29,7 @@ def test_tags_list(self, mock_timezone): "event_id": "a" * 32, "message": "oh no", "level": "error", - "timestamp": iso_format(event_time), + "timestamp": event_time.isoformat(), }, project_id=self.project.id, assert_no_errors=False, diff --git a/tests/acceptance/test_shared_issue.py b/tests/acceptance/test_shared_issue.py index 415661ed9e93c1..3631eb7cedf137 100644 --- a/tests/acceptance/test_shared_issue.py +++ b/tests/acceptance/test_shared_issue.py @@ -1,6 +1,6 @@ from sentry.models.groupshare import GroupShare from sentry.testutils.cases import AcceptanceTestCase -from sentry.testutils.helpers.datetime import before_now, iso_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test from sentry.utils.samples import load_data @@ -17,7 +17,7 @@ def setUp(self): def test_python_event(self): data = load_data(platform="python") - data["timestamp"] = iso_format(before_now(days=1)) + data["timestamp"] = before_now(days=1).isoformat() event = self.store_event(data=data, project_id=self.project.id) assert event.group is not None From 7b0832cd33a70c98178669ee84bcc05d0508eb85 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:13:58 -0500 Subject: [PATCH 451/757] ref: rm browserHistory from metricsDataSwitcher (#82527) --- .../app/views/performance/landing/metricsDataSwitcher.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/static/app/views/performance/landing/metricsDataSwitcher.tsx b/static/app/views/performance/landing/metricsDataSwitcher.tsx index 221ce6572b1803..22ed5d53889b66 100644 --- a/static/app/views/performance/landing/metricsDataSwitcher.tsx +++ b/static/app/views/performance/landing/metricsDataSwitcher.tsx @@ -4,7 +4,6 @@ import type {Location} from 'history'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import type {Organization} from 'sentry/types/organization'; -import {browserHistory} from 'sentry/utils/browserHistory'; import type EventView from 'sentry/utils/discover/eventView'; import type {MetricDataSwitcherOutcome} from 'sentry/utils/performance/contexts/metricsCardinality'; import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality'; @@ -14,6 +13,7 @@ import { METRIC_SEARCH_SETTING_PARAM, } from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {decodeScalar} from 'sentry/utils/queryString'; +import {useNavigate} from 'sentry/utils/useNavigate'; interface MetricDataSwitchProps { children: (props: MetricDataSwitcherOutcome) => React.ReactNode; @@ -91,13 +91,14 @@ function MetricsSwitchHandler({ const mepSearchState = decodeScalar(query[METRIC_SEARCH_SETTING_PARAM], ''); const hasQuery = decodeScalar(query.query, ''); const queryIsTransactionsBased = mepSearchState === MEPState.TRANSACTIONS_ONLY; + const navigate = useNavigate(); const shouldAdjustQuery = hasQuery && queryIsTransactionsBased && !outcome.forceTransactionsOnly; useEffect(() => { if (shouldAdjustQuery) { - browserHistory.push({ + navigate({ pathname: location.pathname, query: { ...location.query, @@ -107,7 +108,7 @@ function MetricsSwitchHandler({ }, }); } - }, [shouldAdjustQuery, location]); + }, [shouldAdjustQuery, location, navigate]); if (hasQuery && queryIsTransactionsBased && !outcome.forceTransactionsOnly) { eventView.query = ''; // TODO: Create switcher provider and move it to the route level to remove the need for this. From 3a45ce2f356e2bd19693855c2eede0439427b976 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:14:05 -0500 Subject: [PATCH 452/757] ref: rm browserHistory from useQueryBasedColumnResize (#82528) seems to work ok with https://sentry.sentry.io/stories/?name=app/components/gridEditable/index.stories.tsx --- .../replays/useQueryBasedColumnResize.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/static/app/components/replays/useQueryBasedColumnResize.tsx b/static/app/components/replays/useQueryBasedColumnResize.tsx index f3a56e445fa340..a23c613a2d57b7 100644 --- a/static/app/components/replays/useQueryBasedColumnResize.tsx +++ b/static/app/components/replays/useQueryBasedColumnResize.tsx @@ -4,8 +4,8 @@ import dropRightWhile from 'lodash/dropRightWhile'; import type {GridColumnOrder} from 'sentry/components/gridEditable'; import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {decodeInteger, decodeList} from 'sentry/utils/queryString'; +import {useNavigate} from 'sentry/utils/useNavigate'; interface Props { columns: GridColumnOrder[]; @@ -19,6 +19,7 @@ export default function useQueryBasedColumnResize({ paramName = 'width', }: Props) { const queryParam = location.query[paramName]; + const navigate = useNavigate(); const columnsWidthWidths = useMemo(() => { const widths = decodeList(queryParam); @@ -34,15 +35,18 @@ export default function useQueryBasedColumnResize({ (column, i) => (i === columnIndex ? resizedColumn.width : column.width) ?? COL_WIDTH_UNDEFINED ); - browserHistory.replace({ - pathname: location.pathname, - query: { - ...location.query, - [paramName]: dropRightWhile(widths, width => width === COL_WIDTH_UNDEFINED), + navigate( + { + pathname: location.pathname, + query: { + ...location.query, + [paramName]: dropRightWhile(widths, width => width === COL_WIDTH_UNDEFINED), + }, }, - }); + {replace: true} + ); }, - [columns, location.pathname, location.query, paramName] + [columns, location.pathname, location.query, paramName, navigate] ); return { From 201e9f44f7344854c6b4b9b54c7ee9d740e97bc8 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:14:12 -0500 Subject: [PATCH 453/757] ref: rm browserHistory from deadRageClickList (#82529) --- static/app/views/replays/deadRageClick/deadRageClickList.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/app/views/replays/deadRageClick/deadRageClickList.tsx b/static/app/views/replays/deadRageClick/deadRageClickList.tsx index f9b41d73ca5fcb..608fd5bbba89d3 100644 --- a/static/app/views/replays/deadRageClick/deadRageClickList.tsx +++ b/static/app/views/replays/deadRageClick/deadRageClickList.tsx @@ -11,9 +11,9 @@ import Pagination from 'sentry/components/pagination'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import {browserHistory} from 'sentry/utils/browserHistory'; import useDeadRageSelectors from 'sentry/utils/replays/hooks/useDeadRageSelectors'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import SelectorTable from 'sentry/views/replays/deadRageClick/selectorTable'; import ReplayTabs from 'sentry/views/replays/tabs'; @@ -21,6 +21,7 @@ import ReplayTabs from 'sentry/views/replays/tabs'; export default function DeadRageClickList() { const organization = useOrganization(); const location = useLocation(); + const navigate = useNavigate(); const {isLoading, isError, data, pageLinks} = useDeadRageSelectors({ per_page: 50, @@ -72,7 +73,7 @@ export default function DeadRageClickList() { { - browserHistory.push({ + navigate({ pathname: path, query: {...searchQuery, cursor}, }); From 95a141d496c86a17b7bf785abfe420c699e57d54 Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Mon, 23 Dec 2024 08:16:20 -0800 Subject: [PATCH 454/757] :mag: nit(slack): clean up form field for slack rule form (#82509) xtreme nit, something that was bothering me when working on some aci stuff --- src/sentry/integrations/slack/actions/notification.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sentry/integrations/slack/actions/notification.py b/src/sentry/integrations/slack/actions/notification.py index da4ba68e335e39..9854b42a8f56d1 100644 --- a/src/sentry/integrations/slack/actions/notification.py +++ b/src/sentry/integrations/slack/actions/notification.py @@ -68,10 +68,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: "channel": {"type": "string", "placeholder": "e.g., #critical, Jane Schmidt"}, "channel_id": {"type": "string", "placeholder": "e.g., CA2FRA079 or UA1J9RTE1"}, "tags": {"type": "string", "placeholder": "e.g., environment,user,my_tag"}, - } - self.form_fields["notes"] = { - "type": "string", - "placeholder": "e.g. @jane, @on-call-team", + "notes": {"type": "string", "placeholder": "e.g., @jane, @on-call-team"}, } self._repository: IssueAlertNotificationMessageRepository = ( From 56bab7942ec57e38919126f4fd679f215a973261 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:36:20 -0500 Subject: [PATCH 455/757] ref: rm browserHistory from useFiltersInLocationQuery and tests (#82530) --- .../hooks/useFiltersInLocationQuery.tsx | 7 +- .../detail/console/useConsoleFilters.spec.tsx | 38 +++--- .../detail/errorList/useErrorFilters.spec.tsx | 38 +++--- .../detail/network/useNetworkFilters.spec.tsx | 115 +++++++++++------- .../detail/tagPanel/useTagFilters.spec.tsx | 6 +- 5 files changed, 117 insertions(+), 87 deletions(-) diff --git a/static/app/utils/replays/hooks/useFiltersInLocationQuery.tsx b/static/app/utils/replays/hooks/useFiltersInLocationQuery.tsx index bd150dfe35ec5a..bdfffa1845caec 100644 --- a/static/app/utils/replays/hooks/useFiltersInLocationQuery.tsx +++ b/static/app/utils/replays/hooks/useFiltersInLocationQuery.tsx @@ -1,17 +1,18 @@ import {useCallback} from 'react'; import type {Query} from 'history'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; function useFiltersInLocationQuery() { const {pathname, query} = useLocation(); + const navigate = useNavigate(); const setFilter = useCallback( (updatedQuery: Partial) => { - browserHistory.replace({pathname, query: {...query, ...updatedQuery}}); + navigate({pathname, query: {...query, ...updatedQuery}}, {replace: true}); }, - [pathname, query] + [pathname, query, navigate] ); return { diff --git a/static/app/views/replays/detail/console/useConsoleFilters.spec.tsx b/static/app/views/replays/detail/console/useConsoleFilters.spec.tsx index eefa3bae3ca7cf..1735aafcf7455b 100644 --- a/static/app/views/replays/detail/console/useConsoleFilters.spec.tsx +++ b/static/app/views/replays/detail/console/useConsoleFilters.spec.tsx @@ -5,14 +5,16 @@ import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary'; import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs'; -import {browserHistory} from 'sentry/utils/browserHistory'; import hydrateBreadcrumbs from 'sentry/utils/replays/hydrateBreadcrumbs'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; import type {FilterFields} from 'sentry/views/replays/detail/console/useConsoleFilters'; import useConsoleFilters from 'sentry/views/replays/detail/console/useConsoleFilters'; jest.mock('sentry/utils/useLocation'); +jest.mock('sentry/utils/useNavigate'); +const mockUseNavigate = jest.mocked(useNavigate); const mockUseLocation = jest.mocked(useLocation); const frames = hydrateBreadcrumbs(ReplayRecordFixture(), [ @@ -93,11 +95,9 @@ const frames = hydrateBreadcrumbs(ReplayRecordFixture(), [ ]); describe('useConsoleFilters', () => { - beforeEach(() => { - jest.mocked(browserHistory.replace).mockReset(); - }); - it('should update the url when setters are called', () => { + const mockNavigate = jest.fn(); + mockUseNavigate.mockReturnValue(mockNavigate); const LOG_FILTER = ['error']; const SEARCH_FILTER = 'component'; @@ -116,23 +116,29 @@ describe('useConsoleFilters', () => { }); result.current.setLogLevel(LOG_FILTER); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_c_logLevel: LOG_FILTER, + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_c_logLevel: LOG_FILTER, + }, }, - }); + {replace: true} + ); rerender({frames}); result.current.setSearchTerm(SEARCH_FILTER); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_c_logLevel: LOG_FILTER, - f_c_search: SEARCH_FILTER, + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_c_logLevel: LOG_FILTER, + f_c_search: SEARCH_FILTER, + }, }, - }); + {replace: true} + ); }); it('should not filter anything when no values are set', async () => { diff --git a/static/app/views/replays/detail/errorList/useErrorFilters.spec.tsx b/static/app/views/replays/detail/errorList/useErrorFilters.spec.tsx index fe726b6098e2f3..72d5350bec13f4 100644 --- a/static/app/views/replays/detail/errorList/useErrorFilters.spec.tsx +++ b/static/app/views/replays/detail/errorList/useErrorFilters.spec.tsx @@ -4,9 +4,9 @@ import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import hydrateErrors from 'sentry/utils/replays/hydrateErrors'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; import type { ErrorSelectOption, FilterFields, @@ -14,7 +14,9 @@ import type { import useErrorFilters from 'sentry/views/replays/detail/errorList/useErrorFilters'; jest.mock('sentry/utils/useLocation'); +jest.mock('sentry/utils/useNavigate'); +const mockUseNavigate = jest.mocked(useNavigate); const mockUseLocation = jest.mocked(useLocation); const { @@ -54,11 +56,9 @@ const { ); describe('useErrorFilters', () => { - beforeEach(() => { - jest.mocked(browserHistory.replace).mockReset(); - }); - it('should update the url when setters are called', () => { + const mockNavigate = jest.fn(); + mockUseNavigate.mockReturnValue(mockNavigate); const errorFrames = [ ERROR_1_JS_RANGEERROR, ERROR_2_NEXTJS_TYPEERROR, @@ -87,23 +87,29 @@ describe('useErrorFilters', () => { }); result.current.setFilters([PROJECT_OPTION]); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_e_project: [PROJECT_OPTION.value], + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_e_project: [PROJECT_OPTION.value], + }, }, - }); + {replace: true} + ); rerender({errorFrames}); result.current.setSearchTerm(SEARCH_FILTER); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_e_project: [PROJECT_OPTION.value], - f_e_search: SEARCH_FILTER, + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_e_project: [PROJECT_OPTION.value], + f_e_search: SEARCH_FILTER, + }, }, - }); + {replace: true} + ); }); it('should not filter anything when no values are set', async () => { diff --git a/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx b/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx index c38915855a0d7d..decda11191b470 100644 --- a/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx +++ b/static/app/views/replays/detail/network/useNetworkFilters.spec.tsx @@ -9,15 +9,17 @@ import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; import {renderHook} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import hydrateSpans from 'sentry/utils/replays/hydrateSpans'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; import type {FilterFields, NetworkSelectOption} from './useNetworkFilters'; import useNetworkFilters from './useNetworkFilters'; jest.mock('sentry/utils/useLocation'); +jest.mock('sentry/utils/useNavigate'); +const mockUseNavigate = jest.mocked(useNavigate); const mockUseLocation = jest.mocked(useLocation); const [ @@ -116,11 +118,9 @@ describe('useNetworkFilters', () => { SPAN_8_FETCH_POST, ]; - beforeEach(() => { - jest.mocked(browserHistory.replace).mockReset(); - }); - it('should update the url when setters are called', () => { + const mockNavigate = jest.fn(); + mockUseNavigate.mockReturnValue(mockNavigate); const TYPE_OPTION: NetworkSelectOption = { value: 'resource.fetch', label: 'resource.fetch', @@ -152,41 +152,53 @@ describe('useNetworkFilters', () => { }); result.current.setFilters([TYPE_OPTION]); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_n_method: [], - f_n_status: [], - f_n_type: [TYPE_OPTION.value], + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_n_method: [], + f_n_status: [], + f_n_type: [TYPE_OPTION.value], + }, }, - }); + {replace: true} + ); rerender({networkFrames}); result.current.setFilters([TYPE_OPTION, STATUS_OPTION]); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_n_method: [], - f_n_status: [STATUS_OPTION.value], - f_n_type: [TYPE_OPTION.value], + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_n_method: [], + f_n_status: [STATUS_OPTION.value], + f_n_type: [TYPE_OPTION.value], + }, }, - }); + {replace: true} + ); rerender({networkFrames}); result.current.setSearchTerm(SEARCH_FILTER); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_n_type: [TYPE_OPTION.value], - f_n_status: [STATUS_OPTION.value], - f_n_search: SEARCH_FILTER, + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_n_type: [TYPE_OPTION.value], + f_n_status: [STATUS_OPTION.value], + f_n_search: SEARCH_FILTER, + }, }, - }); + {replace: true} + ); }); it('should clear details params when setters are called', () => { + const mockNavigate = jest.fn(); + mockUseNavigate.mockReturnValue(mockNavigate); + const TYPE_OPTION: NetworkSelectOption = { value: 'resource.fetch', label: 'resource.fetch', @@ -230,38 +242,47 @@ describe('useNetworkFilters', () => { }); result.current.setFilters([TYPE_OPTION]); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_n_method: [], - f_n_status: [], - f_n_type: [TYPE_OPTION.value], + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_n_method: [], + f_n_status: [], + f_n_type: [TYPE_OPTION.value], + }, }, - }); + {replace: true} + ); rerender({networkFrames}); result.current.setFilters([TYPE_OPTION, STATUS_OPTION]); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_n_method: [], - f_n_status: [STATUS_OPTION.value], - f_n_type: [TYPE_OPTION.value], + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_n_method: [], + f_n_status: [STATUS_OPTION.value], + f_n_type: [TYPE_OPTION.value], + }, }, - }); + {replace: true} + ); rerender({networkFrames}); result.current.setSearchTerm(SEARCH_FILTER); - expect(browserHistory.replace).toHaveBeenLastCalledWith({ - pathname: '/', - query: { - f_n_status: [STATUS_OPTION.value], - f_n_type: [TYPE_OPTION.value], - f_n_search: SEARCH_FILTER, + expect(mockNavigate).toHaveBeenLastCalledWith( + { + pathname: '/', + query: { + f_n_status: [STATUS_OPTION.value], + f_n_type: [TYPE_OPTION.value], + f_n_search: SEARCH_FILTER, + }, }, - }); + {replace: true} + ); }); it('should not filter anything when no values are set', () => { diff --git a/static/app/views/replays/detail/tagPanel/useTagFilters.spec.tsx b/static/app/views/replays/detail/tagPanel/useTagFilters.spec.tsx index f1dd253a6f6bae..32eabd66d6346e 100644 --- a/static/app/views/replays/detail/tagPanel/useTagFilters.spec.tsx +++ b/static/app/views/replays/detail/tagPanel/useTagFilters.spec.tsx @@ -3,22 +3,18 @@ import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; import {renderHook} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {useLocation} from 'sentry/utils/useLocation'; import type {FilterFields} from 'sentry/views/replays/detail/tagPanel/useTagFilters'; import useTagFilters from 'sentry/views/replays/detail/tagPanel/useTagFilters'; jest.mock('sentry/utils/useLocation'); +jest.mock('sentry/utils/useNavigate'); const mockUseLocation = jest.mocked(useLocation); const tags = ReplayRecordFixture().tags; describe('useTagsFilters', () => { - beforeEach(() => { - jest.mocked(browserHistory.push).mockReset(); - }); - it('should not filter anything when no values are set', () => { mockUseLocation.mockReturnValue({ pathname: '/', From b5c79a21f6508ae31b47f4e1cf36a3ed1d70b38d Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Mon, 23 Dec 2024 11:51:07 -0500 Subject: [PATCH 456/757] feat(widget-builder): Add RPC toggle for spans dataset (#82531) This uses a few tricks to set the `useRpc` flag in events/events-stats requests. We use local storage to store the key, but update the query params when the dataset is toggled so we can pick it up on the preview and re-query the dataset. I'm storing the state in local storage because I don't want to update all of the config interfaces just to support this temporary flag. --- .../views/dashboards/datasetConfig/spans.tsx | 9 +++++ .../components/newWidgetBuilder.tsx | 10 ++++++ .../widgetBuilder/components/rpcToggle.tsx | 36 +++++++++++++++++++ .../components/widgetBuilderSlideout.tsx | 10 ++++++ 4 files changed, 65 insertions(+) create mode 100644 static/app/views/dashboards/widgetBuilder/components/rpcToggle.tsx diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index fe743e1015a204..636c2b62865c77 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -21,6 +21,7 @@ import { } from 'sentry/utils/discover/genericDiscoverQuery'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields'; +import localStorage from 'sentry/utils/localStorage'; import type {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl'; import { @@ -36,6 +37,7 @@ import {getSeriesRequestData} from 'sentry/views/dashboards/datasetConfig/utils/ import {DisplayType, type Widget, type WidgetQuery} from 'sentry/views/dashboards/types'; import {eventViewFromWidget} from 'sentry/views/dashboards/utils'; import SpansSearchBar from 'sentry/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/spansSearchBar'; +import {DASHBOARD_RPC_TOGGLE_KEY} from 'sentry/views/dashboards/widgetBuilder/components/rpcToggle'; import type {FieldValueOption} from 'sentry/views/discover/table/queryField'; import {FieldValueKind} from 'sentry/views/discover/table/types'; import {generateFieldOptions} from 'sentry/views/discover/utils'; @@ -203,11 +205,14 @@ function getEventsRequest( const url = `/organizations/${organization.slug}/events/`; const eventView = eventViewFromWidget('', query, pageFilters); + const useRpc = localStorage.getItem(DASHBOARD_RPC_TOGGLE_KEY) === 'true'; + const params: DiscoverQueryRequestParams = { per_page: limit, cursor, referrer, dataset: DiscoverDatasets.SPANS_EAP, + useRpc: useRpc ? '1' : undefined, ...queryExtras, }; @@ -270,5 +275,9 @@ function getSeriesRequest( DiscoverDatasets.SPANS_EAP, referrer ); + + const useRpc = localStorage.getItem(DASHBOARD_RPC_TOGGLE_KEY) === 'true'; + requestData.useRpc = useRpc; + return doEventsRequest(api, requestData); } diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx index d8aaeaa8e58c84..c5cc6c65ebcdb2 100644 --- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx @@ -11,6 +11,8 @@ import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; +import {decodeBoolean} from 'sentry/utils/queryString'; +import useLocationQuery from 'sentry/utils/url/useLocationQuery'; import useKeyPress from 'sentry/utils/useKeyPress'; import {useLocation} from 'sentry/utils/useLocation'; import useMedia from 'sentry/utils/useMedia'; @@ -21,6 +23,7 @@ import { type DashboardFilters, DisplayType, type Widget, + WidgetType, } from 'sentry/views/dashboards/types'; import { DEFAULT_WIDGET_DRAG_POSITIONING, @@ -166,6 +169,11 @@ export function WidgetPreviewContainer({ const organization = useOrganization(); const location = useLocation(); const theme = useTheme(); + const {useRpc} = useLocationQuery({ + fields: { + useRpc: decodeBoolean, + }, + }); const isSmallScreen = useMedia(`(max-width: ${theme.breakpoints.small})`); // if small screen and draggable, enable dragging @@ -247,6 +255,8 @@ export function WidgetPreviewContainer({ }} > ({ + fieldName: 'useRpc', + }); + // This is hacky, but we need to access the RPC toggle state in the spans dataset config + // and I don't want to pass it down as a prop when it's only temporary. + const [isRpcEnabled, setRpcLocalStorage] = useLocalStorageState( + DASHBOARD_RPC_TOGGLE_KEY, + false + ); + + return ( + + { + const newValue = !isRpcEnabled; + setIsRpcEnabled(newValue); + setRpcLocalStorage(newValue); + }} + /> +
{t('Use RPC')}
+
+ ); +} + +export default RPCToggle; diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 1537fa46550252..9f61407e271391 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -8,12 +8,14 @@ import {IconClose} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import useMedia from 'sentry/utils/useMedia'; +import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import { type DashboardDetails, type DashboardFilters, DisplayType, type Widget, + WidgetType, } from 'sentry/views/dashboards/types'; import WidgetBuilderDatasetSelector from 'sentry/views/dashboards/widgetBuilder/components/datasetSelector'; import WidgetBuilderFilterBar from 'sentry/views/dashboards/widgetBuilder/components/filtersBar'; @@ -21,6 +23,7 @@ import WidgetBuilderGroupBySelector from 'sentry/views/dashboards/widgetBuilder/ import WidgetBuilderNameAndDescription from 'sentry/views/dashboards/widgetBuilder/components/nameAndDescFields'; import {WidgetPreviewContainer} from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder'; import WidgetBuilderQueryFilterBuilder from 'sentry/views/dashboards/widgetBuilder/components/queryFilterBuilder'; +import RPCToggle from 'sentry/views/dashboards/widgetBuilder/components/rpcToggle'; import SaveButton from 'sentry/views/dashboards/widgetBuilder/components/saveButton'; import WidgetBuilderSortBySelector from 'sentry/views/dashboards/widgetBuilder/components/sortBySelector'; import WidgetBuilderTypeSelector from 'sentry/views/dashboards/widgetBuilder/components/typeSelector'; @@ -48,6 +51,7 @@ function WidgetBuilderSlideout({ setIsPreviewDraggable, isWidgetInvalid, }: WidgetBuilderSlideoutProps) { + const organization = useOrganization(); const {state} = useWidgetBuilderContext(); const {widgetIndex} = useParams(); const theme = useTheme(); @@ -107,6 +111,12 @@ function WidgetBuilderSlideout({
+ {organization.features.includes('visibility-explore-dataset') && + state.dataset === WidgetType.SPANS && ( +
+ +
+ )}
From 4a0b464262eb89cbebda8c48f04e71c6eb921df0 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 23 Dec 2024 09:02:35 -0800 Subject: [PATCH 457/757] ref(replay): Make ReplayPlayerEventsContext more generic as ReplayReaderProvider (#82532) Doing a rename so that the `ReplayReader` class instance is just passed through the context to the replay player where it's needed. We're only reading from this context in one spot, so the provider doesn't need to do anything special and we can drop those tests. Instead the callsite will grab what it needs from the instance. The instance interface has proven to be pretty strong imo, and it's better to call methods on the ReplayReader as-needed instead of in the provider, so the cache is only filled on-demand --- .../diff/replaySideBySideImageDiff.tsx | 6 +- .../replays/diff/replaySliderDiff.tsx | 6 +- .../replays/player/__stories__/providers.tsx | 6 +- .../replays/player/replayPlayer.tsx | 12 ++-- .../replayPlayerEventsContext.spec.tsx | 58 ------------------- .../providers/replayPlayerEventsContext.tsx | 20 ------- .../providers/replayReaderProvider.tsx | 26 +++++++++ 7 files changed, 42 insertions(+), 92 deletions(-) delete mode 100644 static/app/utils/replays/playback/providers/replayPlayerEventsContext.spec.tsx delete mode 100644 static/app/utils/replays/playback/providers/replayPlayerEventsContext.tsx create mode 100644 static/app/utils/replays/playback/providers/replayReaderProvider.tsx diff --git a/static/app/components/replays/diff/replaySideBySideImageDiff.tsx b/static/app/components/replays/diff/replaySideBySideImageDiff.tsx index 0606a645545032..15c0f1d0b88258 100644 --- a/static/app/components/replays/diff/replaySideBySideImageDiff.tsx +++ b/static/app/components/replays/diff/replaySideBySideImageDiff.tsx @@ -5,9 +5,9 @@ import {After, Before, DiffHeader} from 'sentry/components/replays/diff/utils'; import ReplayPlayer from 'sentry/components/replays/player/replayPlayer'; import ReplayPlayerMeasurer from 'sentry/components/replays/player/replayPlayerMeasurer'; import {space} from 'sentry/styles/space'; -import {ReplayPlayerEventsContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerEventsContext'; import {ReplayPlayerPluginsContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerPluginsContext'; import {ReplayPlayerStateContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerStateContext'; +import {ReplayReaderProvider} from 'sentry/utils/replays/playback/providers/replayReaderProvider'; import type ReplayReader from 'sentry/utils/replays/replayReader'; interface Props { @@ -26,7 +26,7 @@ export function ReplaySideBySideImageDiff({leftOffsetMs, replay, rightOffsetMs}: - + @@ -41,7 +41,7 @@ export function ReplaySideBySideImageDiff({leftOffsetMs, replay, rightOffsetMs}: - + diff --git a/static/app/components/replays/diff/replaySliderDiff.tsx b/static/app/components/replays/diff/replaySliderDiff.tsx index 0e608410db5212..6ff0c82388de58 100644 --- a/static/app/components/replays/diff/replaySliderDiff.tsx +++ b/static/app/components/replays/diff/replaySliderDiff.tsx @@ -8,9 +8,9 @@ import ReplayPlayerMeasurer from 'sentry/components/replays/player/replayPlayerM import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import toPixels from 'sentry/utils/number/toPixels'; -import {ReplayPlayerEventsContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerEventsContext'; import {ReplayPlayerPluginsContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerPluginsContext'; import {ReplayPlayerStateContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerStateContext'; +import {ReplayReaderProvider} from 'sentry/utils/replays/playback/providers/replayReaderProvider'; import type ReplayReader from 'sentry/utils/replays/replayReader'; import {useDimensions} from 'sentry/utils/useDimensions'; import useOrganization from 'sentry/utils/useOrganization'; @@ -114,7 +114,7 @@ function DiffSides({ return ( - + @@ -137,7 +137,7 @@ function DiffSides({ - + diff --git a/static/app/components/replays/player/__stories__/providers.tsx b/static/app/components/replays/player/__stories__/providers.tsx index 6df77654d230d4..acedf2efc98fbc 100644 --- a/static/app/components/replays/player/__stories__/providers.tsx +++ b/static/app/components/replays/player/__stories__/providers.tsx @@ -1,10 +1,10 @@ import type {ReactNode} from 'react'; import {StaticNoSkipReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences'; -import {ReplayPlayerEventsContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerEventsContext'; import {ReplayPlayerPluginsContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerPluginsContext'; import {ReplayPlayerStateContextProvider} from 'sentry/utils/replays/playback/providers/replayPlayerStateContext'; import {ReplayPreferencesContextProvider} from 'sentry/utils/replays/playback/providers/replayPreferencesContext'; +import {ReplayReaderProvider} from 'sentry/utils/replays/playback/providers/replayReaderProvider'; import type ReplayReader from 'sentry/utils/replays/replayReader'; export default function Providers({ @@ -17,9 +17,9 @@ export default function Providers({ return ( - + {children} - + ); diff --git a/static/app/components/replays/player/replayPlayer.tsx b/static/app/components/replays/player/replayPlayer.tsx index a31ea43c52f763..ea63980418475d 100644 --- a/static/app/components/replays/player/replayPlayer.tsx +++ b/static/app/components/replays/player/replayPlayer.tsx @@ -6,13 +6,13 @@ import { baseReplayerCss, sentryReplayerCss, } from 'sentry/components/replays/player/styles'; -import {useReplayPlayerEvents} from 'sentry/utils/replays/playback/providers/replayPlayerEventsContext'; import {useReplayPlayerPlugins} from 'sentry/utils/replays/playback/providers/replayPlayerPluginsContext'; import { useReplayPlayerStateDispatch, useReplayUserAction, } from 'sentry/utils/replays/playback/providers/replayPlayerStateContext'; import {useReplayPrefs} from 'sentry/utils/replays/playback/providers/replayPreferencesContext'; +import {useReplayReader} from 'sentry/utils/replays/playback/providers/replayReaderProvider'; function useReplayerInstance() { // The div that is emitted from react, where we will attach the replayer to @@ -26,7 +26,7 @@ function useReplayerInstance() { const [prefs] = useReplayPrefs(); const initialPrefsRef = useRef(prefs); // don't re-mount the player when prefs change, instead there's a useEffect const getPlugins = useReplayPlayerPlugins(); - const events = useReplayPlayerEvents(); + const replay = useReplayReader(); // Hooks to sync this Replayer state up and out of this component const dispatch = useReplayPlayerStateDispatch(); @@ -39,7 +39,9 @@ function useReplayerInstance() { return () => {}; } - const replayer = new Replayer(events, { + const webFrames = replay.getRRWebFrames(); + + const replayer = new Replayer(webFrames, { root, blockClass: 'sentry-block', mouseTail: { @@ -48,7 +50,7 @@ function useReplayerInstance() { lineWidth: 2, strokeStyle: theme.purple200, }, - plugins: getPlugins(events), + plugins: getPlugins(webFrames), skipInactive: initialPrefsRef.current.isSkippingInactive, speed: initialPrefsRef.current.playbackSpeed, }); @@ -56,7 +58,7 @@ function useReplayerInstance() { replayerRef.current = replayer; dispatch({type: 'didMountPlayer', replayer, dispatch}); return () => dispatch({type: 'didUnmountPlayer', replayer}); - }, [dispatch, events, getPlugins, theme]); + }, [dispatch, getPlugins, replay, theme]); useEffect(() => { if (!replayerRef.current) { diff --git a/static/app/utils/replays/playback/providers/replayPlayerEventsContext.spec.tsx b/static/app/utils/replays/playback/providers/replayPlayerEventsContext.spec.tsx deleted file mode 100644 index c0302e257c6daf..00000000000000 --- a/static/app/utils/replays/playback/providers/replayPlayerEventsContext.spec.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type {ReactNode} from 'react'; -import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; - -import {renderHook} from 'sentry-test/reactTestingLibrary'; - -import { - ReplayPlayerEventsContextProvider, - useReplayPlayerEvents, -} from 'sentry/utils/replays/playback/providers/replayPlayerEventsContext'; -import ReplayReader from 'sentry/utils/replays/replayReader'; - -function makeWrapper(replay: ReplayReader) { - return function ({children}: {children?: ReactNode}) { - return ( - - {children} - - ); - }; -} - -describe('replayPlayerEventsContext', () => { - it('should have a stable to the list of rrweb event frames', () => { - const mockReplay = ReplayReader.factory({ - attachments: [], - errors: [], - fetching: false, - replayRecord: ReplayRecordFixture(), - }); - - const {result, rerender} = renderHook(useReplayPlayerEvents, { - wrapper: makeWrapper(mockReplay!), - }); - - const initialRef = result.current; - - rerender(); - - expect(result.current).toEqual(initialRef); - }); - - it('should return the rrweb frames for the replay', () => { - const mockReplay = ReplayReader.factory({ - attachments: [], - errors: [], - fetching: false, - replayRecord: ReplayRecordFixture(), - }); - const mockRRwebFrames: any[] = []; - mockReplay!.getRRWebFrames = jest.fn().mockReturnValue(mockRRwebFrames); - - const {result} = renderHook(useReplayPlayerEvents, { - wrapper: makeWrapper(mockReplay!), - }); - - expect(result.current).toStrictEqual(mockRRwebFrames); - }); -}); diff --git a/static/app/utils/replays/playback/providers/replayPlayerEventsContext.tsx b/static/app/utils/replays/playback/providers/replayPlayerEventsContext.tsx deleted file mode 100644 index 034c5daa934f8b..00000000000000 --- a/static/app/utils/replays/playback/providers/replayPlayerEventsContext.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import {createContext, useContext} from 'react'; - -import type ReplayReader from 'sentry/utils/replays/replayReader'; -import type {RecordingFrame} from 'sentry/utils/replays/types'; - -const context = createContext([]); - -export function ReplayPlayerEventsContextProvider({ - children, - replay, -}: { - children: React.ReactNode; - replay: ReplayReader; -}) { - return {children}; -} - -export function useReplayPlayerEvents() { - return useContext(context); -} diff --git a/static/app/utils/replays/playback/providers/replayReaderProvider.tsx b/static/app/utils/replays/playback/providers/replayReaderProvider.tsx new file mode 100644 index 00000000000000..c6085c1c579005 --- /dev/null +++ b/static/app/utils/replays/playback/providers/replayReaderProvider.tsx @@ -0,0 +1,26 @@ +import type {ReactNode} from 'react'; +import {createContext, useContext} from 'react'; + +import ReplayReader from 'sentry/utils/replays/replayReader'; + +interface Props { + children: ReactNode; + replay: ReplayReader; +} + +const context = createContext( + ReplayReader.factory({ + attachments: [], + errors: [], + fetching: false, + replayRecord: undefined, + })! +); + +export function ReplayReaderProvider({children, replay}: Props) { + return {children}; +} + +export function useReplayReader() { + return useContext(context); +} From 84478fe144225331746c5d8a6e358e9b4db7842f Mon Sep 17 00:00:00 2001 From: Nikki Kapadia <72356613+nikkikapadia@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:04:28 -0500 Subject: [PATCH 458/757] feat(widget-builder): add unsaved changes warning on close (#82491) Adds this dialog box to confirm closing the widget builder image Closes https://github.com/getsentry/sentry/issues/82328 --- static/app/views/dashboards/detail.tsx | 1 + .../components/widgetBuilderSlideout.spec.tsx | 69 ++++++++++++++++++- .../components/widgetBuilderSlideout.tsx | 14 +++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/static/app/views/dashboards/detail.tsx b/static/app/views/dashboards/detail.tsx index 5973ad9d97fd24..67fbbf5c328087 100644 --- a/static/app/views/dashboards/detail.tsx +++ b/static/app/views/dashboards/detail.tsx @@ -819,6 +819,7 @@ class DashboardDetail extends Component { handleCloseWidgetBuilder = () => { const {organization, router, location, params} = this.props; + this.setState({isWidgetBuilderOpen: false}); router.push( getDashboardLocation({ diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx index 2f841ef5b03279..1e3022c7296327 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx @@ -3,8 +3,15 @@ import {LocationFixture} from 'sentry-fixture/locationFixture'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {RouterFixture} from 'sentry-fixture/routerFixture'; -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import { + render, + renderGlobalModal, + screen, + userEvent, + waitFor, +} from 'sentry-test/reactTestingLibrary'; +import ModalStore from 'sentry/stores/modalStore'; import useCustomMeasurements from 'sentry/utils/useCustomMeasurements'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import WidgetBuilderSlideout from 'sentry/views/dashboards/widgetBuilder/components/widgetBuilderSlideout'; @@ -30,6 +37,10 @@ describe('WidgetBuilderSlideout', () => { }); }); + afterEach(() => { + ModalStore.reset(); + }); + it('should show the sort by step if the widget is a chart and there are fields selected', async () => { render( @@ -135,4 +146,60 @@ describe('WidgetBuilderSlideout', () => { expect(await screen.findByText('count')).toBeInTheDocument(); expect(screen.queryByText('Sort by')).not.toBeInTheDocument(); }); + + it('should show the confirm modal if the widget is unsaved', async () => { + render( + + + , + {organization} + ); + renderGlobalModal(); + + await userEvent.type(await screen.findByPlaceholderText('Name'), 'some name'); + await userEvent.click(await screen.findByText('Close')); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect( + screen.getByText('You have unsaved changes. Are you sure you want to leave?') + ).toBeInTheDocument(); + }); + + it('should not show the confirm modal if the widget is unsaved', async () => { + render( + + + , + {organization} + ); + + renderGlobalModal(); + + await userEvent.click(await screen.findByText('Close')); + + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect( + screen.queryByText('You have unsaved changes. Are you sure you want to leave?') + ).not.toBeInTheDocument(); + }); + }); }); diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 9f61407e271391..e05451b2e890af 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -1,8 +1,10 @@ -import {useEffect, useRef} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import isEqual from 'lodash/isEqual'; import {Button} from 'sentry/components/button'; +import {openConfirmModal} from 'sentry/components/confirm'; import SlideOverPanel from 'sentry/components/slideOverPanel'; import {IconClose} from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -53,6 +55,7 @@ function WidgetBuilderSlideout({ }: WidgetBuilderSlideoutProps) { const organization = useOrganization(); const {state} = useWidgetBuilderContext(); + const [initialState] = useState(state); const {widgetIndex} = useParams(); const theme = useTheme(); @@ -99,7 +102,14 @@ function WidgetBuilderSlideout({ borderless aria-label={t('Close Widget Builder')} icon={} - onClick={onClose} + onClick={() => { + openConfirmModal({ + bypass: isEqual(initialState, state), + message: t('You have unsaved changes. Are you sure you want to leave?'), + priority: 'danger', + onConfirm: onClose, + }); + }} > {t('Close')} From 7f0ec6367f82ccf59d46234a931e5dda4734dc6e Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Mon, 23 Dec 2024 09:22:49 -0800 Subject: [PATCH 459/757] :wrench: chore: remove unused ff handlers (#82535) --- src/sentry/features/temporary.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index a684677d43b680..2bd72c5c5085c1 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -109,8 +109,6 @@ def register_temporary_features(manager: FeatureManager): # Enable the dev toolbar PoC code for employees # Data Secrecy manager.add("organizations:data-secrecy", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable default metric alerts for new projects - manager.add("organizations:default-metric-alerts-new-projects", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) manager.add("organizations:devtoolbar", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, default=False, api_expose=True) manager.add("organizations:email-performance-regression-image", OrganizationFeature, FeatureHandlerStrategy.OPTIONS, api_expose=False) # Enables automatically deriving of code mappings @@ -356,10 +354,6 @@ def register_temporary_features(manager: FeatureManager): manager.add("organizations:release-comparison-performance", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # enable new release set_commits functionality manager.add("organizations:set-commits-updated", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - # Enable new release UI - manager.add("organizations:releases-v2", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - manager.add("organizations:releases-v2-internal", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) - manager.add("organizations:releases-v2-st", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) # Enable playing replays from the replay tab manager.add("organizations:replay-play-from-replay-tab", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable version 2 of reprocessing (completely distinct from v1) From d4eaf091ab5c1373e2072a62777b4a31c057bfa0 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 23 Dec 2024 09:32:50 -0800 Subject: [PATCH 460/757] ref(performance): Remove feature flag check in overview for search-query-builder (#82421) --- .../transactionOverview/content.spec.tsx | 6 +- .../transactionOverview/content.tsx | 56 +++---------------- .../transactionOverview/index.spec.tsx | 27 +++++++-- 3 files changed, 33 insertions(+), 56 deletions(-) diff --git a/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx index f045e3aad0b43d..61f538d426621d 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx @@ -189,6 +189,10 @@ describe('Transaction Summary Content', function () { metrics: [], }, }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/recent-searches/', + body: [], + }); }); afterEach(function () { @@ -227,7 +231,7 @@ describe('Transaction Summary Content', function () { await screen.findByTestId('page-filter-environment-selector') ).toBeInTheDocument(); expect(screen.getByTestId('page-filter-timerange-selector')).toBeInTheDocument(); - expect(screen.getByTestId('smart-search-bar')).toBeInTheDocument(); + expect(screen.getByTestId('search-query-builder')).toBeInTheDocument(); expect(screen.getByTestId('transaction-summary-charts')).toBeInTheDocument(); expect(screen.getByRole('heading', {name: /user misery/i})).toBeInTheDocument(); diff --git a/static/app/views/performance/transactionSummary/transactionOverview/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/content.tsx index ab46fa4342f19e..a76ead6e88ffa3 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/content.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/content.tsx @@ -5,7 +5,6 @@ import omit from 'lodash/omit'; import type {DropdownOption} from 'sentry/components/discover/transactionsList'; import TransactionsList from 'sentry/components/discover/transactionsList'; -import SearchBar from 'sentry/components/events/searchBar'; import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; @@ -13,9 +12,7 @@ import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder'; import {SuspectFunctionsTable} from 'sentry/components/profiling/suspectFunctions/suspectFunctionsTable'; -import type {ActionBarItem} from 'sentry/components/smartSearchBar'; import {Tooltip} from 'sentry/components/tooltip'; -import {MAX_QUERY_LENGTH} from 'sentry/constants'; import {IconWarning} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -32,7 +29,6 @@ import { SPAN_OP_RELATIVE_BREAKDOWN_FIELD, } from 'sentry/utils/discover/fields'; import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery'; -import type {MetricsEnhancedPerformanceDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext'; import {useMEPDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext'; import {decodeScalar} from 'sentry/utils/queryString'; import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay'; @@ -189,29 +185,6 @@ function SummaryContent({ return sortedEventView; } - function generateActionBarItems( - _org: Organization, - _location: Location, - _mepDataContext: MetricsEnhancedPerformanceDataContext - ) { - let items: ActionBarItem[] | undefined = undefined; - if (!canUseTransactionMetricsData(_org, _mepDataContext)) { - items = [ - { - key: 'alert', - makeAction: () => ({ - Button: () => , - menuItem: { - key: 'alert', - }, - }), - }, - ]; - } - - return items; - } - const trailingItems = useMemo(() => { if (!canUseTransactionMetricsData(organization, mepDataContext)) { return ; @@ -352,30 +325,15 @@ function SummaryContent({ const projectIds = useMemo(() => eventView.project.slice(), [eventView.project]); function renderSearchBar() { - if (organization.features.includes('search-query-builder-performance')) { - return ( - - ); - } - return ( - ); } diff --git a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx index 1e48c4c8123daf..e9de9155ad6007 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx @@ -94,6 +94,9 @@ describe('Performance > TransactionSummary', function () { // eslint-disable-next-line no-console jest.spyOn(console, 'error').mockImplementation(jest.fn()); + // Small screen size will hide search bar trailing items like warning icon + Object.defineProperty(Element.prototype, 'clientWidth', {value: 1000}); + MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', @@ -528,6 +531,10 @@ describe('Performance > TransactionSummary', function () { MockApiClient.clearMockResponses(); ProjectsStore.reset(); jest.clearAllMocks(); + + // Cleanup clientWidth mock + // @ts-expect-error + delete HTMLElement.prototype.clientWidth; }); describe('with events', function () { @@ -556,7 +563,9 @@ describe('Performance > TransactionSummary', function () { ).toBeInTheDocument(); // It shows a searchbar - expect(screen.getByLabelText('Search events')).toBeInTheDocument(); + expect( + screen.getByPlaceholderText('Search for events, users, tags, and more') + ).toBeInTheDocument(); // It shows a table expect(screen.getByTestId('transactions-table')).toBeInTheDocument(); @@ -820,13 +829,17 @@ describe('Performance > TransactionSummary', function () { ); // Fill out the search box, and submit it. - await userEvent.type( - screen.getByLabelText('Search events'), - 'user.email:uhoh*{enter}' + await userEvent.click( + screen.getByPlaceholderText('Search for events, users, tags, and more') ); + await userEvent.paste('user.email:uhoh*'); + await userEvent.keyboard('{enter}'); + + await waitFor(() => { + expect(browserHistory.push).toHaveBeenCalledTimes(1); + }); // Check the navigation. - expect(browserHistory.push).toHaveBeenCalledTimes(1); expect(browserHistory.push).toHaveBeenCalledWith({ pathname: '/', query: { @@ -1342,7 +1355,9 @@ describe('Performance > TransactionSummary', function () { // Renders Failure Rate widget expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument(); expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%'); - expect(screen.getByTestId('search-metrics-fallback-warning')).toBeInTheDocument(); + expect( + await screen.findByTestId('search-metrics-fallback-warning') + ).toBeInTheDocument(); }); }); }); From e896b2881a86ae0f85ef1490e24bacbab12fb316 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:44:12 -0500 Subject: [PATCH 461/757] ref: remove timestamp_format (#82537) no reason to use this over the .timestamp() method --- src/sentry/testutils/helpers/datetime.py | 7 +----- .../acceptance/test_organization_events_v2.py | 10 ++++----- tests/relay_integration/test_integration.py | 22 +++++++++---------- .../test_organization_events_anomalies.py | 10 ++++----- tests/sentry/api/serializers/test_event.py | 14 +++++------- 5 files changed, 27 insertions(+), 36 deletions(-) diff --git a/src/sentry/testutils/helpers/datetime.py b/src/sentry/testutils/helpers/datetime.py index a24d8ec8a075e6..206634b893e024 100644 --- a/src/sentry/testutils/helpers/datetime.py +++ b/src/sentry/testutils/helpers/datetime.py @@ -1,11 +1,10 @@ from __future__ import annotations -import time from datetime import UTC, datetime, timedelta import time_machine -__all__ = ["before_now", "timestamp_format"] +__all__ = ["before_now"] def before_now(**kwargs: float) -> datetime: @@ -13,10 +12,6 @@ def before_now(**kwargs: float) -> datetime: return date - timedelta(microseconds=date.microsecond % 1000) -def timestamp_format(datetime): - return time.mktime(datetime.utctimetuple()) + datetime.microsecond / 1e6 - - class MockClock: """Returns a distinct, increasing timestamp each time it is called.""" diff --git a/tests/acceptance/test_organization_events_v2.py b/tests/acceptance/test_organization_events_v2.py index 064529cd9c2f83..4d415ef2a611f9 100644 --- a/tests/acceptance/test_organization_events_v2.py +++ b/tests/acceptance/test_organization_events_v2.py @@ -9,7 +9,7 @@ from sentry.discover.models import DiscoverSavedQuery from sentry.testutils.cases import AcceptanceTestCase, SnubaTestCase -from sentry.testutils.helpers.datetime import before_now, timestamp_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.silo import no_silo_test from sentry.utils.samples import load_data @@ -117,8 +117,8 @@ def build_span_tree(span_tree, spans, parent_span_id): (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta())) span_start_time = start_datetime + start_delta - span["start_timestamp"] = timestamp_format(span_start_time) - span["timestamp"] = timestamp_format(span_start_time + span_length) + span["start_timestamp"] = span_start_time.timestamp() + span["timestamp"] = (span_start_time + span_length).timestamp() spans.append(span) if isinstance(child, dict): @@ -135,8 +135,8 @@ def build_span_tree(span_tree, spans, parent_span_id): (start_delta, span_length) = time_offsets.get(span_id, (timedelta(), timedelta())) span_start_time = start_datetime + start_delta - span["start_timestamp"] = timestamp_format(span_start_time) - span["timestamp"] = timestamp_format(span_start_time + span_length) + span["start_timestamp"] = span_start_time.timestamp() + span["timestamp"] = (span_start_time + span_length).timestamp() spans.append(span) return spans diff --git a/tests/relay_integration/test_integration.py b/tests/relay_integration/test_integration.py index ebbaaf4bf16bfb..2416ee3dc1a10a 100644 --- a/tests/relay_integration/test_integration.py +++ b/tests/relay_integration/test_integration.py @@ -7,7 +7,7 @@ from sentry.models.eventattachment import EventAttachment from sentry.tasks.relay import invalidate_project_config from sentry.testutils.cases import TransactionTestCase -from sentry.testutils.helpers.datetime import before_now, timestamp_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.relay import RelayStoreHelper from sentry.testutils.skips import requires_kafka @@ -158,8 +158,8 @@ def test_transaction(self): "op": "react.mount", "parent_span_id": "8f5a2b8768cafb4e", "span_id": "bd429c44b67a3eb4", - "start_timestamp": timestamp_format(before_now(minutes=1, milliseconds=250)), - "timestamp": timestamp_format(before_now(minutes=1)), + "start_timestamp": before_now(minutes=1, milliseconds=250).timestamp(), + "timestamp": before_now(minutes=1).timestamp(), "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { @@ -167,8 +167,8 @@ def test_transaction(self): "op": "browser", "parent_span_id": "bd429c44b67a3eb4", "span_id": "a99fd04e79e17631", - "start_timestamp": timestamp_format(before_now(minutes=1, milliseconds=200)), - "timestamp": timestamp_format(before_now(minutes=1)), + "start_timestamp": before_now(minutes=1, milliseconds=200).timestamp(), + "timestamp": before_now(minutes=1).timestamp(), "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { @@ -176,8 +176,8 @@ def test_transaction(self): "op": "resource", "parent_span_id": "bd429c44b67a3eb4", "span_id": "a71a5e67db5ce938", - "start_timestamp": timestamp_format(before_now(minutes=1, milliseconds=200)), - "timestamp": timestamp_format(before_now(minutes=1)), + "start_timestamp": before_now(minutes=1, milliseconds=200).timestamp(), + "timestamp": before_now(minutes=1).timestamp(), "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { @@ -185,8 +185,8 @@ def test_transaction(self): "op": "http", "parent_span_id": "a99fd04e79e17631", "span_id": "abe79ad9292b90a9", - "start_timestamp": timestamp_format(before_now(minutes=1, milliseconds=200)), - "timestamp": timestamp_format(before_now(minutes=1)), + "start_timestamp": before_now(minutes=1, milliseconds=200).timestamp(), + "timestamp": before_now(minutes=1).timestamp(), "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { @@ -194,8 +194,8 @@ def test_transaction(self): "op": "db", "parent_span_id": "abe79ad9292b90a9", "span_id": "9c045ea336297177", - "start_timestamp": timestamp_format(before_now(minutes=1, milliseconds=200)), - "timestamp": timestamp_format(before_now(minutes=1)), + "start_timestamp": before_now(minutes=1, milliseconds=200).timestamp(), + "timestamp": before_now(minutes=1).timestamp(), "trace_id": "ff62a8b040f340bda5d830223def1d81", }, ], diff --git a/tests/sentry/api/endpoints/test_organization_events_anomalies.py b/tests/sentry/api/endpoints/test_organization_events_anomalies.py index 9f62b2f0013432..7ee64e72a337bd 100644 --- a/tests/sentry/api/endpoints/test_organization_events_anomalies.py +++ b/tests/sentry/api/endpoints/test_organization_events_anomalies.py @@ -19,7 +19,7 @@ ) from sentry.seer.anomaly_detection.utils import translate_direction from sentry.testutils.cases import APITestCase -from sentry.testutils.helpers.datetime import before_now, freeze_time, timestamp_format +from sentry.testutils.helpers.datetime import before_now, freeze_time from sentry.testutils.helpers.features import with_feature from sentry.testutils.outbox import outbox_runner @@ -39,10 +39,10 @@ class OrganizationEventsAnomaliesEndpointTest(APITestCase): direction=translate_direction(AlertRuleThresholdType.ABOVE.value), expected_seasonality=AlertRuleSeasonality.AUTO.value, ) - historical_timestamp_1 = timestamp_format(four_weeks_ago) - historical_timestamp_2 = timestamp_format(four_weeks_ago + timedelta(days=10)) - current_timestamp_1 = timestamp_format(one_week_ago) - current_timestamp_2 = timestamp_format(one_week_ago + timedelta(minutes=10)) + historical_timestamp_1 = four_weeks_ago.timestamp() + historical_timestamp_2 = (four_weeks_ago + timedelta(days=10)).timestamp() + current_timestamp_1 = one_week_ago.timestamp() + current_timestamp_2 = (one_week_ago + timedelta(minutes=10)).timestamp() data = { "project_id": 1, "config": config, diff --git a/tests/sentry/api/serializers/test_event.py b/tests/sentry/api/serializers/test_event.py index 8d9d56a7441f4d..c8dc5003bd9802 100644 --- a/tests/sentry/api/serializers/test_event.py +++ b/tests/sentry/api/serializers/test_event.py @@ -12,7 +12,7 @@ from sentry.models.release import Release from sentry.sdk_updates import SdkIndexState from sentry.testutils.cases import TestCase -from sentry.testutils.helpers.datetime import before_now, timestamp_format +from sentry.testutils.helpers.datetime import before_now from sentry.testutils.performance_issues.event_generators import get_event from sentry.testutils.skips import requires_snuba from sentry.utils.samples import load_data @@ -579,10 +579,8 @@ def test_event_db_span_formatting(self): "op": "db", "parent_span_id": "abe79ad9292b90a9", "span_id": "9c045ea336297177", - "start_timestamp": timestamp_format( - before_now(minutes=1, milliseconds=200) - ), - "timestamp": timestamp_format(before_now(minutes=1)), + "start_timestamp": before_now(minutes=1, milliseconds=200).timestamp(), + "timestamp": before_now(minutes=1).timestamp(), "trace_id": "ff62a8b040f340bda5d830223def1d81", }, { @@ -590,10 +588,8 @@ def test_event_db_span_formatting(self): "op": "http", "parent_span_id": "a99fd04e79e17631", "span_id": "abe79ad9292b90a9", - "start_timestamp": timestamp_format( - before_now(minutes=1, milliseconds=200) - ), - "timestamp": timestamp_format(before_now(minutes=1)), + "start_timestamp": before_now(minutes=1, milliseconds=200).timestamp(), + "timestamp": before_now(minutes=1).timestamp(), "trace_id": "ff62a8b040f340bda5d830223def1d81", }, ], From 0fc8de68886b906a845a37c5eafe870089088c42 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 23 Dec 2024 09:45:16 -0800 Subject: [PATCH 462/757] ref(replay): Rename the old `useReplayReader` to `useLoadReplayReader` to disambiguate (#82536) This old thing, which i'm renaming, wasn't just a getter, it would actually load up replay data and create a ReplayReader instance. I've thought the name wasn't best most descriptive for a while... but now it's changed! I'm calling the file in this PR 'old' because https://github.com/getsentry/sentry/pull/82532 recently added a new `useReplayReader` hook which i think is living up to it's name better: this new one is just a pass-through for objects of type ReplayReader. --- .../events/eventHydrationDiff/replayDiffContent.tsx | 4 ++-- .../app/components/events/eventReplay/index.spec.tsx | 6 +++--- .../events/eventReplay/replayClipPreview.spec.tsx | 12 ++++++------ .../events/eventReplay/replayClipPreview.tsx | 4 ++-- .../events/eventReplay/replayClipPreviewPlayer.tsx | 4 ++-- .../events/eventReplay/replayPreview.spec.tsx | 12 ++++++------ .../components/events/eventReplay/replayPreview.tsx | 4 ++-- .../interfaces/breadcrumbs/breadcrumbs.spec.tsx | 2 +- .../replays/player/__stories__/replaySlugChooser.tsx | 4 ++-- ...yReader.spec.tsx => useLoadReplayReader.spec.tsx} | 8 ++++---- .../{useReplayReader.tsx => useLoadReplayReader.tsx} | 2 +- .../utils/replays/hooks/useLogReplayDataLoaded.tsx | 4 ++-- .../issueDetails/groupReplays/groupReplays.spec.tsx | 8 ++++---- .../views/issueDetails/groupReplays/groupReplays.tsx | 4 ++-- .../groupReplays/replayClipPreviewWrapper.tsx | 4 ++-- static/app/views/replays/details.tsx | 4 ++-- 16 files changed, 43 insertions(+), 43 deletions(-) rename static/app/utils/replays/hooks/{useReplayReader.spec.tsx => useLoadReplayReader.spec.tsx} (84%) rename static/app/utils/replays/hooks/{useReplayReader.tsx => useLoadReplayReader.tsx} (98%) diff --git a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx index 3e7e89261d8309..04eebfc1885f00 100644 --- a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx +++ b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx @@ -7,7 +7,7 @@ import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import {getReplayDiffOffsetsFromEvent} from 'sentry/utils/replays/getDiffTimestamps'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; @@ -19,7 +19,7 @@ interface Props { } export default function ReplayDiffContent({event, group, orgSlug, replaySlug}: Props) { - const replayContext = useReplayReader({ + const replayContext = useLoadReplayReader({ orgSlug, replaySlug, }); diff --git a/static/app/components/events/eventReplay/index.spec.tsx b/static/app/components/events/eventReplay/index.spec.tsx index f6d4b358ddc1d5..e989653ae5cd5a 100644 --- a/static/app/components/events/eventReplay/index.spec.tsx +++ b/static/app/components/events/eventReplay/index.spec.tsx @@ -8,17 +8,17 @@ import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; import {render, screen} from 'sentry-test/reactTestingLibrary'; import EventReplay from 'sentry/components/events/eventReplay'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import { useHaveSelectedProjectsSentAnyReplayEvents, useReplayOnboardingSidebarPanel, } from 'sentry/utils/replays/hooks/useReplayOnboarding'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; import ReplayReader from 'sentry/utils/replays/replayReader'; import useProjects from 'sentry/utils/useProjects'; import type {ReplayError} from 'sentry/views/replays/types'; jest.mock('sentry/utils/replays/hooks/useReplayOnboarding'); -jest.mock('sentry/utils/replays/hooks/useReplayReader'); +jest.mock('sentry/utils/replays/hooks/useLoadReplayReader'); jest.mock('sentry/utils/useProjects'); jest.mock('sentry/utils/replays/hooks/useReplayOnboarding'); // Replay clip preview is very heavy, mock it out @@ -70,7 +70,7 @@ const mockReplay = ReplayReader.factory({ }), }); -jest.mocked(useReplayReader).mockImplementation(() => { +jest.mocked(useLoadReplayReader).mockImplementation(() => { return { attachments: [], errors: mockErrors, diff --git a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx index defb415050dbf1..769c060962e726 100644 --- a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx +++ b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx @@ -7,15 +7,15 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {render as baseRender, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import type {Organization} from 'sentry/types/organization'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import ReplayReader from 'sentry/utils/replays/replayReader'; import type RequestError from 'sentry/utils/requestError/requestError'; import ReplayClipPreview from './replayClipPreview'; -jest.mock('sentry/utils/replays/hooks/useReplayReader'); +jest.mock('sentry/utils/replays/hooks/useLoadReplayReader'); -const mockUseReplayReader = jest.mocked(useReplayReader); +const mockUseLoadReplayReader = jest.mocked(useLoadReplayReader); const mockOrgSlug = 'sentry-emerging-tech'; const mockReplaySlug = 'replays:761104e184c64d439ee1014b72b4d83b'; @@ -48,7 +48,7 @@ const mockReplay = ReplayReader.factory({ }, }); -mockUseReplayReader.mockImplementation(() => { +mockUseLoadReplayReader.mockImplementation(() => { return { attachments: [], errors: [], @@ -120,7 +120,7 @@ describe('ReplayClipPreview', () => { it('Should render a placeholder when is fetching the replay data', () => { // Change the mocked hook to return a loading state - mockUseReplayReader.mockImplementationOnce(() => { + mockUseLoadReplayReader.mockImplementationOnce(() => { return { attachments: [], errors: [], @@ -141,7 +141,7 @@ describe('ReplayClipPreview', () => { it('Should throw error when there is a fetch error', () => { // Change the mocked hook to return a fetch error - mockUseReplayReader.mockImplementationOnce(() => { + mockUseLoadReplayReader.mockImplementationOnce(() => { return { attachments: [], errors: [], diff --git a/static/app/components/events/eventReplay/replayClipPreview.tsx b/static/app/components/events/eventReplay/replayClipPreview.tsx index 54d2c3299800f6..8d4c011fafaffa 100644 --- a/static/app/components/events/eventReplay/replayClipPreview.tsx +++ b/static/app/components/events/eventReplay/replayClipPreview.tsx @@ -2,7 +2,7 @@ import {useMemo} from 'react'; import ReplayClipPreviewPlayer from 'sentry/components/events/eventReplay/replayClipPreviewPlayer'; import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; interface ReplayClipPreviewProps extends Omit< @@ -33,7 +33,7 @@ function ReplayClipPreview({ [clipOffsets.durationBeforeMs, clipOffsets.durationAfterMs, eventTimestampMs] ); - const replayReaderResult = useReplayReader({ + const replayReaderResult = useLoadReplayReader({ orgSlug, replaySlug, clipWindow, diff --git a/static/app/components/events/eventReplay/replayClipPreviewPlayer.tsx b/static/app/components/events/eventReplay/replayClipPreviewPlayer.tsx index 6094ae9c35957d..fe4cccb6050f13 100644 --- a/static/app/components/events/eventReplay/replayClipPreviewPlayer.tsx +++ b/static/app/components/events/eventReplay/replayClipPreviewPlayer.tsx @@ -18,7 +18,7 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import type {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab'; -import type useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import type useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import type RequestError from 'sentry/utils/requestError/requestError'; import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; import useOrganization from 'sentry/utils/useOrganization'; @@ -28,7 +28,7 @@ import type {ReplayRecord} from 'sentry/views/replays/types'; interface ReplayClipPreviewPlayerProps { analyticsContext: string; orgSlug: string; - replayReaderResult: ReturnType; + replayReaderResult: ReturnType; focusTab?: TabKey; fullReplayButtonProps?: Partial>; handleBackClick?: () => void; diff --git a/static/app/components/events/eventReplay/replayPreview.spec.tsx b/static/app/components/events/eventReplay/replayPreview.spec.tsx index 3197984e1413dc..fc57d720f8286a 100644 --- a/static/app/components/events/eventReplay/replayPreview.spec.tsx +++ b/static/app/components/events/eventReplay/replayPreview.spec.tsx @@ -6,15 +6,15 @@ import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {render as baseRender, screen} from 'sentry-test/reactTestingLibrary'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import ReplayReader from 'sentry/utils/replays/replayReader'; import type RequestError from 'sentry/utils/requestError/requestError'; import ReplayPreview from './replayPreview'; -jest.mock('sentry/utils/replays/hooks/useReplayReader'); +jest.mock('sentry/utils/replays/hooks/useLoadReplayReader'); -const mockUseReplayReader = jest.mocked(useReplayReader); +const mockUseLoadReplayReader = jest.mocked(useLoadReplayReader); const mockOrgSlug = 'sentry-emerging-tech'; const mockReplaySlug = 'replays:761104e184c64d439ee1014b72b4d83b'; @@ -39,7 +39,7 @@ const mockReplay = ReplayReader.factory({ }), }); -mockUseReplayReader.mockImplementation(() => { +mockUseLoadReplayReader.mockImplementation(() => { return { attachments: [], errors: [], @@ -84,7 +84,7 @@ const defaultProps = { describe('ReplayPreview', () => { it('Should render a placeholder when is fetching the replay data', () => { // Change the mocked hook to return a loading state - mockUseReplayReader.mockImplementationOnce(() => { + mockUseLoadReplayReader.mockImplementationOnce(() => { return { attachments: [], errors: [], @@ -105,7 +105,7 @@ describe('ReplayPreview', () => { it('Should throw error when there is a fetch error', () => { // Change the mocked hook to return a fetch error - mockUseReplayReader.mockImplementationOnce(() => { + mockUseLoadReplayReader.mockImplementationOnce(() => { return { attachments: [], errors: [], diff --git a/static/app/components/events/eventReplay/replayPreview.tsx b/static/app/components/events/eventReplay/replayPreview.tsx index e531a79e32e631..8a5575a62871c0 100644 --- a/static/app/components/events/eventReplay/replayPreview.tsx +++ b/static/app/components/events/eventReplay/replayPreview.tsx @@ -15,7 +15,7 @@ import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {trackAnalytics} from 'sentry/utils/analytics'; import type {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import type RequestError from 'sentry/utils/requestError/requestError'; import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; import useOrganization from 'sentry/utils/useOrganization'; @@ -60,7 +60,7 @@ function ReplayPreview({ orgSlug, replaySlug, }: Props) { - const {fetching, replay, replayRecord, fetchError, replayId} = useReplayReader({ + const {fetching, replay, replayRecord, fetchError, replayId} = useLoadReplayReader({ orgSlug, replaySlug, }); diff --git a/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.spec.tsx b/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.spec.tsx index 27a6ac62ea07e7..17046a88659e67 100644 --- a/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.spec.tsx +++ b/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.spec.tsx @@ -10,7 +10,7 @@ import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs'; import useProjects from 'sentry/utils/useProjects'; jest.mock('sentry/utils/replays/hooks/useReplayOnboarding'); -jest.mock('sentry/utils/replays/hooks/useReplayReader'); +jest.mock('sentry/utils/replays/hooks/useLoadReplayReader'); jest.mock('sentry/utils/useProjects'); describe('Breadcrumbs', () => { diff --git a/static/app/components/replays/player/__stories__/replaySlugChooser.tsx b/static/app/components/replays/player/__stories__/replaySlugChooser.tsx index 4cdb1db67ce1f8..c6b7c1709d76f7 100644 --- a/static/app/components/replays/player/__stories__/replaySlugChooser.tsx +++ b/static/app/components/replays/player/__stories__/replaySlugChooser.tsx @@ -2,7 +2,7 @@ import {Fragment, type ReactNode} from 'react'; import {css} from '@emotion/react'; import Providers from 'sentry/components/replays/player/__stories__/providers'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import useOrganization from 'sentry/utils/useOrganization'; import {useSessionStorage} from 'sentry/utils/useSessionStorage'; @@ -29,7 +29,7 @@ export default function ReplaySlugChooser({children}: {children: ReactNode}) { function LoadReplay({children, replaySlug}: {children: ReactNode; replaySlug: string}) { const organization = useOrganization(); - const {fetchError, fetching, replay} = useReplayReader({ + const {fetchError, fetching, replay} = useLoadReplayReader({ orgSlug: organization.slug, replaySlug, }); diff --git a/static/app/utils/replays/hooks/useReplayReader.spec.tsx b/static/app/utils/replays/hooks/useLoadReplayReader.spec.tsx similarity index 84% rename from static/app/utils/replays/hooks/useReplayReader.spec.tsx rename to static/app/utils/replays/hooks/useLoadReplayReader.spec.tsx index 758279bd4766f3..3d590d32b0738a 100644 --- a/static/app/utils/replays/hooks/useReplayReader.spec.tsx +++ b/static/app/utils/replays/hooks/useLoadReplayReader.spec.tsx @@ -1,7 +1,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {renderHook} from 'sentry-test/reactTestingLibrary'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import {OrganizationContext} from 'sentry/views/organizationContext'; jest.mock('sentry/utils/replays/hooks/useReplayData', () => ({ @@ -17,13 +17,13 @@ const wrapper = ({children}: {children?: React.ReactNode}) => ( ); -describe('useReplayReader', () => { +describe('useLoadReplayReader', () => { beforeEach(() => { MockApiClient.clearMockResponses(); }); it('should accept a replaySlug with project and id parts', () => { - const {result} = renderHook(useReplayReader, { + const {result} = renderHook(useLoadReplayReader, { wrapper, initialProps: { orgSlug: organization.slug, @@ -39,7 +39,7 @@ describe('useReplayReader', () => { }); it('should accept a replaySlug with only the replay-id', () => { - const {result} = renderHook(useReplayReader, { + const {result} = renderHook(useLoadReplayReader, { wrapper, initialProps: { orgSlug: organization.slug, diff --git a/static/app/utils/replays/hooks/useReplayReader.tsx b/static/app/utils/replays/hooks/useLoadReplayReader.tsx similarity index 98% rename from static/app/utils/replays/hooks/useReplayReader.tsx rename to static/app/utils/replays/hooks/useLoadReplayReader.tsx index 23744b31abcb20..aef2196b8592f0 100644 --- a/static/app/utils/replays/hooks/useReplayReader.tsx +++ b/static/app/utils/replays/hooks/useLoadReplayReader.tsx @@ -20,7 +20,7 @@ interface ReplayReaderResult extends ReturnType { replayId: string; } -export default function useReplayReader({ +export default function useLoadReplayReader({ orgSlug, replaySlug, clipWindow, diff --git a/static/app/utils/replays/hooks/useLogReplayDataLoaded.tsx b/static/app/utils/replays/hooks/useLogReplayDataLoaded.tsx index 921c3be6276dae..36f9273c47adb3 100644 --- a/static/app/utils/replays/hooks/useLogReplayDataLoaded.tsx +++ b/static/app/utils/replays/hooks/useLogReplayDataLoaded.tsx @@ -2,14 +2,14 @@ import {useEffect} from 'react'; import * as Sentry from '@sentry/react'; import {trackAnalytics} from 'sentry/utils/analytics'; -import type useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import type useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import type {BreadcrumbFrame} from 'sentry/utils/replays/types'; import useOrganization from 'sentry/utils/useOrganization'; import useProjectFromSlug from 'sentry/utils/useProjectFromSlug'; interface Props extends Pick< - ReturnType, + ReturnType, 'fetchError' | 'fetching' | 'projectSlug' | 'replay' > {} diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx index c437c1de6a4b77..47c0bd83aec60e 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx @@ -11,7 +11,7 @@ import {resetMockDate, setMockDate} from 'sentry-test/utils'; import ProjectsStore from 'sentry/stores/projectsStore'; import {browserHistory} from 'sentry/utils/browserHistory'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import ReplayReader from 'sentry/utils/replays/replayReader'; import GroupReplays from 'sentry/views/issueDetails/groupReplays'; @@ -21,8 +21,8 @@ const mockReplayUrl = '/organizations/org-slug/replays/'; const REPLAY_ID_1 = '346789a703f6454384f1de473b8b9fcc'; const REPLAY_ID_2 = 'b05dae9b6be54d21a4d5ad9f8f02b780'; -jest.mock('sentry/utils/replays/hooks/useReplayReader'); -const mockUseReplayReader = jest.mocked(useReplayReader); +jest.mock('sentry/utils/replays/hooks/useLoadReplayReader'); +const mockUseLoadReplayReader = jest.mocked(useLoadReplayReader); const mockEventTimestamp = new Date('2022-09-22T16:59:41Z'); const mockEventTimestampMs = mockEventTimestamp.getTime(); @@ -50,7 +50,7 @@ const mockReplay = ReplayReader.factory({ }, }); -mockUseReplayReader.mockImplementation(() => { +mockUseLoadReplayReader.mockImplementation(() => { return { attachments: [], errors: [], diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.tsx index a5321df37b74ac..5397942912ea89 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.tsx @@ -16,8 +16,8 @@ import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import type EventView from 'sentry/utils/discover/eventView'; import useReplayCountForIssues from 'sentry/utils/replayCount/useReplayCountForIssues'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import useReplayList from 'sentry/utils/replays/hooks/useReplayList'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useUrlParams from 'sentry/utils/useUrlParams'; @@ -132,7 +132,7 @@ function GroupReplaysTableInner({ overlayContent?: React.ReactNode; }) { const orgSlug = organization.slug; - const {fetching, replay} = useReplayReader({ + const {fetching, replay} = useLoadReplayReader({ orgSlug, replaySlug, group, diff --git a/static/app/views/issueDetails/groupReplays/replayClipPreviewWrapper.tsx b/static/app/views/issueDetails/groupReplays/replayClipPreviewWrapper.tsx index d6788562c0f02f..6e844c5362cd35 100644 --- a/static/app/views/issueDetails/groupReplays/replayClipPreviewWrapper.tsx +++ b/static/app/views/issueDetails/groupReplays/replayClipPreviewWrapper.tsx @@ -1,7 +1,7 @@ import ReplayClipPreviewPlayer from 'sentry/components/events/eventReplay/replayClipPreviewPlayer'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import type {Group} from 'sentry/types/group'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import type {ReplayColumn} from 'sentry/views/replays/replayTable/types'; import type {ReplayListRecord} from 'sentry/views/replays/types'; @@ -20,7 +20,7 @@ type Props = { export function ReplayClipPreviewWrapper(props: Props) { const {selectedReplayIndex} = props; const {analyticsContext} = useReplayContext(); - const replayReaderData = useReplayReader({ + const replayReaderData = useLoadReplayReader({ orgSlug: props.orgSlug, replaySlug: props.replaySlug, group: props.group, diff --git a/static/app/views/replays/details.tsx b/static/app/views/replays/details.tsx index 9179dc9fde3ae5..af57abd584c0a3 100644 --- a/static/app/views/replays/details.tsx +++ b/static/app/views/replays/details.tsx @@ -16,10 +16,10 @@ import type {RouteComponentProps} from 'sentry/types/legacyReactRouter'; import {decodeScalar} from 'sentry/utils/queryString'; import type {TimeOffsetLocationQueryParams} from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs'; import useInitialTimeOffsetMs from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs'; +import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import useLogReplayDataLoaded from 'sentry/utils/replays/hooks/useLogReplayDataLoaded'; import useMarkReplayViewed from 'sentry/utils/replays/hooks/useMarkReplayViewed'; import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview'; -import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; import {ReplayPreferencesContextProvider} from 'sentry/utils/replays/playback/providers/replayPreferencesContext'; import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames'; import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; @@ -55,7 +55,7 @@ function ReplayDetails({params: {replaySlug}}: Props) { replay, replayId, replayRecord, - } = useReplayReader({ + } = useLoadReplayReader({ replaySlug, orgSlug, }); From de6d3f53faa80f22a0fab1d9de5412b70e6c2c00 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Mon, 23 Dec 2024 13:21:31 -0500 Subject: [PATCH 463/757] =?UTF-8?q?feat(statistical-detectors):=20Skip=20l?= =?UTF-8?q?ow=20volume=20objects=20for=20regression=20d=E2=80=A6=20(#82538?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …etection Sometimes, the transactions + functions being monitored has such low volume that any changes in duration is not significant but rather due to noise. So we should skip them during the detection phase and not emit issues for them. --- src/sentry/options/defaults.py | 14 +++++++++ src/sentry/statistical_detectors/detector.py | 31 ++++++++++++++++--- src/sentry/tasks/statistical_detectors.py | 8 +++++ .../tasks/test_statistical_detectors.py | 28 ++++++++++++++--- 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 702a7a2146735c..24e58389f9e49e 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -2115,6 +2115,20 @@ flags=FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE, ) +register( + "statistical_detectors.throughput.threshold.transactions", + default=50, + type=Int, + flags=FLAG_AUTOMATOR_MODIFIABLE, +) + +register( + "statistical_detectors.throughput.threshold.functions", + default=25, + type=Int, + flags=FLAG_AUTOMATOR_MODIFIABLE, +) + register( "options_automator_slack_webhook_enabled", default=True, diff --git a/src/sentry/statistical_detectors/detector.py b/src/sentry/statistical_detectors/detector.py index 0b9540ddc5a293..c633d46c7beca0 100644 --- a/src/sentry/statistical_detectors/detector.py +++ b/src/sentry/statistical_detectors/detector.py @@ -62,6 +62,10 @@ class RegressionDetector(ABC): resolution_rel_threshold: float escalation_rel_threshold: float + @classmethod + @abstractmethod + def min_throughput_threshold(cls) -> int: ... + @classmethod def configure_tags(cls): sentry_sdk.set_tag("regression.source", cls.source) @@ -105,20 +109,28 @@ def detect_trends( unique_project_ids: set[int] = set() total_count = 0 + skipped_count = 0 regressed_count = 0 improved_count = 0 algorithm = cls.detector_algorithm_factory() store = cls.detector_store_factory() - for payloads in chunked(cls.all_payloads(projects, start), batch_size): - total_count += len(payloads) + for raw_payloads in chunked(cls.all_payloads(projects, start), batch_size): + total_count += len(raw_payloads) - raw_states = store.bulk_read_states(payloads) + raw_states = store.bulk_read_states(raw_payloads) + payloads = [] states = [] - for raw_state, payload in zip(raw_states, payloads): + for raw_state, payload in zip(raw_states, raw_payloads): + # If the number of events is too low, then we skip updating + # to minimize false positives + if payload.count <= cls.min_throughput_threshold(): + skipped_count += 1 + continue + metrics.distribution( "statistical_detectors.objects.throughput", value=payload.count, @@ -133,6 +145,7 @@ def detect_trends( elif trend_type == TrendType.Improved: improved_count += 1 + payloads.append(payload) states.append(None if new_state is None else new_state.to_redis_dict()) yield TrendBundle( @@ -142,7 +155,8 @@ def detect_trends( state=new_state, ) - store.bulk_write_states(payloads, states) + if payloads and states: + store.bulk_write_states(payloads, states) metrics.incr( "statistical_detectors.projects.active", @@ -158,6 +172,13 @@ def detect_trends( sample_rate=1.0, ) + metrics.incr( + "statistical_detectors.objects.skipped", + amount=skipped_count, + tags={"source": cls.source, "kind": cls.kind}, + sample_rate=1.0, + ) + metrics.incr( "statistical_detectors.objects.regressed", amount=regressed_count, diff --git a/src/sentry/tasks/statistical_detectors.py b/src/sentry/tasks/statistical_detectors.py index acc63ab4a7a6d6..8a1f1259ab94e4 100644 --- a/src/sentry/tasks/statistical_detectors.py +++ b/src/sentry/tasks/statistical_detectors.py @@ -236,6 +236,10 @@ class EndpointRegressionDetector(RegressionDetector): resolution_rel_threshold = 0.1 escalation_rel_threshold = 0.75 + @classmethod + def min_throughput_threshold(cls) -> int: + return options.get("statistical_detectors.throughput.threshold.transactions") + @classmethod def detector_algorithm_factory(cls) -> DetectorAlgorithm: return MovingAverageRelativeChangeDetector( @@ -278,6 +282,10 @@ class FunctionRegressionDetector(RegressionDetector): resolution_rel_threshold = 0.1 escalation_rel_threshold = 0.75 + @classmethod + def min_throughput_threshold(cls) -> int: + return options.get("statistical_detectors.throughput.threshold.functions") + @classmethod def detector_algorithm_factory(cls) -> DetectorAlgorithm: return MovingAverageRelativeChangeDetector( diff --git a/tests/sentry/tasks/test_statistical_detectors.py b/tests/sentry/tasks/test_statistical_detectors.py index 0f72a08aa41f24..f093662c80b4b4 100644 --- a/tests/sentry/tasks/test_statistical_detectors.py +++ b/tests/sentry/tasks/test_statistical_detectors.py @@ -274,6 +274,10 @@ def test_detect_function_trends_query_timerange(functions_query, timestamp, proj assert params.end == datetime(2023, 8, 1, 11, 1, tzinfo=UTC) +@pytest.mark.parametrize( + ["count", "should_emit"], + [(100, True), (10, False)], +) @mock.patch("sentry.tasks.statistical_detectors.query_transactions") @mock.patch("sentry.tasks.statistical_detectors.detect_transaction_change_points") @django_db_all @@ -282,6 +286,8 @@ def test_detect_transaction_trends( query_transactions, timestamp, project, + count, + should_emit, ): n = 50 timestamps = [timestamp - timedelta(hours=n - i) for i in range(n)] @@ -292,7 +298,7 @@ def test_detect_transaction_trends( project_id=project.id, group="/123", fingerprint="/123", - count=100, + count=count, value=100 if i < n / 2 else 300, timestamp=ts, ), @@ -307,7 +313,11 @@ def test_detect_transaction_trends( with override_options(options): for ts in timestamps: detect_transaction_trends([project.organization.id], [project.id], ts) - assert detect_transaction_change_points.apply_async.called + + if should_emit: + assert detect_transaction_change_points.apply_async.called + else: + assert not detect_transaction_change_points.apply_async.called @mock.patch("sentry.issues.status_change_message.uuid4", return_value=uuid.UUID(int=0)) @@ -725,6 +735,10 @@ def mock_regressions(): assert regressions == [] +@pytest.mark.parametrize( + ["count", "should_emit"], + [(100, True), (10, False)], +) @mock.patch("sentry.tasks.statistical_detectors.query_functions") @mock.patch("sentry.tasks.statistical_detectors.detect_function_change_points") @django_db_all @@ -733,6 +747,8 @@ def test_detect_function_trends( query_functions, timestamp, project, + count, + should_emit, ): n = 50 timestamps = [timestamp - timedelta(hours=n - i) for i in range(n)] @@ -743,7 +759,7 @@ def test_detect_function_trends( project_id=project.id, group=123, fingerprint=f"{123:x}", - count=100, + count=count, value=100 if i < n / 2 else 300, timestamp=ts, ), @@ -758,7 +774,11 @@ def test_detect_function_trends( with override_options(options): for ts in timestamps: detect_function_trends([project.id], ts) - assert detect_function_change_points.apply_async.called + + if should_emit: + assert detect_function_change_points.apply_async.called + else: + assert not detect_function_change_points.apply_async.called @mock.patch("sentry.tasks.statistical_detectors.functions.query") From da50ea647b894869f868ffa16332cf19af83542e Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Mon, 23 Dec 2024 13:43:38 -0500 Subject: [PATCH 464/757] fix(widget-builder): Field doubles when changing table to chart (#82539) If you open up the widget builder, change the dataset, and then change the display type from table to line, you'll see that the yAxis gets duplicated. This is because the logic was incorrectly setting a yAxis if we weren't "switching to a table", which would only occur previously if you were selecting the issue dataset. --- .../hooks/useWidgetBuilderState.spec.tsx | 70 ++++++++++++++++--- .../hooks/useWidgetBuilderState.tsx | 7 +- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx index b43b896f52d590..9d9ff40e7053cb 100644 --- a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx @@ -248,8 +248,9 @@ describe('useWidgetBuilderState', () => { LocationFixture({ query: { displayType: DisplayType.TABLE, - field: ['event.type', 'potato'], - yAxis: [ + field: [ + 'event.type', + 'potato', 'count()', 'count_unique(user)', 'count_unique(potato)', @@ -267,8 +268,6 @@ describe('useWidgetBuilderState', () => { expect(result.current.state.fields).toEqual([ {field: 'event.type', alias: undefined, kind: 'field'}, {field: 'potato', alias: undefined, kind: 'field'}, - ]); - expect(result.current.state.yAxis).toEqual([ { function: ['count', '', undefined, undefined], alias: undefined, @@ -321,6 +320,61 @@ describe('useWidgetBuilderState', () => { }, ]); }); + + it('does not duplicate fields when changing display from table to chart', () => { + mockedUsedLocation.mockReturnValue( + LocationFixture({ + query: { + displayType: DisplayType.TABLE, + dataset: WidgetType.ERRORS, + field: ['count()'], + }, + }) + ); + + const {result} = renderHook(() => useWidgetBuilderState(), { + wrapper: WidgetBuilderProvider, + }); + + expect(result.current.state.fields).toEqual([ + { + function: ['count', '', undefined, undefined], + alias: undefined, + kind: 'function', + }, + ]); + + act(() => { + result.current.dispatch({ + type: BuilderStateAction.SET_DATASET, + payload: WidgetType.SPANS, + }); + }); + + expect(result.current.state.fields).toEqual([ + { + function: ['count', 'span.duration', undefined, undefined], + alias: undefined, + kind: 'function', + }, + ]); + expect(result.current.state.yAxis).toEqual([]); + + act(() => { + result.current.dispatch({ + type: BuilderStateAction.SET_DISPLAY_TYPE, + payload: DisplayType.LINE, + }); + }); + + expect(result.current.state.yAxis).toEqual([ + { + function: ['count', 'span.duration', undefined, undefined], + alias: undefined, + kind: 'function', + }, + ]); + }); }); describe('dataset', () => { @@ -425,13 +479,7 @@ describe('useWidgetBuilderState', () => { kind: 'function', }, ]); - expect(result.current.state.yAxis).toEqual([ - { - function: ['count', 'span.duration', undefined, undefined], - alias: undefined, - kind: 'function', - }, - ]); + expect(result.current.state.yAxis).toEqual([]); expect(result.current.state.query).toEqual(['']); expect(result.current.state.sort).toEqual([ { diff --git a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx index 30c4559a62d7aa..2ea71776770984 100644 --- a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx +++ b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx @@ -147,18 +147,18 @@ function useWidgetBuilderState(): { case BuilderStateAction.SET_DATASET: setDataset(action.payload); - let newDisplayType; + let nextDisplayType = displayType; if (action.payload === WidgetType.ISSUE) { // Issues only support table display type setDisplayType(DisplayType.TABLE); - newDisplayType = DisplayType.TABLE; + nextDisplayType = DisplayType.TABLE; } const config = getDatasetConfig(action.payload); setFields( config.defaultWidgetQuery.fields?.map(field => explodeField({field})) ); - if (newDisplayType === DisplayType.TABLE) { + if (nextDisplayType === DisplayType.TABLE) { setYAxis([]); } else { setYAxis( @@ -201,6 +201,7 @@ function useWidgetBuilderState(): { setLimit, fields, yAxis, + displayType, ] ); From 388861ff10ed10742598aef57688328f796599e3 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:33:39 -0500 Subject: [PATCH 465/757] ref(tsc): convert sentryAppExternalIssueModal to FC (#82464) --- .../group/sentryAppExternalIssueModal.tsx | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/static/app/components/group/sentryAppExternalIssueModal.tsx b/static/app/components/group/sentryAppExternalIssueModal.tsx index d0bdf650b5458d..c0d273a808cbef 100644 --- a/static/app/components/group/sentryAppExternalIssueModal.tsx +++ b/static/app/components/group/sentryAppExternalIssueModal.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment} from 'react'; +import {Fragment, useState} from 'react'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import SentryAppExternalIssueForm from 'sentry/components/group/sentryAppExternalIssueForm'; @@ -15,58 +15,57 @@ type Props = ModalRenderProps & { sentryAppInstallation: SentryAppInstallation; }; -type State = { - action: 'create' | 'link'; -}; - -class SentryAppExternalIssueModal extends Component { - state: State = { - action: 'create', - }; +function SentryAppExternalIssueModal(props: Props) { + const [action, setAction] = useState<'create' | 'link'>('create'); + const { + Header, + Body, + sentryAppComponent, + sentryAppInstallation, + group, + closeModal, + event, + } = props; - showLink = () => { - this.setState({action: 'link'}); + const showLink = () => { + setAction('link'); }; - showCreate = () => { - this.setState({action: 'create'}); + const showCreate = () => { + setAction('create'); }; - onSubmitSuccess = () => { - this.props.closeModal(); + const onSubmitSuccess = () => { + closeModal(); }; - render() { - const {Header, Body, sentryAppComponent, sentryAppInstallation, group} = this.props; - const {action} = this.state; - const name = sentryAppComponent.sentryApp.name; - const config = sentryAppComponent.schema[action]; + const name = sentryAppComponent.sentryApp.name; + const config = sentryAppComponent.schema[action]; - return ( - -
{tct('[name] Issue', {name})}
- -
  • - {t('Create')} -
  • -
  • - {t('Link')} -
  • -
    - - - -
    - ); - } + return ( + +
    {tct('[name] Issue', {name})}
    + +
  • + {t('Create')} +
  • +
  • + {t('Link')} +
  • +
    + + + +
    + ); } export default SentryAppExternalIssueModal; From 9286c35fc825261eb71e24d2162d793f0b7cefef Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:33:57 -0500 Subject: [PATCH 466/757] ref(tsc): convert suggestedOwnerHovercard to FC (#82465) recommend reviewing with whitespace off --- .../group/suggestedOwnerHovercard.tsx | 286 +++++++++--------- 1 file changed, 135 insertions(+), 151 deletions(-) diff --git a/static/app/components/group/suggestedOwnerHovercard.tsx b/static/app/components/group/suggestedOwnerHovercard.tsx index 1bb0108c0684c8..3bde36f6ca542c 100644 --- a/static/app/components/group/suggestedOwnerHovercard.tsx +++ b/static/app/components/group/suggestedOwnerHovercard.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment} from 'react'; +import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import moment from 'moment-timezone'; @@ -48,160 +48,144 @@ type Props = { rules?: any[] | null; }; -type State = { - commitsExpanded: boolean; - rulesExpanded: boolean; -}; +function SuggestedOwnerHovercard(props: Props) { + const [commitsExpanded, setCommitsExpanded] = useState(false); + const [rulesExpanded, setRulesExpanded] = useState(false); -class SuggestedOwnerHovercard extends Component { - state: State = { - commitsExpanded: false, - rulesExpanded: false, + const {organization, actor, commits, rules, release, projectId} = props; + const modalData = { + initialData: [ + { + emails: actor.email ? new Set([actor.email]) : new Set([]), + }, + ], + source: 'suggested_assignees', }; - render() { - const {organization, actor, commits, rules, release, projectId, ...props} = - this.props; - const {commitsExpanded, rulesExpanded} = this.state; - const modalData = { - initialData: [ - { - emails: actor.email ? new Set([actor.email]) : new Set([]), - }, - ], - source: 'suggested_assignees', - }; - - return ( - - - - {actor.name || actor.email} - - {actor.id === undefined && ( - - {tct( - 'The email [actorEmail] is not a member of your organization. [inviteUser:Invite] them or link additional emails in [accountSettings:account settings].', - { - actorEmail: {actor.email}, - accountSettings: , - inviteUser: openInviteMembersModal(modalData)} />, - } - )} - - )} - - } - body={ - - {commits !== undefined && !release && ( - - -
    {t('Commits')}
    -
    -
    - {commits - .slice(0, commitsExpanded ? commits.length : 3) - .map(({message, dateCreated}, i) => ( - - - - - ))} -
    - {commits.length > 3 && !commitsExpanded ? ( - this.setState({commitsExpanded: true})} - > - {t('View more')} - - ) : null} -
    - )} - {commits !== undefined && release && ( - - -
    {t('Suspect Release')}
    -
    -
    - - - - {tct('[actor] [verb] [commits] in [release]', { - actor: actor.name, - verb: commits.length > 1 ? t('made') : t('last committed'), - commits: - commits.length > 1 ? ( - // Link to release commits - - {t('%s commits', commits.length)} - - ) : ( - - ), - release: ( - + return ( + + + + {actor.name || actor.email} + + {actor.id === undefined && ( + + {tct( + 'The email [actorEmail] is not a member of your organization. [inviteUser:Invite] them or link additional emails in [accountSettings:account settings].', + { + actorEmail: {actor.email}, + accountSettings: , + inviteUser: openInviteMembersModal(modalData)} />, + } + )} + + )} + + } + body={ + + {commits !== undefined && !release && ( + + +
    {t('Commits')}
    +
    +
    + {commits + .slice(0, commitsExpanded ? commits.length : 3) + .map(({message, dateCreated}, i) => ( + + + + + ))} +
    + {commits.length > 3 && !commitsExpanded ? ( + setCommitsExpanded(true)} + > + {t('View more')} + + ) : null} +
    + )} + {commits !== undefined && release && ( + + +
    {t('Suspect Release')}
    +
    +
    + + + + {tct('[actor] [verb] [commits] in [release]', { + actor: actor.name, + verb: commits.length > 1 ? t('made') : t('last committed'), + commits: + commits.length > 1 ? ( + // Link to release commits + + {t('%s commits', commits.length)} + + ) : ( + ), - })} - - -
    -
    - )} - {defined(rules) && ( - - -
    {t('Matching Ownership Rules')}
    -
    -
    - {rules - .slice(0, rulesExpanded ? rules.length : 3) - .map(([type, matched], i) => ( - - - {matched} - - ))} -
    - {rules.length > 3 && !rulesExpanded ? ( - this.setState({rulesExpanded: true})} - > - {t('View more')} - - ) : null} -
    - )} -
    - } - {...props} - /> - ); - } + release: ( + + ), + })} + + +
    + + )} + {defined(rules) && ( + + +
    {t('Matching Ownership Rules')}
    +
    +
    + {rules + .slice(0, rulesExpanded ? rules.length : 3) + .map(([type, matched], i) => ( + + + {matched} + + ))} +
    + {rules.length > 3 && !rulesExpanded ? ( + setRulesExpanded(true)} + > + {t('View more')} + + ) : null} +
    + )} + + } + {...props} + /> + ); } const tagColors = { From 3643a750e567fb69b472d97d959838b41d04a143 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:34:04 -0500 Subject: [PATCH 467/757] ref(tsc): convert suggestProjectModal to FC (#82469) --- .../components/modals/suggestProjectModal.tsx | 79 ++++++++----------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/static/app/components/modals/suggestProjectModal.tsx b/static/app/components/modals/suggestProjectModal.tsx index 5afee43327c48a..1afbde062081a9 100644 --- a/static/app/components/modals/suggestProjectModal.tsx +++ b/static/app/components/modals/suggestProjectModal.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment} from 'react'; +import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import * as qs from 'query-string'; @@ -30,38 +30,30 @@ type Props = ModalRenderProps & { organization: Organization; }; -type State = { - askTeammate: boolean; -}; - -class SuggestProjectModal extends Component { - state: State = { - askTeammate: false, - }; +function SuggestProjectModal(props: Props) { + const [askTeammate, setAskTeammate] = useState(false); + const {matchedUserAgentString, organization, closeModal, Body, Header, Footer} = props; - handleGetStartedClick = () => { - const {matchedUserAgentString, organization} = this.props; + const handleGetStartedClick = () => { trackAnalytics('growth.clicked_mobile_prompt_setup_project', { matchedUserAgentString, organization, }); }; - handleAskTeammate = () => { - const {matchedUserAgentString, organization} = this.props; - this.setState({askTeammate: true}); + const handleAskTeammate = () => { + setAskTeammate(true); trackAnalytics('growth.clicked_mobile_prompt_ask_teammate', { matchedUserAgentString, organization, }); }; - goBack = () => { - this.setState({askTeammate: false}); + const goBack = () => { + setAskTeammate(false); }; - handleSubmitSuccess = () => { - const {matchedUserAgentString, organization, closeModal} = this.props; + const handleSubmitSuccess = () => { addSuccessMessage('Notified teammate successfully'); trackAnalytics('growth.submitted_mobile_prompt_ask_teammate', { matchedUserAgentString, @@ -70,28 +62,27 @@ class SuggestProjectModal extends Component { closeModal(); }; - handlePreSubmit = () => { + const handlePreSubmit = () => { addLoadingMessage(t('Submitting\u2026')); }; - handleSubmitError = () => { + const handleSubmitError = () => { addErrorMessage(t('Error notifying teammate')); }; - renderAskTeammate() { - const {Body, organization} = this.props; + const renderAskTeammate = () => { return (
    - {t('Back')} + {t('Back')} } > @@ -109,11 +100,9 @@ class SuggestProjectModal extends Component { ); - } - - renderMain() { - const {Body, Footer, organization} = this.props; + }; + const renderMain = () => { const paramString = qs.stringify({ referrer: 'suggest_project', category: 'mobile', @@ -167,14 +156,14 @@ class SuggestProjectModal extends Component { {hasAccess && ( {t('Get Started')} @@ -186,22 +175,18 @@ class SuggestProjectModal extends Component { ); - } + }; - render() { - const {Header} = this.props; - const {askTeammate} = this.state; - const header = askTeammate ? t('Tell a Teammate') : t('Try Sentry for Mobile'); - return ( - -
    - - {header} -
    - {this.state.askTeammate ? this.renderAskTeammate() : this.renderMain()} -
    - ); - } + const header = askTeammate ? t('Tell a Teammate') : t('Try Sentry for Mobile'); + return ( + +
    + + {header} +
    + {askTeammate ? renderAskTeammate() : renderMain()} +
    + ); } const ModalContainer = styled('div')` From 938a1fc50a678f06af64585f5a65b3d152aa005f Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:34:50 -0500 Subject: [PATCH 468/757] ref(tsc): convert arrayValue to FC (#82481) --- static/app/utils/discover/arrayValue.tsx | 59 ++++++++++-------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/static/app/utils/discover/arrayValue.tsx b/static/app/utils/discover/arrayValue.tsx index 6095d92159c06c..55c977d80a43c8 100644 --- a/static/app/utils/discover/arrayValue.tsx +++ b/static/app/utils/discover/arrayValue.tsx @@ -1,4 +1,4 @@ -import {Component} from 'react'; +import {useState} from 'react'; import styled from '@emotion/styled'; import {t} from 'sentry/locale'; @@ -9,42 +9,33 @@ import {nullableValue} from './fieldRenderers'; type Props = { value: Array; }; -type State = { - expanded: boolean; -}; -class ArrayValue extends Component { - state: State = { - expanded: false, - }; - handleToggle = () => { - this.setState(prevState => ({ - expanded: !prevState.expanded, - })); +function ArrayValue(props: Props) { + const [expanded, setExpanded] = useState(false); + const {value} = props; + + const handleToggle = () => { + setExpanded(!expanded); }; - render() { - const {expanded} = this.state; - const {value} = this.props; - return ( - - {expanded && - value - .slice(0, value.length - 1) - .map((item, i) => ( - {nullableValue(item)} - ))} - {nullableValue(value.slice(-1)[0])} - {value.length > 1 ? ( - - - - ) : null} - - ); - } + return ( + + {expanded && + value + .slice(0, value.length - 1) + .map((item, i) => ( + {nullableValue(item)} + ))} + {nullableValue(value.slice(-1)[0])} + {value.length > 1 ? ( + + + + ) : null} + + ); } const ArrayContainer = styled('div')<{expanded: boolean}>` From e92cfdf90ab71829742b062ffecde5654bef9bac Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:35:14 -0500 Subject: [PATCH 469/757] ref(tsc): convert histogram index to FC (#82484) --- .../app/utils/performance/histogram/index.tsx | 49 ++++++++----------- .../transactionVitals/index.spec.tsx | 9 +++- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/static/app/utils/performance/histogram/index.tsx b/static/app/utils/performance/histogram/index.tsx index 1f60a53f93d953..478d424bde68fe 100644 --- a/static/app/utils/performance/histogram/index.tsx +++ b/static/app/utils/performance/histogram/index.tsx @@ -1,9 +1,8 @@ -import {Component} from 'react'; import type {Location} from 'history'; import type {SelectValue} from 'sentry/types/core'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {decodeScalar} from 'sentry/utils/queryString'; +import {useNavigate} from 'sentry/utils/useNavigate'; import {FILTER_OPTIONS} from './constants'; import type {DataFilter} from './types'; @@ -22,34 +21,30 @@ type Props = { zoomKeys: string[]; }; -class Histogram extends Component { - isZoomed() { - const {location, zoomKeys} = this.props; - return zoomKeys.map(key => location.query[key]).some(value => value !== undefined); - } +function Histogram(props: Props) { + const {location, zoomKeys, children} = props; + const navigate = useNavigate(); - handleResetView = () => { - const {location, zoomKeys} = this.props; + const isZoomed = () => { + return zoomKeys.map(key => location.query[key]).some(value => value !== undefined); + }; - browserHistory.push({ + const handleResetView = () => { + navigate({ pathname: location.pathname, query: removeHistogramQueryStrings(location, zoomKeys), }); }; - getActiveFilter() { - const {location} = this.props; - + const getActiveFilter = () => { const dataFilter = location.query.dataFilter ? decodeScalar(location.query.dataFilter) : FILTER_OPTIONS[0].value; return FILTER_OPTIONS.find(item => item.value === dataFilter) || FILTER_OPTIONS[0]; - } - - handleFilterChange = (value: string) => { - const {location, zoomKeys} = this.props; + }; - browserHistory.push({ + const handleFilterChange = (value: string) => { + navigate({ pathname: location.pathname, query: { ...removeHistogramQueryStrings(location, zoomKeys), @@ -58,16 +53,14 @@ class Histogram extends Component { }); }; - render() { - const childrenProps = { - isZoomed: this.isZoomed(), - handleResetView: this.handleResetView, - activeFilter: this.getActiveFilter(), - handleFilterChange: this.handleFilterChange, - filterOptions: FILTER_OPTIONS, - }; - return this.props.children(childrenProps); - } + const childrenProps = { + isZoomed: isZoomed(), + handleResetView, + activeFilter: getActiveFilter(), + handleFilterChange, + filterOptions: FILTER_OPTIONS, + }; + return children(childrenProps); } export function removeHistogramQueryStrings(location: Location, zoomKeys: string[]) { diff --git a/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx index 48ba077a60845b..10ca08b0e3f799 100644 --- a/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionVitals/index.spec.tsx @@ -14,8 +14,8 @@ import { import ProjectsStore from 'sentry/stores/projectsStore'; import type {Project} from 'sentry/types/project'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; import TransactionVitals from 'sentry/views/performance/transactionSummary/transactionVitals'; import { VITAL_GROUPS, @@ -25,6 +25,9 @@ import { jest.mock('sentry/utils/useLocation'); const mockUseLocation = jest.mocked(useLocation); +jest.mock('sentry/utils/useNavigate'); + +const mockUseNavigate = jest.mocked(useNavigate); interface HistogramData { count: number; @@ -300,6 +303,8 @@ describe('Performance > Web Vitals', function () { }); it('resets view properly', async function () { + const mockNavigate = jest.fn(); + mockUseNavigate.mockReturnValue(mockNavigate); const {organization, router} = initialize({ query: { fidStart: '20', @@ -314,7 +319,7 @@ describe('Performance > Web Vitals', function () { await userEvent.click(screen.getByRole('button', {name: 'Reset View'})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(mockNavigate).toHaveBeenCalledWith({ query: expect.not.objectContaining( ZOOM_KEYS.reduce((obj, key) => { obj[key] = expect.anything(); From 4120ab0ac28d80c7627fb7aad87df8763e57bce9 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:39:44 -0500 Subject: [PATCH 470/757] ref(tsc): convert sources index to FC (#82474) --- .../app/components/search/sources/index.tsx | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/static/app/components/search/sources/index.tsx b/static/app/components/search/sources/index.tsx index e4d06f11a6b6e5..24b98acbb0d5f5 100644 --- a/static/app/components/search/sources/index.tsx +++ b/static/app/components/search/sources/index.tsx @@ -1,4 +1,4 @@ -import {Component} from 'react'; +import {useCallback} from 'react'; import type {Fuse} from 'sentry/utils/fuzzySearch'; @@ -23,53 +23,53 @@ type SourceResult = { results: Result[]; }; -class SearchSources extends Component { - // `allSources` will be an array of all result objects from each source - renderResults(allSources: SourceResult[]) { - const {children} = this.props; +function SearchSources(props: Props) { + const {children, sources} = props; - // loading means if any result has `isLoading` OR any result is null - const isLoading = !!allSources.find(arg => arg.isLoading || arg.results === null); + // `allSources` will be an array of all result objects from each source + const renderResults = useCallback( + (allSources: SourceResult[]) => { + // loading means if any result has `isLoading` OR any result is null + const isLoading = !!allSources.find(arg => arg.isLoading || arg.results === null); - const foundResults = isLoading - ? [] - : allSources - .flatMap(({results}) => results ?? []) - .sort((a, b) => (a.score ?? 0) - (b.score ?? 0)); - const hasAnyResults = !!foundResults.length; + const foundResults = isLoading + ? [] + : allSources + .flatMap(({results}) => results ?? []) + .sort((a, b) => (a.score ?? 0) - (b.score ?? 0)); + const hasAnyResults = !!foundResults.length; - return children({ - isLoading, - results: foundResults, - hasAnyResults, - }); - } + return children({ + isLoading, + results: foundResults, + hasAnyResults, + }); + }, + [children] + ); - renderSources(sources: Props['sources'], results: SourceResult[], idx: number) { - if (idx >= sources.length) { - return this.renderResults(results); - } - const Source = sources[idx]; - return ( - - {(args: SourceResult) => { - // Mutate the array instead of pushing because we don't know how often - // this child function will be called and pushing will cause duplicate - // results to be pushed for all calls down the chain. - results[idx] = args; - return this.renderSources(sources, results, idx + 1); - }} - - ); - } + const renderSources = useCallback( + (results: SourceResult[], idx: number) => { + if (idx >= sources.length) { + return renderResults(results); + } + const Source = sources[idx]; + return ( + + {(args: SourceResult) => { + // Mutate the array instead of pushing because we don't know how often + // this child function will be called and pushing will cause duplicate + // results to be pushed for all calls down the chain. + results[idx] = args; + return renderSources(results, idx + 1); + }} + + ); + }, + [props, renderResults, sources] + ); - render() { - return this.renderSources( - this.props.sources, - new Array(this.props.sources.length), - 0 - ); - } + return renderSources(new Array(sources.length), 0); } export default SearchSources; From fbb540cbefeab5c8f844a5a8a363c171bfe8fb54 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:50:48 -0500 Subject: [PATCH 471/757] ref(insights): Split up `DurationChart` component in HTTP insights (#82497) This is a temporary clarifying measure. Every module has a `components/durationChart.tsx` right now, and they all look largely the same. The only exception is the one inside the `http` module. That one is different because it can also plot samples! It's used by the HTTP module and the Queues module. While I work on consolidating the chart widgets, I'm going to rename this component to `DurationChartWithSamples`, and add a `DurationChart` that's a simple version. This will allow me to swap out all the charts on the landing pages in Insights without affecting the sample panes. --- .../http/components/charts/durationChart.tsx | 46 +---------- .../charts/durationChartWithSamples.tsx | 76 +++++++++++++++++++ .../http/components/httpSamplesPanel.tsx | 4 +- .../components/messageSpanSamplesPanel.tsx | 4 +- 4 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 static/app/views/insights/http/components/charts/durationChartWithSamples.tsx diff --git a/static/app/views/insights/http/components/charts/durationChart.tsx b/static/app/views/insights/http/components/charts/durationChart.tsx index 39a856dace93c9..c65a8ab4a38196 100644 --- a/static/app/views/insights/http/components/charts/durationChart.tsx +++ b/static/app/views/insights/http/components/charts/durationChart.tsx @@ -1,6 +1,4 @@ -import type {ComponentProps} from 'react'; - -import type {EChartHighlightHandler, Series} from 'sentry/types/echarts'; +import type {Series} from 'sentry/types/echarts'; import {AVG_COLOR} from 'sentry/views/insights/colors'; import Chart, {ChartType} from 'sentry/views/insights/common/components/chart'; import ChartPanel from 'sentry/views/insights/common/components/chartPanel'; @@ -11,47 +9,9 @@ interface Props { isLoading: boolean; series: Series[]; error?: Error | null; - onHighlight?: (highlights: Highlight[], event: Event) => void; // TODO: Correctly type this - scatterPlot?: ComponentProps['scatterPlot']; } -interface Highlight { - dataPoint: Series['data'][number]; - series: Series[]; -} - -export function DurationChart({ - series, - scatterPlot, - isLoading, - error, - onHighlight, -}: Props) { - // TODO: This is duplicated from `DurationChart` in `SampleList`. Resolve the duplication - const handleChartHighlight: EChartHighlightHandler = function (event) { - // ignore mouse hovering over the chart legend - if (!event.batch) { - return; - } - - // TODO: Gross hack. Even though `scatterPlot` is a separate prop, it's just an array of `Series` that gets appended to the main series. To find the point that was hovered, we re-construct the correct series order. It would have been cleaner to just pass the scatter plot as its own, single series - const allSeries = [...series, ...(scatterPlot ?? [])]; - - const highlightedDataPoints = event.batch.map(batch => { - let {seriesIndex} = batch; - const {dataIndex} = batch; - // TODO: More hacks. The Chart component partitions the data series into a complete and incomplete series. Wrap the series index to work around overflowing index. - seriesIndex = seriesIndex % allSeries.length; - - const highlightedSeries = allSeries?.[seriesIndex]; - const highlightedDataPoint = highlightedSeries?.data?.[dataIndex]; - - return {series: highlightedSeries, dataPoint: highlightedDataPoint}; - }); - - onHighlight?.(highlightedDataPoints, event); - }; - +export function DurationChart({series, isLoading, error}: Props) { return ( void; // TODO: Correctly type this + scatterPlot?: ComponentProps['scatterPlot']; +} + +interface Highlight { + dataPoint: Series['data'][number]; + series: Series[]; +} + +export function DurationChartWithSamples({ + series, + scatterPlot, + isLoading, + error, + onHighlight, +}: Props) { + // TODO: This is duplicated from `DurationChart` in `SampleList`. Resolve the duplication + const handleChartHighlight: EChartHighlightHandler = function (event) { + // ignore mouse hovering over the chart legend + if (!event.batch) { + return; + } + + // TODO: Gross hack. Even though `scatterPlot` is a separate prop, it's just an array of `Series` that gets appended to the main series. To find the point that was hovered, we re-construct the correct series order. It would have been cleaner to just pass the scatter plot as its own, single series + const allSeries = [...series, ...(scatterPlot ?? [])]; + + const highlightedDataPoints = event.batch.map(batch => { + let {seriesIndex} = batch; + const {dataIndex} = batch; + // TODO: More hacks. The Chart component partitions the data series into a complete and incomplete series. Wrap the series index to work around overflowing index. + seriesIndex = seriesIndex % allSeries.length; + + const highlightedSeries = allSeries?.[seriesIndex]; + const highlightedDataPoint = highlightedSeries?.data?.[dataIndex]; + + return {series: highlightedSeries, dataPoint: highlightedDataPoint}; + }); + + onHighlight?.(highlightedDataPoints, event); + }; + + return ( + + + + ); +} diff --git a/static/app/views/insights/http/components/httpSamplesPanel.tsx b/static/app/views/insights/http/components/httpSamplesPanel.tsx index 79c979ca79bbe1..8a0796906448ec 100644 --- a/static/app/views/insights/http/components/httpSamplesPanel.tsx +++ b/static/app/views/insights/http/components/httpSamplesPanel.tsx @@ -44,7 +44,7 @@ import { getThroughputTitle, } from 'sentry/views/insights/common/views/spans/types'; import {useSampleScatterPlotSeries} from 'sentry/views/insights/common/views/spanSummaryPage/sampleList/durationChart/useSampleScatterPlotSeries'; -import {DurationChart} from 'sentry/views/insights/http/components/charts/durationChart'; +import {DurationChartWithSamples} from 'sentry/views/insights/http/components/charts/durationChartWithSamples'; import {ResponseCodeCountChart} from 'sentry/views/insights/http/components/charts/responseCodeCountChart'; import {SpanSamplesTable} from 'sentry/views/insights/http/components/tables/spanSamplesTable'; import {HTTP_RESPONSE_STATUS_CODES} from 'sentry/views/insights/http/data/definitions'; @@ -430,7 +430,7 @@ export function HTTPSamplesPanel() { {query.panel === 'duration' && ( - - Date: Mon, 23 Dec 2024 14:54:01 -0500 Subject: [PATCH 472/757] feat(new-trace): Removing duplicate db span descriptions (#82542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2024-12-23 at 2 41 23 PM Co-authored-by: Abdullah Khan --- .../traceDrawer/details/span/index.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx index 0186e7aab20926..d3bc1864a803f9 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx @@ -30,7 +30,7 @@ import {TraceDrawerComponents} from '.././styles'; import {IssueList} from '../issues/issues'; import Alerts from './sections/alerts'; -import {hasFormattedSpanDescription, SpanDescription} from './sections/description'; +import {SpanDescription} from './sections/description'; import {GeneralInfo} from './sections/generalInfo'; import {hasSpanHTTPInfo, SpanHTTPInfo} from './sections/http'; import {hasSpanKeys, SpanKeys} from './sections/keys'; @@ -120,7 +120,6 @@ function LegacySpanNodeDetailHeader({ function SpanSections({ node, - project, organization, location, onParentClick, @@ -137,7 +136,6 @@ function SpanSections({ return ( ; onParentClick: (node: TraceTreeNode) => void; organization: Organization; - project: Project | undefined; }) { return ( - {hasFormattedSpanDescription(node) ? ( - - ) : null} Date: Mon, 23 Dec 2024 23:40:57 +0300 Subject: [PATCH 473/757] ref: Remove dead template (#82524) This file doesn't seem to be referenced by any Python or HTML file which means it is dead. Let's remove it. --- .../templates/sentry/bases/forceauth_modal.html | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/sentry/templates/sentry/bases/forceauth_modal.html diff --git a/src/sentry/templates/sentry/bases/forceauth_modal.html b/src/sentry/templates/sentry/bases/forceauth_modal.html deleted file mode 100644 index a9adbebfe622a8..00000000000000 --- a/src/sentry/templates/sentry/bases/forceauth_modal.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "sentry/layout.html" %} - -{% load i18n %} - -{% block wrapperclass %}{{ block.super }} narrow hide-sidebar{% endblock %} - -{% block content %} -

    Account: {{ request.user.get_display_name }}

    -
    - {% block main %}{% endblock %} -{% endblock %} From a0b0d2f5693ddcdeffefff3e56999f8dadb5ce5e Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:57:14 -0500 Subject: [PATCH 474/757] ref(tsc): convert sentryAppPublishRequestModal to FC (#82468) Co-authored-by: Scott Cooper --- .../modals/sentryAppPublishRequestModal.tsx | 87 +++++++++---------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/static/app/components/modals/sentryAppPublishRequestModal.tsx b/static/app/components/modals/sentryAppPublishRequestModal.tsx index 786ab5c2f00acd..c549bb05e48a1c 100644 --- a/static/app/components/modals/sentryAppPublishRequestModal.tsx +++ b/static/app/components/modals/sentryAppPublishRequestModal.tsx @@ -1,4 +1,4 @@ -import {Component, Fragment} from 'react'; +import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import intersection from 'lodash/intersection'; @@ -59,11 +59,11 @@ type Props = ModalRenderProps & { app: SentryApp; }; -export default class SentryAppPublishRequestModal extends Component { - form = new FormModel({transformData}); +export default function SentryAppPublishRequestModal(props: Props) { + const [form] = useState(() => new FormModel({transformData})); + const {app, closeModal, Header, Body} = props; - get formFields() { - const {app} = this.props; + const formFields = () => { const permissions = getPermissionSelectionsFromScopes(app.scopes); const permissionQuestionBaseText = @@ -132,57 +132,54 @@ export default class SentryAppPublishRequestModal extends Component { } return baseFields; - } + }; - handleSubmitSuccess = () => { - addSuccessMessage(t('Request to publish %s successful.', this.props.app.slug)); - this.props.closeModal(); + const handleSubmitSuccess = () => { + addSuccessMessage(t('Request to publish %s successful.', app.slug)); + closeModal(); }; - handleSubmitError = err => { + const handleSubmitError = err => { addErrorMessage( tct('Request to publish [app] fails. [detail]', { - app: this.props.app.slug, + app: app.slug, detail: err?.responseJSON?.detail, }) ); }; - render() { - const {app, Header, Body} = this.props; - const endpoint = `/sentry-apps/${app.slug}/publish-request/`; - const forms = [ - { - title: t('Questions to answer'), - fields: this.formFields, - }, - ]; - return ( - -
    {t('Publish Request Questionnaire')}
    - - - {t( - `Please fill out this questionnaire in order to get your integration evaluated for publication. + const endpoint = `/sentry-apps/${app.slug}/publish-request/`; + const forms = [ + { + title: t('Questions to answer'), + fields: formFields(), + }, + ]; + return ( + +
    {t('Publish Request Questionnaire')}
    + + + {t( + `Please fill out this questionnaire in order to get your integration evaluated for publication. Once your integration has been approved, users outside of your organization will be able to install it.` - )} - -
    this.props.closeModal()} - > - - - -
    - ); - } + )} +
    +
    + + + +
    + ); } const Explanation = styled('div')` From 39c845f6f48a1a05053b64e535baaae49da81cc8 Mon Sep 17 00:00:00 2001 From: Nikki Kapadia <72356613+nikkikapadia@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:02:31 -0500 Subject: [PATCH 475/757] feat(widget-builder): Add `legendAlias` to widget builder state object (#82544) Added `legendAlias` as a state object. Also handles the conversion to and from `Widget` type. Legend aliases are only available on chart widgets. --- .../hooks/useWidgetBuilderState.spec.tsx | 27 ++++++++++++ .../hooks/useWidgetBuilderState.tsx | 42 +++++++++++++++++-- .../convertBuilderStateToWidget.spec.tsx | 14 +++++++ .../utils/convertBuilderStateToWidget.ts | 5 ++- ...convertWidgetToBuilderStateParams.spec.tsx | 27 +++++++++++- .../convertWidgetToBuilderStateParams.ts | 3 ++ 6 files changed, 113 insertions(+), 5 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx index 9d9ff40e7053cb..ed3e09d2d2c0c3 100644 --- a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx @@ -680,4 +680,31 @@ describe('useWidgetBuilderState', () => { expect(result.current.state.limit).toEqual(10); }); }); + + describe('legendAlias', () => { + it('can decode and update legendAlias', () => { + mockedUsedLocation.mockReturnValue( + LocationFixture({ + query: { + legendAlias: ['test', 'test2'], + }, + }) + ); + + const {result} = renderHook(() => useWidgetBuilderState(), { + wrapper: WidgetBuilderProvider, + }); + + expect(result.current.state.legendAlias).toEqual(['test', 'test2']); + + act(() => { + result.current.dispatch({ + type: BuilderStateAction.SET_LEGEND_ALIAS, + payload: ['test3', 'test4'], + }); + }); + + expect(result.current.state.legendAlias).toEqual(['test3', 'test4']); + }); + }); }); diff --git a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx index 2ea71776770984..7fb5877a878e3e 100644 --- a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx +++ b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx @@ -25,6 +25,7 @@ export type WidgetBuilderStateQueryParams = { description?: string; displayType?: DisplayType; field?: (string | undefined)[]; + legendAlias?: string[]; limit?: number; query?: string[]; sort?: string[]; @@ -42,6 +43,7 @@ export const BuilderStateAction = { SET_QUERY: 'SET_QUERY', SET_SORT: 'SET_SORT', SET_LIMIT: 'SET_LIMIT', + SET_LEGEND_ALIAS: 'SET_LEGEND_ALIAS', } as const; type WidgetAction = @@ -53,13 +55,15 @@ type WidgetAction = | {payload: Column[]; type: typeof BuilderStateAction.SET_Y_AXIS} | {payload: string[]; type: typeof BuilderStateAction.SET_QUERY} | {payload: Sort[]; type: typeof BuilderStateAction.SET_SORT} - | {payload: number; type: typeof BuilderStateAction.SET_LIMIT}; + | {payload: number; type: typeof BuilderStateAction.SET_LIMIT} + | {payload: string[]; type: typeof BuilderStateAction.SET_LEGEND_ALIAS}; export interface WidgetBuilderState { dataset?: WidgetType; description?: string; displayType?: DisplayType; fields?: Column[]; + legendAlias?: string[]; limit?: number; query?: string[]; sort?: Sort[]; @@ -109,10 +113,36 @@ function useWidgetBuilderState(): { decoder: decodeScalar, deserializer: deserializeLimit, }); + const [legendAlias, setLegendAlias] = useQueryParamState({ + fieldName: 'legendAlias', + decoder: decodeList, + }); const state = useMemo( - () => ({title, description, displayType, dataset, fields, yAxis, query, sort, limit}), - [title, description, displayType, dataset, fields, yAxis, query, sort, limit] + () => ({ + title, + description, + displayType, + dataset, + fields, + yAxis, + query, + sort, + limit, + legendAlias, + }), + [ + title, + description, + displayType, + dataset, + fields, + yAxis, + query, + sort, + limit, + legendAlias, + ] ); const dispatch = useCallback( @@ -128,6 +158,7 @@ function useWidgetBuilderState(): { setDisplayType(action.payload); if (action.payload === DisplayType.BIG_NUMBER) { setSort([]); + setLegendAlias([]); } const [aggregates, columns] = partition(fields, field => { const fieldString = generateFieldAsString(field); @@ -136,6 +167,7 @@ function useWidgetBuilderState(): { if (action.payload === DisplayType.TABLE) { setYAxis([]); setFields([...columns, ...aggregates, ...(yAxis ?? [])]); + setLegendAlias([]); } else { setFields(columns); setYAxis([ @@ -185,6 +217,9 @@ function useWidgetBuilderState(): { case BuilderStateAction.SET_LIMIT: setLimit(action.payload); break; + case BuilderStateAction.SET_LEGEND_ALIAS: + setLegendAlias(action.payload); + break; default: break; } @@ -199,6 +234,7 @@ function useWidgetBuilderState(): { setQuery, setSort, setLimit, + setLegendAlias, fields, yAxis, displayType, diff --git a/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.spec.tsx b/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.spec.tsx index cc0a23cbbc252e..341650cd75c229 100644 --- a/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.spec.tsx @@ -85,4 +85,18 @@ describe('convertBuilderStateToWidget', function () { expect(widget.queries[0].fieldAliases).toEqual(['test', '', 'another one']); }); + + it('adds legend aliases to the widget queries', function () { + const mockState: WidgetBuilderState = { + legendAlias: ['test', 'test2'], + query: ['transaction.duration:>100', 'transaction.duration:>50'], + }; + + const widget = convertBuilderStateToWidget(mockState); + + expect(widget.queries[0].name).toEqual('test'); + expect(widget.queries[0].conditions).toEqual('transaction.duration:>100'); + expect(widget.queries[1].name).toEqual('test2'); + expect(widget.queries[1].conditions).toEqual('transaction.duration:>50'); + }); }); diff --git a/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.ts b/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.ts index beec3f6f10b3cd..7009d472a6f423 100644 --- a/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.ts +++ b/static/app/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget.ts @@ -16,6 +16,8 @@ export function convertBuilderStateToWidget(state: WidgetBuilderState): Widget { const defaultQuery = datasetConfig.defaultWidgetQuery; const queries = defined(state.query) && state.query.length > 0 ? state.query : ['']; + const legendAlias = + defined(state.legendAlias) && state.legendAlias.length > 0 ? state.legendAlias : []; const fields = state.fields?.map(generateFieldAsString); const fieldAliases = state.fields?.map(field => field.alias ?? ''); @@ -40,7 +42,7 @@ export function convertBuilderStateToWidget(state: WidgetBuilderState): Widget { ? _formatSort(state.sort[0]) : defaultSort; - const widgetQueries: WidgetQuery[] = queries.map(query => { + const widgetQueries: WidgetQuery[] = queries.map((query, index) => { return { ...defaultQuery, fields, @@ -49,6 +51,7 @@ export function convertBuilderStateToWidget(state: WidgetBuilderState): Widget { conditions: query, orderby: sort, fieldAliases: fieldAliases ?? [], + name: legendAlias[index] ?? '', }; }); diff --git a/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.spec.tsx b/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.spec.tsx index 76230d5f186edb..6ed3c133f6a137 100644 --- a/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.spec.tsx @@ -1,4 +1,4 @@ -import {WidgetType} from 'sentry/views/dashboards/types'; +import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import {convertWidgetToBuilderStateParams} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams'; import {getDefaultWidget} from 'sentry/views/dashboards/widgetBuilder/utils/getDefaultWidget'; @@ -28,4 +28,29 @@ describe('convertWidgetToBuilderStateParams', () => { const params = convertWidgetToBuilderStateParams(widget); expect(params.field).toEqual(['{"field":"geo.country","alias":"test"}']); }); + + it('adds legend aliases to the builder params on charts', () => { + const widget = { + ...getDefaultWidget(WidgetType.ERRORS), + displayType: DisplayType.AREA, + queries: [ + { + aggregates: [], + columns: [], + conditions: 'transaction.duration:>100', + orderby: '', + name: 'test', + }, + { + aggregates: [], + columns: [], + conditions: 'transaction.duration:>50', + orderby: '', + name: 'test2', + }, + ], + }; + const params = convertWidgetToBuilderStateParams(widget); + expect(params.legendAlias).toEqual(['test', 'test2']); + }); }); diff --git a/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.ts b/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.ts index 8f7b57b309f137..719f50ab94501a 100644 --- a/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.ts +++ b/static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.ts @@ -30,6 +30,7 @@ export function convertWidgetToBuilderStateParams( let yAxis = widget.queries.flatMap(q => q.aggregates); const query = widget.queries.flatMap(q => q.conditions); const sort = widget.queries.flatMap(q => q.orderby); + let legendAlias = widget.queries.flatMap(q => q.name); let field: string[] = []; if ( @@ -38,6 +39,7 @@ export function convertWidgetToBuilderStateParams( ) { field = widget.queries.flatMap(widgetQuery => stringifyFields(widgetQuery, 'fields')); yAxis = []; + legendAlias = []; } else { field = widget.queries.flatMap(widgetQuery => stringifyFields(widgetQuery, 'columns') @@ -54,5 +56,6 @@ export function convertWidgetToBuilderStateParams( yAxis, query, sort, + legendAlias, }; } From bace0d5dfd18749125f7695e998fc2027710d1c2 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 23 Dec 2024 13:50:42 -0800 Subject: [PATCH 476/757] fix(issues): Prevent tag drawer overflow w/ long tags (#82545) --- static/app/components/events/eventDrawer.tsx | 3 +++ .../app/views/issueDetails/groupTags/groupTagsDrawer.tsx | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/static/app/components/events/eventDrawer.tsx b/static/app/components/events/eventDrawer.tsx index 59dd58926a59ec..fb26d789b2c55f 100644 --- a/static/app/components/events/eventDrawer.tsx +++ b/static/app/components/events/eventDrawer.tsx @@ -47,6 +47,9 @@ export const EventDrawerHeader = styled(DrawerHeader)` max-height: ${MIN_NAV_HEIGHT}px; box-shadow: none; border-bottom: 1px solid ${p => p.theme.border}; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; `; export const EventNavigator = styled('div')` diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 38dcb40b115316..1c7d9dcef4b5a6 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -12,7 +12,6 @@ import { EventDrawerContainer, EventDrawerHeader, EventNavigator, - Header, NavigationCrumbs, SearchInput, ShortId, @@ -211,3 +210,10 @@ const Container = styled('div')` gap: ${space(2)}; margin-bottom: ${space(2)}; `; + +const Header = styled('h3')` + ${p => p.theme.overflowEllipsis}; + font-size: ${p => p.theme.fontSizeExtraLarge}; + font-weight: ${p => p.theme.fontWeightBold}; + margin: 0; +`; From 48164d4c97f3560f6e8fd2c70bddf4f85d7bafc1 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 23 Dec 2024 13:56:35 -0800 Subject: [PATCH 477/757] feat(devservices): Only display deprecation warning message if `USE_NEW_DEVSERVICES` is not set (#82487) If people are setting `USE_NEW_DEVSERVICES`, they already know that `sentry devservices` will be deprecated soon. --- src/sentry/runner/commands/devservices.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/sentry/runner/commands/devservices.py b/src/sentry/runner/commands/devservices.py index 8d30d52dcd3908..d4d2d7dfca3f62 100644 --- a/src/sentry/runner/commands/devservices.py +++ b/src/sentry/runner/commands/devservices.py @@ -303,8 +303,9 @@ def up( """ from sentry.runner import configure - click.secho( - """ + if os.environ.get("USE_NEW_DEVSERVICES", "0") != "1": + click.secho( + """ WARNING: We're transitioning from `sentry devservices` to the new and improved `devservices` in January 2025. To give the new devservices a try, set the `USE_NEW_DEVSERVICES` environment variable to `1`. For a full list of commands, see https://github.com/getsentry/devservices?tab=readme-ov-file#commands @@ -314,8 +315,8 @@ def up( Thanks for helping the Dev Infra team improve this experience! """, - fg="yellow", - ) + fg="yellow", + ) configure() @@ -534,8 +535,9 @@ def down(project: str, service: list[str]) -> None: an explicit list of services to bring down. """ - click.secho( - """ + if os.environ.get("USE_NEW_DEVSERVICES", "0") != "1": + click.secho( + """ WARNING: We're transitioning from `sentry devservices` to the new and improved `devservices` in January 2025. To give the new devservices a try, set the `USE_NEW_DEVSERVICES` environment variable to `1`. For a full list of commands, see https://github.com/getsentry/devservices?tab=readme-ov-file#commands @@ -544,9 +546,9 @@ def down(project: str, service: list[str]) -> None: For Sentry employees - if you hit any bumps or have feedback, we'd love to hear from you in #discuss-dev-infra. Thanks for helping the Dev Infra team improve this experience! - """, - fg="yellow", - ) + """, + fg="yellow", + ) def _down(container: docker.models.containers.Container) -> None: click.secho(f"> Stopping '{container.name}' container", fg="red") From 8d4c5bfdeef90f56a491adf6c08202dcd6ff23be Mon Sep 17 00:00:00 2001 From: Lyn Nagara <1779792+lynnagara@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:15:59 -0800 Subject: [PATCH 478/757] ref: cleanup unused queue name (#82510) this now uses split queues and the default is defined here https://github.com/getsentry/sentry/blob/32371819967b9ca39f10aefcfb0da086c80e4141/src/sentry/conf/server.py#L858 remove this line that does nothing except cause noise in logs --- src/sentry/profiles/task.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry/profiles/task.py b/src/sentry/profiles/task.py index bbea532d41ddfc..60debead1d3095 100644 --- a/src/sentry/profiles/task.py +++ b/src/sentry/profiles/task.py @@ -43,7 +43,6 @@ @instrumented_task( name="sentry.profiles.task.process_profile", - queue="profiles.process", retry_backoff=True, retry_backoff_max=20, retry_jitter=True, From 153d109cbcf8551923005a4ff2e2b2b7dd7423e0 Mon Sep 17 00:00:00 2001 From: Lyn Nagara <1779792+lynnagara@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:20:01 -0800 Subject: [PATCH 479/757] support stale topic for ingest-events (#82549) similarly to transactions, will get a dedicated lowpri topic later, for now testing by sending stale events to ingest-events-dlq --- src/sentry/consumers/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/consumers/__init__.py b/src/sentry/consumers/__init__.py index 6076fc5c35e12a..e23994e7624859 100644 --- a/src/sentry/consumers/__init__.py +++ b/src/sentry/consumers/__init__.py @@ -347,6 +347,7 @@ def ingest_transactions_options() -> list[click.Option]: "consumer_type": ConsumerType.Events, }, "dlq_topic": Topic.INGEST_EVENTS_DLQ, + "stale_topic": Topic.INGEST_EVENTS_DLQ, }, "ingest-feedback-events": { "topic": Topic.INGEST_FEEDBACK_EVENTS, From de6d8449e7ed71639b3d07d490379630a020baca Mon Sep 17 00:00:00 2001 From: Lyn Nagara <1779792+lynnagara@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:23:29 -0800 Subject: [PATCH 480/757] ref: remove dlq limits (#82552) we no longer impose any limits on how many messages can be dlq'ed on any other consumers, remove it from `ingest-metrics`, which was the last remaining one --- src/sentry/consumers/__init__.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/sentry/consumers/__init__.py b/src/sentry/consumers/__init__.py index e23994e7624859..cbd369fcc298fc 100644 --- a/src/sentry/consumers/__init__.py +++ b/src/sentry/consumers/__init__.py @@ -9,7 +9,7 @@ from arroyo.backends.kafka.configuration import build_kafka_consumer_configuration from arroyo.backends.kafka.consumer import KafkaConsumer from arroyo.commit import ONCE_PER_SECOND -from arroyo.dlq import DlqLimit, DlqPolicy +from arroyo.dlq import DlqPolicy from arroyo.processing.processor import StreamProcessor from arroyo.processing.strategies import Healthcheck from arroyo.processing.strategies.abstract import ProcessingStrategy, ProcessingStrategyFactory @@ -382,8 +382,6 @@ def ingest_transactions_options() -> list[click.Option]: "ingest_profile": "release-health", }, "dlq_topic": Topic.INGEST_METRICS_DLQ, - "dlq_max_invalid_ratio": 0.01, - "dlq_max_consecutive_count": 1000, }, "ingest-generic-metrics": { "topic": Topic.INGEST_PERFORMANCE_METRICS, @@ -393,8 +391,6 @@ def ingest_transactions_options() -> list[click.Option]: "ingest_profile": "performance", }, "dlq_topic": Topic.INGEST_GENERIC_METRICS_DLQ, - "dlq_max_invalid_ratio": None, - "dlq_max_consecutive_count": None, }, "generic-metrics-last-seen-updater": { "topic": Topic.SNUBA_GENERIC_METRICS, @@ -607,10 +603,7 @@ def build_consumer_config(group_id: str): if dlq_producer: dlq_policy = DlqPolicy( dlq_producer, - DlqLimit( - max_invalid_ratio=consumer_definition.get("dlq_max_invalid_ratio"), - max_consecutive_count=consumer_definition.get("dlq_max_consecutive_count"), - ), + None, None, ) From f5a26073e5120d288f8c7bcf794545ddc3b57bd2 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Thu, 26 Dec 2024 09:35:58 -0500 Subject: [PATCH 481/757] ref: remove unused MetricsKeyIndexer model (#82405) last reference removed in 9d11561b99c8c4e35347e9d2c5e208d2cfd41822 --- .../backup/model_dependencies/detailed.json | 18 +--------- fixtures/backup/model_dependencies/flat.json | 3 +- .../backup/model_dependencies/sorted.json | 3 +- .../backup/model_dependencies/truncate.json | 3 +- migrations_lockfile.txt | 2 +- ...803_delete_unused_metricskeyindexer_pt1.py | 29 ++++++++++++++++ .../sentry_metrics/indexer/postgres/models.py | 33 ++----------------- .../test_default_comparators.pysnap | 4 --- .../incidents/test_subscription_processor.py | 2 -- 9 files changed, 36 insertions(+), 61 deletions(-) create mode 100644 src/sentry/migrations/0803_delete_unused_metricskeyindexer_pt1.py diff --git a/fixtures/backup/model_dependencies/detailed.json b/fixtures/backup/model_dependencies/detailed.json index 320f0179a46a90..fa37af84c3316f 100644 --- a/fixtures/backup/model_dependencies/detailed.json +++ b/fixtures/backup/model_dependencies/detailed.json @@ -3223,22 +3223,6 @@ ] ] }, - "sentry.metricskeyindexer": { - "dangling": false, - "foreign_keys": {}, - "model": "sentry.metricskeyindexer", - "relocation_dependencies": [], - "relocation_scope": "Excluded", - "silos": [ - "Region" - ], - "table_name": "sentry_metricskeyindexer", - "uniques": [ - [ - "string" - ] - ] - }, "sentry.monitor": { "dangling": false, "foreign_keys": { @@ -6739,4 +6723,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/fixtures/backup/model_dependencies/flat.json b/fixtures/backup/model_dependencies/flat.json index 58f74b82415750..dbab6290805ea0 100644 --- a/fixtures/backup/model_dependencies/flat.json +++ b/fixtures/backup/model_dependencies/flat.json @@ -447,7 +447,6 @@ "sentry.lostpasswordhash": [ "sentry.user" ], - "sentry.metricskeyindexer": [], "sentry.monitor": [ "sentry.organization", "sentry.project", @@ -931,4 +930,4 @@ "workflow_engine.dataconditiongroup", "workflow_engine.workflow" ] -} \ No newline at end of file +} diff --git a/fixtures/backup/model_dependencies/sorted.json b/fixtures/backup/model_dependencies/sorted.json index bbecc9845fc647..dd666a1a710ca4 100644 --- a/fixtures/backup/model_dependencies/sorted.json +++ b/fixtures/backup/model_dependencies/sorted.json @@ -18,7 +18,6 @@ "sentry.identityprovider", "sentry.integration", "sentry.integrationfeature", - "sentry.metricskeyindexer", "sentry.monitorlocation", "sentry.option", "sentry.organization", @@ -248,4 +247,4 @@ "sentry.incidentsnapshot", "sentry.incidentproject", "sentry.incidentactivity" -] \ No newline at end of file +] diff --git a/fixtures/backup/model_dependencies/truncate.json b/fixtures/backup/model_dependencies/truncate.json index a147528b4d2456..415a6f5e10ec8b 100644 --- a/fixtures/backup/model_dependencies/truncate.json +++ b/fixtures/backup/model_dependencies/truncate.json @@ -18,7 +18,6 @@ "sentry_identityprovider", "sentry_integration", "sentry_integrationfeature", - "sentry_metricskeyindexer", "sentry_monitorlocation", "sentry_option", "sentry_organization", @@ -248,4 +247,4 @@ "sentry_incidentsnapshot", "sentry_incidentproject", "sentry_incidentactivity" -] \ No newline at end of file +] diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 0ec7da63fda40d..68a5f78f741f7f 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -15,7 +15,7 @@ remote_subscriptions: 0003_drop_remote_subscription replays: 0004_index_together -sentry: 0802_remove_grouping_auto_update_option +sentry: 0803_delete_unused_metricskeyindexer_pt1 social_auth: 0002_default_auto_field diff --git a/src/sentry/migrations/0803_delete_unused_metricskeyindexer_pt1.py b/src/sentry/migrations/0803_delete_unused_metricskeyindexer_pt1.py new file mode 100644 index 00000000000000..e468f091384ec2 --- /dev/null +++ b/src/sentry/migrations/0803_delete_unused_metricskeyindexer_pt1.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.4 on 2024-12-19 20:24 + +from sentry.new_migrations.migrations import CheckedMigration +from sentry.new_migrations.monkey.models import SafeDeleteModel +from sentry.new_migrations.monkey.state import DeletionAction + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("sentry", "0802_remove_grouping_auto_update_option"), + ] + + operations = [ + SafeDeleteModel(name="MetricsKeyIndexer", deletion_action=DeletionAction.MOVE_TO_PENDING), + ] diff --git a/src/sentry/sentry_metrics/indexer/postgres/models.py b/src/sentry/sentry_metrics/indexer/postgres/models.py index 7575951f74c3a5..142af00288e919 100644 --- a/src/sentry/sentry_metrics/indexer/postgres/models.py +++ b/src/sentry/sentry_metrics/indexer/postgres/models.py @@ -1,8 +1,8 @@ import logging -from typing import Any, ClassVar, Self +from typing import ClassVar, Self from django.conf import settings -from django.db import connections, models, router +from django.db import models from django.utils import timezone from sentry.backup.scopes import RelocationScope @@ -16,35 +16,6 @@ from collections.abc import Mapping -@region_silo_model -class MetricsKeyIndexer(Model): - __relocation_scope__ = RelocationScope.Excluded - - string = models.CharField(max_length=200) - date_added = models.DateTimeField(default=timezone.now) - - objects: ClassVar[BaseManager[Self]] = BaseManager( - cache_fields=("pk", "string"), cache_ttl=settings.SENTRY_METRICS_INDEXER_CACHE_TTL - ) - - class Meta: - db_table = "sentry_metricskeyindexer" - app_label = "sentry" - constraints = [ - models.UniqueConstraint(fields=["string"], name="unique_string"), - ] - - @classmethod - def get_next_values(cls, num: int) -> Any: - using = router.db_for_write(cls) - connection = connections[using].cursor() - - connection.execute( - "SELECT nextval('sentry_metricskeyindexer_id_seq') from generate_series(1,%s)", [num] - ) - return connection.fetchall() - - class BaseIndexer(Model): string = models.CharField(max_length=MAX_INDEXED_COLUMN_LENGTH) organization_id = BoundedBigIntegerField() diff --git a/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap b/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap index e99d9a2677316a..cdcc5a4d6b104e 100644 --- a/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap +++ b/tests/sentry/backup/snapshots/test_comparators/test_default_comparators.pysnap @@ -808,10 +808,6 @@ source: tests/sentry/backup/test_comparators.py fields: - user model_name: sentry.lostpasswordhash -- comparators: - - class: ForeignKeyComparator - fields: [] - model_name: sentry.metricskeyindexer - comparators: - class: UUID4Comparator fields: diff --git a/tests/sentry/incidents/test_subscription_processor.py b/tests/sentry/incidents/test_subscription_processor.py index 8b9793d1d45a21..d7ff092b15028b 100644 --- a/tests/sentry/incidents/test_subscription_processor.py +++ b/tests/sentry/incidents/test_subscription_processor.py @@ -65,7 +65,6 @@ ) from sentry.seer.anomaly_detection.utils import has_anomaly, translate_direction from sentry.sentry_metrics.configuration import UseCaseKey -from sentry.sentry_metrics.indexer.postgres.models import MetricsKeyIndexer from sentry.sentry_metrics.utils import resolve_tag_key from sentry.snuba.dataset import Dataset from sentry.snuba.models import QuerySubscription, SnubaQuery, SnubaQueryEventType @@ -3715,7 +3714,6 @@ def test_multiple_threshold_resolve_is_reset_when_count_is_lower_than_min_thresh def test_ensure_case_when_no_metrics_index_not_found_is_handled_gracefully( self, helper_metrics ): - MetricsKeyIndexer.objects.all().delete() rule = self.crash_rate_alert_rule subscription = rule.snuba_query.subscriptions.filter(project=self.project).get() processor = SubscriptionProcessor(subscription) From af4c71a0eaf3a56417a8f1975252277fdb640180 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Thu, 26 Dec 2024 09:42:02 -0500 Subject: [PATCH 482/757] ref: delete some dead code in metrics / sentry_metrics (#82407) candidates for deletion were found automatically utilizing [dead](https://github.com/asottile/dead) commits are split by deleted function (with references to when they were last referenced) --- .../endpoints/organization_metrics_meta.py | 1 - src/sentry/integrations/slack/metrics.py | 22 -- src/sentry/search/events/builder/metrics.py | 4 - src/sentry/search/events/datasets/metrics.py | 5 - .../search/events/datasets/spans_metrics.py | 278 +----------------- src/sentry/sentry_metrics/configuration.py | 22 -- .../sentry_metrics/consumers/indexer/batch.py | 23 +- .../consumers/indexer/common.py | 3 - .../sentry_metrics/indexer/id_generator.py | 54 ---- .../indexer/postgres/postgres_v2.py | 1 - .../sentry_metrics/querying/constants.py | 11 - .../querying/data/mapping/base.py | 5 +- .../querying/data/transformation/stats.py | 48 --- src/sentry/sentry_metrics/querying/errors.py | 4 - .../sentry_metrics/querying/metadata/utils.py | 3 - src/sentry/sentry_metrics/querying/utils.py | 10 - src/sentry/snuba/metrics/datasource.py | 220 +------------- src/sentry/snuba/metrics/extraction.py | 7 - src/sentry/snuba/metrics/fields/snql.py | 8 - .../snuba/metrics/naming_layer/mapping.py | 6 - src/sentry/snuba/metrics/query_builder.py | 6 - src/sentry/snuba/metrics/utils.py | 3 - src/sentry/tasks/on_demand_metrics.py | 4 - src/sentry/utils/batching_kafka_consumer.py | 3 - tests/sentry/sentry_metrics/test_batch.py | 109 ------- .../sentry_metrics/test_id_generator.py | 37 --- tests/sentry/snuba/metrics/test_datasource.py | 60 ---- tests/sentry/snuba/metrics/test_extraction.py | 9 - tests/sentry/tasks/test_on_demand_metrics.py | 2 - 29 files changed, 16 insertions(+), 952 deletions(-) delete mode 100644 src/sentry/sentry_metrics/indexer/id_generator.py delete mode 100644 src/sentry/sentry_metrics/querying/data/transformation/stats.py delete mode 100644 tests/sentry/sentry_metrics/test_id_generator.py diff --git a/src/sentry/api/endpoints/organization_metrics_meta.py b/src/sentry/api/endpoints/organization_metrics_meta.py index d31a96d9dc2a03..42f7ac530d692e 100644 --- a/src/sentry/api/endpoints/organization_metrics_meta.py +++ b/src/sentry/api/endpoints/organization_metrics_meta.py @@ -11,7 +11,6 @@ from sentry.snuba import metrics_performance COUNT_UNPARAM = "count_unparameterized_transactions()" -COUNT_HAS_TXN = "count_has_transaction_name()" COUNT_NULL = "count_null_transactions()" diff --git a/src/sentry/integrations/slack/metrics.py b/src/sentry/integrations/slack/metrics.py index 1174d265489b69..fe5c52da59e973 100644 --- a/src/sentry/integrations/slack/metrics.py +++ b/src/sentry/integrations/slack/metrics.py @@ -17,28 +17,6 @@ SLACK_NOTIFY_RECIPIENT_SUCCESS_DATADOG_METRIC = "sentry.integrations.slack.notify_recipient.success" SLACK_NOTIFY_RECIPIENT_FAILURE_DATADOG_METRIC = "sentry.integrations.slack.notify_recipient.failure" -# Bot commands -SLACK_BOT_COMMAND_LINK_IDENTITY_SUCCESS_DATADOG_METRIC = ( - "sentry.integrations.slack.link_identity_view.success" -) -SLACK_BOT_COMMAND_LINK_IDENTITY_FAILURE_DATADOG_METRIC = ( - "sentry.integrations.slack.link_identity_view.failure" -) -SLACK_BOT_COMMAND_UNLINK_IDENTITY_SUCCESS_DATADOG_METRIC = ( - "sentry.integrations.slack.unlink_identity_view.success" -) -SLACK_BOT_COMMAND_UNLINK_IDENTITY_FAILURE_DATADOG_METRIC = ( - "sentry.integrations.slack.unlink_identity_view.failure" -) -SLACK_BOT_COMMAND_UNLINK_TEAM_SUCCESS_DATADOG_METRIC = ( - "sentry.integrations.slack.unlink_team.success" -) -SLACK_BOT_COMMAND_UNLINK_TEAM_FAILURE_DATADOG_METRIC = ( - "sentry.integrations.slack.unlink_team.failure" -) -SLACK_BOT_COMMAND_LINK_TEAM_SUCCESS_DATADOG_METRIC = "sentry.integrations.slack.link_team.success" -SLACK_BOT_COMMAND_LINK_TEAM_FAILURE_DATADOG_METRIC = "sentry.integrations.slack.link_team.failure" - # Webhooks SLACK_WEBHOOK_DM_ENDPOINT_SUCCESS_DATADOG_METRIC = "sentry.integrations.slack.dm_endpoint.success" SLACK_WEBHOOK_DM_ENDPOINT_FAILURE_DATADOG_METRIC = "sentry.integrations.slack.dm_endpoint.failure" diff --git a/src/sentry/search/events/builder/metrics.py b/src/sentry/search/events/builder/metrics.py index d57893ae33b027..46c84384d8bcb8 100644 --- a/src/sentry/search/events/builder/metrics.py +++ b/src/sentry/search/events/builder/metrics.py @@ -1953,10 +1953,6 @@ def __init__( [column for column in self.columns if column not in self.aggregates] ) - @cached_property - def non_aggregate_columns(self) -> list[str]: - return list(set(self.original_selected_columns) - set(self.timeseries_columns)) - @property def translated_groupby(self) -> list[str]: """Get the names of the groupby columns to create the series names""" diff --git a/src/sentry/search/events/datasets/metrics.py b/src/sentry/search/events/datasets/metrics.py index 54e02d63f41e86..f071a2a041cd21 100644 --- a/src/sentry/search/events/datasets/metrics.py +++ b/src/sentry/search/events/datasets/metrics.py @@ -80,11 +80,6 @@ def resolve_metric(self, value: str) -> int: self.builder.metric_ids.add(metric_id) return metric_id - def resolve_value(self, value: str) -> int: - value_id = self.builder.resolve_tag_value(value) - - return value_id - @property def should_skip_interval_calculation(self): return self.builder.builder_config.skip_time_conditions and ( diff --git a/src/sentry/search/events/datasets/spans_metrics.py b/src/sentry/search/events/datasets/spans_metrics.py index 358dab0107a887..5713605e3b93d0 100644 --- a/src/sentry/search/events/datasets/spans_metrics.py +++ b/src/sentry/search/events/datasets/spans_metrics.py @@ -5,7 +5,7 @@ from typing import TypedDict import sentry_sdk -from snuba_sdk import AliasedExpression, Column, Condition, Function, Identifier, Op, OrderBy +from snuba_sdk import Column, Condition, Function, Identifier, Op, OrderBy from sentry.api.event_search import SearchFilter from sentry.exceptions import IncompatibleMetricsQuery, InvalidSearchQuery @@ -16,7 +16,6 @@ from sentry.search.events.fields import SnQLStringArg, get_function_alias from sentry.search.events.types import SelectType, WhereType from sentry.search.utils import DEVICE_CLASS -from sentry.snuba.metrics.naming_layer.mri import SpanMRI from sentry.snuba.referrer import Referrer @@ -1362,278 +1361,3 @@ def _resolve_trace_error_count( @property def orderby_converter(self) -> Mapping[str, OrderBy]: return {} - - -class SpansMetricsLayerDatasetConfig(DatasetConfig): - missing_function_error = IncompatibleMetricsQuery - - def __init__(self, builder: spans_metrics.SpansMetricsQueryBuilder): - self.builder = builder - self.total_span_duration: float | None = None - - def resolve_mri(self, value: str) -> Column: - """Given the public facing column name resolve it to the MRI and return a Column""" - # If the query builder has not detected a transaction use the light self time metric to get a performance boost - if value == "span.self_time" and not self.builder.has_transaction: - return Column(constants.SELF_TIME_LIGHT) - else: - return Column(constants.SPAN_METRICS_MAP[value]) - - @property - def search_filter_converter( - self, - ) -> Mapping[str, Callable[[SearchFilter], WhereType | None]]: - return {} - - @property - def field_alias_converter(self) -> Mapping[str, Callable[[str], SelectType]]: - return { - constants.SPAN_MODULE_ALIAS: lambda alias: field_aliases.resolve_span_module( - self.builder, alias - ) - } - - @property - def function_converter(self) -> Mapping[str, fields.MetricsFunction]: - """Make sure to update METRIC_FUNCTION_LIST_BY_TYPE when adding functions here, can't be a dynamic list since - the Metric Layer will actually handle which dataset each function goes to - """ - - function_converter = { - function.name: function - for function in [ - fields.MetricsFunction( - "count_unique", - required_args=[ - fields.MetricArg( - "column", - allowed_columns=["user"], - allow_custom_measurements=False, - ) - ], - snql_metric_layer=lambda args, alias: Function( - "count_unique", - [self.resolve_mri("user")], - alias, - ), - default_result_type="integer", - ), - fields.MetricsFunction( - "epm", - snql_metric_layer=lambda args, alias: Function( - "rate", - [ - self.resolve_mri("span.self_time"), - args["interval"], - 60, - ], - alias, - ), - optional_args=[fields.IntervalDefault("interval", 1, None)], - default_result_type="rate", - ), - fields.MetricsFunction( - "eps", - snql_metric_layer=lambda args, alias: Function( - "rate", - [ - self.resolve_mri("span.self_time"), - args["interval"], - 1, - ], - alias, - ), - optional_args=[fields.IntervalDefault("interval", 1, None)], - default_result_type="rate", - ), - fields.MetricsFunction( - "count", - snql_metric_layer=lambda args, alias: Function( - "count", - [ - self.resolve_mri("span.self_time"), - ], - alias, - ), - default_result_type="integer", - ), - fields.MetricsFunction( - "sum", - optional_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", - allowed_columns=constants.SPAN_METRIC_SUMMABLE_COLUMNS, - allow_custom_measurements=False, - ), - ), - ], - snql_metric_layer=lambda args, alias: Function( - "sum", - [self.resolve_mri(args["column"])], - alias, - ), - default_result_type="duration", - ), - fields.MetricsFunction( - "avg", - optional_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", - allowed_columns=constants.SPAN_METRIC_DURATION_COLUMNS.union( - constants.SPAN_METRIC_BYTES_COLUMNS - ), - ), - ), - ], - snql_metric_layer=lambda args, alias: Function( - "avg", - [self.resolve_mri(args["column"])], - alias, - ), - result_type_fn=self.reflective_result_type(), - default_result_type="duration", - ), - fields.MetricsFunction( - "percentile", - required_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", allowed_columns=constants.SPAN_METRIC_DURATION_COLUMNS - ), - ), - fields.NumberRange("percentile", 0, 1), - ], - snql_metric_layer=lambda args, alias: function_aliases.resolve_metrics_layer_percentile( - args, - alias, - self.resolve_mri, - ), - result_type_fn=self.reflective_result_type(), - default_result_type="duration", - ), - fields.MetricsFunction( - "p50", - optional_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", - allowed_columns=constants.SPAN_METRIC_DURATION_COLUMNS, - allow_custom_measurements=False, - ), - ), - ], - snql_metric_layer=lambda args, alias: function_aliases.resolve_metrics_layer_percentile( - args=args, alias=alias, resolve_mri=self.resolve_mri, fixed_percentile=0.50 - ), - default_result_type="duration", - ), - fields.MetricsFunction( - "p75", - optional_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", - allowed_columns=constants.SPAN_METRIC_DURATION_COLUMNS, - allow_custom_measurements=False, - ), - ), - ], - snql_metric_layer=lambda args, alias: function_aliases.resolve_metrics_layer_percentile( - args=args, alias=alias, resolve_mri=self.resolve_mri, fixed_percentile=0.75 - ), - default_result_type="duration", - ), - fields.MetricsFunction( - "p95", - optional_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", - allowed_columns=constants.SPAN_METRIC_DURATION_COLUMNS, - allow_custom_measurements=False, - ), - ), - ], - snql_metric_layer=lambda args, alias: function_aliases.resolve_metrics_layer_percentile( - args=args, alias=alias, resolve_mri=self.resolve_mri, fixed_percentile=0.95 - ), - default_result_type="duration", - ), - fields.MetricsFunction( - "p99", - optional_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", - allowed_columns=constants.SPAN_METRIC_DURATION_COLUMNS, - allow_custom_measurements=False, - ), - ), - ], - snql_metric_layer=lambda args, alias: function_aliases.resolve_metrics_layer_percentile( - args=args, alias=alias, resolve_mri=self.resolve_mri, fixed_percentile=0.99 - ), - default_result_type="duration", - ), - fields.MetricsFunction( - "p100", - optional_args=[ - fields.with_default( - "span.self_time", - fields.MetricArg( - "column", - allowed_columns=constants.SPAN_METRIC_DURATION_COLUMNS, - allow_custom_measurements=False, - ), - ), - ], - snql_metric_layer=lambda args, alias: function_aliases.resolve_metrics_layer_percentile( - args=args, alias=alias, resolve_mri=self.resolve_mri, fixed_percentile=1.0 - ), - default_result_type="duration", - ), - fields.MetricsFunction( - "http_error_count", - snql_metric_layer=lambda args, alias: AliasedExpression( - Column( - SpanMRI.HTTP_ERROR_COUNT_LIGHT.value - if not self.builder.has_transaction - else SpanMRI.HTTP_ERROR_COUNT.value - ), - alias, - ), - default_result_type="integer", - ), - fields.MetricsFunction( - "http_error_rate", - snql_metric_layer=lambda args, alias: AliasedExpression( - Column( - SpanMRI.HTTP_ERROR_RATE_LIGHT.value - if not self.builder.has_transaction - else SpanMRI.HTTP_ERROR_RATE.value - ), - alias, - ), - default_result_type="percentage", - ), - ] - } - - for alias, name in constants.SPAN_FUNCTION_ALIASES.items(): - if name in function_converter: - function_converter[alias] = function_converter[name].alias_as(alias) - - return function_converter - - @property - def orderby_converter(self) -> Mapping[str, OrderBy]: - return {} diff --git a/src/sentry/sentry_metrics/configuration.py b/src/sentry/sentry_metrics/configuration.py index 0f812f29362341..8c8b63752f26f5 100644 --- a/src/sentry/sentry_metrics/configuration.py +++ b/src/sentry/sentry_metrics/configuration.py @@ -27,8 +27,6 @@ class UseCaseKey(Enum): # backwards compatibility RELEASE_HEALTH_PG_NAMESPACE = "releasehealth" PERFORMANCE_PG_NAMESPACE = "performance" -RELEASE_HEALTH_CS_NAMESPACE = "releasehealth.cs" -PERFORMANCE_CS_NAMESPACE = "performance.cs" RELEASE_HEALTH_SCHEMA_VALIDATION_RULES_OPTION_NAME = ( "sentry-metrics.indexer.release-health.schema-validation-rules" @@ -172,23 +170,3 @@ def initialize_main_process_state(config: MetricsIngestConfiguration) -> None: global_tag_map = {"pipeline": config.internal_metrics_tag or ""} add_global_tags(_all_threads=True, **global_tag_map) - - -HARD_CODED_UNITS = {"span.duration": "millisecond"} -ALLOWED_TYPES = {"c", "d", "s", "g"} - -# METRICS_AGGREGATES specifies the aggregates that are available for a metric type - AGGREGATES_TO_METRICS reverses this, -# and provides a map from the aggregate to the metric type in the form {'count': 'c', 'avg':'g', ...}. This is needed -# when the UI lets the user select the aggregate, and the backend infers the metric_type from it. It is programmatic -# and not hard-coded, so that in case of a change, the two mappings are aligned. -METRIC_TYPE_TO_AGGREGATE = { - "c": ["count"], - "g": ["avg", "min", "max", "sum"], - "d": ["p50", "p75", "p90", "p95", "p99"], - "s": ["count_unique"], -} -AGGREGATE_TO_METRIC_TYPE = { - aggregate: metric_type - for metric_type, aggregate_list in METRIC_TYPE_TO_AGGREGATE.items() - for aggregate in aggregate_list -} diff --git a/src/sentry/sentry_metrics/consumers/indexer/batch.py b/src/sentry/sentry_metrics/consumers/indexer/batch.py index ef52a8ffe1b44b..707a0a643ab291 100644 --- a/src/sentry/sentry_metrics/consumers/indexer/batch.py +++ b/src/sentry/sentry_metrics/consumers/indexer/batch.py @@ -1,7 +1,7 @@ import logging import random from collections import defaultdict -from collections.abc import Callable, Iterable, Mapping, MutableMapping, MutableSequence, Sequence +from collections.abc import Callable, Iterable, Mapping, MutableMapping, MutableSequence from dataclasses import dataclass from typing import Any, cast @@ -248,27 +248,6 @@ def _validate_message(self, parsed_payload: ParsedMessage) -> None: ) raise ValueError(f"Invalid metric tags: {tags}") - @metrics.wraps("process_messages.filter_messages") - def filter_messages(self, keys_to_remove: Sequence[BrokerMeta]) -> None: - # XXX: it is useful to be able to get a sample of organization ids that are affected by rate limits, but this is really slow. - for broker_meta in keys_to_remove: - if _should_sample_debug_log(): - sentry_sdk.set_tag( - "sentry_metrics.organization_id", - self.parsed_payloads_by_meta[broker_meta]["org_id"], - ) - sentry_sdk.set_tag( - "sentry_metrics.metric_name", self.parsed_payloads_by_meta[broker_meta]["name"] - ) - logger.error( - "process_messages.dropped_message", - extra={ - "reason": "cardinality_limit", - }, - ) - - self.filtered_msg_meta.update(keys_to_remove) - @metrics.wraps("process_messages.extract_strings") def extract_strings(self) -> Mapping[UseCaseID, Mapping[OrgId, set[str]]]: strings: Mapping[UseCaseID, Mapping[OrgId, set[str]]] = defaultdict( diff --git a/src/sentry/sentry_metrics/consumers/indexer/common.py b/src/sentry/sentry_metrics/consumers/indexer/common.py index 9b0a18c8281fb9..54fbfdf066520d 100644 --- a/src/sentry/sentry_metrics/consumers/indexer/common.py +++ b/src/sentry/sentry_metrics/consumers/indexer/common.py @@ -26,9 +26,6 @@ class BrokerMeta(NamedTuple): logger = logging.getLogger(__name__) -DEFAULT_QUEUED_MAX_MESSAGE_KBYTES = 50000 -DEFAULT_QUEUED_MIN_MESSAGES = 100000 - @dataclass(frozen=True) class IndexerOutputMessageBatch: diff --git a/src/sentry/sentry_metrics/indexer/id_generator.py b/src/sentry/sentry_metrics/indexer/id_generator.py deleted file mode 100644 index db0514468d14d9..00000000000000 --- a/src/sentry/sentry_metrics/indexer/id_generator.py +++ /dev/null @@ -1,54 +0,0 @@ -import random -import time - -_VERSION_BITS = 4 -_TS_BITS = 32 -_RANDOM_BITS = 28 -_TOTAL_BITS = _VERSION_BITS + _TS_BITS + _RANDOM_BITS -assert _TOTAL_BITS == 64 - -_VERSION = 2 - -# Warning! The version must be an even number as this is already -# written to a BigInt field in Postgres -assert _VERSION % 2 == 0 - -# 1st January 2022 -_INDEXER_EPOCH_START = 1641024000 - - -def reverse_bits(number: int, bit_size: int) -> int: - return int(bin(number)[2:].zfill(bit_size)[::-1], 2) - - -# we will have room b/n version and time since for a while -# so let's reverse the version bits to grow to the right -# instead of left should we need more than 3 bits for version - -_VERSION_PREFIX = reverse_bits(_VERSION, _VERSION_BITS) - - -def get_id() -> int: - """ - Generates IDs for use by indexer storages that do not have autoincrement sequences. - - This function does not provide any guarantee of uniqueness, just a low probability of collisions. - It relies on the database to be strongly consistent and reject writes with duplicate IDs. These should - be retried with a newly generated ID. - - The ID generated is in roughly incrementing order. - - Metric IDs are 64 bit but this function only generates IDs that fit in 63 bits. The leading bit is always zero. - This is because they were stored in Postgres as BigInt (signed 64 bit) and we do not want to change that now. - In ClickHouse it is an unsigned 64 bit integer. - """ - - now = int(time.time()) - time_since_epoch = now - _INDEXER_EPOCH_START - rand = random.getrandbits(_RANDOM_BITS) - - id = _VERSION_PREFIX << (_TOTAL_BITS - _VERSION_BITS) - id |= time_since_epoch << (_TOTAL_BITS - _VERSION_BITS - _TS_BITS) - id |= rand - - return id diff --git a/src/sentry/sentry_metrics/indexer/postgres/postgres_v2.py b/src/sentry/sentry_metrics/indexer/postgres/postgres_v2.py index 2eaac04915ff6c..8155a49ef506b0 100644 --- a/src/sentry/sentry_metrics/indexer/postgres/postgres_v2.py +++ b/src/sentry/sentry_metrics/indexer/postgres/postgres_v2.py @@ -31,7 +31,6 @@ __all__ = ["PostgresIndexer"] -_INDEXER_CACHE_METRIC = "sentry_metrics.indexer.memcache" _INDEXER_DB_METRIC = "sentry_metrics.indexer.postgres" _PARTITION_KEY = "pg" diff --git a/src/sentry/sentry_metrics/querying/constants.py b/src/sentry/sentry_metrics/querying/constants.py index 64ed5b42be3f9a..508202c3262d84 100644 --- a/src/sentry/sentry_metrics/querying/constants.py +++ b/src/sentry/sentry_metrics/querying/constants.py @@ -2,17 +2,6 @@ # Snuba can return at most 10.000 rows. SNUBA_QUERY_LIMIT = 10000 -# Intervals in seconds which are used by the product to query data. -DEFAULT_QUERY_INTERVALS = [ - 60 * 60 * 24, # 1 day - 60 * 60 * 12, # 12 hours - 60 * 60 * 4, # 4 hours - 60 * 60 * 2, # 2 hours - 60 * 60, # 1 hour - 60 * 30, # 30 min - 60 * 5, # 5 min - 60, # 1 min -] # Operators in formulas that use coefficients. COEFFICIENT_OPERATORS = { ArithmeticOperator.DIVIDE.value, diff --git a/src/sentry/sentry_metrics/querying/data/mapping/base.py b/src/sentry/sentry_metrics/querying/data/mapping/base.py index e241dd0229a571..b67384654bf093 100644 --- a/src/sentry/sentry_metrics/querying/data/mapping/base.py +++ b/src/sentry/sentry_metrics/querying/data/mapping/base.py @@ -1,6 +1,6 @@ import abc from collections.abc import Sequence -from typing import Any, TypeVar +from typing import Any from sentry.models.project import Project @@ -26,9 +26,6 @@ def backward(self, projects: Sequence[Project], value: Any) -> Any: return value -TMapper = TypeVar("TMapper", bound=Mapper) - - class MapperConfig: def __init__(self): self.mappers: set[type[Mapper]] = set() diff --git a/src/sentry/sentry_metrics/querying/data/transformation/stats.py b/src/sentry/sentry_metrics/querying/data/transformation/stats.py deleted file mode 100644 index 45cc44c37f4147..00000000000000 --- a/src/sentry/sentry_metrics/querying/data/transformation/stats.py +++ /dev/null @@ -1,48 +0,0 @@ -from collections.abc import Mapping, Sequence -from dataclasses import dataclass -from typing import Any - -from sentry.sentry_metrics.querying.data.execution import QueryResult -from sentry.sentry_metrics.querying.data.transformation.base import QueryResultsTransformer -from sentry.utils.outcomes import Outcome - - -@dataclass(frozen=True) -class MetricsOutcomesResult: - series: Sequence[Mapping[str, Any]] - totals: Sequence[Mapping[str, Any]] - - -class MetricsOutcomesTransformer(QueryResultsTransformer[Mapping[str, Any]]): - def transform_result(self, result: Sequence[Mapping[str, Any]]) -> Sequence[Mapping[str, Any]]: - ret_val = [] - - for item in result: - ret_val_item = {} - for key in item: - if key == "outcome.id": - outcome = int(item[key]) - ret_val_item["outcome"] = Outcome(outcome).api_name() - elif key in "aggregate_value": - ret_val_item["quantity"] = item[key] - else: - ret_val_item[key] = item[key] - - ret_val.append(ret_val_item) - - return ret_val - - def transform(self, query_results: Sequence[QueryResult]) -> Mapping[str, Any]: - """ - Transforms the query results into the format returned by outcomes queries. - Performs necessary mappings to match that format such as outcome.id -> outcome - - """ - - if not query_results or len(query_results) == 0: - return {"series": [], "totals": []} - - series = self.transform_result(query_results[0].series) - totals = self.transform_result(query_results[0].totals) - - return {"series": series, "totals": totals} diff --git a/src/sentry/sentry_metrics/querying/errors.py b/src/sentry/sentry_metrics/querying/errors.py index 56ef0a2d916afd..365e2f628e7e84 100644 --- a/src/sentry/sentry_metrics/querying/errors.py +++ b/src/sentry/sentry_metrics/querying/errors.py @@ -8,7 +8,3 @@ class MetricsQueryExecutionError(Exception): class LatestReleaseNotFoundError(Exception): pass - - -class CorrelationsQueryExecutionError(Exception): - pass diff --git a/src/sentry/sentry_metrics/querying/metadata/utils.py b/src/sentry/sentry_metrics/querying/metadata/utils.py index 88d00b58c5623e..6b9c8941647389 100644 --- a/src/sentry/sentry_metrics/querying/metadata/utils.py +++ b/src/sentry/sentry_metrics/querying/metadata/utils.py @@ -19,9 +19,6 @@ class OperationsConfiguration: def __init__(self): self.hidden_operations = set() - def hide_operation(self, operation: str) -> None: - self.hidden_operations.add(operation) - def hide_operations(self, operations: list[str]) -> None: self.hidden_operations.update(operations) diff --git a/src/sentry/sentry_metrics/querying/utils.py b/src/sentry/sentry_metrics/querying/utils.py index 527cf26721bef8..e5304d44513634 100644 --- a/src/sentry/sentry_metrics/querying/utils.py +++ b/src/sentry/sentry_metrics/querying/utils.py @@ -1,5 +1,3 @@ -import re - from django.conf import settings from rediscluster import RedisCluster @@ -12,11 +10,3 @@ def get_redis_client_for_metrics_meta() -> RedisCluster: """ cluster_key = settings.SENTRY_METRIC_META_REDIS_CLUSTER return redis.redis_clusters.get(cluster_key) # type: ignore[return-value] - - -def remove_if_match(pattern, string: str) -> str: - """ - Removes a pattern from a string. - """ - # Use the re.sub function to replace the matched characters with an empty string - return re.sub(pattern, "", string) diff --git a/src/sentry/snuba/metrics/datasource.py b/src/sentry/snuba/metrics/datasource.py index ea290073a10fb2..78729018fe3b8b 100644 --- a/src/sentry/snuba/metrics/datasource.py +++ b/src/sentry/snuba/metrics/datasource.py @@ -1,10 +1,3 @@ -from __future__ import annotations - -from functools import lru_cache - -import sentry_sdk -from rest_framework.exceptions import NotFound - """ Module that gets both metadata and time series from Snuba. For metadata, it fetch metrics metadata (metric names, tag names, tag values, ...) from snuba. @@ -13,12 +6,7 @@ efficient, we only look at the past 24 hours. """ -__all__ = ( - "get_all_tags", - "get_tag_values", - "get_series", - "get_single_metric_info", -) +from __future__ import annotations import logging from collections import defaultdict, deque @@ -29,14 +17,14 @@ from operator import itemgetter from typing import Any -from snuba_sdk import And, Column, Condition, Function, Op, Or, Query, Request +import sentry_sdk +from rest_framework.exceptions import NotFound +from snuba_sdk import Column, Condition, Function, Op, Query, Request from snuba_sdk.conditions import ConditionGroup from sentry.exceptions import InvalidParams from sentry.models.project import Project from sentry.sentry_metrics import indexer -from sentry.sentry_metrics.indexer.strings import PREFIX as SHARED_STRINGS_PREFIX -from sentry.sentry_metrics.indexer.strings import SHARED_STRINGS from sentry.sentry_metrics.use_case_id_registry import UseCaseID from sentry.sentry_metrics.utils import ( MetricIndexNotFound, @@ -49,7 +37,6 @@ from sentry.snuba.metrics.fields import run_metrics_query from sentry.snuba.metrics.fields.base import ( SnubaDataType, - build_metrics_query, get_derived_metrics, org_id_from_projects, ) @@ -80,7 +67,15 @@ get_intervals, to_intervals, ) -from sentry.utils.snuba import bulk_snuba_queries, raw_snql_query +from sentry.utils.snuba import raw_snql_query + +__all__ = ( + "get_all_tags", + "get_tag_values", + "get_series", + "get_single_metric_info", +) + logger = logging.getLogger(__name__) @@ -107,133 +102,6 @@ def _get_metrics_for_entity( ) -def _get_metrics_by_project_for_entity_query( - entity_key: EntityKey, - project_ids: Sequence[int], - org_id: int, - use_case_id: UseCaseID, - start: datetime | None = None, - end: datetime | None = None, -) -> Request: - where = [Condition(Column("use_case_id"), Op.EQ, use_case_id.value)] - where.extend(_get_mri_constraints_for_use_case(entity_key, use_case_id)) - - return build_metrics_query( - entity_key=entity_key, - select=[Column("project_id"), Column("metric_id")], - groupby=[Column("project_id"), Column("metric_id")], - where=where, - project_ids=project_ids, - org_id=org_id, - use_case_id=use_case_id, - start=start, - end=end, - ) - - -@lru_cache(maxsize=len(EntityKey) * len(UseCaseID)) -def _get_mri_constraints_for_use_case(entity_key: EntityKey, use_case_id: UseCaseID): - # Sessions exist on a different infrastructure that works differently, - # thus this optimization does not apply. - if use_case_id == UseCaseID.SESSIONS: - return [] - - conditions = [] - - # Look for the min/max of the metric id range for the given use case id to - # constrain the search ClickHouse must do otherwise, it'll attempt a full scan. - # - # This assumes that metric ids are divided into non-overlapping ranges by the - # use case id, so we can focus on a particular range for better performance. - min_metric_id = SHARED_STRINGS_PREFIX << 1 # larger than possible metric ids - max_metric_id = 0 - - for mri, id in SHARED_STRINGS.items(): - parsed_mri = parse_mri(mri) - if parsed_mri is not None and parsed_mri.namespace == use_case_id.value: - min_metric_id = min(id, min_metric_id) - max_metric_id = max(id, max_metric_id) - - # It's possible that there's a metric id within the use case that is not - # hard coded so we should always check the range of custom metric ids. - condition = Condition(Column("metric_id"), Op.LT, SHARED_STRINGS_PREFIX) - - # If we find a valid range, we extend the condition to check it as well. - if min_metric_id <= max_metric_id: - condition = Or( - [ - condition, - # Expand the search to include the range of the hard coded - # metric ids if a valid range was found. - And( - [ - Condition(Column("metric_id"), Op.GTE, min_metric_id), - Condition(Column("metric_id"), Op.LTE, max_metric_id), - ] - ), - ] - ) - - conditions.append(condition) - - # This is added to every use case id because the MRI is the primary ORDER BY - # on the table, and without it, these granules will be scanned no matter what - # the use case id is. - excluded_mris = [] - - if use_case_id == UseCaseID.TRANSACTIONS: - # This on_demand MRI takes up the majority of dataset and makes the query slow - # because ClickHouse ends up scanning the whole table. - # - # These are used for on demand metrics extraction and end users should not - # need to know about these metrics. - # - # As an optimization, we explicitly exclude these MRIs in the query to allow - # Clickhouse to skip the granules containing strictly these MRIs. - if entity_key == EntityKey.GenericMetricsCounters: - excluded_mris.append("c:transactions/on_demand@none") - elif entity_key == EntityKey.GenericMetricsDistributions: - excluded_mris.append("d:transactions/on_demand@none") - elif entity_key == EntityKey.GenericMetricsSets: - excluded_mris.append("s:transactions/on_demand@none") - elif entity_key == EntityKey.GenericMetricsGauges: - excluded_mris.append("g:transactions/on_demand@none") - - if excluded_mris: - conditions.append( - Condition( - Column("metric_id"), - Op.NOT_IN, - # these are shared strings, so just using org id 0 as a placeholder - [indexer.resolve(use_case_id, 0, mri) for mri in excluded_mris], - ) - ) - - return conditions - - -def _get_metrics_by_project_for_entity( - entity_key: EntityKey, - project_ids: Sequence[int], - org_id: int, - use_case_id: UseCaseID, - start: datetime | None = None, - end: datetime | None = None, -) -> list[SnubaDataType]: - return run_metrics_query( - entity_key=entity_key, - select=[Column("project_id"), Column("metric_id")], - groupby=[Column("project_id"), Column("metric_id")], - where=[Condition(Column("use_case_id"), Op.EQ, use_case_id.value)], - referrer="snuba.metrics.get_metrics_names_for_entity", - project_ids=project_ids, - org_id=org_id, - use_case_id=use_case_id, - start=start, - end=end, - ) - - def get_available_derived_metrics( projects: Sequence[Project], supported_metric_ids_in_entities: dict[MetricType, Sequence[int]], @@ -300,68 +168,6 @@ def get_metrics_blocking_state_of_projects( return metrics_blocking_state_by_mri -def get_stored_metrics_of_projects( - projects: Sequence[Project], - use_case_ids: Sequence[UseCaseID], - start: datetime | None = None, - end: datetime | None = None, -) -> Mapping[str, Sequence[int]]: - org_id = projects[0].organization_id - project_ids = [project.id for project in projects] - - # We compute a list of all the queries that we want to run in parallel across entities and use cases. - requests = [] - use_case_id_to_index = defaultdict(list) - for use_case_id in use_case_ids: - entity_keys = get_entity_keys_of_use_case_id(use_case_id=use_case_id) - for entity_key in entity_keys or (): - requests.append( - _get_metrics_by_project_for_entity_query( - entity_key=entity_key, - project_ids=project_ids, - org_id=org_id, - use_case_id=use_case_id, - start=start, - end=end, - ) - ) - use_case_id_to_index[use_case_id].append(len(requests) - 1) - - # We run the queries all in parallel. - results = bulk_snuba_queries( - requests=requests, - referrer="snuba.metrics.datasource.get_stored_metrics_of_projects", - use_cache=True, - ) - - # We reverse resolve all the metric ids by bulking together all the resolutions of the same use case id to maximize - # the parallelism. - resolved_metric_ids = defaultdict(dict) - for use_case_id, results_indexes in use_case_id_to_index.items(): - metrics_ids = [] - for result_index in results_indexes: - data = results[result_index]["data"] - for row in data or (): - metrics_ids.append(row["metric_id"]) - - # We have to partition the resolved metric ids per use case id, since the indexer values might clash across - # use cases. - resolved_metric_ids[use_case_id].update( - bulk_reverse_resolve(use_case_id, org_id, [metric_id for metric_id in metrics_ids]) - ) - - # We iterate over each result and compute a map of `metric_id -> project_id`. - grouped_stored_metrics = defaultdict(list) - for use_case_id, results_indexes in use_case_id_to_index.items(): - for result_index in results_indexes: - data = results[result_index]["data"] - for row in data or (): - resolved_metric_id = resolved_metric_ids[use_case_id][row["metric_id"]] - grouped_stored_metrics[resolved_metric_id].append(row["project_id"]) - - return grouped_stored_metrics - - def get_custom_measurements( project_ids: Sequence[int], organization_id: int, diff --git a/src/sentry/snuba/metrics/extraction.py b/src/sentry/snuba/metrics/extraction.py index 99b0fc625cf705..a838ca7cc8a6a9 100644 --- a/src/sentry/snuba/metrics/extraction.py +++ b/src/sentry/snuba/metrics/extraction.py @@ -313,8 +313,6 @@ def get_default_spec_version(cls: Any) -> SpecVersion: "event.type=transaction", ] -Variables = dict[str, Any] - query_builder = UnresolvedQuery( dataset=Dataset.Transactions, params={} ) # Workaround to get all updated discover functions instead of using the deprecated events fields. @@ -1326,11 +1324,6 @@ def condition(self) -> RuleCondition | None: is extracted.""" return self._process_query() - def is_project_dependent(self) -> bool: - """Returns whether the spec is unique to a project, which is required for some forms of caching""" - tags_specs_generator = _ONDEMAND_OP_TO_PROJECT_SPEC_GENERATOR.get(self.op) - return tags_specs_generator is not None - def tags_conditions(self, project: Project) -> list[TagSpec]: """Returns a list of tag conditions that will specify how tags are injected into metrics by Relay, and a bool if those specs may be project specific.""" tags_specs_generator = _ONDEMAND_OP_TO_SPEC_GENERATOR.get(self.op) diff --git a/src/sentry/snuba/metrics/fields/snql.py b/src/sentry/snuba/metrics/fields/snql.py index c73af996b7a1ac..e4f25719760ee7 100644 --- a/src/sentry/snuba/metrics/fields/snql.py +++ b/src/sentry/snuba/metrics/fields/snql.py @@ -225,14 +225,6 @@ def _snql_on_tx_satisfaction_factory( return _snql_on_tx_satisfaction_factory -def _dist_count_aggregation_on_tx_satisfaction_factory( - org_id: int, satisfaction: str, metric_ids: Sequence[int], alias: str | None = None -) -> Function: - return _aggregation_on_tx_satisfaction_func_factory("countIf")( - org_id, satisfaction, metric_ids, alias - ) - - def _set_count_aggregation_on_tx_satisfaction_factory( org_id: int, satisfaction: str, metric_ids: Sequence[int], alias: str | None = None ) -> Function: diff --git a/src/sentry/snuba/metrics/naming_layer/mapping.py b/src/sentry/snuba/metrics/naming_layer/mapping.py index ae3cb23455d651..5e3cbf86cd0155 100644 --- a/src/sentry/snuba/metrics/naming_layer/mapping.py +++ b/src/sentry/snuba/metrics/naming_layer/mapping.py @@ -92,12 +92,6 @@ def get_public_name_from_mri(internal_name: TransactionMRI | SessionMRI | str) - return internal_name -def is_private_mri(internal_name: TransactionMRI | SessionMRI | str) -> bool: - public_name = get_public_name_from_mri(internal_name) - # If the public name is the same as internal name it means that the internal is "private". - return public_name == internal_name - - def _extract_name_from_custom_metric_mri(mri: str) -> str | None: parsed_mri = parse_mri(mri) if parsed_mri is None: diff --git a/src/sentry/snuba/metrics/query_builder.py b/src/sentry/snuba/metrics/query_builder.py index eac1874aa33497..b36ab5e349e5e4 100644 --- a/src/sentry/snuba/metrics/query_builder.py +++ b/src/sentry/snuba/metrics/query_builder.py @@ -45,7 +45,6 @@ resolve_tag_key, resolve_tag_value, resolve_weak, - reverse_resolve, reverse_resolve_tag_value, ) from sentry.snuba.dataset import Dataset @@ -615,11 +614,6 @@ def get_date_range(params: Mapping) -> tuple[datetime, datetime, int]: return start, end, interval -def parse_tag(use_case_id: UseCaseID, org_id: int, tag_string: str) -> str: - tag_key = int(tag_string.replace("tags_raw[", "").replace("tags[", "").replace("]", "")) - return reverse_resolve(use_case_id, org_id, tag_key) - - def get_metric_object_from_metric_field( metric_field: MetricField, ) -> MetricExpressionBase: diff --git a/src/sentry/snuba/metrics/utils.py b/src/sentry/snuba/metrics/utils.py index cef92af6e666c6..3655b538494684 100644 --- a/src/sentry/snuba/metrics/utils.py +++ b/src/sentry/snuba/metrics/utils.py @@ -275,9 +275,6 @@ def generate_operation_regex(): OPERATIONS_TO_ENTITY = { op: entity for entity, operations in AVAILABLE_OPERATIONS.items() for op in operations } -GENERIC_OPERATIONS_TO_ENTITY = { - op: entity for entity, operations in AVAILABLE_GENERIC_OPERATIONS.items() for op in operations -} METRIC_TYPE_TO_ENTITY: Mapping[MetricType, EntityKey] = { "counter": EntityKey.MetricsCounters, diff --git a/src/sentry/tasks/on_demand_metrics.py b/src/sentry/tasks/on_demand_metrics.py index 2b38c0956ff1cb..25e095405880c1 100644 --- a/src/sentry/tasks/on_demand_metrics.py +++ b/src/sentry/tasks/on_demand_metrics.py @@ -61,10 +61,6 @@ def _set_currently_processing_batch(current_batch: int) -> None: cache.set(_get_widget_processing_batch_key(), current_batch, timeout=3600) -def _set_cardinality_cache(cache_key: str, is_low_cardinality: bool) -> None: - cache.set(cache_key, is_low_cardinality, timeout=_WIDGET_QUERY_CARDINALITY_TTL) - - def _get_previous_processing_batch() -> int: return cache.get(_get_widget_processing_batch_key(), 0) diff --git a/src/sentry/utils/batching_kafka_consumer.py b/src/sentry/utils/batching_kafka_consumer.py index eb4e4b7c154c5f..47f0530e32b42c 100644 --- a/src/sentry/utils/batching_kafka_consumer.py +++ b/src/sentry/utils/batching_kafka_consumer.py @@ -8,9 +8,6 @@ logger = logging.getLogger("sentry.batching-kafka-consumer") -DEFAULT_QUEUED_MAX_MESSAGE_KBYTES = 50000 -DEFAULT_QUEUED_MIN_MESSAGES = 10000 - def wait_for_topics(admin_client: AdminClient, topics: list[str], timeout: int = 10) -> None: """ diff --git a/tests/sentry/sentry_metrics/test_batch.py b/tests/sentry/sentry_metrics/test_batch.py index 8a62738e2b2948..6c7039e4e1b790 100644 --- a/tests/sentry/sentry_metrics/test_batch.py +++ b/tests/sentry/sentry_metrics/test_batch.py @@ -1954,115 +1954,6 @@ def test_one_org_limited(caplog, settings): ] -@pytest.mark.django_db -def test_cardinality_limiter(caplog, settings): - """ - Test functionality of the indexer batch related to cardinality-limiting. More concretely, assert that `IndexerBatch.filter_messages`: - - 1. removes the messages from the outgoing batch - 2. prevents strings from filtered messages from being extracted & indexed - 3. does not crash when strings from filtered messages are not passed into reconstruct_messages - 4. still extracts strings that exist both in filtered and unfiltered messages (eg "environment") - """ - settings.SENTRY_METRICS_INDEXER_DEBUG_LOG_SAMPLE_RATE = 1.0 - - outer_message = _construct_outer_message( - [ - (counter_payload, counter_headers), - (distribution_payload, distribution_headers), - (set_payload, set_headers), - ] - ) - - batch = IndexerBatch( - outer_message, - True, - False, - tags_validator=ReleaseHealthTagsValidator().is_allowed, - schema_validator=MetricsSchemaValidator( - INGEST_CODEC, RELEASE_HEALTH_SCHEMA_VALIDATION_RULES_OPTION_NAME - ).validate, - ) - keys_to_remove = list(batch.parsed_payloads_by_meta)[:2] - # the messages come in a certain order, and Python dictionaries preserve - # their insertion order. So we can hardcode offsets here. - assert keys_to_remove == [ - BrokerMeta(partition=Partition(Topic("topic"), 0), offset=0), - BrokerMeta(partition=Partition(Topic("topic"), 0), offset=1), - ] - batch.filter_messages(keys_to_remove) - assert batch.extract_strings() == { - UseCaseID.SESSIONS: { - 1: { - "environment", - "errored", - "production", - # Note, we only extracted one MRI, of the one metric that we didn't - # drop - "s:sessions/error@none", - "session.status", - }, - } - } - assert not batch.invalid_msg_meta - - snuba_payloads = batch.reconstruct_messages( - { - UseCaseID.SESSIONS: { - 1: { - "environment": 1, - "errored": 2, - "production": 3, - "s:sessions/error@none": 4, - "session.status": 5, - }, - } - }, - { - UseCaseID.SESSIONS: { - 1: { - "environment": Metadata(id=1, fetch_type=FetchType.CACHE_HIT), - "errored": Metadata(id=2, fetch_type=FetchType.CACHE_HIT), - "production": Metadata(id=3, fetch_type=FetchType.CACHE_HIT), - "s:sessions/error@none": Metadata(id=4, fetch_type=FetchType.CACHE_HIT), - "session.status": Metadata(id=5, fetch_type=FetchType.CACHE_HIT), - } - } - }, - ).data - - assert _deconstruct_messages(snuba_payloads) == [ - ( - { - "mapping_meta": { - "c": { - "1": "environment", - "2": "errored", - "3": "production", - "4": "s:sessions/error@none", - "5": "session.status", - }, - }, - "metric_id": 4, - "org_id": 1, - "project_id": 3, - "retention_days": 90, - "tags": {"1": 3, "5": 2}, - "timestamp": ts, - "type": "s", - "use_case_id": "sessions", - "value": [3], - "sentry_received_timestamp": BROKER_TIMESTAMP.timestamp(), - }, - [ - *set_headers, - ("mapping_sources", b"c"), - ("metric_type", "s"), - ], - ) - ] - - def test_aggregation_options(): with override_options( diff --git a/tests/sentry/sentry_metrics/test_id_generator.py b/tests/sentry/sentry_metrics/test_id_generator.py deleted file mode 100644 index 60e40cb9a7688f..00000000000000 --- a/tests/sentry/sentry_metrics/test_id_generator.py +++ /dev/null @@ -1,37 +0,0 @@ -import time -from unittest.mock import patch - -from sentry.sentry_metrics.indexer.id_generator import _INDEXER_EPOCH_START, get_id - - -def test_get_id() -> None: - # Function returns a different ID each time it's called - assert get_id() != get_id() - - # IDs fit in 63 bits (leading bit must be a zero) - assert get_id() < pow(2, 63) - - # Starts with 0100 (leading zero + version) - id_binary_string = bin(get_id())[2:].zfill(64) - assert id_binary_string.startswith("0100") - - -def test_get_id_time_since() -> None: - """ - This verifies that the middle 32bits are the correct time since. - - (4bits) (32bits) (28bits) - version | time since (s) | random | - - 0100 | 00000001001000101000000111100011 | 1110100001100010100101011111 - - """ - hardcoded_time = time.time() - - with patch("time.time") as mock_time: - mock_time.return_value = hardcoded_time - - id_string = bin(get_id())[2:].zfill(64) - original_time = int(id_string[3:36], 2) + _INDEXER_EPOCH_START - - assert original_time == int(hardcoded_time) diff --git a/tests/sentry/snuba/metrics/test_datasource.py b/tests/sentry/snuba/metrics/test_datasource.py index 8b6f90c8f0da77..3dd801f1d95ba8 100644 --- a/tests/sentry/snuba/metrics/test_datasource.py +++ b/tests/sentry/snuba/metrics/test_datasource.py @@ -1,10 +1,7 @@ -import time - import pytest from sentry.sentry_metrics.use_case_id_registry import UseCaseID from sentry.snuba.metrics import get_tag_values -from sentry.snuba.metrics.datasource import get_stored_metrics_of_projects from sentry.snuba.metrics.naming_layer import TransactionMetricKey, TransactionMRI from sentry.testutils.cases import BaseMetricsLayerTestCase, TestCase from sentry.testutils.helpers.datetime import freeze_time @@ -20,63 +17,6 @@ class DatasourceTestCase(BaseMetricsLayerTestCase, TestCase): def now(self): return BaseMetricsLayerTestCase.MOCK_DATETIME - def test_get_stored_mris(self): - self.store_performance_metric( - name=TransactionMRI.DURATION.value, - tags={"release": "1.0"}, - value=1, - ) - - self.store_session( - self.build_session( - distinct_id="39887d89-13b2-4c84-8c23-5d13d2102666", - session_id="5d52fd05-fcc9-4bf3-9dc9-267783670341", - status="exited", - release="foo@1.0.0", - environment="prod", - started=time.time() // 60 * 60, - received=time.time(), - ) - ) - - custom_mri = "d:custom/PageLoad.2@millisecond" - self.store_metric( - self.project.organization.id, - self.project.id, - custom_mri, - {}, - int(self.now.timestamp()), - 10, - ) - - mris = get_stored_metrics_of_projects([self.project], [UseCaseID.TRANSACTIONS]) - assert mris == { - "d:transactions/duration@millisecond": [self.project.id], - } - - mris = get_stored_metrics_of_projects([self.project], [UseCaseID.SESSIONS]) - assert mris == { - "d:sessions/duration@second": [self.project.id], - "c:sessions/session@none": [self.project.id], - "s:sessions/user@none": [self.project.id], - } - - mris = get_stored_metrics_of_projects([self.project], [UseCaseID.CUSTOM]) - assert mris == { - custom_mri: [self.project.id], - } - - mris = get_stored_metrics_of_projects( - [self.project], [UseCaseID.TRANSACTIONS, UseCaseID.SESSIONS, UseCaseID.CUSTOM] - ) - assert mris == { - "d:transactions/duration@millisecond": [self.project.id], - "d:sessions/duration@second": [self.project.id], - "c:sessions/session@none": [self.project.id], - "s:sessions/user@none": [self.project.id], - custom_mri: [self.project.id], - } - def test_get_tag_values_with_mri(self): releases = ["1.0", "2.0"] for release in ("1.0", "2.0"): diff --git a/tests/sentry/snuba/metrics/test_extraction.py b/tests/sentry/snuba/metrics/test_extraction.py index 8a07c98d26c7a4..d8a562697e7887 100644 --- a/tests/sentry/snuba/metrics/test_extraction.py +++ b/tests/sentry/snuba/metrics/test_extraction.py @@ -738,15 +738,6 @@ def test_spec_apdex_without_condition(_get_satisfactory_metric, default_project) assert spec.tags_conditions(default_project) == apdex_tag_spec(default_project, ["10"]) -@django_db_all -def test_spec_is_dependent_on_project(default_project) -> None: - spec = OnDemandMetricSpec("apdex(10)", "") - assert spec.is_project_dependent() is True - - spec = OnDemandMetricSpec("failure_rate()", "") - assert spec.is_project_dependent() is False - - def test_spec_custom_tag() -> None: custom_tag_spec = OnDemandMetricSpec("count()", "foo:bar") diff --git a/tests/sentry/tasks/test_on_demand_metrics.py b/tests/sentry/tasks/test_on_demand_metrics.py index d51bf6a63b3d75..1c252e94dc5e3f 100644 --- a/tests/sentry/tasks/test_on_demand_metrics.py +++ b/tests/sentry/tasks/test_on_demand_metrics.py @@ -454,7 +454,6 @@ def test_schedule_on_demand_check( ), # Only 2 widgets are on-demand ], ) -@mock.patch("sentry.tasks.on_demand_metrics._set_cardinality_cache") @mock.patch("sentry.search.events.builder.base.raw_snql_query") @pytest.mark.parametrize( "widget_type", [DashboardWidgetTypes.DISCOVER, DashboardWidgetTypes.TRANSACTION_LIKE] @@ -462,7 +461,6 @@ def test_schedule_on_demand_check( @django_db_all def test_process_widget_specs( raw_snql_query: Any, - _set_cardinality_cache: Any, feature_flags: dict[str, bool], option_enable: bool, widget_query_ids: Sequence[int], From e8dc2efdafc2d03f9e118deffe6e7a5b79ddbf5b Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Thu, 26 Dec 2024 12:25:48 -0500 Subject: [PATCH 483/757] ref: delete some empty test files (#82559) --- tests/sentry/hybridcloud/test_organization_provisioning.py | 0 tests/sentry/workflow_engine/models/test_detector.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/sentry/hybridcloud/test_organization_provisioning.py delete mode 100644 tests/sentry/workflow_engine/models/test_detector.py diff --git a/tests/sentry/hybridcloud/test_organization_provisioning.py b/tests/sentry/hybridcloud/test_organization_provisioning.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/tests/sentry/workflow_engine/models/test_detector.py b/tests/sentry/workflow_engine/models/test_detector.py deleted file mode 100644 index e69de29bb2d1d6..00000000000000 From 552edc49618dde901317ad13611701619e0c6d24 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Thu, 26 Dec 2024 12:27:09 -0500 Subject: [PATCH 484/757] ref: fix mock_redis_buffer (#82560) when implemented as a `contextmanager` it cannot decorate a class leading to tests/sentry/rules/processing/test_processor.py::RuleProcessorTest never running --- src/sentry/testutils/helpers/redis.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sentry/testutils/helpers/redis.py b/src/sentry/testutils/helpers/redis.py index dd41b06dfd4e67..9e4bdc0303c5be 100644 --- a/src/sentry/testutils/helpers/redis.py +++ b/src/sentry/testutils/helpers/redis.py @@ -9,11 +9,8 @@ from sentry.testutils.helpers import override_options -@contextmanager def mock_redis_buffer(): - buffer = RedisBuffer() - with patch("sentry.buffer.backend", new=buffer): - yield buffer + return patch("sentry.buffer.backend", new=RedisBuffer()) @contextmanager From 0f88319145fdaedaf080f779431a6d145dc0e20d Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Dec 2024 10:13:08 -0800 Subject: [PATCH 485/757] ci(eslint): Remove jquery from env list (#82561) The `vendor.js` file mentioned is gone now. --- .eslintrc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9223e46ef1a626..4766e33786d4d7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -789,7 +789,6 @@ module.exports = { browser: true, es6: true, jest: true, - jquery: true, // hard-loaded into vendor.js }, globals: { From 247661c4f8367f253ddf343eefe83a56c3af136c Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Thu, 26 Dec 2024 10:14:54 -0800 Subject: [PATCH 486/757] fix(issues): Adjust tag progress bar (#82439) --- .../groupTags/tagDetailsDrawerContent.tsx | 25 +++-------------- .../groupTags/tagDistribution.tsx | 27 +++++++++++++------ 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx b/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx index a8f3a9f8b3d91f..b5539c9c61b58b 100644 --- a/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx +++ b/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.tsx @@ -1,6 +1,5 @@ import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; -import Color from 'color'; import type {LocationDescriptor} from 'history'; import {useFetchIssueTag, useFetchIssueTagValues} from 'sentry/actionCreators/group'; @@ -27,6 +26,7 @@ import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; +import {TagBar} from 'sentry/views/issueDetails/groupTags/tagDistribution'; import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery'; type TagSort = 'date' | 'count'; @@ -176,7 +176,7 @@ function TagDetailsRow({ {tagValue.count.toLocaleString()} {displayPercentage} {tag.totalValues ? ( - + ) : ( '--' )} @@ -294,7 +294,7 @@ function TagValueActionsMenu({ const Table = styled('div')` display: grid; - grid-template-columns: repeat(4, auto) 1fr auto; + grid-template-columns: 1fr repeat(3, auto) 45px min-content; column-gap: ${space(2)}; row-gap: ${space(0.5)}; margin: 0 -${space(1)}; @@ -385,22 +385,3 @@ const OverflowTimeSince = styled(TimeSince)` const ExternalLinkbutton = styled(Button)` color: ${p => p.theme.subText}; `; - -const TagBarContainer = styled('div')` - height: ${space(1)}; - position: relative; - flex: 1; - min-width: ${space(1)}; - display: flex; - align-items: center; - &:before { - position: absolute; - inset: 0; - content: ''; - border-radius: 3px; - background: ${p => - `linear-gradient(to right, ${Color(p.theme.gray300).alpha(0.5).toString()} 20px, ${Color(p.theme.gray300).alpha(0.7).toString()} 100%)`}; - box-shadow: inset 0 0 0 1px ${p => p.theme.translucentInnerBorder}; - width: 100%; - } -`; diff --git a/static/app/views/issueDetails/groupTags/tagDistribution.tsx b/static/app/views/issueDetails/groupTags/tagDistribution.tsx index b975f365f3485c..1c89474b11a647 100644 --- a/static/app/views/issueDetails/groupTags/tagDistribution.tsx +++ b/static/app/views/issueDetails/groupTags/tagDistribution.tsx @@ -105,7 +105,7 @@ export function TagDistribution({tag}: {tag: GroupTag}) { ); } -function TagBar({ +export function TagBar({ percentage, style, ...props @@ -114,7 +114,11 @@ function TagBar({ className?: string; style?: React.CSSProperties; }) { - return ; + return ( + + + + ); } const TagPanel = styled(Link)` @@ -173,21 +177,28 @@ const TagValue = styled('div')` margin-right: ${space(0.5)}; `; +const TagBarPlaceholder = styled('div')` + position: relative; + height: ${space(1)}; + width: 100%; + border-radius: 3px; + box-shadow: inset 0 0 0 1px ${p => p.theme.translucentBorder}; + background: ${p => Color(p.theme.gray300).alpha(0.1).toString()}; + overflow: hidden; +`; + const TagBarContainer = styled('div')` height: ${space(1)}; - position: relative; - flex: 1; + position: absolute; + left: 0; + top: 0; min-width: ${space(1)}; - display: flex; - align-items: center; &:before { position: absolute; inset: 0; content: ''; - border-radius: 3px; background: ${p => `linear-gradient(to right, ${Color(p.theme.gray300).alpha(0.5).toString()} 0px, ${Color(p.theme.gray300).alpha(0.7).toString()} ${progressBarWidth})`}; - box-shadow: inset 0 0 0 1px ${p => p.theme.translucentInnerBorder}; width: 100%; } `; From 05bc530d1d97a328f72cba623d6c163a7dd5a24e Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 26 Dec 2024 10:15:40 -0800 Subject: [PATCH 487/757] ci(eslint): Tell eslint the correct react version in use (#82562) --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4766e33786d4d7..4d62e1e791359f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -801,7 +801,7 @@ module.exports = { settings: { react: { - version: '17.0.2', // React version, can not `detect` because of getsentry + version: '18.2.0', // React version, can not `detect` because of getsentry }, 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'], From 1aece4849c57857ece1c6838b212fb49ebc1a78a Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Thu, 26 Dec 2024 11:59:50 -0800 Subject: [PATCH 488/757] :wrench: chore: Change SLO to Halt temporarily for Slack Team Linking (#82565) --- src/sentry/integrations/slack/webhooks/base.py | 2 +- .../slack/webhooks/commands/test_link_team.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sentry/integrations/slack/webhooks/base.py b/src/sentry/integrations/slack/webhooks/base.py index 83db81f63d922e..f503adab1cb466 100644 --- a/src/sentry/integrations/slack/webhooks/base.py +++ b/src/sentry/integrations/slack/webhooks/base.py @@ -200,7 +200,7 @@ def link_team_handler(self, input: CommandInput) -> IntegrationResponse[Response for message, reason in self.TEAM_HALT_MAPPINGS.items(): if message in str(response.data): return IntegrationResponse( - interaction_result=EventLifecycleOutcome.SUCCESS, + interaction_result=EventLifecycleOutcome.HALTED, response=response, outcome_reason=str(reason), ) diff --git a/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py b/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py index a90a4057ae0183..3f366d4436c31e 100644 --- a/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py +++ b/tests/sentry/integrations/slack/webhooks/commands/test_link_team.py @@ -65,7 +65,7 @@ def test_link_another_team_to_channel(self, mock_record): data = orjson.loads(response.content) assert CHANNEL_ALREADY_LINKED_MESSAGE in get_response_text(data) - assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) @with_feature("organizations:slack-multiple-team-single-channel-linking") @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @@ -109,7 +109,7 @@ def test_link_team_from_dm(self, mock_record): data = orjson.loads(response.content) assert LINK_FROM_CHANNEL_MESSAGE in get_response_text(data) - assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) @responses.activate @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @@ -123,7 +123,7 @@ def test_link_team_identity_does_not_exist(self, mock_record): data = self.send_slack_message("link team", user_id=OTHER_SLACK_ID) assert LINK_USER_FIRST_MESSAGE in get_response_text(data) - assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) @responses.activate @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") @@ -142,7 +142,7 @@ def test_link_team_insufficient_role(self, mock_record): data = self.send_slack_message("link team", user_id=OTHER_SLACK_ID) assert INSUFFICIENT_ROLE_MESSAGE in get_response_text(data) - assert_slo_metric(mock_record, EventLifecycleOutcome.SUCCESS) + assert_slo_metric(mock_record, EventLifecycleOutcome.HALTED) @responses.activate @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") From 47142d42f74914255522a811a12a7ee5481c22d4 Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Thu, 26 Dec 2024 12:01:46 -0800 Subject: [PATCH 489/757] :wrench: chore: remove release option (#82558) --- src/sentry/options/defaults.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py index 24e58389f9e49e..c7e2a4209be293 100644 --- a/src/sentry/options/defaults.py +++ b/src/sentry/options/defaults.py @@ -2307,12 +2307,6 @@ flags=FLAG_AUTOMATOR_MODIFIABLE, ) -register( - "releases_v2.single-tenant", - default=False, - flags=FLAG_AUTOMATOR_MODIFIABLE, -) - # The flag disables the file io on main thread detector register( "performance_issues.file_io_main_thread.disabled", From c11f4c099257b2388d150f71fb39516e3d3f2328 Mon Sep 17 00:00:00 2001 From: mia hsu <55610339+ameliahsu@users.noreply.github.com> Date: Thu, 26 Dec 2024 13:46:40 -0800 Subject: [PATCH 490/757] ref(crons): new merge/aggregate status functions without `MonitorBucketEnvMapping` (#82566) introducing new functions to replace: - `getAggregateStatus` - `getAggregateStatusFromMultipleBuckets` - `mergeEnvMappings` since `checkinTimeline` doesn't need to know anything about environment, we want to change these functions to take in `StatsBucket` instead of `MonitorBucketEnvMapping` types replacing usage + deleting old functions will come in following PRs --- .../utils/getAggregateStatus.spec.tsx | 24 +++++++++++- .../timeline/utils/getAggregateStatus.tsx | 9 ++++- ...ggregateStatusFromMultipleBuckets.spec.tsx | 29 +++++++++++++- .../getAggregateStatusFromMultipleBuckets.tsx | 24 +++++++++++- .../timeline/utils/mergeEnvMappings.spec.tsx | 39 ++++++++++++++++++- .../timeline/utils/mergeEnvMappings.tsx | 11 ++++++ 6 files changed, 129 insertions(+), 7 deletions(-) diff --git a/static/app/views/monitors/components/timeline/utils/getAggregateStatus.spec.tsx b/static/app/views/monitors/components/timeline/utils/getAggregateStatus.spec.tsx index ddb73855482981..b0ab489cee5d1b 100644 --- a/static/app/views/monitors/components/timeline/utils/getAggregateStatus.spec.tsx +++ b/static/app/views/monitors/components/timeline/utils/getAggregateStatus.spec.tsx @@ -1,6 +1,9 @@ import {CheckInStatus} from 'sentry/views/monitors/types'; -import {getAggregateStatus} from './getAggregateStatus'; +import { + getAggregateStatus, + getAggregateStatusFromStatsBucket, +} from './getAggregateStatus'; type StatusCounts = [ in_progress: number, @@ -18,6 +21,18 @@ export function generateEnvMapping(name: string, counts: StatusCounts) { }; } +export function generateStats(counts: StatusCounts) { + const [in_progress, ok, missed, timeout, error, unknown] = counts; + return { + in_progress, + ok, + missed, + timeout, + error, + unknown, + }; +} + describe('getAggregateStatus', function () { it('aggregates correctly across multiple envs', function () { const envData = { @@ -27,3 +42,10 @@ describe('getAggregateStatus', function () { expect(getAggregateStatus(envData)).toEqual(CheckInStatus.ERROR); }); }); + +describe('getAggregateStatusFromStatsBucket', function () { + it('aggregates correctly', function () { + const stats = generateStats([0, 1, 2, 0, 1, 0]); + expect(getAggregateStatusFromStatsBucket(stats)).toEqual(CheckInStatus.ERROR); + }); +}); diff --git a/static/app/views/monitors/components/timeline/utils/getAggregateStatus.tsx b/static/app/views/monitors/components/timeline/utils/getAggregateStatus.tsx index c75aabc554c526..d7bf8cc95986a7 100644 --- a/static/app/views/monitors/components/timeline/utils/getAggregateStatus.tsx +++ b/static/app/views/monitors/components/timeline/utils/getAggregateStatus.tsx @@ -1,4 +1,4 @@ -import type {MonitorBucketEnvMapping} from '../types'; +import type {MonitorBucketEnvMapping, StatsBucket} from '../types'; import {CHECKIN_STATUS_PRECEDENT} from './constants'; @@ -12,3 +12,10 @@ export function getAggregateStatus(envData: MonitorBucketEnvMapping) { return currentStatus; }, CHECKIN_STATUS_PRECEDENT[0]); } + +export function getAggregateStatusFromStatsBucket(stats: StatsBucket) { + return ( + [...CHECKIN_STATUS_PRECEDENT].reverse().find(status => stats[status] > 0) || + CHECKIN_STATUS_PRECEDENT[0] + ); +} diff --git a/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.spec.tsx b/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.spec.tsx index 2a3e023fde162e..cec4ec0569395a 100644 --- a/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.spec.tsx +++ b/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.spec.tsx @@ -1,6 +1,9 @@ import {CheckInStatus} from 'sentry/views/monitors/types'; -import {getAggregateStatusFromMultipleBuckets} from './getAggregateStatusFromMultipleBuckets'; +import { + getAggregateStatusFromMultipleBuckets, + getAggregateStatusFromMultipleStatsBuckets, +} from './getAggregateStatusFromMultipleBuckets'; type StatusCounts = [ in_progress: number, @@ -18,6 +21,18 @@ export function generateEnvMapping(name: string, counts: StatusCounts) { }; } +export function generateStats(counts: StatusCounts) { + const [in_progress, ok, missed, timeout, error, unknown] = counts; + return { + in_progress, + ok, + missed, + timeout, + error, + unknown, + }; +} + describe('getAggregateStatusFromMultipleBuckets', function () { it('aggregates correctly across multiple envs', function () { const envData1 = generateEnvMapping('prod', [2, 1, 2, 1, 0, 0]); @@ -29,3 +44,15 @@ describe('getAggregateStatusFromMultipleBuckets', function () { expect(status).toEqual(CheckInStatus.TIMEOUT); }); }); + +describe('getAggregateStatusFromMultipleStatsBuckets', function () { + it('aggregates correctly across multiple envs', function () { + const stats1 = generateStats([2, 1, 2, 1, 0, 0]); + const stats2 = generateStats([1, 2, 0, 0, 0, 0]); + const stats3 = generateStats([1, 1, 1, 3, 0, 0]); + + const status = getAggregateStatusFromMultipleStatsBuckets([stats1, stats2, stats3]); + + expect(status).toEqual(CheckInStatus.TIMEOUT); + }); +}); diff --git a/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.tsx b/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.tsx index c073bb4ac56094..c229e037a48eaf 100644 --- a/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.tsx +++ b/static/app/views/monitors/components/timeline/utils/getAggregateStatusFromMultipleBuckets.tsx @@ -1,7 +1,10 @@ -import type {MonitorBucketEnvMapping} from '../types'; +import type {MonitorBucketEnvMapping, StatsBucket} from '../types'; import {CHECKIN_STATUS_PRECEDENT} from './constants'; -import {getAggregateStatus} from './getAggregateStatus'; +import { + getAggregateStatus, + getAggregateStatusFromStatsBucket, +} from './getAggregateStatus'; /** * Given multiple env buckets [{prod: {ok: 1, ...}, {prod: {ok: 0, ...}}] @@ -21,3 +24,20 @@ export function getAggregateStatusFromMultipleBuckets( CHECKIN_STATUS_PRECEDENT[0] ); } + +/** + * Given multiple stats buckets [{..., error: 1, unknown: 0}, {..., error: 0, unknown: 4}] + * returns the aggregate status across all buckets (unknown) + */ +export function getAggregateStatusFromMultipleStatsBuckets(statsArr: StatsBucket[]) { + return statsArr + .map(getAggregateStatusFromStatsBucket) + .reduce( + (aggregateStatus, currentStatus) => + CHECKIN_STATUS_PRECEDENT.indexOf(currentStatus) > + CHECKIN_STATUS_PRECEDENT.indexOf(aggregateStatus) + ? currentStatus + : aggregateStatus, + CHECKIN_STATUS_PRECEDENT[0] + ); +} diff --git a/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.spec.tsx b/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.spec.tsx index 87bfd6b4a7b373..913c5e2a321b8b 100644 --- a/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.spec.tsx +++ b/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.spec.tsx @@ -1,6 +1,30 @@ -import type {MonitorBucketEnvMapping} from 'sentry/views/monitors/components/timeline/types'; +import type { + MonitorBucketEnvMapping, + StatsBucket, +} from 'sentry/views/monitors/components/timeline/types'; -import {mergeEnvMappings} from './mergeEnvMappings'; +import {mergeEnvMappings, mergeStats} from './mergeEnvMappings'; + +type StatusCounts = [ + in_progress: number, + ok: number, + missed: number, + timeout: number, + error: number, + unknown: number, +]; + +export function generateStats(counts: StatusCounts) { + const [in_progress, ok, missed, timeout, error, unknown] = counts; + return { + in_progress, + ok, + missed, + timeout, + error, + unknown, + }; +} describe('mergeEnvMappings', function () { it('merges two empty mappings', function () { @@ -52,3 +76,14 @@ describe('mergeEnvMappings', function () { expect(mergedMapping).toEqual(expectedMerged); }); }); + +describe('mergeStats', function () { + it('merges two filled mappings', function () { + const statsA: StatsBucket = generateStats([0, 0, 1, 2, 1, 0]); + const statsB: StatsBucket = generateStats([2, 1, 1, 0, 2, 0]); + const expectedMerged: StatsBucket = generateStats([2, 1, 2, 2, 3, 0]); + const mergedStats = mergeStats(statsA, statsB); + + expect(mergedStats).toEqual(expectedMerged); + }); +}); diff --git a/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.tsx b/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.tsx index c2046bd2c2e854..e2adee78dd57f5 100644 --- a/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.tsx +++ b/static/app/views/monitors/components/timeline/utils/mergeEnvMappings.tsx @@ -24,3 +24,14 @@ export function mergeEnvMappings( return mergedEnvs; }, {}); } + +/** + * Combines job status counts + */ +export function mergeStats(statsA: StatsBucket, statsB: StatsBucket): StatsBucket { + const combinedStats = {} as StatsBucket; + for (const status of CHECKIN_STATUS_PRECEDENT) { + combinedStats[status] = (statsA[status] ?? 0) + (statsB[status] ?? 0); + } + return combinedStats; +} From 82155bcc0999b34641b48b174d7577e5f538d1ed Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 27 Dec 2024 12:10:37 +0100 Subject: [PATCH 491/757] Remove metrics from Java SDK onboarding docs (#81036) ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- static/app/gettingStartedDocs/android/android.tsx | 2 -- static/app/gettingStartedDocs/java/java.tsx | 2 -- static/app/gettingStartedDocs/java/log4j2.tsx | 2 -- static/app/gettingStartedDocs/java/logback.tsx | 2 -- static/app/gettingStartedDocs/java/spring-boot.tsx | 2 -- static/app/gettingStartedDocs/java/spring.tsx | 2 -- 6 files changed, 12 deletions(-) diff --git a/static/app/gettingStartedDocs/android/android.tsx b/static/app/gettingStartedDocs/android/android.tsx index a59faeddb9bab9..653f52e5ddb29a 100644 --- a/static/app/gettingStartedDocs/android/android.tsx +++ b/static/app/gettingStartedDocs/android/android.tsx @@ -11,7 +11,6 @@ import type { OnboardingConfig, } from 'sentry/components/onboarding/gettingStartedDoc/types'; import {MobileBetaBanner} from 'sentry/components/onboarding/gettingStartedDoc/utils'; -import {getAndroidMetricsOnboarding} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding'; import { getReplayMobileConfigureDescription, getReplayVerifyStep, @@ -445,7 +444,6 @@ const docs: Docs = { onboarding, feedbackOnboardingCrashApi: feedbackOnboardingCrashApiJava, crashReportOnboarding: feedbackOnboardingCrashApiJava, - customMetricsOnboarding: getAndroidMetricsOnboarding(), platformOptions, replayOnboarding, }; diff --git a/static/app/gettingStartedDocs/java/java.tsx b/static/app/gettingStartedDocs/java/java.tsx index f236144f27b656..060ee5b67d0cb5 100644 --- a/static/app/gettingStartedDocs/java/java.tsx +++ b/static/app/gettingStartedDocs/java/java.tsx @@ -13,7 +13,6 @@ import { getCrashReportApiIntroduction, getCrashReportInstallDescription, } from 'sentry/components/onboarding/gettingStartedDoc/utils/feedbackOnboarding'; -import {getJavaMetricsOnboarding} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding'; import {t, tct} from 'sentry/locale'; import {getPackageVersion} from 'sentry/utils/gettingStartedDocs/getPackageVersion'; @@ -343,7 +342,6 @@ const docs: Docs = { platformOptions, feedbackOnboardingCrashApi: feedbackOnboardingCrashApiJava, crashReportOnboarding: feedbackOnboardingCrashApiJava, - customMetricsOnboarding: getJavaMetricsOnboarding(), onboarding, }; diff --git a/static/app/gettingStartedDocs/java/log4j2.tsx b/static/app/gettingStartedDocs/java/log4j2.tsx index 74535e7bff903b..b81d4034a124c0 100644 --- a/static/app/gettingStartedDocs/java/log4j2.tsx +++ b/static/app/gettingStartedDocs/java/log4j2.tsx @@ -9,7 +9,6 @@ import type { DocsParams, OnboardingConfig, } from 'sentry/components/onboarding/gettingStartedDoc/types'; -import {getJavaMetricsOnboarding} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding'; import {feedbackOnboardingCrashApiJava} from 'sentry/gettingStartedDocs/java/java'; import {t, tct} from 'sentry/locale'; import {getPackageVersion} from 'sentry/utils/gettingStartedDocs/getPackageVersion'; @@ -333,7 +332,6 @@ const docs: Docs = { platformOptions, feedbackOnboardingCrashApi: feedbackOnboardingCrashApiJava, crashReportOnboarding: feedbackOnboardingCrashApiJava, - customMetricsOnboarding: getJavaMetricsOnboarding(), onboarding, }; diff --git a/static/app/gettingStartedDocs/java/logback.tsx b/static/app/gettingStartedDocs/java/logback.tsx index 909724e27d39cb..f32b0ee76a26e4 100644 --- a/static/app/gettingStartedDocs/java/logback.tsx +++ b/static/app/gettingStartedDocs/java/logback.tsx @@ -9,7 +9,6 @@ import type { DocsParams, OnboardingConfig, } from 'sentry/components/onboarding/gettingStartedDoc/types'; -import {getJavaMetricsOnboarding} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding'; import {feedbackOnboardingCrashApiJava} from 'sentry/gettingStartedDocs/java/java'; import {t, tct} from 'sentry/locale'; import {getPackageVersion} from 'sentry/utils/gettingStartedDocs/getPackageVersion'; @@ -342,7 +341,6 @@ const docs: Docs = { onboarding, feedbackOnboardingCrashApi: feedbackOnboardingCrashApiJava, crashReportOnboarding: feedbackOnboardingCrashApiJava, - customMetricsOnboarding: getJavaMetricsOnboarding(), platformOptions, }; diff --git a/static/app/gettingStartedDocs/java/spring-boot.tsx b/static/app/gettingStartedDocs/java/spring-boot.tsx index 5787e0a0cff4d1..6a09f67afd806a 100644 --- a/static/app/gettingStartedDocs/java/spring-boot.tsx +++ b/static/app/gettingStartedDocs/java/spring-boot.tsx @@ -9,7 +9,6 @@ import type { DocsParams, OnboardingConfig, } from 'sentry/components/onboarding/gettingStartedDoc/types'; -import {getJavaMetricsOnboarding} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding'; import {feedbackOnboardingCrashApiJava} from 'sentry/gettingStartedDocs/java/java'; import { feedbackOnboardingJsLoader, @@ -299,7 +298,6 @@ const docs: Docs = { platformOptions, replayOnboardingJsLoader, crashReportOnboarding: feedbackOnboardingCrashApiJava, - customMetricsOnboarding: getJavaMetricsOnboarding(), feedbackOnboardingJsLoader, }; diff --git a/static/app/gettingStartedDocs/java/spring.tsx b/static/app/gettingStartedDocs/java/spring.tsx index 32641237815a69..babb60c7acfc14 100644 --- a/static/app/gettingStartedDocs/java/spring.tsx +++ b/static/app/gettingStartedDocs/java/spring.tsx @@ -9,7 +9,6 @@ import type { DocsParams, OnboardingConfig, } from 'sentry/components/onboarding/gettingStartedDoc/types'; -import {getJavaMetricsOnboarding} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding'; import {feedbackOnboardingCrashApiJava} from 'sentry/gettingStartedDocs/java/java'; import { feedbackOnboardingJsLoader, @@ -372,7 +371,6 @@ const docs: Docs = { platformOptions, crashReportOnboarding: feedbackOnboardingCrashApiJava, replayOnboardingJsLoader, - customMetricsOnboarding: getJavaMetricsOnboarding(), feedbackOnboardingJsLoader, }; From f3243b8c92e5eb491af2024a6925379d307225b4 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:31:16 -0500 Subject: [PATCH 492/757] ref: report coverage on all python files (#82232) would like to be able to use codecov to look at coverage in tests as well (where it should be 100%) --- codecov.yml | 2 +- tests/sentry/organizations/test_absolute_url.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index faaa4864e1e98f..4b8b260fe743f9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -53,7 +53,7 @@ flags: after_n_builds: 4 backend: paths: - - 'src/sentry/**/*.py' + - '**/*.py' carryforward: true # Do not send any status checks until n coverage reports are uploaded. # NOTE: If you change this, make sure to change `comment.after_n_builds` below as well. diff --git a/tests/sentry/organizations/test_absolute_url.py b/tests/sentry/organizations/test_absolute_url.py index cdc2177d379522..2fb5d12af7fab1 100644 --- a/tests/sentry/organizations/test_absolute_url.py +++ b/tests/sentry/organizations/test_absolute_url.py @@ -18,7 +18,7 @@ "/settings/acme/developer-settings/release-bot/", "/settings/developer-settings/release-bot/", ), - # Settings views for orgs with acccount/billing in their slugs. + # Settings views for orgs with account/billing in their slugs. ("/settings/account-on/", "/settings/organization/"), ("/settings/billing-co/", "/settings/organization/"), ("/settings/account-on/integrations/", "/settings/integrations/"), From 518b63d4f7c644d1c15b4e90c466dae127dffea6 Mon Sep 17 00:00:00 2001 From: Michael Sun <55160142+MichaelSun48@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:07:18 -0500 Subject: [PATCH 493/757] ref(issue-views): Fold navigate behavior into issueViews context (#82540) This PR refactors the issue views family of components by moving lots of the navigation logic into the IssueViews context created in [this PR](https://github.com/getsentry/sentry/pull/82429). Many of the tab actions that were folded into the IssueViews context require changes to the query parameters, which is why it makes sense to combine these two actions in the reducer function. For example: * Duplicating a tab requires the newly created tab to be selected (change viewId parameter) * Discarding a tab's changes requires the query/sort to be reset to the original query/sort (change query/sort param --- .../app/views/issueList/customViewsHeader.tsx | 17 +-- .../groupSearchViewTabs/draggableTabBar.tsx | 116 +++++++----------- .../groupSearchViewTabs/issueViews.tsx | 91 ++++++++++---- 3 files changed, 114 insertions(+), 110 deletions(-) diff --git a/static/app/views/issueList/customViewsHeader.tsx b/static/app/views/issueList/customViewsHeader.tsx index bccbb96cc1ef01..b7af77c0a46bdc 100644 --- a/static/app/views/issueList/customViewsHeader.tsx +++ b/static/app/views/issueList/customViewsHeader.tsx @@ -267,17 +267,12 @@ function CustomViewsIssueListHeaderTabsContent({ } if (query) { if (!tabListState?.selectionManager.isSelected(TEMPORARY_TAB_KEY)) { - dispatch({type: 'SET_TEMP_VIEW', query, sort}); - navigate( - normalizeUrl({ - ...location, - query: { - ...queryParamsWithPageFilters, - viewId: undefined, - }, - }), - {replace: true} - ); + dispatch({ + type: 'SET_TEMP_VIEW', + query, + sort, + updateQueryParams: {newQueryParams: {viewId: undefined}, replace: true}, + }); tabListState?.setSelectedKey(TEMPORARY_TAB_KEY); return; } diff --git a/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx b/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx index 3a7318b6d45df8..9d9c6b37a5381d 100644 --- a/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx +++ b/static/app/views/issueList/groupSearchViewTabs/draggableTabBar.tsx @@ -17,7 +17,6 @@ import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; import {useHotkeys} from 'sentry/utils/useHotkeys'; import {useLocation} from 'sentry/utils/useLocation'; -import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import {DraggableTabMenuButton} from 'sentry/views/issueList/groupSearchViewTabs/draggableTabMenuButton'; import EditableTabTitle from 'sentry/views/issueList/groupSearchViewTabs/editableTabTitle'; @@ -40,7 +39,6 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) { const [editingTabKey, setEditingTabKey] = useState(null); const organization = useOrganization(); - const navigate = useNavigate(); const location = useLocation(); const {cursor: _cursor, page: _page, ...queryParams} = router?.location?.query ?? {}; @@ -66,43 +64,6 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) { [dispatch, tabListState?.selectedKey, tabs] ); - const handleDuplicateView = () => { - const newViewId = generateTempViewId(); - const duplicatedTab = state.views.find( - view => view.key === tabListState?.selectedKey - ); - if (!duplicatedTab) { - return; - } - dispatch({type: 'DUPLICATE_VIEW', newViewId, syncViews: true}); - navigate({ - ...location, - query: { - ...queryParams, - query: duplicatedTab.query, - sort: duplicatedTab.querySort, - viewId: newViewId, - }, - }); - }; - - const handleDiscardChanges = () => { - dispatch({type: 'DISCARD_CHANGES'}); - const originalTab = state.views.find(view => view.key === tabListState?.selectedKey); - if (originalTab) { - // TODO(msun): Move navigate logic to IssueViewsContext - navigate({ - ...location, - query: { - ...queryParams, - query: originalTab.query, - sort: originalTab.querySort, - viewId: originalTab.id, - }, - }); - } - }; - const handleNewViewsSaved: NewTabContext['onNewViewsSaved'] = useCallback< NewTabContext['onNewViewsSaved'] >( @@ -145,37 +106,23 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) { updatedTabs = [...updatedTabs, ...remainingNewViews]; } - dispatch({type: 'SET_VIEWS', views: updatedTabs, syncViews: true}); - navigate( - { - ...location, - query: { - ...queryParams, - query, - sort: IssueSortOptions.DATE, - }, + dispatch({ + type: 'SET_VIEWS', + views: updatedTabs, + syncViews: true, + updateQueryParams: { + newQueryParams: {query, sort: IssueSortOptions.DATE}, + replace: true, }, - {replace: true} - ); + }); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [location, navigate, setNewViewActive, tabs, viewId] + [location, setNewViewActive, tabs, viewId] ); - const handleCreateNewView = () => { - const tempId = generateTempViewId(); - dispatch({type: 'CREATE_NEW_VIEW', tempId}); - tabListState?.setSelectedKey(tempId); - navigate({ - ...location, - query: { - ...queryParams, - query: '', - viewId: tempId, - }, - }); - }; + useEffect(() => { + setOnNewViewsSaved(handleNewViewsSaved); + }, [setOnNewViewsSaved, handleNewViewsSaved]); const handleDeleteView = (tab: IssueView) => { dispatch({type: 'DELETE_VIEW', syncViews: true}); @@ -184,9 +131,28 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) { tabListState?.setSelectedKey(tabs.filter(tb => tb.key !== tab.key)[0].key); }; - useEffect(() => { - setOnNewViewsSaved(handleNewViewsSaved); - }, [setOnNewViewsSaved, handleNewViewsSaved]); + const handleDuplicateView = (tab: IssueView) => { + const newViewId = generateTempViewId(); + dispatch({ + type: 'DUPLICATE_VIEW', + newViewId, + syncViews: true, + updateQueryParams: { + newQueryParams: {viewId: newViewId, query: tab.query, sort: tab.querySort}, + }, + }); + }; + + const handleAddView = () => { + const tempId = generateTempViewId(); + dispatch({ + type: 'CREATE_NEW_VIEW', + tempId, + updateQueryParams: { + newQueryParams: {viewId: tempId, query: ''}, + }, + }); + }; const makeMenuOptions = (tab: IssueView): MenuItemProps[] => { if (tab.key === TEMPORARY_TAB_KEY) { @@ -198,15 +164,21 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) { if (tab.unsavedChanges) { return makeUnsavedChangesMenuOptions({ onRename: () => setEditingTabKey(tab.key), - onDuplicate: handleDuplicateView, + onDuplicate: () => handleDuplicateView(tab), onDelete: tabs.length > 1 ? () => handleDeleteView(tab) : undefined, onSave: () => dispatch({type: 'SAVE_CHANGES', syncViews: true}), - onDiscard: handleDiscardChanges, + onDiscard: () => + dispatch({ + type: 'DISCARD_CHANGES', + updateQueryParams: { + newQueryParams: {viewId: tab.id, query: tab.query, sort: tab.querySort}, + }, + }), }); } return makeDefaultMenuOptions({ onRename: () => setEditingTabKey(tab.key), - onDuplicate: handleDuplicateView, + onDuplicate: () => handleDuplicateView(tab), onDelete: tabs.length > 1 ? () => handleDeleteView(tab) : undefined, }); }; @@ -223,7 +195,7 @@ export function DraggableTabBar({initialTabKey, router}: DraggableTabBarProps) { } onReorderComplete={() => dispatch({type: 'SYNC_VIEWS_TO_BACKEND'})} defaultSelectedKey={initialTabKey} - onAddView={handleCreateNewView} + onAddView={handleAddView} orientation="horizontal" editingTabKey={editingTabKey ?? undefined} hideBorder diff --git a/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx b/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx index d9e477533878b1..b6538e049b2a10 100644 --- a/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx +++ b/static/app/views/issueList/groupSearchViewTabs/issueViews.tsx @@ -42,9 +42,20 @@ export interface IssueView { unsavedChanges?: [string, IssueSortOptions]; } +export type IssueViewsQueryUpdateParams = { + // TODO(msun): Once project/env/datetime are added as view configs, add them here + newQueryParams: { + query?: string; + sort?: IssueSortOptions; + viewId?: string; + }; + replace?: boolean; +}; + type BaseIssueViewsAction = { /** If true, the new views state created by the action will be synced to the backend */ syncViews?: boolean; + updateQueryParams?: IssueViewsQueryUpdateParams; }; type ReorderTabsAction = { @@ -112,7 +123,7 @@ type SetViewsAction = { type SyncViewsToBackendAction = { /** Syncs the current views state to the backend. Does not make any changes to the views state. */ type: 'SYNC_VIEWS_TO_BACKEND'; -}; +} & Exclude; export type IssueViewsActions = | ReorderTabsAction @@ -247,7 +258,11 @@ function deleteView(state: IssueViewsState, tabListState: TabListState) { return {...state, views: newViews}; } -function createNewView(state: IssueViewsState, action: CreateNewViewAction) { +function createNewView( + state: IssueViewsState, + action: CreateNewViewAction, + tabListState: TabListState +) { const newTabs: IssueView[] = [ ...state.views, { @@ -259,6 +274,7 @@ function createNewView(state: IssueViewsState, action: CreateNewViewAction) { isCommitted: false, }, ]; + tabListState?.setSelectedKey(action.tempId); return {...state, views: newTabs}; } @@ -417,30 +433,6 @@ export function IssueViewsStateProvider({ onSuccess: replaceWithPersistantViewIds, }); - const debounceUpdateViews = useMemo( - () => - debounce((newTabs: IssueView[]) => { - if (newTabs) { - updateViews({ - orgSlug: organization.slug, - groupSearchViews: newTabs - .filter(tab => tab.isCommitted) - .map(tab => ({ - // Do not send over an ID if it's a temporary or default tab so that - // the backend will save these and generate permanent Ids for them - ...(tab.id[0] !== '_' && !tab.id.startsWith('default') - ? {id: tab.id} - : {}), - name: tab.label, - query: tab.query, - querySort: tab.querySort, - })), - }); - } - }, 500), - [organization.slug, updateViews] - ); - const reducer: Reducer = useCallback( (state, action): IssueViewsState => { if (!tabListState) { @@ -460,7 +452,7 @@ export function IssueViewsStateProvider({ case 'DELETE_VIEW': return deleteView(state, tabListState); case 'CREATE_NEW_VIEW': - return createNewView(state, action); + return createNewView(state, action, tabListState); case 'SET_TEMP_VIEW': return setTempView(state, action); case 'DISCARD_TEMP_VIEW': @@ -504,14 +496,59 @@ export function IssueViewsStateProvider({ tempView: initialTempView, }); + const debounceUpdateViews = useMemo( + () => + debounce((newTabs: IssueView[]) => { + if (newTabs) { + updateViews({ + orgSlug: organization.slug, + groupSearchViews: newTabs + .filter(tab => tab.isCommitted) + .map(tab => ({ + // Do not send over an ID if it's a temporary or default tab so that + // the backend will save these and generate permanent Ids for them + ...(tab.id[0] !== '_' && !tab.id.startsWith('default') + ? {id: tab.id} + : {}), + name: tab.label, + query: tab.query, + querySort: tab.querySort, + })), + }); + } + }, 500), + [organization.slug, updateViews] + ); + + const updateQueryParams = (params: IssueViewsQueryUpdateParams) => { + const {newQueryParams, replace = false} = params; + navigate( + normalizeUrl({ + ...location, + query: { + ...queryParams, + ...newQueryParams, + }, + }), + {replace} + ); + }; + const dispatchWrapper = (action: IssueViewsActions) => { const newState = reducer(state, action); dispatch(action); + // These actions are outside of the dispatch to avoid introducing side effects in the reducer if (action.type === 'SYNC_VIEWS_TO_BACKEND' || action.syncViews) { debounceUpdateViews(newState.views); } + if (action.updateQueryParams) { + queueMicrotask(() => { + updateQueryParams(action.updateQueryParams!); + }); + } + const actionAnalyticsKey = ACTION_ANALYTICS_MAP[action.type]; if (actionAnalyticsKey) { trackAnalytics(actionAnalyticsKey, { From 9d3111b087ef69288e067539da2f428838f774e4 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 27 Dec 2024 11:44:20 -0500 Subject: [PATCH 494/757] fix(eap): Handle multi y axis top events query (#82547) When there are multiple y axis in the rpc results, we were over writing the values and returning only 1 timeseries instead of 1 for each y axis. --- src/sentry/snuba/spans_rpc.py | 47 ++++++++++++------- ..._organization_events_stats_span_indexed.py | 42 +++++++++++++++++ 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/sentry/snuba/spans_rpc.py b/src/sentry/snuba/spans_rpc.py index bfc312ac11c03a..9b56b742b8c78e 100644 --- a/src/sentry/snuba/spans_rpc.py +++ b/src/sentry/snuba/spans_rpc.py @@ -1,4 +1,5 @@ import logging +from collections import defaultdict from datetime import timedelta from typing import Any @@ -223,7 +224,7 @@ def run_timeseries_query( result: SnubaData = [] confidences: SnubaData = [] for timeseries in rpc_response.result_timeseries: - processed, confidence = _process_timeseries(timeseries, params, granularity_secs) + processed, confidence = _process_all_timeseries([timeseries], params, granularity_secs) if len(result) == 0: result = processed confidences = confidence @@ -259,7 +260,7 @@ def run_timeseries_query( if comp_rpc_response.result_timeseries: timeseries = comp_rpc_response.result_timeseries[0] - processed, _ = _process_timeseries(timeseries, params, granularity_secs) + processed, _ = _process_all_timeseries([timeseries], params, granularity_secs) label = get_function_alias(timeseries.label) for existing, new in zip(result, processed): existing["comparisonCount"] = new[label] @@ -380,7 +381,7 @@ def run_top_events_timeseries_query( other_response = snuba_rpc.timeseries_rpc(other_request) """Process the results""" - map_result_key_to_timeseries = {} + map_result_key_to_timeseries = defaultdict(list) for timeseries in rpc_response.result_timeseries: groupby_attributes = timeseries.group_by_attributes remapped_groupby = {} @@ -395,12 +396,12 @@ def run_top_events_timeseries_query( resolved_groupby, _ = search_resolver.resolve_attribute(col) remapped_groupby[col] = groupby_attributes[resolved_groupby.internal_name] result_key = create_result_key(remapped_groupby, groupby_columns, {}) - map_result_key_to_timeseries[result_key] = timeseries + map_result_key_to_timeseries[result_key].append(timeseries) final_result = {} # Top Events actually has the order, so we need to iterate through it, regenerate the result keys for index, row in enumerate(top_events["data"]): result_key = create_result_key(row, groupby_columns, {}) - result_data, result_confidence = _process_timeseries( + result_data, result_confidence = _process_all_timeseries( map_result_key_to_timeseries[result_key], params, granularity_secs, @@ -416,8 +417,8 @@ def run_top_events_timeseries_query( granularity_secs, ) if other_response.result_timeseries: - result_data, result_confidence = _process_timeseries( - other_response.result_timeseries[0], + result_data, result_confidence = _process_all_timeseries( + [timeseries for timeseries in other_response.result_timeseries], params, granularity_secs, ) @@ -434,19 +435,29 @@ def run_top_events_timeseries_query( return final_result -def _process_timeseries( - timeseries: TimeSeries, params: SnubaParams, granularity_secs: int, order: int | None = None +def _process_all_timeseries( + all_timeseries: list[TimeSeries], + params: SnubaParams, + granularity_secs: int, + order: int | None = None, ) -> tuple[SnubaData, SnubaData]: result: SnubaData = [] confidence: SnubaData = [] - # Timeseries serialization expects the function alias (eg. `count` not `count()`) - label = get_function_alias(timeseries.label) - if len(result) < len(timeseries.buckets): - for bucket in timeseries.buckets: - result.append({"time": bucket.seconds}) - confidence.append({"time": bucket.seconds}) - for index, data_point in enumerate(timeseries.data_points): - result[index][label] = process_value(data_point.data) - confidence[index][label] = CONFIDENCES.get(data_point.reliability, None) + + for timeseries in all_timeseries: + # Timeseries serialization expects the function alias (eg. `count` not `count()`) + label = get_function_alias(timeseries.label) + if result: + for index, bucket in enumerate(timeseries.buckets): + assert result[index]["time"] == bucket.seconds + assert confidence[index]["time"] == bucket.seconds + else: + for bucket in timeseries.buckets: + result.append({"time": bucket.seconds}) + confidence.append({"time": bucket.seconds}) + + for index, data_point in enumerate(timeseries.data_points): + result[index][label] = process_value(data_point.data) + confidence[index][label] = CONFIDENCES.get(data_point.reliability, None) return result, confidence diff --git a/tests/snuba/api/endpoints/test_organization_events_stats_span_indexed.py b/tests/snuba/api/endpoints/test_organization_events_stats_span_indexed.py index 9ff42ed6b08148..b23105e52d2529 100644 --- a/tests/snuba/api/endpoints/test_organization_events_stats_span_indexed.py +++ b/tests/snuba/api/endpoints/test_organization_events_stats_span_indexed.py @@ -482,6 +482,48 @@ def test_top_events_empty_other(self): assert result[1][0]["count"] == expected, key assert response.data["foo"]["meta"]["dataset"] == self.dataset + def test_top_events_multi_y_axis(self): + # Each of these denotes how many events to create in each minute + for transaction in ["foo", "bar", "baz"]: + self.store_spans( + [ + self.create_span( + {"sentry_tags": {"transaction": transaction, "status": "success"}}, + start_ts=self.day_ago + timedelta(minutes=1), + duration=2000, + ), + ], + is_eap=self.is_eap, + ) + + response = self._do_request( + data={ + "start": self.day_ago, + "end": self.day_ago + timedelta(minutes=6), + "interval": "1m", + "yAxis": ["count()", "p50(span.duration)"], + "field": ["transaction", "count()", "p50(span.duration)"], + "orderby": ["transaction"], + "project": self.project.id, + "dataset": self.dataset, + "excludeOther": 0, + "topEvents": 2, + }, + ) + assert response.status_code == 200, response.content + + for key in ["Other", "bar", "baz"]: + assert key in response.data + for y_axis in ["count()", "p50(span.duration)"]: + assert y_axis in response.data[key] + assert response.data[key][y_axis]["meta"]["dataset"] == self.dataset + counts = response.data[key]["count()"]["data"][0:6] + for expected, result in zip([0, 1, 0, 0, 0, 0], counts): + assert result[1][0]["count"] == expected, key + p50s = response.data[key]["p50(span.duration)"]["data"][0:6] + for expected, result in zip([0, 2000, 0, 0, 0, 0], p50s): + assert result[1][0]["count"] == expected, key + def test_top_events_with_project(self): # Each of these denotes how many events to create in each minute projects = [self.create_project(), self.create_project()] From 94d458d79838529361a8892f896a149c00622e91 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Fri, 27 Dec 2024 09:41:11 -0800 Subject: [PATCH 495/757] fix(issues): Open tag csv in new window (#82546) --- .../app/views/issueDetails/groupTags/groupTagsDrawer.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx index 1c7d9dcef4b5a6..028e59145f037a 100644 --- a/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx +++ b/static/app/views/issueDetails/groupTags/groupTagsDrawer.tsx @@ -114,7 +114,13 @@ export function GroupTagsDrawer({group}: {group: Group}) { { key: 'export-page', label: t('Export Page to CSV'), - to: `${organization.slug}/${project.slug}/issues/${group.id}/tags/${tagKey}/export/`, + // TODO(issues): Dropdown menu doesn't support hrefs yet + onAction: () => { + window.open( + `/${organization.slug}/${project.slug}/issues/${group.id}/tags/${tagKey}/export/`, + '_blank' + ); + }, }, { key: 'export-all', From 63eb539093d294f1921cf4b0b4860b76610c70a1 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 27 Dec 2024 13:09:37 -0500 Subject: [PATCH 496/757] test(js): Remove some browserHistory from tests (#82581) It's already assigned jest mocks --- .../app/views/alerts/rules/issue/details/ruleDetails.spec.tsx | 1 - static/app/views/alerts/rules/issue/index.spec.tsx | 1 - static/app/views/performance/content.spec.tsx | 1 - static/app/views/performance/table.spec.tsx | 1 - .../transactionSpans/spanDetails/index.spec.tsx | 3 +-- .../transactionSummary/transactionTags/index.spec.tsx | 1 - static/app/views/performance/trends/index.spec.tsx | 3 +-- static/app/views/performance/vitalDetail/index.spec.tsx | 1 - 8 files changed, 2 insertions(+), 10 deletions(-) diff --git a/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx b/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx index 0d1d7af82ceb16..3fd83d0f4b872a 100644 --- a/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx +++ b/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx @@ -41,7 +41,6 @@ describe('AlertRuleDetails', () => { }; beforeEach(() => { - browserHistory.push = jest.fn(); MockApiClient.addMockResponse({ url: `/projects/${organization.slug}/${project.slug}/rules/${rule.id}/`, body: rule, diff --git a/static/app/views/alerts/rules/issue/index.spec.tsx b/static/app/views/alerts/rules/issue/index.spec.tsx index bf17d3a6a5c597..1aff61b922759b 100644 --- a/static/app/views/alerts/rules/issue/index.spec.tsx +++ b/static/app/views/alerts/rules/issue/index.spec.tsx @@ -125,7 +125,6 @@ const createWrapper = (props = {}) => { describe('IssueRuleEditor', function () { beforeEach(function () { MockApiClient.clearMockResponses(); - browserHistory.replace = jest.fn(); MockApiClient.addMockResponse({ url: '/projects/org-slug/project-slug/rules/configuration/', body: ProjectAlertRuleConfigurationFixture(), diff --git a/static/app/views/performance/content.spec.tsx b/static/app/views/performance/content.spec.tsx index 2e452beedd5391..1df41d058e22c6 100644 --- a/static/app/views/performance/content.spec.tsx +++ b/static/app/views/performance/content.spec.tsx @@ -75,7 +75,6 @@ function initializeTrendsData(query, addDefaultQuery = true) { describe('Performance > Content', function () { beforeEach(function () { act(() => void TeamStore.loadInitialData([], false, null)); - browserHistory.push = jest.fn(); jest.spyOn(pageFilters, 'updateDateTime'); MockApiClient.addMockResponse({ diff --git a/static/app/views/performance/table.spec.tsx b/static/app/views/performance/table.spec.tsx index 4c19caff60a3c6..9ac52097156451 100644 --- a/static/app/views/performance/table.spec.tsx +++ b/static/app/views/performance/table.spec.tsx @@ -109,7 +109,6 @@ function mockEventView(data) { describe('Performance > Table', function () { let eventsMock; beforeEach(function () { - browserHistory.push = jest.fn(); mockUseLocation.mockReturnValue( LocationFixture({pathname: '/organizations/org-slug/performance/summary'}) ); diff --git a/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx index 79d9dde6b32abf..21e78adb92a492 100644 --- a/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx @@ -38,8 +38,7 @@ describe('Performance > Transaction Spans > Span Summary', function () { afterEach(function () { MockApiClient.clearMockResponses(); ProjectsStore.reset(); - // need to typecast to any to be able to call mockReset - (browserHistory.push as any).mockReset(); + jest.mocked(browserHistory.push).mockReset(); }); describe('Without Span Data', function () { diff --git a/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx index c0d7e1ebed05ee..677832e5610a8c 100644 --- a/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx @@ -55,7 +55,6 @@ describe('Performance > Transaction Tags', function () { mockUseLocation.mockReturnValue( LocationFixture({pathname: '/organizations/org-slug/performance/summary/tags/'}) ); - browserHistory.replace = jest.fn(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [], diff --git a/static/app/views/performance/trends/index.spec.tsx b/static/app/views/performance/trends/index.spec.tsx index ad5a8e86f517d9..f0a69a618980ff 100644 --- a/static/app/views/performance/trends/index.spec.tsx +++ b/static/app/views/performance/trends/index.spec.tsx @@ -176,7 +176,6 @@ describe('Performance > Trends', function () { state: undefined, }); - browserHistory.push = jest.fn(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [], @@ -747,7 +746,7 @@ describe('Performance > Trends', function () { } ); - (browserHistory.push as any).mockReset(); + jest.mocked(browserHistory.push).mockReset(); const byTransactionLink = await screen.findByTestId('breadcrumb-link'); diff --git a/static/app/views/performance/vitalDetail/index.spec.tsx b/static/app/views/performance/vitalDetail/index.spec.tsx index b99eb90388aab5..600e7a2fc8634a 100644 --- a/static/app/views/performance/vitalDetail/index.spec.tsx +++ b/static/app/views/performance/vitalDetail/index.spec.tsx @@ -74,7 +74,6 @@ describe('Performance > VitalDetail', function () { beforeEach(function () { TeamStore.loadInitialData([], false, null); ProjectsStore.loadInitialData([project]); - browserHistory.push = jest.fn(); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/projects/`, body: [], From f222d5f30362a977f365229ba4f43d60c8b5bc57 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 27 Dec 2024 13:23:15 -0500 Subject: [PATCH 497/757] test(rr6): Assign browserHistory to the same mocks as router (#82582) --- static/app/views/dashboards/orgDashboards.spec.tsx | 3 ++- tests/js/sentry-test/reactTestingLibrary.tsx | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/static/app/views/dashboards/orgDashboards.spec.tsx b/static/app/views/dashboards/orgDashboards.spec.tsx index 02675e75007661..79893feb1ef8cc 100644 --- a/static/app/views/dashboards/orgDashboards.spec.tsx +++ b/static/app/views/dashboards/orgDashboards.spec.tsx @@ -237,7 +237,8 @@ describe('OrgDashboards', () => { {router: initialData.router} ); - expect(browserHistory.replace).not.toHaveBeenCalled(); + // The first call is done by the page filters + expect(initialData.router.replace).not.toHaveBeenCalledTimes(2); }); it('does not add query params for page filters if none are saved', () => { diff --git a/tests/js/sentry-test/reactTestingLibrary.tsx b/tests/js/sentry-test/reactTestingLibrary.tsx index 1217a8ec69c286..7b0c4ee9f5f496 100644 --- a/tests/js/sentry-test/reactTestingLibrary.tsx +++ b/tests/js/sentry-test/reactTestingLibrary.tsx @@ -12,6 +12,7 @@ import {GlobalDrawer} from 'sentry/components/globalDrawer'; import GlobalModal from 'sentry/components/globalModal'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; +import {DANGEROUS_SET_TEST_HISTORY} from 'sentry/utils/browserHistory'; import {QueryClientProvider} from 'sentry/utils/queryClient'; import {lightTheme} from 'sentry/utils/theme'; import {OrganizationContext} from 'sentry/views/organizationContext'; @@ -104,6 +105,15 @@ function makeAllTheProviders(options: ProviderOptions) { }; } + DANGEROUS_SET_TEST_HISTORY({ + goBack: router.goBack, + push: router.push, + replace: router.replace, + listen: jest.fn(() => {}), + listenBefore: jest.fn(), + getCurrentLocation: jest.fn(() => ({pathname: '', query: {}})), + }); + // By default react-router 6 catches exceptions and displays the stack // trace. For tests we want them to bubble out function ErrorBoundary(): React.ReactNode { From 4288093028e900534a58d221a2f97baef61f544d Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 27 Dec 2024 13:29:52 -0500 Subject: [PATCH 498/757] styles(explore): Make more room to fit legend above chart (#82577) The legend is overlapping with itself and the y axis labels. Make some small adjustments so it's easier to read. # Screenshots ## Before ![image](https://github.com/user-attachments/assets/26b03bad-eb14-461b-95e0-42d67dbe3e19) ## After ![image](https://github.com/user-attachments/assets/468d15f5-5eea-4a63-8e57-5a2b2b90eae1) --- static/app/views/explore/charts/index.tsx | 6 +++++- .../views/insights/common/components/chart.tsx | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/static/app/views/explore/charts/index.tsx b/static/app/views/explore/charts/index.tsx index f6f50d2e521473..a1aaf3c122e786 100644 --- a/static/app/views/explore/charts/index.tsx +++ b/static/app/views/explore/charts/index.tsx @@ -334,10 +334,14 @@ export function ExploreCharts({query, setConfidence, setError}: ExploreChartsPro grid={{ left: '0', right: '0', - top: '8px', + top: '32px', // make room to fit the legend above the chart bottom: '0', }} legendFormatter={value => formatVersion(value)} + legendOptions={{ + itemGap: 24, + top: '4px', + }} data={data} error={error} loading={loading} diff --git a/static/app/views/insights/common/components/chart.tsx b/static/app/views/insights/common/components/chart.tsx index 8e3093c7a80dae..6b0f924b527159 100644 --- a/static/app/views/insights/common/components/chart.tsx +++ b/static/app/views/insights/common/components/chart.tsx @@ -2,7 +2,7 @@ import type {RefObject} from 'react'; import {createContext, useContext, useEffect, useMemo, useReducer, useRef} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; -import type {LineSeriesOption} from 'echarts'; +import type {LegendComponentOption, LineSeriesOption} from 'echarts'; import * as echarts from 'echarts/core'; import type { MarkLineOption, @@ -88,6 +88,7 @@ type Props = { hideYAxis?: boolean; hideYAxisSplitLine?: boolean; legendFormatter?: (name: string) => string; + legendOptions?: LegendComponentOption; log?: boolean; markLine?: MarkLineOption; onClick?: EChartClickHandler; @@ -139,6 +140,7 @@ function Chart({ error, onLegendSelectChanged, onDataZoom, + legendOptions, /** * Setting a default formatter for some reason causes `>` to * render correctly instead of rendering as `>` in the legend. @@ -346,6 +348,10 @@ function Chart({ })(deDupedParams, asyncTicket); }; + const legend = isLegendVisible + ? {top: 0, right: 10, formatter: legendFormatter, ...legendOptions} + : undefined; + const areaChartProps = { seriesOptions: { showSymbol: false, @@ -353,7 +359,7 @@ function Chart({ grid, yAxes, utc, - legend: isLegendVisible ? {top: 0, right: 10, formatter: legendFormatter} : undefined, + legend, isGroupedByDate: true, showTimeInTooltip: true, tooltip: { @@ -401,9 +407,7 @@ function Chart({ tooltip={areaChartProps.tooltip} colors={colors} grid={grid} - legend={ - isLegendVisible ? {top: 0, right: 10, formatter: legendFormatter} : undefined - } + legend={legend} onClick={onClick} onMouseOut={onMouseOut} onMouseOver={onMouseOver} @@ -487,9 +491,7 @@ function Chart({ }} colors={colors} grid={grid} - legend={ - isLegendVisible ? {top: 0, right: 10, formatter: legendFormatter} : undefined - } + legend={legend} onClick={onClick} /> ); From 9524f2118b979f4747a7b8f86d88db24f3358be1 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Fri, 27 Dec 2024 10:37:22 -0800 Subject: [PATCH 499/757] ref(ui): Add missing test types (#82579) --- .../app/actionCreators/pageFilters.spec.tsx | 2 +- static/app/components/autoComplete.spec.tsx | 24 ++++++++++++++--- .../charts/components/xAxis.spec.tsx | 22 ++++++++++------ .../components/charts/components/xAxis.tsx | 4 +-- .../events/eventViewHierarchy.spec.tsx | 4 +-- .../highlights/highlightsDataSection.spec.tsx | 2 +- .../stackTrace/rawContent.spec.tsx | 4 +-- .../interfaces/searchBarAction.spec.tsx | 2 +- .../interfaces/spans/traceView.spec.tsx | 4 +-- .../events/viewHierarchy/index.spec.tsx | 6 ++--- .../components/forms/formField/index.spec.tsx | 2 +- .../group/externalIssueForm.spec.tsx | 13 ++++++---- .../components/idBadge/memberBadge.spec.tsx | 2 +- static/app/components/indicators.spec.tsx | 3 ++- static/app/components/lastCommit.spec.tsx | 4 +-- .../modals/createTeamModal.spec.tsx | 2 +- ...dashboardWidgetQuerySelectorModal.spec.tsx | 16 +++++++----- .../modals/featureTourModal.spec.tsx | 3 ++- .../modals/inviteMembersModal/index.spec.tsx | 16 +++++++++--- .../addToDashboardModal.spec.tsx | 4 +-- .../modals/widgetViewerModal.spec.tsx | 13 +++++----- .../rules/metric/ruleConditionsForm.spec.tsx | 6 ++--- .../dashboards/datasetConfig/errors.spec.tsx | 9 +++++-- .../datasetConfig/transactions.spec.tsx | 6 ++++- static/app/views/dashboards/detail.spec.tsx | 15 ++++++----- .../views/dashboards/orgDashboards.spec.tsx | 26 +++++-------------- static/app/views/dashboards/utils.spec.tsx | 22 ++++++++++------ .../components/datasetSelector.spec.tsx | 4 +-- .../components/nameAndDescFields.spec.tsx | 4 +-- .../components/typeSelector.spec.tsx | 4 +-- .../components/visualize.spec.tsx | 4 +-- .../components/widgetBuilderSlideout.spec.tsx | 2 +- .../hooks/useWidgetBuilderState.spec.tsx | 2 +- .../widgetCard/widgetQueries.spec.tsx | 6 ++--- .../views/dataExport/dataDownload.spec.tsx | 2 +- static/app/views/discover/homepage.spec.tsx | 5 +++- static/app/views/discover/miniGraph.spec.tsx | 4 ++- static/app/views/discover/queryList.spec.tsx | 24 ++++++++--------- .../views/discover/savedQuery/index.spec.tsx | 26 ++++++++++--------- .../discover/table/columnEditModal.spec.tsx | 13 ++++++---- static/app/views/discover/tags.spec.tsx | 2 +- .../contexts/pageParamsContext/index.spec.tsx | 13 +++++++--- .../explore/hooks/useAddToDashboard.spec.tsx | 4 +-- .../explore/hooks/useChartInterval.spec.tsx | 4 ++- .../hooks/useDragNDropColumns.spec.tsx | 18 ++++++++++--- .../performanceScoreBreakdownChart.spec.tsx | 2 +- .../components/webVitalsDetailPanel.spec.tsx | 3 ++- .../webVitals/views/pageOverview.spec.tsx | 2 +- .../views/webVitalsLandingPage.spec.tsx | 2 +- .../http/views/httpDomainSummaryPage.spec.tsx | 3 ++- .../http/views/httpLandingPage.spec.tsx | 3 ++- .../components/eventSamples.spec.tsx | 2 +- .../tables/spanOperationTable.spec.tsx | 2 +- .../views/screenSummaryPage.spec.tsx | 2 +- .../components/eventSamples.spec.tsx | 2 +- .../tables/eventSamplesTable.spec.tsx | 4 ++- .../tables/screenLoadSpansTable.spec.tsx | 2 +- .../queues/charts/throughputChart.spec.tsx | 2 +- .../queues/views/queuesLandingPage.spec.tsx | 3 ++- .../awsLambdaCloudformation.spec.tsx | 2 +- .../issueDetails/traceDataSection.spec.tsx | 2 +- .../views/organizationRestore/index.spec.tsx | 3 ++- .../views/organizationStats/index.spec.tsx | 4 +-- static/app/views/performance/content.spec.tsx | 17 +++++++++--- static/app/views/performance/table.spec.tsx | 8 +++--- .../transactionEvents/eventsTable.spec.tsx | 16 +----------- .../transactionEvents/index.spec.tsx | 2 +- .../transactionOverview/content.spec.tsx | 2 +- .../transactionOverview/tagExplorer.spec.tsx | 18 +++---------- .../spanDetails/index.spec.tsx | 2 +- .../transactionThresholdButton.spec.tsx | 7 ++++- .../performance/vitalDetail/index.spec.tsx | 8 ++++-- .../views/projectsDashboard/index.spec.tsx | 4 +-- static/app/views/releases/list/index.spec.tsx | 24 +++++++++++------ .../app/views/relocation/relocation.spec.tsx | 6 ++--- .../detail/network/details/content.spec.tsx | 18 ++++++------- .../network/truncateJson/fixJson.spec.ts | 4 +-- .../detail/network/useSortNetwork.spec.tsx | 4 +-- .../views/replays/list/listContent.spec.tsx | 6 ++--- .../settings/account/accountClose.spec.tsx | 2 +- .../settings/account/apiNewToken.spec.tsx | 12 ++++----- .../settings/account/passwordForm.spec.tsx | 2 +- .../components/dataScrubbing/index.spec.tsx | 2 +- .../permissionSelection.spec.tsx | 8 +++--- .../permissionsObserver.spec.tsx | 2 +- .../sentryApplicationDashboard/index.spec.tsx | 4 +-- .../sentryApplicationDetails.spec.tsx | 10 +++---- .../integrationExternalMappingForm.spec.tsx | 6 +++-- .../integrationListDirectory.spec.tsx | 2 +- .../sentryAppDetailedView.spec.tsx | 10 +++---- .../organizationTeams/teamMembers.spec.tsx | 2 +- .../project/projectFilters/index.spec.tsx | 4 ++- .../viewCodeOwnerModal.spec.tsx | 4 ++- .../projectGeneralSettings/index.spec.tsx | 4 +-- .../projectPerformance.spec.tsx | 6 +++-- .../projectPerformance/projectPerformance.tsx | 13 ++++++---- static/app/views/unsubscribe/issue.spec.tsx | 4 ++- .../views/userFeedback/userFeedbackEmpty.tsx | 2 +- .../sentry-test/reactTestingLibrary.spec.tsx | 2 +- 99 files changed, 383 insertions(+), 286 deletions(-) diff --git a/static/app/actionCreators/pageFilters.spec.tsx b/static/app/actionCreators/pageFilters.spec.tsx index 5fb56853b7a600..1c587d4c71e3dc 100644 --- a/static/app/actionCreators/pageFilters.spec.tsx +++ b/static/app/actionCreators/pageFilters.spec.tsx @@ -32,7 +32,7 @@ describe('PageFilters ActionCreators', function () { }); describe('initializeUrlState', function () { - let router; + let router: ReturnType; const key = `global-selection:${organization.slug}`; beforeEach(() => { diff --git a/static/app/components/autoComplete.spec.tsx b/static/app/components/autoComplete.spec.tsx index e85f910f870f6a..b92558de4ae0d2 100644 --- a/static/app/components/autoComplete.spec.tsx +++ b/static/app/components/autoComplete.spec.tsx @@ -23,7 +23,7 @@ const items = [ * "controlled" props where does not handle state */ describe('AutoComplete', function () { - let input; + let input: HTMLInputElement; let autoCompleteState: any[] = []; const mocks = { onSelect: jest.fn(), @@ -36,12 +36,30 @@ describe('AutoComplete', function () { autoCompleteState = []; }); - function List({registerItemCount, itemCount, ...props}) { + function List({ + registerItemCount, + itemCount, + ...props + }: { + children: React.ReactNode; + itemCount: number; + registerItemCount: (count?: number) => void; + }) { useEffect(() => void registerItemCount(itemCount), [itemCount, registerItemCount]); return
      ; } - function Item({registerVisibleItem, item, index, ...props}) { + function Item({ + registerVisibleItem, + item, + index, + ...props + }: { + children: React.ReactNode; + index: number; + item: {name?: string}; + registerVisibleItem: (index: number, item: any) => () => void; + }) { useEffect(() => registerVisibleItem(index, item), [registerVisibleItem, index, item]); return
    • ; } diff --git a/static/app/components/charts/components/xAxis.spec.tsx b/static/app/components/charts/components/xAxis.spec.tsx index 750772771428e9..7c744b23c667bf 100644 --- a/static/app/components/charts/components/xAxis.spec.tsx +++ b/static/app/components/charts/components/xAxis.spec.tsx @@ -9,8 +9,8 @@ jest.mock('moment-timezone', () => { }); describe('Chart XAxis', function () { - let axisLabelFormatter; - let xAxisObj; + let axisLabelFormatter: (value: string | number, index: number) => string; + let xAxisObj!: ReturnType; const props: XAxisProps = { isGroupedByDate: true, theme: lightTheme, @@ -27,7 +27,8 @@ describe('Chart XAxis', function () { utc: false, }); - axisLabelFormatter = xAxisObj.axisLabel.formatter; + // @ts-expect-error formatter type is missing + axisLabelFormatter = xAxisObj.axisLabel!.formatter; }); it('formats axis label for first data point', function () { @@ -47,7 +48,8 @@ describe('Chart XAxis', function () { utc: true, }); - axisLabelFormatter = xAxisObj.axisLabel.formatter; + // @ts-expect-error formatter type is missing + axisLabelFormatter = xAxisObj.axisLabel!.formatter; }); it('formats axis label for first data point', function () { @@ -67,7 +69,8 @@ describe('Chart XAxis', function () { period: '7d', }); - axisLabelFormatter = xAxisObj.axisLabel.formatter; + // @ts-expect-error formatter type is missing + axisLabelFormatter = xAxisObj.axisLabel!.formatter; }); it('formats axis label for first data point', function () { @@ -89,7 +92,8 @@ describe('Chart XAxis', function () { utc: false, }); - axisLabelFormatter = xAxisObj.axisLabel.formatter; + // @ts-expect-error formatter type is missing + axisLabelFormatter = xAxisObj.axisLabel!.formatter; }); it('formats axis label for first data point', function () { @@ -109,7 +113,8 @@ describe('Chart XAxis', function () { utc: true, }); - axisLabelFormatter = xAxisObj.axisLabel.formatter; + // @ts-expect-error formatter type is missing + axisLabelFormatter = xAxisObj.axisLabel!.formatter; }); it('formats axis label for first data point', function () { @@ -130,7 +135,8 @@ describe('Chart XAxis', function () { utc: true, }); - axisLabelFormatter = xAxisObj.axisLabel.formatter; + // @ts-expect-error formatter type is missing + axisLabelFormatter = xAxisObj.axisLabel!.formatter; }); it('formats axis label for first data point', function () { diff --git a/static/app/components/charts/components/xAxis.tsx b/static/app/components/charts/components/xAxis.tsx index e9d95a072b6a1a..528e85a9d54f7e 100644 --- a/static/app/components/charts/components/xAxis.tsx +++ b/static/app/components/charts/components/xAxis.tsx @@ -33,7 +33,7 @@ function XAxis({ addSecondsToTimeFormat = false, ...props }: XAxisProps): XAXisComponentOption { - const AxisLabelFormatter = (value: string, index: number) => { + const AxisLabelFormatter = (value: string | number, index: number) => { const firstItem = index === 0; // Always show the date of the first item. Otherwise check the interval duration const showDate = firstItem ? true : !computeShortInterval({start, end, period}); @@ -51,7 +51,7 @@ function XAxis({ } if (props.truncate) { - return truncationFormatter(value, props.truncate); + return truncationFormatter(value as string, props.truncate); } return undefined; diff --git a/static/app/components/events/eventViewHierarchy.spec.tsx b/static/app/components/events/eventViewHierarchy.spec.tsx index 2455dc4f2922fe..06e38cbc521f98 100644 --- a/static/app/components/events/eventViewHierarchy.spec.tsx +++ b/static/app/components/events/eventViewHierarchy.spec.tsx @@ -53,8 +53,8 @@ const organization = OrganizationFixture({ const event = EventFixture(); describe('Event View Hierarchy', function () { - let mockAttachment; - let mockProject; + let mockAttachment!: ReturnType; + let mockProject!: ReturnType; beforeEach(function () { mockAttachment = EventAttachmentFixture({type: 'event.view_hierarchy'}); mockProject = ProjectFixture(); diff --git a/static/app/components/events/highlights/highlightsDataSection.spec.tsx b/static/app/components/events/highlights/highlightsDataSection.spec.tsx index 54bfef402d4749..9188e583dc853f 100644 --- a/static/app/components/events/highlights/highlightsDataSection.spec.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.spec.tsx @@ -21,7 +21,7 @@ describe('HighlightsDataSection', function () { contexts: TEST_EVENT_CONTEXTS, tags: TEST_EVENT_TAGS, }); - const eventTagMap = TEST_EVENT_TAGS.reduce( + const eventTagMap = TEST_EVENT_TAGS.reduce>( (tagMap, tag) => ({...tagMap, [tag.key]: tag.value}), {} ); diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/rawContent.spec.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/rawContent.spec.tsx index b53c5a6a694736..00f6eec8428db4 100644 --- a/static/app/components/events/interfaces/crashContent/stackTrace/rawContent.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/stackTrace/rawContent.spec.tsx @@ -161,7 +161,7 @@ describe('RawStacktraceContent', () => { ); }); - const inAppFrame = (fnName, line) => + const inAppFrame = (fnName: string, line: number) => FrameFixture({ function: fnName, module: 'example.application', @@ -169,7 +169,7 @@ describe('RawStacktraceContent', () => { filename: 'application', platform: undefined, }); - const systemFrame = (fnName, line) => + const systemFrame = (fnName: string, line: number) => FrameFixture({ function: fnName, module: 'example.application', diff --git a/static/app/components/events/interfaces/searchBarAction.spec.tsx b/static/app/components/events/interfaces/searchBarAction.spec.tsx index 744f6721adb507..b1af6870ff031f 100644 --- a/static/app/components/events/interfaces/searchBarAction.spec.tsx +++ b/static/app/components/events/interfaces/searchBarAction.spec.tsx @@ -63,7 +63,7 @@ const options: NonNullable< ]; describe('SearchBarAction', () => { - let handleFilter; + let handleFilter!: jest.Mock; beforeEach(() => { handleFilter = jest.fn(); diff --git a/static/app/components/events/interfaces/spans/traceView.spec.tsx b/static/app/components/events/interfaces/spans/traceView.spec.tsx index 8a916e488f39b3..6e08acbbbea14f 100644 --- a/static/app/components/events/interfaces/spans/traceView.spec.tsx +++ b/static/app/components/events/interfaces/spans/traceView.spec.tsx @@ -20,14 +20,14 @@ import ProjectsStore from 'sentry/stores/projectsStore'; import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext'; import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery'; -function initializeData(settings) { +function initializeData(settings: Parameters[0]) { const data = _initializeData(settings); ProjectsStore.loadInitialData(data.projects); return data; } describe('TraceView', () => { - let data; + let data!: ReturnType; beforeEach(() => { data = initializeData({}); diff --git a/static/app/components/events/viewHierarchy/index.spec.tsx b/static/app/components/events/viewHierarchy/index.spec.tsx index ae97f3986a3c05..5c847dd867b197 100644 --- a/static/app/components/events/viewHierarchy/index.spec.tsx +++ b/static/app/components/events/viewHierarchy/index.spec.tsx @@ -2,7 +2,7 @@ import {ProjectFixture} from 'sentry-fixture/project'; import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; -import {ViewHierarchy} from '.'; +import {ViewHierarchy, type ViewHierarchyData} from './index'; // Mocks for useVirtualizedTree hook class ResizeObserver { @@ -47,8 +47,8 @@ const DEFAULT_MOCK_DATA = { }; describe('View Hierarchy', function () { - let MOCK_DATA; - let project; + let MOCK_DATA!: ViewHierarchyData; + let project!: ReturnType; beforeEach(() => { MOCK_DATA = DEFAULT_MOCK_DATA; project = ProjectFixture(); diff --git a/static/app/components/forms/formField/index.spec.tsx b/static/app/components/forms/formField/index.spec.tsx index 5935a53f1787d5..6c1e75f5e9cf96 100644 --- a/static/app/components/forms/formField/index.spec.tsx +++ b/static/app/components/forms/formField/index.spec.tsx @@ -5,7 +5,7 @@ import Form from 'sentry/components/forms/form'; import FormModel from 'sentry/components/forms/model'; describe('FormField + model', function () { - let model; + let model!: FormModel; beforeEach(function () { model = new FormModel(); diff --git a/static/app/components/group/externalIssueForm.spec.tsx b/static/app/components/group/externalIssueForm.spec.tsx index 1ac255859ba244..98559091df17c2 100644 --- a/static/app/components/group/externalIssueForm.spec.tsx +++ b/static/app/components/group/externalIssueForm.spec.tsx @@ -11,8 +11,8 @@ import ExternalIssueForm from 'sentry/components/group/externalIssueForm'; jest.mock('lodash/debounce', () => { const debounceMap = new Map(); const mockDebounce = - (fn, timeout) => - (...args) => { + (fn: (...args: any[]) => void, timeout: number) => + (...args: any[]) => { if (debounceMap.has(fn)) { clearTimeout(debounceMap.get(fn)); } @@ -28,7 +28,9 @@ jest.mock('lodash/debounce', () => { }); describe('ExternalIssueForm', () => { - let group, integration, formConfig; + let group!: ReturnType; + let integration!: ReturnType; + let formConfig!: any; const onChange = jest.fn(); beforeEach(() => { MockApiClient.clearMockResponses(); @@ -48,7 +50,7 @@ describe('ExternalIssueForm', () => { match: [MockApiClient.matchQuery({action: 'create'})], }); - const styledWrapper = styled(c => c.children); + const styledWrapper = styled((c: {children: React.ReactNode}) => c.children); const wrapper = render( { }); }); describe('link', () => { - let externalIssueField, getFormConfigRequest; + let externalIssueField!: any; + let getFormConfigRequest!: jest.Mock; beforeEach(() => { externalIssueField = { name: 'externalIssue', diff --git a/static/app/components/idBadge/memberBadge.spec.tsx b/static/app/components/idBadge/memberBadge.spec.tsx index e4c959a25fb60f..737c06c4ea36c8 100644 --- a/static/app/components/idBadge/memberBadge.spec.tsx +++ b/static/app/components/idBadge/memberBadge.spec.tsx @@ -6,7 +6,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import MemberBadge from 'sentry/components/idBadge/memberBadge'; describe('MemberBadge', function () { - let member; + let member!: ReturnType; beforeEach(() => { member = MemberFixture(); }); diff --git a/static/app/components/indicators.spec.tsx b/static/app/components/indicators.spec.tsx index 5883ecbd211a99..dc8a4708c0998c 100644 --- a/static/app/components/indicators.spec.tsx +++ b/static/app/components/indicators.spec.tsx @@ -5,6 +5,7 @@ import { addMessage, addSuccessMessage, clearIndicators, + type Indicator, } from 'sentry/actionCreators/indicator'; import Indicators from 'sentry/components/indicators'; import IndicatorStore from 'sentry/stores/indicatorStore'; @@ -39,7 +40,7 @@ describe('Indicators', function () { const {container} = render(); // when "type" is empty, we should treat it as loading state - let indicator; + let indicator!: Indicator; act(() => { indicator = IndicatorStore.add('Loading'); }); diff --git a/static/app/components/lastCommit.spec.tsx b/static/app/components/lastCommit.spec.tsx index f45b8b070f7bd5..7884c1f0b52398 100644 --- a/static/app/components/lastCommit.spec.tsx +++ b/static/app/components/lastCommit.spec.tsx @@ -5,7 +5,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import LastCommit from 'sentry/components/lastCommit'; describe('LastCommit', function () { - let mockedCommit; + let mockedCommit!: ReturnType; const mockedCommitTitle = '(improve) Add Links to Spike-Protection Email (#2408)'; beforeEach(() => { @@ -17,7 +17,7 @@ describe('LastCommit', function () { }); it('links to the commit in GitHub', function () { - mockedCommit.repository.provider = {id: 'github'}; + mockedCommit.repository!.provider = {id: 'github', name: 'GitHub'}; const mockedCommitURL = `${mockedCommit.repository?.url}/commit/${mockedCommit.id}`; render(); diff --git a/static/app/components/modals/createTeamModal.spec.tsx b/static/app/components/modals/createTeamModal.spec.tsx index b986e7aa5759d7..ec45ac444744b9 100644 --- a/static/app/components/modals/createTeamModal.spec.tsx +++ b/static/app/components/modals/createTeamModal.spec.tsx @@ -21,7 +21,7 @@ describe('CreateTeamModal', function () { }); it('calls createTeam action creator on submit', async function () { - const styledWrapper = styled(c => c.children); + const styledWrapper = styled((c: {children: React.ReactNode}) => c.children); render(
      {props.children}
      ; const api = new MockApiClient(); -function renderModal({initialData, widget}) { +function renderModal({ + initialData, + widget, +}: { + initialData: ReturnType; + widget: Widget; +}) { return render( AddDashboardWidgetModal', function () { router: {}, projects: [], }); - let mockQuery; - let mockWidget; + let mockQuery!: Widget['queries'][number]; + let mockWidget!: Widget; beforeEach(function () { mockQuery = { @@ -41,7 +48,6 @@ describe('Modals -> AddDashboardWidgetModal', function () { fields: ['count()', 'failure_count()'], aggregates: ['count()', 'failure_count()'], columns: [], - id: '1', name: 'Query Name', orderby: '', }; @@ -93,12 +99,10 @@ describe('Modals -> AddDashboardWidgetModal', function () { mockWidget.queries.push({ ...mockQuery, conditions: 'title:/organizations/:orgId/performance/', - id: '2', }); mockWidget.queries.push({ ...mockQuery, conditions: 'title:/organizations/:orgId/', - id: '3', }); renderModal({initialData, widget: mockWidget}); const queryFields = screen.getAllByRole('textbox'); diff --git a/static/app/components/modals/featureTourModal.spec.tsx b/static/app/components/modals/featureTourModal.spec.tsx index 35a9ffd7c34951..8cf37f754562ec 100644 --- a/static/app/components/modals/featureTourModal.spec.tsx +++ b/static/app/components/modals/featureTourModal.spec.tsx @@ -21,7 +21,8 @@ const steps = [ ]; describe('FeatureTourModal', function () { - let onAdvance, onCloseModal; + let onAdvance!: jest.Mock; + let onCloseModal!: jest.Mock; const createWrapper = (props = {}) => render( diff --git a/static/app/components/modals/inviteMembersModal/index.spec.tsx b/static/app/components/modals/inviteMembersModal/index.spec.tsx index 49a3e7f58aec2c..339967db5086c5 100644 --- a/static/app/components/modals/inviteMembersModal/index.spec.tsx +++ b/static/app/components/modals/inviteMembersModal/index.spec.tsx @@ -15,7 +15,7 @@ import type {Scope} from 'sentry/types/core'; import type {DetailedTeam} from 'sentry/types/organization'; describe('InviteMembersModal', function () { - const styledWrapper = styled(c => c.children); + const styledWrapper = styled((c: {children: React.ReactNode}) => c.children); type MockApiResponseFn = ( client: typeof MockApiClient, @@ -39,7 +39,7 @@ describe('InviteMembersModal', function () { const defaultMockModalProps = { Body: styledWrapper(), - Header: p => {p.children}, + Header: (p: {children?: React.ReactNode}) => {p.children}, Footer: styledWrapper(), closeModal: () => {}, CloseButton: makeCloseButton(() => {}), @@ -297,7 +297,11 @@ describe('InviteMembersModal', function () { }); it('marks failed invites', async function () { - const failedCreateMemberMock = (client, orgSlug, _) => { + const failedCreateMemberMock = ( + client: typeof MockApiClient, + orgSlug: string, + _: any + ) => { return client.addMockResponse({ url: `/organizations/${orgSlug}/members/`, method: 'POST', @@ -402,7 +406,11 @@ describe('InviteMembersModal', function () { }); it('POSTS to the invite-request endpoint', async function () { - const createInviteRequestMock = (client, orgSlug, _) => { + const createInviteRequestMock = ( + client: typeof MockApiClient, + orgSlug: string, + _: any + ) => { return client.addMockResponse({ url: `/organizations/${orgSlug}/invite-requests/`, method: 'POST', diff --git a/static/app/components/modals/widgetBuilder/addToDashboardModal.spec.tsx b/static/app/components/modals/widgetBuilder/addToDashboardModal.spec.tsx index 94543d87695743..e784b084790a7a 100644 --- a/static/app/components/modals/widgetBuilder/addToDashboardModal.spec.tsx +++ b/static/app/components/modals/widgetBuilder/addToDashboardModal.spec.tsx @@ -34,8 +34,8 @@ const mockWidgetAsQueryParams = { }; describe('add to dashboard modal', () => { - let eventsStatsMock; - let initialData; + let eventsStatsMock!: jest.Mock; + let initialData!: ReturnType; const testDashboardListItem: DashboardListItem = { id: '1', diff --git a/static/app/components/modals/widgetViewerModal.spec.tsx b/static/app/components/modals/widgetViewerModal.spec.tsx index 8edf9f12b1edd4..d1b4e1f5ab4d8a 100644 --- a/static/app/components/modals/widgetViewerModal.spec.tsx +++ b/static/app/components/modals/widgetViewerModal.spec.tsx @@ -96,8 +96,9 @@ async function renderModal({ } describe('Modals -> WidgetViewerModal', function () { - let initialData, initialDataWithFlag; - let widgetLegendState: WidgetLegendSelectionState; + let initialData!: ReturnType; + let initialDataWithFlag!: ReturnType; + let widgetLegendState!: WidgetLegendSelectionState; beforeEach(() => { initialData = initializeOrg({ organization: { @@ -675,7 +676,8 @@ describe('Modals -> WidgetViewerModal', function () { }); describe('TopN Chart Widget', function () { - let mockQuery, mockWidget; + let mockQuery!: Widget['queries'][number]; + let mockWidget!: Widget; function mockEventsStats() { return MockApiClient.addMockResponse({ @@ -746,7 +748,6 @@ describe('Modals -> WidgetViewerModal', function () { fields: ['error.type', 'count()'], aggregates: ['count()'], columns: ['error.type'], - id: '1', name: 'Query Name', orderby: '', }; @@ -1137,7 +1138,7 @@ describe('Modals -> WidgetViewerModal', function () { }); describe('Issue Table Widget', function () { - let issuesMock; + let issuesMock!: jest.Mock; const mockQuery = { conditions: 'is:unresolved', fields: ['events', 'status', 'title'], @@ -1353,7 +1354,7 @@ describe('Modals -> WidgetViewerModal', function () { }); describe('Release Health Widgets', function () { - let metricsMock; + let metricsMock!: jest.Mock; const mockQuery = { conditions: '', fields: [`sum(session)`], diff --git a/static/app/views/alerts/rules/metric/ruleConditionsForm.spec.tsx b/static/app/views/alerts/rules/metric/ruleConditionsForm.spec.tsx index c38e48b0529bda..286b8344f6ce94 100644 --- a/static/app/views/alerts/rules/metric/ruleConditionsForm.spec.tsx +++ b/static/app/views/alerts/rules/metric/ruleConditionsForm.spec.tsx @@ -23,10 +23,10 @@ describe('RuleConditionsForm', () => { dataset: Dataset.ERRORS, disabled: false, isEditing: true, - onComparisonDeltaChange: _ => {}, + onComparisonDeltaChange: () => {}, onFilterSearch: mockSearch, - onMonitorTypeSelect: _ => {}, - onTimeWindowChange: _ => {}, + onMonitorTypeSelect: () => {}, + onTimeWindowChange: () => {}, project: projects[0], thresholdChart:
      chart
      , timeWindow: 30, diff --git a/static/app/views/dashboards/datasetConfig/errors.spec.tsx b/static/app/views/dashboards/datasetConfig/errors.spec.tsx index acac6d5dc039d9..880d5ff5e3b4eb 100644 --- a/static/app/views/dashboards/datasetConfig/errors.spec.tsx +++ b/static/app/views/dashboards/datasetConfig/errors.spec.tsx @@ -7,6 +7,7 @@ import {WidgetFixture} from 'sentry-fixture/widget'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import type {Client} from 'sentry/api'; import type {EventViewOptions} from 'sentry/utils/discover/eventView'; import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; @@ -105,7 +106,9 @@ describe('ErrorsConfig', function () { }); describe('getEventsRequest', function () { - let api, organization, mockEventsRequest; + let api!: Client; + let organization!: ReturnType; + let mockEventsRequest!: jest.Mock; beforeEach(function () { MockApiClient.clearMockResponses(); @@ -152,7 +155,9 @@ describe('ErrorsConfig', function () { }); describe('getSeriesRequest', function () { - let api, organization, mockEventsRequest; + let api!: Client; + let organization!: ReturnType; + let mockEventsRequest!: jest.Mock; beforeEach(function () { MockApiClient.clearMockResponses(); diff --git a/static/app/views/dashboards/datasetConfig/transactions.spec.tsx b/static/app/views/dashboards/datasetConfig/transactions.spec.tsx index acc5a02f32336d..42a9433aeefda5 100644 --- a/static/app/views/dashboards/datasetConfig/transactions.spec.tsx +++ b/static/app/views/dashboards/datasetConfig/transactions.spec.tsx @@ -2,13 +2,17 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {PageFiltersFixture} from 'sentry-fixture/pageFilters'; import {WidgetFixture} from 'sentry-fixture/widget'; +import type {Client} from 'sentry/api'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {TransactionsConfig} from 'sentry/views/dashboards/datasetConfig/transactions'; describe('TransactionsConfig', function () { describe('getEventsRequest', function () { - let api, organization, mockEventsRequest, mockEventsStatsRequest; + let api!: Client; + let organization!: ReturnType; + let mockEventsRequest!: jest.Mock; + let mockEventsStatsRequest!: jest.Mock; beforeEach(function () { MockApiClient.clearMockResponses(); diff --git a/static/app/views/dashboards/detail.spec.tsx b/static/app/views/dashboards/detail.spec.tsx index 9dc75149c2613d..479c778dca97d9 100644 --- a/static/app/views/dashboards/detail.spec.tsx +++ b/static/app/views/dashboards/detail.spec.tsx @@ -45,7 +45,7 @@ describe('Dashboards > Detail', function () { const projects = [ProjectFixture()]; describe('prebuilt dashboards', function () { - let initialData; + let initialData!: ReturnType; beforeEach(function () { act(() => ProjectsStore.loadInitialData(projects)); @@ -231,7 +231,10 @@ describe('Dashboards > Detail', function () { }); describe('custom dashboards', function () { - let initialData, widgets, mockVisit, mockPut; + let initialData!: ReturnType; + let widgets!: ReturnType[]; + let mockVisit!: jest.Mock; + let mockPut!: jest.Mock; beforeEach(function () { window.confirm = jest.fn(); @@ -1680,8 +1683,8 @@ describe('Dashboards > Detail', function () { { router: initialData.router, organization: { - features: ['dashboards-edit-access'], ...initialData.organization, + features: ['dashboards-edit-access'], }, } ); @@ -2121,7 +2124,7 @@ describe('Dashboards > Detail', function () { }); describe('widget builder redesign', function () { - let mockUpdateDashboard; + let mockUpdateDashboard!: jest.SpyInstance; beforeEach(function () { initialData = initializeOrg({ organization: OrganizationFixture({ @@ -2212,7 +2215,7 @@ describe('Dashboards > Detail', function () { { organization: initialData.organization, // Mock the widgetIndex param so it's available when the widget builder opens - router: {...initialData.router, params: {widgetIndex: 0}}, + router: {...initialData.router, params: {widgetIndex: '0'}}, } ); @@ -2298,7 +2301,7 @@ describe('Dashboards > Detail', function () { { organization: initialData.organization, // Mock the widgetIndex param so it's available when the widget builder opens - router: {...initialData.router, params: {widgetIndex: 0}}, + router: {...initialData.router, params: {widgetIndex: '0'}}, } ); diff --git a/static/app/views/dashboards/orgDashboards.spec.tsx b/static/app/views/dashboards/orgDashboards.spec.tsx index 79893feb1ef8cc..d40d98ffcd8618 100644 --- a/static/app/views/dashboards/orgDashboards.spec.tsx +++ b/static/app/views/dashboards/orgDashboards.spec.tsx @@ -16,7 +16,7 @@ describe('OrgDashboards', () => { features: ['dashboards-basic', 'dashboards-edit'], }); - let initialData; + let initialData!: ReturnType; beforeEach(() => { initialData = initializeOrg({ organization, @@ -84,11 +84,9 @@ describe('OrgDashboards', () => { ) : (
      loading
      @@ -150,11 +148,9 @@ describe('OrgDashboards', () => { ) : (
      loading
      @@ -223,11 +219,9 @@ describe('OrgDashboards', () => { ) : (
      loading
      @@ -254,11 +248,9 @@ describe('OrgDashboards', () => { ) : (
      loading
      @@ -301,11 +293,9 @@ describe('OrgDashboards', () => { ) : (
      loading
      @@ -329,11 +319,9 @@ describe('OrgDashboards', () => { ) : (
      loading
      diff --git a/static/app/views/dashboards/utils.spec.tsx b/static/app/views/dashboards/utils.spec.tsx index 1141c3fb235747..73f37c3e4f2a89 100644 --- a/static/app/views/dashboards/utils.spec.tsx +++ b/static/app/views/dashboards/utils.spec.tsx @@ -30,7 +30,7 @@ describe('Dashboards util', () => { projects: [], }; describe('constructWidgetFromQuery', () => { - let baseQuery; + let baseQuery!: NonNullable[0]>; beforeEach(() => { baseQuery = { displayType: 'line', @@ -105,7 +105,7 @@ describe('Dashboards util', () => { }); }); describe('eventViewFromWidget', () => { - let widget; + let widget!: Widget; beforeEach(() => { widget = { title: 'Test Query', @@ -149,7 +149,7 @@ describe('Dashboards util', () => { }); describe('getWidgetDiscoverUrl', function () { - let widget; + let widget!: Widget; beforeEach(() => { widget = { title: 'Test Query', @@ -198,7 +198,7 @@ describe('Dashboards util', () => { }); }); describe('getWidgetIssueUrl', function () { - let widget; + let widget!: Widget; beforeEach(() => { widget = { title: 'Test Query', @@ -211,6 +211,8 @@ describe('Dashboards util', () => { conditions: 'is:unresolved', fields: ['events'], orderby: 'date', + aggregates: [], + columns: [], }, ], }; @@ -367,7 +369,7 @@ describe('Dashboards util', () => { }); describe('isWidgetUsingTransactionName', () => { - let baseQuery; + let baseQuery!: NonNullable[0]>; beforeEach(() => { baseQuery = { displayType: 'line', @@ -386,7 +388,7 @@ describe('isWidgetUsingTransactionName', () => { }); it('returns true when widget uses transaction as a selected field', () => { - baseQuery.queryFields.push('transaction'); + (baseQuery.queryFields as string[]).push('transaction'); const widget = constructWidgetFromQuery(baseQuery)!; expect(isWidgetUsingTransactionName(widget)).toEqual(true); }); @@ -404,13 +406,17 @@ describe('isWidgetUsingTransactionName', () => { }); it('returns true when widget uses performance_score as aggregate', () => { - baseQuery.queryFields.push('performance_score(measurements.score.total)'); + (baseQuery.queryFields as string[]).push( + 'performance_score(measurements.score.total)' + ); const widget = constructWidgetFromQuery(baseQuery)!; expect(isUsingPerformanceScore(widget)).toEqual(true); }); it('returns true when widget uses performance_score as condition', () => { - baseQuery.queryConditions.push('performance_score(measurements.score.total):>0.5'); + (baseQuery.queryConditions as string[]).push( + 'performance_score(measurements.score.total):>0.5' + ); const widget = constructWidgetFromQuery(baseQuery)!; expect(isUsingPerformanceScore(widget)).toEqual(true); }); diff --git a/static/app/views/dashboards/widgetBuilder/components/datasetSelector.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/datasetSelector.spec.tsx index c8117b8dabd105..753b4e203dcc6f 100644 --- a/static/app/views/dashboards/widgetBuilder/components/datasetSelector.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/datasetSelector.spec.tsx @@ -14,8 +14,8 @@ jest.mock('sentry/utils/useNavigate', () => ({ const mockUseNavigate = jest.mocked(useNavigate); describe('DatasetSelector', function () { - let router; - let organization; + let router!: ReturnType; + let organization!: ReturnType; beforeEach(function () { router = RouterFixture(); organization = OrganizationFixture({}); diff --git a/static/app/views/dashboards/widgetBuilder/components/nameAndDescFields.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/nameAndDescFields.spec.tsx index d1c8e62fec67bd..2b063f496e9a28 100644 --- a/static/app/views/dashboards/widgetBuilder/components/nameAndDescFields.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/nameAndDescFields.spec.tsx @@ -14,8 +14,8 @@ jest.mock('sentry/utils/useNavigate', () => ({ const mockUseNavigate = jest.mocked(useNavigate); describe('WidgetBuilder', () => { - let router; - let organization; + let router!: ReturnType; + let organization!: ReturnType; beforeEach(function () { router = RouterFixture({ location: { diff --git a/static/app/views/dashboards/widgetBuilder/components/typeSelector.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/typeSelector.spec.tsx index b74c22a8c6b0e8..087c33ca48cd56 100644 --- a/static/app/views/dashboards/widgetBuilder/components/typeSelector.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/typeSelector.spec.tsx @@ -14,8 +14,8 @@ jest.mock('sentry/utils/useNavigate', () => ({ const mockUseNavigate = jest.mocked(useNavigate); describe('TypeSelector', () => { - let router; - let organization; + let router!: ReturnType; + let organization!: ReturnType; beforeEach(function () { router = RouterFixture(); organization = OrganizationFixture({}); diff --git a/static/app/views/dashboards/widgetBuilder/components/visualize.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/visualize.spec.tsx index b22dc8b5ec7b29..9e785b04ad1490 100644 --- a/static/app/views/dashboards/widgetBuilder/components/visualize.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/visualize.spec.tsx @@ -17,8 +17,8 @@ jest.mock('sentry/views/explore/contexts/spanTagsContext'); jest.mock('sentry/utils/useNavigate'); describe('Visualize', () => { - let organization; - let mockNavigate; + let organization!: ReturnType; + let mockNavigate!: jest.Mock; beforeEach(() => { organization = OrganizationFixture({ diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx index 1e3022c7296327..94a223ca1365fa 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.spec.tsx @@ -22,7 +22,7 @@ jest.mock('sentry/utils/useCustomMeasurements'); jest.mock('sentry/views/explore/contexts/spanTagsContext'); describe('WidgetBuilderSlideout', () => { - let organization; + let organization!: ReturnType; beforeEach(() => { organization = OrganizationFixture(); diff --git a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx index ed3e09d2d2c0c3..9157d90f589893 100644 --- a/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.spec.tsx @@ -20,7 +20,7 @@ const mockedUsedLocation = jest.mocked(useLocation); const mockedUseNavigate = jest.mocked(useNavigate); describe('useWidgetBuilderState', () => { - let mockNavigate; + let mockNavigate!: jest.Mock; beforeEach(() => { mockNavigate = jest.fn(); mockedUseNavigate.mockReturnValue(mockNavigate); diff --git a/static/app/views/dashboards/widgetCard/widgetQueries.spec.tsx b/static/app/views/dashboards/widgetCard/widgetQueries.spec.tsx index cefed1f6e400f6..27161db9e8e66e 100644 --- a/static/app/views/dashboards/widgetCard/widgetQueries.spec.tsx +++ b/static/app/views/dashboards/widgetCard/widgetQueries.spec.tsx @@ -20,7 +20,7 @@ import WidgetQueries, { describe('Dashboards > WidgetQueries', function () { const initialData = initializeOrg(); - const renderWithProviders = component => + const renderWithProviders = (component: React.ReactNode) => render( @@ -674,7 +674,7 @@ describe('Dashboards > WidgetQueries', function () { displayType: DisplayType.LINE, interval: '5m', }; - let childProps; + let childProps!: GenericWidgetQueriesChildrenProps; const {rerender} = renderWithProviders( WidgetQueries', function () { // Did not re-query expect(eventsStatsMock).toHaveBeenCalledTimes(1); - expect(childProps.timeseriesResults[0].seriesName).toEqual( + expect(childProps.timeseriesResults![0].seriesName).toEqual( 'this query alias changed : count()' ); }); diff --git a/static/app/views/dataExport/dataDownload.spec.tsx b/static/app/views/dataExport/dataDownload.spec.tsx index 790407c46475dc..207318511ae906 100644 --- a/static/app/views/dataExport/dataDownload.spec.tsx +++ b/static/app/views/dataExport/dataDownload.spec.tsx @@ -16,7 +16,7 @@ describe('DataDownload', function () { dataExportId: '721', }; - const getDataExportDetails = (body, statusCode = 200) => + const getDataExportDetails = (body: any, statusCode = 200) => MockApiClient.addMockResponse({ url: `/organizations/${mockRouteParams.orgId}/data-export/${mockRouteParams.dataExportId}/`, body, diff --git a/static/app/views/discover/homepage.spec.tsx b/static/app/views/discover/homepage.spec.tsx index f41dcca26acc5a..1977c30883eaca 100644 --- a/static/app/views/discover/homepage.spec.tsx +++ b/static/app/views/discover/homepage.spec.tsx @@ -22,7 +22,10 @@ import Homepage from './homepage'; describe('Discover > Homepage', () => { const features = ['global-views', 'discover-query']; - let initialData, organization, mockHomepage, measurementsMetaMock; + let initialData: ReturnType; + let organization: ReturnType; + let mockHomepage: jest.Mock; + let measurementsMetaMock: jest.Mock; beforeEach(() => { organization = OrganizationFixture({ diff --git a/static/app/views/discover/miniGraph.spec.tsx b/static/app/views/discover/miniGraph.spec.tsx index 6523227b33f78d..f9df7648a14fe4 100644 --- a/static/app/views/discover/miniGraph.spec.tsx +++ b/static/app/views/discover/miniGraph.spec.tsx @@ -17,7 +17,9 @@ describe('Discover > MiniGraph', function () { pathname: '/', }); - let organization, eventView, initialData; + let organization!: ReturnType; + let eventView!: ReturnType; + let initialData!: ReturnType; beforeEach(() => { organization = OrganizationFixture({ diff --git a/static/app/views/discover/queryList.spec.tsx b/static/app/views/discover/queryList.spec.tsx index b0f6cfea9faccf..040b3f6750b118 100644 --- a/static/app/views/discover/queryList.spec.tsx +++ b/static/app/views/discover/queryList.spec.tsx @@ -1,4 +1,5 @@ import {DiscoverSavedQueryFixture} from 'sentry-fixture/discover'; +import {LocationFixture} from 'sentry-fixture/locationFixture'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {RouterFixture} from 'sentry-fixture/routerFixture'; @@ -20,15 +21,14 @@ import QueryList from 'sentry/views/discover/queryList'; jest.mock('sentry/actionCreators/modal'); describe('Discover > QueryList', function () { - let location, - savedQueries, - organization, - deleteMock, - duplicateMock, - queryChangeMock, - updateHomepageMock, - eventsStatsMock, - wrapper; + let location: ReturnType; + let savedQueries: ReturnType[]; + let organization: ReturnType; + let deleteMock: jest.Mock; + let duplicateMock: jest.Mock; + let queryChangeMock: jest.Mock; + let updateHomepageMock: jest.Mock; + let eventsStatsMock: jest.Mock; const {router} = initializeOrg(); @@ -74,17 +74,15 @@ describe('Discover > QueryList', function () { statusCode: 204, }); - location = { + location = LocationFixture({ pathname: '/organizations/org-slug/discover/queries/', query: {cursor: '0:1:1', statsPeriod: '14d'}, - }; + }); queryChangeMock = jest.fn(); }); afterEach(() => { jest.clearAllMocks(); - wrapper?.unmount(); - wrapper = null; }); it('renders an empty list', function () { diff --git a/static/app/views/discover/savedQuery/index.spec.tsx b/static/app/views/discover/savedQuery/index.spec.tsx index 463becf79a91a4..5c7d227f5c9efe 100644 --- a/static/app/views/discover/savedQuery/index.spec.tsx +++ b/static/app/views/discover/savedQuery/index.spec.tsx @@ -1,4 +1,6 @@ +import {LocationFixture} from 'sentry-fixture/locationFixture'; import {OrganizationFixture} from 'sentry-fixture/organization'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; @@ -14,12 +16,12 @@ import * as utils from 'sentry/views/discover/savedQuery/utils'; jest.mock('sentry/actionCreators/modal'); function mount( - location, - organization, - router, - eventView, - savedQuery, - yAxis, + location: ReturnType, + organization: Organization, + router: ReturnType, + eventView: EventView, + savedQuery: SavedQuery | NewQuery | undefined, + yAxis: string[], disabled = false, setSavedQuery = jest.fn() ) { @@ -28,7 +30,7 @@ function mount( location={location} organization={organization} eventView={eventView} - savedQuery={savedQuery} + savedQuery={savedQuery as SavedQuery} disabled={disabled} updateCallback={() => {}} yAxis={yAxis} @@ -47,13 +49,13 @@ describe('Discover > SaveQueryButtonGroup', function () { let errorsViewSaved: EventView; let errorsViewModified: EventView; let errorsQuery: NewQuery; - const location = { + const location = LocationFixture({ pathname: '/organization/eventsv2/', query: {}, - }; - const router = { + }); + const router = RouterFixture({ location: {query: {}}, - }; + }); const yAxis = ['count()', 'failure_count()']; beforeEach(() => { @@ -331,7 +333,7 @@ describe('Discover > SaveQueryButtonGroup', function () { organization, router, errorsViewSaved, - {...savedQuery, yAxis: 'count()'}, + {...savedQuery, yAxis: ['count()']}, ['count()'] ); diff --git a/static/app/views/discover/table/columnEditModal.spec.tsx b/static/app/views/discover/table/columnEditModal.spec.tsx index 4937ba16896d2a..1743e7856420a3 100644 --- a/static/app/views/discover/table/columnEditModal.spec.tsx +++ b/static/app/views/discover/table/columnEditModal.spec.tsx @@ -28,7 +28,7 @@ function mountModal( React.ComponentProps, 'columns' | 'onApply' | 'customMeasurements' | 'dataset' >, - initialData + initialData: ReturnType ) { return render( screen.findAllByTestId('queryField'); // Get the nth label (value) within the row of the column editor. -const findAllQueryFieldNthCell = async nth => +const findAllQueryFieldNthCell = async (nth: number) => (await findAllQueryFields()) .map(f => within(f).getAllByTestId('label')[nth]) .filter(Boolean); const getAllQueryFields = () => screen.getAllByTestId('queryField'); -const getAllQueryFieldsNthCell = nth => +const getAllQueryFieldsNthCell = (nth: number) => getAllQueryFields() .map(f => within(f).getAllByTestId('label')[nth]) .filter(Boolean); -const openMenu = async (row, column = 0) => { +const openMenu = async (row: number, column = 0) => { const queryFields = await screen.findAllByTestId('queryField'); const queryField = queryFields[row]; expect(queryField).toBeInTheDocument(); @@ -77,7 +77,10 @@ const openMenu = async (row, column = 0) => { } }; -const selectByLabel = async (label, options) => { +const selectByLabel = async ( + label: string, + options: {at: number; control?: boolean; name?: string} +) => { await openMenu(options.at); const menuOptions = screen.getAllByTestId('menu-list-item-label'); // TODO: Can likely switch to menuitem role and match against label const opt = menuOptions.find(e => e.textContent?.includes(label)); diff --git a/static/app/views/discover/tags.spec.tsx b/static/app/views/discover/tags.spec.tsx index a649a447cdde36..e0c2783efc3d1a 100644 --- a/static/app/views/discover/tags.spec.tsx +++ b/static/app/views/discover/tags.spec.tsx @@ -34,7 +34,7 @@ const commonQueryConditions = { }; describe('Tags', function () { - function generateUrl(key, value) { + function generateUrl(key: string, value: string) { return `/endpoint/${key}/${value}`; } diff --git a/static/app/views/explore/contexts/pageParamsContext/index.spec.tsx b/static/app/views/explore/contexts/pageParamsContext/index.spec.tsx index 8b7baee6454876..b4e793f54470e4 100644 --- a/static/app/views/explore/contexts/pageParamsContext/index.spec.tsx +++ b/static/app/views/explore/contexts/pageParamsContext/index.spec.tsx @@ -17,8 +17,15 @@ import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; import {ChartType} from 'sentry/views/insights/common/components/chart'; describe('PageParamsProvider', function () { - let pageParams, setPageParams; - let setDataset, setFields, setGroupBys, setMode, setQuery, setSortBys, setVisualizes; + let pageParams: ReturnType; + let setPageParams: ReturnType; + let setDataset: ReturnType; + let setFields: ReturnType; + let setGroupBys: ReturnType; + let setMode: ReturnType; + let setQuery: ReturnType; + let setSortBys: ReturnType; + let setVisualizes: ReturnType; function Component() { pageParams = useExplorePageParams(); @@ -33,7 +40,7 @@ describe('PageParamsProvider', function () { return
      ; } - function renderTestComponent(defaultPageParams?) { + function renderTestComponent(defaultPageParams?: any) { render( diff --git a/static/app/views/explore/hooks/useAddToDashboard.spec.tsx b/static/app/views/explore/hooks/useAddToDashboard.spec.tsx index 170964a7b08d1a..d56659be9973fe 100644 --- a/static/app/views/explore/hooks/useAddToDashboard.spec.tsx +++ b/static/app/views/explore/hooks/useAddToDashboard.spec.tsx @@ -14,7 +14,8 @@ import {ChartType} from 'sentry/views/insights/common/components/chart'; jest.mock('sentry/actionCreators/modal'); describe('AddToDashboardButton', () => { - let setMode, setVisualizes; + let setMode: ReturnType; + let setVisualizes: ReturnType; function TestPage({visualizeIndex}: {visualizeIndex: number}) { setMode = useSetExploreMode(); @@ -227,7 +228,6 @@ describe('AddToDashboardButton', () => { 'p90(span.duration)', ], chartType: ChartType.LINE, - label: 'Custom Explore Widget', }, ]) ); diff --git a/static/app/views/explore/hooks/useChartInterval.spec.tsx b/static/app/views/explore/hooks/useChartInterval.spec.tsx index b35242484b2a06..04f52d8ba69a25 100644 --- a/static/app/views/explore/hooks/useChartInterval.spec.tsx +++ b/static/app/views/explore/hooks/useChartInterval.spec.tsx @@ -12,7 +12,9 @@ describe('useChartInterval', function () { }); it('allows changing chart interval', async function () { - let chartInterval, setChartInterval, intervalOptions; + let chartInterval!: ReturnType[0]; + let setChartInterval!: ReturnType[1]; + let intervalOptions!: ReturnType[2]; function TestPage() { [chartInterval, setChartInterval, intervalOptions] = useChartInterval(); diff --git a/static/app/views/explore/hooks/useDragNDropColumns.spec.tsx b/static/app/views/explore/hooks/useDragNDropColumns.spec.tsx index b56f5b6c91c553..c119de9b774a64 100644 --- a/static/app/views/explore/hooks/useDragNDropColumns.spec.tsx +++ b/static/app/views/explore/hooks/useDragNDropColumns.spec.tsx @@ -8,7 +8,9 @@ describe('useDragNDropColumns', () => { const initialColumns = ['span.op', 'span_id', 'timestamp']; it('should insert a column', () => { - let columns, setColumns, insertColumn; + let columns!: string[]; + let setColumns: (columns: string[]) => void; + let insertColumn: ReturnType['insertColumn']; function TestPage() { [columns, setColumns] = useState(initialColumns); @@ -26,7 +28,11 @@ describe('useDragNDropColumns', () => { }); it('should update a column at a specific index', () => { - let columns, setColumns, updateColumnAtIndex; + let columns!: string[]; + let setColumns: (columns: string[]) => void; + let updateColumnAtIndex: ReturnType< + typeof useDragNDropColumns + >['updateColumnAtIndex']; function TestPage() { [columns, setColumns] = useState(initialColumns); @@ -44,7 +50,9 @@ describe('useDragNDropColumns', () => { }); it('should delete a column at a specific index', () => { - let columns, setColumns, deleteColumnAtIndex; + let columns!: string[]; + let setColumns: (columns: string[]) => void; + let deleteColumnAtIndex: (index: number) => void; function TestPage() { [columns, setColumns] = useState(initialColumns); @@ -62,7 +70,9 @@ describe('useDragNDropColumns', () => { }); it('should swap two columns at specific indices', () => { - let columns, setColumns, onDragEnd; + let columns!: string[]; + let setColumns: (columns: string[]) => void; + let onDragEnd: (arg: any) => void; function TestPage() { [columns, setColumns] = useState(initialColumns); diff --git a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx index 525b58e68b2ae4..c9773ba36c12ad 100644 --- a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx @@ -14,7 +14,7 @@ jest.mock('sentry/utils/usePageFilters'); describe('PerformanceScoreBreakdownChart', function () { const organization = OrganizationFixture(); - let eventsStatsMock; + let eventsStatsMock: jest.Mock; beforeEach(function () { jest.mocked(useLocation).mockReturnValue({ diff --git a/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx b/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx index 9efddce3c5b599..0de0bf2f021368 100644 --- a/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx @@ -11,7 +11,8 @@ jest.mock('sentry/utils/usePageFilters'); describe('WebVitalsDetailPanel', function () { const organization = OrganizationFixture(); - let eventsMock, eventsStatsMock; + let eventsMock: jest.Mock; + let eventsStatsMock: jest.Mock; beforeEach(function () { jest.mocked(useLocation).mockReturnValue({ diff --git a/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx b/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx index 7b4fb8c26d50d7..bdd75bcbaa7aae 100644 --- a/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx +++ b/static/app/views/insights/browser/webVitals/views/pageOverview.spec.tsx @@ -14,7 +14,7 @@ describe('PageOverview', function () { features: ['insights-initial-modules'], }); - let eventsMock; + let eventsMock: jest.Mock; beforeEach(function () { jest.mocked(useLocation).mockReturnValue({ diff --git a/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx b/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx index 25cb3ea6eecf03..2ebc8613a0c75c 100644 --- a/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx +++ b/static/app/views/insights/browser/webVitals/views/webVitalsLandingPage.spec.tsx @@ -19,7 +19,7 @@ describe('WebVitalsLandingPage', function () { features: ['insights-initial-modules'], }); - let eventsMock; + let eventsMock: jest.Mock; beforeEach(function () { jest.mocked(useOnboardingProject).mockReturnValue(undefined); diff --git a/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx b/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx index aa04150addb9cf..5d504e00d3dea8 100644 --- a/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx +++ b/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx @@ -17,7 +17,8 @@ jest.mock('sentry/utils/usePageFilters'); describe('HTTPSummaryPage', function () { const organization = OrganizationFixture({features: ['insights-initial-modules']}); - let domainChartsRequestMock, domainTransactionsListRequestMock; + let domainChartsRequestMock: jest.Mock; + let domainTransactionsListRequestMock: jest.Mock; jest.mocked(usePageFilters).mockReturnValue({ isReady: true, diff --git a/static/app/views/insights/http/views/httpLandingPage.spec.tsx b/static/app/views/insights/http/views/httpLandingPage.spec.tsx index 4fa1cc74b73178..1657ab55093302 100644 --- a/static/app/views/insights/http/views/httpLandingPage.spec.tsx +++ b/static/app/views/insights/http/views/httpLandingPage.spec.tsx @@ -17,7 +17,8 @@ jest.mock('sentry/views/insights/common/queries/useOnboardingProject'); describe('HTTPLandingPage', function () { const organization = OrganizationFixture({features: ['insights-initial-modules']}); - let spanListRequestMock, spanChartsRequestMock; + let spanListRequestMock!: jest.Mock; + let spanChartsRequestMock!: jest.Mock; jest.mocked(useOnboardingProject).mockReturnValue(undefined); diff --git a/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx b/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx index 32bf3335e719e3..33e45ffe09a94a 100644 --- a/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx +++ b/static/app/views/insights/mobile/appStarts/components/eventSamples.spec.tsx @@ -18,7 +18,7 @@ describe('ScreenLoadEventSamples', function () { const organization = OrganizationFixture(); const project = ProjectFixture(); - let mockEventsRequest; + let mockEventsRequest!: jest.Mock; beforeEach(function () { jest.mocked(usePageFilters).mockReturnValue({ isReady: true, diff --git a/static/app/views/insights/mobile/appStarts/components/tables/spanOperationTable.spec.tsx b/static/app/views/insights/mobile/appStarts/components/tables/spanOperationTable.spec.tsx index 5f174ba07a3f7f..58bf5c707979a6 100644 --- a/static/app/views/insights/mobile/appStarts/components/tables/spanOperationTable.spec.tsx +++ b/static/app/views/insights/mobile/appStarts/components/tables/spanOperationTable.spec.tsx @@ -14,7 +14,7 @@ jest.mock('sentry/utils/useLocation'); describe('SpanOpSelector', function () { const organization = OrganizationFixture(); const project = ProjectFixture(); - let mockEventsRequest; + let mockEventsRequest: jest.Mock; jest.mocked(usePageFilters).mockReturnValue({ isReady: true, diff --git a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx index e17117d7c1fd60..b965b569931c9a 100644 --- a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx +++ b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx @@ -49,7 +49,7 @@ describe('Screen Summary', function () { }); describe('Native Project', function () { - let eventsMock; + let eventsMock: jest.Mock; beforeEach(() => { MockApiClient.addMockResponse({ diff --git a/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx b/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx index 934106ea00fb69..480bfe5e9d9225 100644 --- a/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/eventSamples.spec.tsx @@ -18,7 +18,7 @@ describe('ScreenLoadEventSamples', function () { const organization = OrganizationFixture(); const project = ProjectFixture(); - let mockEventsRequest; + let mockEventsRequest: jest.Mock; beforeEach(function () { jest.mocked(usePageFilters).mockReturnValue({ isReady: true, diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx index 91a838219fb7e8..8c8910fc6a4fd3 100644 --- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx @@ -8,7 +8,9 @@ import EventView from 'sentry/utils/discover/eventView'; import {EventSamplesTable} from 'sentry/views/insights/mobile/screenload/components/tables/eventSamplesTable'; describe('EventSamplesTable', function () { - let mockLocation, mockQuery: NewQuery, mockEventView; + let mockLocation: ReturnType; + let mockQuery: NewQuery; + let mockEventView: EventView; beforeEach(function () { mockLocation = LocationFixture({ query: { diff --git a/static/app/views/insights/mobile/screenload/components/tables/screenLoadSpansTable.spec.tsx b/static/app/views/insights/mobile/screenload/components/tables/screenLoadSpansTable.spec.tsx index c5023023c2bc0f..20b4eab0c57ca5 100644 --- a/static/app/views/insights/mobile/screenload/components/tables/screenLoadSpansTable.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/tables/screenLoadSpansTable.spec.tsx @@ -42,7 +42,7 @@ describe('ScreenLoadSpansTable', function () { }, }); - let eventsMock; + let eventsMock: jest.Mock; beforeEach(function () { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/releases/`, diff --git a/static/app/views/insights/queues/charts/throughputChart.spec.tsx b/static/app/views/insights/queues/charts/throughputChart.spec.tsx index 093752e68bf6f8..06e4bf4a2d0d1b 100644 --- a/static/app/views/insights/queues/charts/throughputChart.spec.tsx +++ b/static/app/views/insights/queues/charts/throughputChart.spec.tsx @@ -8,7 +8,7 @@ import {Referrer} from 'sentry/views/insights/queues/referrers'; describe('throughputChart', () => { const organization = OrganizationFixture(); - let eventsStatsMock; + let eventsStatsMock!: jest.Mock; beforeEach(() => { eventsStatsMock = MockApiClient.addMockResponse({ diff --git a/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx b/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx index 66d70bec26d1ab..4a68d53ffd33eb 100644 --- a/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx +++ b/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx @@ -58,7 +58,8 @@ describe('queuesLandingPage', () => { initiallyLoaded: false, }); - let eventsMock, eventsStatsMock; + let eventsMock: jest.Mock; + let eventsStatsMock: jest.Mock; beforeEach(() => { eventsMock = MockApiClient.addMockResponse({ diff --git a/static/app/views/integrationPipeline/awsLambdaCloudformation.spec.tsx b/static/app/views/integrationPipeline/awsLambdaCloudformation.spec.tsx index 02c17fddb82167..6f2ac16f8a5bdf 100644 --- a/static/app/views/integrationPipeline/awsLambdaCloudformation.spec.tsx +++ b/static/app/views/integrationPipeline/awsLambdaCloudformation.spec.tsx @@ -7,7 +7,7 @@ import selectEvent from 'sentry-test/selectEvent'; import AwsLambdaCloudformation from 'sentry/views/integrationPipeline/awsLambdaCloudformation'; describe('AwsLambdaCloudformation', () => { - let windowAssignMock; + let windowAssignMock!: jest.Mock; beforeEach(() => { windowAssignMock = jest.fn(); diff --git a/static/app/views/issueDetails/traceDataSection.spec.tsx b/static/app/views/issueDetails/traceDataSection.spec.tsx index db2d4ce9122828..497a6aa9a31a0e 100644 --- a/static/app/views/issueDetails/traceDataSection.spec.tsx +++ b/static/app/views/issueDetails/traceDataSection.spec.tsx @@ -64,7 +64,7 @@ describe('TraceDataSection', () => { culprit: 'n/a', 'error.value': ['some-other-error-value', 'The last error value'], timestamp: firstEventTimestamp, - 'issue.id': event['issue.id'], + 'issue.id': (event as any)['issue.id'], project: project.slug, 'project.name': project.name, title: event.title, diff --git a/static/app/views/organizationRestore/index.spec.tsx b/static/app/views/organizationRestore/index.spec.tsx index 858e6d5b927eb4..1fe8641287df17 100644 --- a/static/app/views/organizationRestore/index.spec.tsx +++ b/static/app/views/organizationRestore/index.spec.tsx @@ -6,7 +6,8 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import OrganizationRestore from 'sentry/views/organizationRestore'; describe('OrganizationRestore', function () { - let mockUpdate, mockGet; + let mockUpdate!: jest.Mock; + let mockGet!: jest.Mock; const pendingDeleteOrg = OrganizationFixture({ status: {id: 'pending_deletion', name: 'Pending Deletion'}, }); diff --git a/static/app/views/organizationStats/index.spec.tsx b/static/app/views/organizationStats/index.spec.tsx index 19a929190da3a3..82fd195e61ab8e 100644 --- a/static/app/views/organizationStats/index.spec.tsx +++ b/static/app/views/organizationStats/index.spec.tsx @@ -43,7 +43,7 @@ describe('OrganizationStats', function () { routeParams: {}, }; - let mockRequest; + let mockRequest: jest.Mock; beforeEach(() => { MockApiClient.clearMockResponses(); @@ -227,7 +227,7 @@ describe('OrganizationStats', function () { }); it('does not leak query params onto next page links', async () => { - const dummyLocation = PAGE_QUERY_PARAMS.reduce( + const dummyLocation = PAGE_QUERY_PARAMS.reduce<{query: Record}>( (location, param) => { location.query[param] = ''; return location; diff --git a/static/app/views/performance/content.spec.tsx b/static/app/views/performance/content.spec.tsx index 1df41d058e22c6..ab98d0b077c724 100644 --- a/static/app/views/performance/content.spec.tsx +++ b/static/app/views/performance/content.spec.tsx @@ -8,6 +8,8 @@ import * as pageFilters from 'sentry/actionCreators/pageFilters'; import OrganizationStore from 'sentry/stores/organizationStore'; import ProjectsStore from 'sentry/stores/projectsStore'; import TeamStore from 'sentry/stores/teamStore'; +import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; +import type {Project} from 'sentry/types/project'; import {browserHistory} from 'sentry/utils/browserHistory'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import PerformanceContent from 'sentry/views/performance/content'; @@ -15,7 +17,7 @@ import {DEFAULT_MAX_DURATION} from 'sentry/views/performance/trends/utils'; const FEATURES = ['performance-view']; -function WrappedComponent({router}) { +function WrappedComponent({router}: {router: InjectedRouter}) { return ( @@ -23,7 +25,11 @@ function WrappedComponent({router}) { ); } -function initializeData(projects, query, features = FEATURES) { +function initializeData( + projects: Project[], + query: Record, + features = FEATURES +) { const organization = OrganizationFixture({ features, }); @@ -42,7 +48,10 @@ function initializeData(projects, query, features = FEATURES) { return initialData; } -function initializeTrendsData(query, addDefaultQuery = true) { +function initializeTrendsData( + query: Record, + addDefaultQuery = true +) { const projects = [ ProjectFixture({id: '1', firstTransactionEvent: false}), ProjectFixture({id: '2', firstTransactionEvent: true}), @@ -287,7 +296,7 @@ describe('Performance > Content', function () { ProjectFixture({id: '1', firstTransactionEvent: false}), ProjectFixture({id: '2', firstTransactionEvent: true}), ]; - const {router} = initializeData(projects, {project: [1]}); + const {router} = initializeData(projects, {project: ['1']}); render(, { router, diff --git a/static/app/views/performance/table.spec.tsx b/static/app/views/performance/table.spec.tsx index 9ac52097156451..eb805c688d2054 100644 --- a/static/app/views/performance/table.spec.tsx +++ b/static/app/views/performance/table.spec.tsx @@ -32,7 +32,7 @@ const initializeData = (settings = {}, features: string[] = []) => { }); }; -function WrappedComponent({data, ...rest}) { +function WrappedComponent({data, ...rest}: any) { return ( @@ -49,7 +49,7 @@ function WrappedComponent({data, ...rest}) { ); } -function mockEventView(data) { +function mockEventView(data: ReturnType) { const eventView = new EventView({ id: '1', name: 'my query', @@ -90,7 +90,7 @@ function mockEventView(data) { ], sorts: [{field: 'tpm ', kind: 'desc'}], query: 'event.type:transaction transaction:/api*', - project: [data.projects[0].id, data.projects[1].id], + project: [Number(data.projects[0].id), Number(data.projects[1].id)], start: '2019-10-01T00:00:00', end: '2019-10-02T00:00:00', statsPeriod: '14d', @@ -107,7 +107,7 @@ function mockEventView(data) { } describe('Performance > Table', function () { - let eventsMock; + let eventsMock: jest.Mock; beforeEach(function () { mockUseLocation.mockReturnValue( LocationFixture({pathname: '/organizations/org-slug/performance/summary'}) diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx index b1d09b7aec677b..446d6369e8a616 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx @@ -50,7 +50,7 @@ describe('Performance GridEditable Table', function () { let fields = EVENTS_TABLE_RESPONSE_FIELDS; const organization = OrganizationFixture(); const transactionName = 'transactionName'; - let data; + let data: typeof MOCK_EVENTS_TABLE_DATA; const query = 'transaction.duration:<15m event.type:transaction transaction:/api/0/organizations/{organization_slug}/events/'; @@ -179,14 +179,6 @@ describe('Performance GridEditable Table', function () { 'spans.http', ]; - data.forEach(result => { - delete result['span_ops_breakdown.relative']; - delete result['spans.resource']; - delete result['spans.browser']; - delete result['spans.db']; - delete result['spans.total.time']; - }); - const eventView = EventView.fromNewQueryWithLocation( { id: undefined, @@ -267,9 +259,6 @@ describe('Performance GridEditable Table', function () { const initialData = initializeData(); fields = [...fields, 'replayId']; - data.forEach(result => { - result.replayId = 'mock_replay_id'; - }); const eventView = EventView.fromNewQueryWithLocation( { @@ -305,9 +294,6 @@ describe('Performance GridEditable Table', function () { const initialData = initializeData(); fields = [...fields, 'profile.id']; - data.forEach(result => { - result['profile.id'] = 'mock_profile_id'; - }); const eventView = EventView.fromNewQueryWithLocation( { diff --git a/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx index ed874de0ce1415..101bec6af6406f 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx @@ -18,7 +18,7 @@ jest.mock('sentry/utils/useLocation'); const mockUseLocation = jest.mocked(useLocation); -function WrappedComponent({data}) { +function WrappedComponent({data}: {data: ReturnType}) { return ( , additionalFeatures: string[] = []) { const features = ['transaction-event', 'performance-view', ...additionalFeatures]; const organization = OrganizationFixture({ features, diff --git a/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx index 1b51bf36aa1bc3..f40991cd803209 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx @@ -17,7 +17,7 @@ jest.mock('sentry/utils/useLocation'); const mockUseLocation = jest.mocked(useLocation); -function WrapperComponent(props) { +function WrapperComponent(props: React.ComponentProps) { return ( @@ -27,7 +27,7 @@ function WrapperComponent(props) { ); } -function initialize(query, additionalFeatures = []) { +function initialize(query: Record, additionalFeatures = []) { const features = ['transaction-event', 'performance-view', ...additionalFeatures]; const organization = OrganizationFixture({ features, @@ -53,13 +53,12 @@ function initialize(query, additionalFeatures = []) { transactionName, location: initialData.router.location, eventView, - api: MockApiClient, }; } describe('WrapperComponent', function () { const facetUrl = '/organizations/org-slug/events-facets-performance/'; - let facetApiMock; + let facetApiMock: jest.Mock; beforeEach(function () { mockUseLocation.mockReturnValue( LocationFixture({pathname: '/organizations/org-slug/performance/summary'}) @@ -111,14 +110,12 @@ describe('WrapperComponent', function () { organization, location, eventView, - api, spanOperationBreakdownFilter, transactionName, } = initialize({}); render( [0]) { const data = _initializeData(settings); act(() => void ProjectsStore.loadInitialData(data.projects)); return data; diff --git a/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx b/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx index 15e3e1f7a910d5..17db2b26a85be4 100644 --- a/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx @@ -10,10 +10,15 @@ import { } from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; +import type {Organization} from 'sentry/types/organization'; import EventView from 'sentry/utils/discover/eventView'; import TransactionThresholdButton from 'sentry/views/performance/transactionSummary/transactionThresholdButton'; -function renderComponent(eventView, organization, onChangeThreshold) { +function renderComponent( + eventView: EventView, + organization: Organization, + onChangeThreshold: () => void +) { return render( VitalDetail', function () { }, match: [ (_url, options) => { - return options.query?.field?.find(f => f === 'p50(measurements.lcp)'); + return (options.query?.field as string[])?.some( + f => f === 'p50(measurements.lcp)' + ); }, ], }); @@ -188,7 +190,9 @@ describe('Performance > VitalDetail', function () { }, match: [ (_url, options) => { - return options.query?.field?.find(f => f === 'p50(measurements.cls)'); + return (options.query?.field as string[])?.some( + f => f === 'p50(measurements.cls)' + ); }, ], }); diff --git a/static/app/views/projectsDashboard/index.spec.tsx b/static/app/views/projectsDashboard/index.spec.tsx index 832120ba6ace83..e8035ce41393f4 100644 --- a/static/app/views/projectsDashboard/index.spec.tsx +++ b/static/app/views/projectsDashboard/index.spec.tsx @@ -21,8 +21,8 @@ jest.unmock('lodash/debounce'); jest.mock('lodash/debounce', () => { const debounceMap = new Map(); const mockDebounce = - (fn, timeout) => - (...args) => { + (fn: (...args: any[]) => void, timeout: number) => + (...args: any[]) => { if (debounceMap.has(fn)) { clearTimeout(debounceMap.get(fn)); } diff --git a/static/app/views/releases/list/index.spec.tsx b/static/app/views/releases/list/index.spec.tsx index 18f8f7ade1c03b..3e4dd7c22e326a 100644 --- a/static/app/views/releases/list/index.spec.tsx +++ b/static/app/views/releases/list/index.spec.tsx @@ -1,3 +1,4 @@ +import {LocationFixture} from 'sentry-fixture/locationFixture'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; import {ReleaseFixture} from 'sentry-fixture/release'; @@ -60,7 +61,8 @@ describe('ReleasesList', () => { }, }, }; - let endpointMock, sessionApiMock; + let endpointMock: jest.Mock; + let sessionApiMock: jest.Mock; beforeEach(() => { act(() => ProjectsStore.loadInitialData(projects)); @@ -130,7 +132,7 @@ describe('ReleasesList', () => { }); it('displays the right empty state', async () => { - let location; + let location: ReturnType; const project = ProjectFixture({ id: '3', @@ -155,7 +157,7 @@ describe('ReleasesList', () => { body: [], }); // does not have releases set up and no releases - location = {...routerProps.location, query: {}}; + location = LocationFixture({...routerProps.location, query: {}}); const {rerender} = render( { expect(screen.queryByTestId('release-panel')).not.toBeInTheDocument(); // has releases set up and no releases - location = {query: {query: 'abc'}}; + location = LocationFixture({query: {query: 'abc'}}); rerender( { await screen.findByText("There are no releases that match: 'abc'.") ).toBeInTheDocument(); - location = {query: {sort: ReleasesSortOption.SESSIONS, statsPeriod: '7d'}}; + location = LocationFixture({ + query: {sort: ReleasesSortOption.SESSIONS, statsPeriod: '7d'}, + }); rerender( { await screen.findByText('There are no releases with data in the last 7 days.') ).toBeInTheDocument(); - location = {query: {sort: ReleasesSortOption.USERS_24_HOURS, statsPeriod: '7d'}}; + location = LocationFixture({ + query: {sort: ReleasesSortOption.USERS_24_HOURS, statsPeriod: '7d'}, + }); rerender( { ) ).toBeInTheDocument(); - location = {query: {sort: ReleasesSortOption.SESSIONS_24_HOURS, statsPeriod: '7d'}}; + location = LocationFixture({ + query: {sort: ReleasesSortOption.SESSIONS_24_HOURS, statsPeriod: '7d'}, + }); rerender( { ) ).toBeInTheDocument(); - location = {query: {sort: ReleasesSortOption.BUILD}}; + location = LocationFixture({query: {sort: ReleasesSortOption.BUILD}}); rerender( expect(screen.getByTestId(step)).toBeInTheDocument()); } - async function waitForRenderError(step) { + async function waitForRenderError(step: string) { renderPage(step); await waitFor(() => expect(screen.getByTestId('loading-error')).toBeInTheDocument()); } diff --git a/static/app/views/replays/detail/network/details/content.spec.tsx b/static/app/views/replays/detail/network/details/content.spec.tsx index 715b57921e8381..c915fb274ceacc 100644 --- a/static/app/views/replays/detail/network/details/content.spec.tsx +++ b/static/app/views/replays/detail/network/details/content.spec.tsx @@ -181,7 +181,7 @@ describe('NetworkDetailsContent', () => { ); @@ -204,7 +204,7 @@ describe('NetworkDetailsContent', () => { ); @@ -228,7 +228,7 @@ describe('NetworkDetailsContent', () => { ); @@ -285,7 +285,7 @@ describe('NetworkDetailsContent', () => { ); @@ -310,7 +310,7 @@ describe('NetworkDetailsContent', () => { ); @@ -330,7 +330,7 @@ describe('NetworkDetailsContent', () => { ); @@ -387,7 +387,7 @@ describe('NetworkDetailsContent', () => { ); @@ -412,7 +412,7 @@ describe('NetworkDetailsContent', () => { ); @@ -432,7 +432,7 @@ describe('NetworkDetailsContent', () => { ); diff --git a/static/app/views/replays/detail/network/truncateJson/fixJson.spec.ts b/static/app/views/replays/detail/network/truncateJson/fixJson.spec.ts index 3348c4e3ab884e..29dca6f274d9a6 100644 --- a/static/app/views/replays/detail/network/truncateJson/fixJson.spec.ts +++ b/static/app/views/replays/detail/network/truncateJson/fixJson.spec.ts @@ -69,8 +69,8 @@ describe('Unit | coreHandlers | util | truncateJson | fixJson', () => { }); test.each(['1', '2'])('it works for fixture %s', fixture => { - const input = fixtures[fixture].incompleteJson.trim(); - const expected = fixtures[fixture].completeJson.trim(); + const input = fixtures[fixture as keyof typeof fixtures].incompleteJson.trim(); + const expected = fixtures[fixture as keyof typeof fixtures].completeJson.trim(); const actual = fixJson(input); expect(actual).toEqual(expected); diff --git a/static/app/views/replays/detail/network/useSortNetwork.spec.tsx b/static/app/views/replays/detail/network/useSortNetwork.spec.tsx index 017b2d4804edfe..18427409135878 100644 --- a/static/app/views/replays/detail/network/useSortNetwork.spec.tsx +++ b/static/app/views/replays/detail/network/useSortNetwork.spec.tsx @@ -14,13 +14,13 @@ import useSortNetwork from './useSortNetwork'; jest.mock('sentry/utils/useUrlParams', () => { const map = new Map(); - return (name, dflt) => { + return (name: string, dflt: string) => { if (!map.has(name)) { map.set(name, dflt); } return { getParamValue: () => map.get(name), - setParamValue: value => { + setParamValue: (value: string) => { map.set(name, value); }, }; diff --git a/static/app/views/replays/list/listContent.spec.tsx b/static/app/views/replays/list/listContent.spec.tsx index ab4e2dfd131325..0958faafdf905d 100644 --- a/static/app/views/replays/list/listContent.spec.tsx +++ b/static/app/views/replays/list/listContent.spec.tsx @@ -36,8 +36,8 @@ mockUseReplayOnboardingSidebarPanel.mockReturnValue({activateSidebar: jest.fn()} const mockUseAllMobileProj = jest.mocked(useAllMobileProj); mockUseAllMobileProj.mockReturnValue({allMobileProj: false}); -const AM1_FEATURES = []; -const AM2_FEATURES = ['session-replay']; +const AM1_FEATURES: string[] = []; +const AM2_FEATURES: string[] = ['session-replay']; function getMockOrganizationFixture({features}: {features: string[]}) { const mockOrg = OrganizationFixture({ @@ -49,7 +49,7 @@ function getMockOrganizationFixture({features}: {features: string[]}) { } describe('ReplayList', () => { - let mockFetchReplayListRequest; + let mockFetchReplayListRequest: jest.Mock; beforeEach(() => { mockUseHaveSelectedProjectsSentAnyReplayEvents.mockClear(); mockUseProjectSdkNeedsUpdate.mockClear(); diff --git a/static/app/views/settings/account/accountClose.spec.tsx b/static/app/views/settings/account/accountClose.spec.tsx index fa2f46292d9777..1f27e3780b0216 100644 --- a/static/app/views/settings/account/accountClose.spec.tsx +++ b/static/app/views/settings/account/accountClose.spec.tsx @@ -10,7 +10,7 @@ import { import AccountClose from 'sentry/views/settings/account/accountClose'; describe('AccountClose', function () { - let deleteMock; + let deleteMock: jest.Mock; const soloOrgSlug = 'solo-owner'; const nonSingleOwnerSlug = 'non-single-owner'; diff --git a/static/app/views/settings/account/apiNewToken.spec.tsx b/static/app/views/settings/account/apiNewToken.spec.tsx index 4a693fc0317f7e..d96fcfaaa9b15e 100644 --- a/static/app/views/settings/account/apiNewToken.spec.tsx +++ b/static/app/views/settings/account/apiNewToken.spec.tsx @@ -8,10 +8,10 @@ describe('ApiNewToken', function () { render(); }); - it('renders with disabled "Create Token" button', async function () { + it('renders with disabled "Create Token" button', function () { render(); - expect(await screen.getByRole('button', {name: 'Create Token'})).toBeDisabled(); + expect(screen.getByRole('button', {name: 'Create Token'})).toBeDisabled(); }); it('submits with correct hierarchical scopes', async function () { @@ -22,9 +22,9 @@ describe('ApiNewToken', function () { }); render(); - const createButton = await screen.getByRole('button', {name: 'Create Token'}); + const createButton = screen.getByRole('button', {name: 'Create Token'}); - const selectByValue = (name, value) => + const selectByValue = (name: string, value: string) => selectEvent.select(screen.getByRole('textbox', {name}), value); // Assigning Admin here will also grant read + write access to the resource @@ -77,7 +77,7 @@ describe('ApiNewToken', function () { render(); const createButton = screen.getByRole('button', {name: 'Create Token'}); - const selectByValue = (name, value) => + const selectByValue = (name: string, value: string) => selectEvent.select(screen.getByRole('textbox', {name}), value); await selectByValue('Project', 'Admin'); @@ -115,7 +115,7 @@ describe('ApiNewToken', function () { render(); const createButton = screen.getByRole('button', {name: 'Create Token'}); - const selectByValue = (name, value) => + const selectByValue = (name: string, value: string) => selectEvent.select(screen.getByRole('textbox', {name}), value); await selectByValue('Project', 'Admin'); diff --git a/static/app/views/settings/account/passwordForm.spec.tsx b/static/app/views/settings/account/passwordForm.spec.tsx index f84b0ab8062bec..b42bc1281c7c3b 100644 --- a/static/app/views/settings/account/passwordForm.spec.tsx +++ b/static/app/views/settings/account/passwordForm.spec.tsx @@ -5,7 +5,7 @@ import PasswordForm from 'sentry/views/settings/account/passwordForm'; const ENDPOINT = '/users/me/password/'; describe('PasswordForm', function () { - let putMock; + let putMock: jest.Mock; beforeEach(function () { MockApiClient.clearMockResponses(); diff --git a/static/app/views/settings/components/dataScrubbing/index.spec.tsx b/static/app/views/settings/components/dataScrubbing/index.spec.tsx index bf584b345d8b61..81234bbbb15007 100644 --- a/static/app/views/settings/components/dataScrubbing/index.spec.tsx +++ b/static/app/views/settings/components/dataScrubbing/index.spec.tsx @@ -92,7 +92,7 @@ describe('Data Scrubbing', function () { expect(screen.getByRole('button', {name: 'Add Rule'})).toBeDisabled(); - for (const index in JSON.parse(relayPiiConfig).rules) { + for (const index in JSON.parse(relayPiiConfig).rules as number[]) { expect(screen.getAllByRole('button', {name: 'Edit Rule'})[index]).toBeDisabled(); expect( screen.getAllByRole('button', {name: 'Delete Rule'})[index] diff --git a/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx index c3e0d0a91de07e..c8a1ebdcdef785 100644 --- a/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/permissionSelection.spec.tsx @@ -7,8 +7,8 @@ import ModalStore from 'sentry/stores/modalStore'; import PermissionSelection from 'sentry/views/settings/organizationDeveloperSettings/permissionSelection'; describe('PermissionSelection', () => { - let onChange; - let model; + let onChange: jest.Mock; + let model: FormModel; beforeEach(() => { model = new FormModel(); @@ -42,7 +42,7 @@ describe('PermissionSelection', () => { }); it('lists human readable permissions', async () => { - const expectOptions = async (name, options) => { + const expectOptions = async (name: string, options: string[]) => { for (const option of options) { await selectEvent.select(screen.getByRole('textbox', {name}), option); } @@ -57,7 +57,7 @@ describe('PermissionSelection', () => { }); it('stores the permissions the User has selected', async () => { - const selectByValue = (name, value) => + const selectByValue = (name: string, value: string) => selectEvent.select(screen.getByRole('textbox', {name}), value); await selectByValue('Project', 'Read & Write'); diff --git a/static/app/views/settings/organizationDeveloperSettings/permissionsObserver.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/permissionsObserver.spec.tsx index 80a7a096ee9457..0219c825f2b883 100644 --- a/static/app/views/settings/organizationDeveloperSettings/permissionsObserver.spec.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/permissionsObserver.spec.tsx @@ -5,7 +5,7 @@ import FormModel from 'sentry/components/forms/model'; import PermissionsObserver from 'sentry/views/settings/organizationDeveloperSettings/permissionsObserver'; describe('PermissionsObserver', () => { - let model; + let model: FormModel; beforeEach(() => { model = new FormModel(); diff --git a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx index c47eadf1faa8b7..4a73319ab8edcc 100644 --- a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/index.spec.tsx @@ -14,8 +14,8 @@ describe('Sentry Application Dashboard', function () { const NUM_INSTALLS = 5; const NUM_UNINSTALLS = 2; - let sentryApp; - let webhookRequest; + let sentryApp: ReturnType; + let webhookRequest: ReturnType; afterEach(() => { MockApiClient.clearMockResponses(); diff --git a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDetails.spec.tsx b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDetails.spec.tsx index 5e278600ee0f71..771a8c1f4cdba4 100644 --- a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDetails.spec.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDetails.spec.tsx @@ -16,10 +16,10 @@ import selectEvent from 'sentry-test/selectEvent'; import SentryApplicationDetails from 'sentry/views/settings/organizationDeveloperSettings/sentryApplicationDetails'; describe('Sentry Application Details', function () { - let sentryApp; - let token; - let createAppRequest; - let editAppRequest; + let sentryApp: ReturnType; + let token: ReturnType; + let createAppRequest: jest.Mock; + let editAppRequest: jest.Mock; const maskedValue = '************oken'; @@ -576,7 +576,7 @@ describe('Sentry Application Details', function () { it('handles client secret rotation', async function () { sentryApp = SentryAppFixture(); - sentryApp.clientSecret = null; + sentryApp.clientSecret = undefined; MockApiClient.addMockResponse({ url: `/sentry-apps/${sentryApp.slug}/`, diff --git a/static/app/views/settings/organizationIntegrations/integrationExternalMappingForm.spec.tsx b/static/app/views/settings/organizationIntegrations/integrationExternalMappingForm.spec.tsx index 8664ed09972d9d..8be42c52f4bb96 100644 --- a/static/app/views/settings/organizationIntegrations/integrationExternalMappingForm.spec.tsx +++ b/static/app/views/settings/organizationIntegrations/integrationExternalMappingForm.spec.tsx @@ -11,7 +11,7 @@ describe('IntegrationExternalMappingForm', function () { dataEndpoint, getBaseFormEndpoint: jest.fn(_mapping => dataEndpoint), sentryNamesMapper: mappings => mappings, - }; + } satisfies Partial>; const MOCK_USER_MAPPING = { id: '1', userId: '1', @@ -30,7 +30,9 @@ describe('IntegrationExternalMappingForm', function () { {id: '3', name: 'option3'}, ]; - let getResponse, postResponse, putResponse; + let getResponse: jest.Mock; + let postResponse: jest.Mock; + let putResponse: jest.Mock; beforeEach(() => { jest.clearAllMocks(); diff --git a/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx b/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx index d9e7547553f348..fb39627ac811a6 100644 --- a/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx +++ b/static/app/views/settings/organizationIntegrations/integrationListDirectory.spec.tsx @@ -13,7 +13,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import IntegrationListDirectory from 'sentry/views/settings/organizationIntegrations/integrationListDirectory'; -const mockResponse = mocks => { +const mockResponse = (mocks: [string, unknown][]) => { mocks.forEach(([url, body]) => MockApiClient.addMockResponse({url, body})); }; diff --git a/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx b/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx index 93c74d5928ee6f..cde2ce289ca466 100644 --- a/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx +++ b/static/app/views/settings/organizationIntegrations/sentryAppDetailedView.spec.tsx @@ -37,9 +37,9 @@ describe('SentryAppDetailedView', function () { }); describe('Published Sentry App', function () { - let createRequest; - let deleteRequest; - let sentryAppInteractionRequest; + let createRequest: jest.Mock; + let deleteRequest: jest.Mock; + let sentryAppInteractionRequest: jest.Mock; beforeEach(() => { sentryAppInteractionRequest = MockApiClient.addMockResponse({ @@ -221,7 +221,7 @@ describe('SentryAppDetailedView', function () { }); describe('Unpublished Sentry App without Redirect Url', function () { - let createRequest; + let createRequest: jest.Mock; beforeEach(() => { MockApiClient.addMockResponse({ @@ -312,7 +312,7 @@ describe('SentryAppDetailedView', function () { }); describe('Unpublished Sentry App with Redirect Url', function () { - let createRequest; + let createRequest: jest.Mock; beforeEach(() => { MockApiClient.addMockResponse({ url: `/sentry-apps/go-to-google/interaction/`, diff --git a/static/app/views/settings/organizationTeams/teamMembers.spec.tsx b/static/app/views/settings/organizationTeams/teamMembers.spec.tsx index 690b9f171259f6..b648114d333075 100644 --- a/static/app/views/settings/organizationTeams/teamMembers.spec.tsx +++ b/static/app/views/settings/organizationTeams/teamMembers.spec.tsx @@ -19,7 +19,7 @@ jest.mock('sentry/actionCreators/modal', () => ({ })); describe('TeamMembers', function () { - let createMock; + let createMock: jest.Mock; const organization = OrganizationFixture(); const team = TeamFixture(); diff --git a/static/app/views/settings/project/projectFilters/index.spec.tsx b/static/app/views/settings/project/projectFilters/index.spec.tsx index 5716671d7ffbb9..7fbe4bf28cd986 100644 --- a/static/app/views/settings/project/projectFilters/index.spec.tsx +++ b/static/app/views/settings/project/projectFilters/index.spec.tsx @@ -92,7 +92,9 @@ describe('ProjectFilters', function () { for (const filter of Object.keys(FILTERS)) { const mock = createFilterMock(filter); - await userEvent.click(screen.getByRole('checkbox', {name: FILTERS[filter]})); + await userEvent.click( + screen.getByRole('checkbox', {name: FILTERS[filter as keyof typeof FILTERS]}) + ); expect(mock).toHaveBeenCalledWith( getFilterEndpoint(filter), expect.objectContaining({ diff --git a/static/app/views/settings/project/projectOwnership/viewCodeOwnerModal.spec.tsx b/static/app/views/settings/project/projectOwnership/viewCodeOwnerModal.spec.tsx index 0ff1674cd12698..bbfe57ac31b762 100644 --- a/static/app/views/settings/project/projectOwnership/viewCodeOwnerModal.spec.tsx +++ b/static/app/views/settings/project/projectOwnership/viewCodeOwnerModal.spec.tsx @@ -5,7 +5,9 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import ViewCodeOwnerModal from './viewCodeOwnerModal'; describe('ViewCodeOwnerModal', () => { - const mockComponent: any = ({children}) =>
      {children}
      ; + const mockComponent: any = ({children}: {children: React.ReactNode}) => ( +
      {children}
      + ); it('should display parsed codeowners file', () => { const ownershipSyntax = `codeowners:/src/sentry/migrations/ #developer-infrastructure\n`; diff --git a/static/app/views/settings/projectGeneralSettings/index.spec.tsx b/static/app/views/settings/projectGeneralSettings/index.spec.tsx index 003731a5e15ba7..2aa4e099777df6 100644 --- a/static/app/views/settings/projectGeneralSettings/index.spec.tsx +++ b/static/app/views/settings/projectGeneralSettings/index.spec.tsx @@ -24,7 +24,7 @@ import ProjectGeneralSettings from 'sentry/views/settings/projectGeneralSettings jest.mock('sentry/actionCreators/indicator'); jest.mock('sentry/components/organizations/pageFilters/persistence'); -function getField(role, name) { +function getField(role: string, name: string) { return screen.getByRole(role, {name}); } @@ -40,7 +40,7 @@ describe('projectGeneralSettings', function () { verifySSL: true, }); const groupingConfigs = GroupingConfigsFixture(); - let putMock; + let putMock: jest.Mock; const router = RouterFixture(); const routerProps = { diff --git a/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx b/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx index b1d41af3470ef7..47b73ad8962f7b 100644 --- a/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx +++ b/static/app/views/settings/projectPerformance/projectPerformance.spec.tsx @@ -27,7 +27,9 @@ describe('projectPerformance', function () { }); const project = ProjectFixture(); const configUrl = '/projects/org-slug/project-slug/transaction-threshold/configure/'; - let getMock, postMock, deleteMock; + let getMock: jest.Mock; + let postMock: jest.Mock; + let deleteMock: jest.Mock; const router = RouterFixture(); const routerProps = { @@ -351,7 +353,7 @@ describe('projectPerformance', function () { // Ensure that PUT request is fired to update // project settings - const expectedPUTPayload = {}; + const expectedPUTPayload: Record = {}; expectedPUTPayload[threshold] = newValue; expect(performanceIssuesPutMock).toHaveBeenCalledWith( '/projects/org-slug/project-slug/performance-issues/configure/', diff --git a/static/app/views/settings/projectPerformance/projectPerformance.tsx b/static/app/views/settings/projectPerformance/projectPerformance.tsx index cba82eb1e05362..73aa25cc759bfd 100644 --- a/static/app/views/settings/projectPerformance/projectPerformance.tsx +++ b/static/app/views/settings/projectPerformance/projectPerformance.tsx @@ -162,7 +162,7 @@ class ProjectPerformance extends DeprecatedAsyncView { return endpoints; } - getRetentionPrioritiesData(...data) { + getRetentionPrioritiesData(...data: any) { return { dynamicSamplingBiases: Object.entries(data[1].form).map(([key, value]) => ({ id: key, @@ -943,10 +943,13 @@ class ProjectPerformance extends DeprecatedAsyncView { saveOnBlur allowUndo initialData={ - project.dynamicSamplingBiases?.reduce((acc, bias) => { - acc[bias.id] = bias.active; - return acc; - }, {}) ?? {} + project.dynamicSamplingBiases?.reduce>( + (acc, bias) => { + acc[bias.id] = bias.active; + return acc; + }, + {} + ) ?? {} } onSubmitSuccess={(response, _instance, id, change) => { ProjectsStore.onUpdateSuccess(response); diff --git a/static/app/views/unsubscribe/issue.spec.tsx b/static/app/views/unsubscribe/issue.spec.tsx index 06cbfea73243e5..0d3040e0b22cb2 100644 --- a/static/app/views/unsubscribe/issue.spec.tsx +++ b/static/app/views/unsubscribe/issue.spec.tsx @@ -5,7 +5,9 @@ import UnsubscribeIssue from 'sentry/views/unsubscribe/issue'; describe('UnsubscribeIssue', function () { const params = {orgId: 'acme', id: '9876'}; - let mockUpdate, mockGet; + let mockUpdate: jest.Mock; + let mockGet: jest.Mock; + beforeEach(() => { mockUpdate = MockApiClient.addMockResponse({ url: '/organizations/acme/unsubscribe/issue/9876/?_=signature-value', diff --git a/static/app/views/userFeedback/userFeedbackEmpty.tsx b/static/app/views/userFeedback/userFeedbackEmpty.tsx index a3c0b60db01027..5b52c82061de63 100644 --- a/static/app/views/userFeedback/userFeedbackEmpty.tsx +++ b/static/app/views/userFeedback/userFeedbackEmpty.tsx @@ -54,7 +54,7 @@ export function UserFeedbackEmpty({projectIds, issueTab = false}: Props) { window.sentryEmbedCallback = function (embed) { // Mock the embed's submit xhr to always be successful // NOTE: this will not have errors if the form is empty - embed.submit = function (_body) { + embed.submit = function (_body: Record) { this._submitInProgress = true; setTimeout(() => { this._submitInProgress = false; diff --git a/tests/js/sentry-test/reactTestingLibrary.spec.tsx b/tests/js/sentry-test/reactTestingLibrary.spec.tsx index bae343582d90db..8a440a3ef0f94d 100644 --- a/tests/js/sentry-test/reactTestingLibrary.spec.tsx +++ b/tests/js/sentry-test/reactTestingLibrary.spec.tsx @@ -6,7 +6,7 @@ describe('rerender', () => { // Taken from https://testing-library.com/docs/example-update-props/ let idCounter = 1; - function NumberDisplay({number}) { + function NumberDisplay({number}: {number: number}) { const id = useRef(idCounter++); // to ensure we don't remount a different instance return ( From 2575b664fc3918dfa2c381ac5127e8baf7f345a8 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 27 Dec 2024 10:38:10 -0800 Subject: [PATCH 500/757] ref(profiling): Remove feature flag checks for new search (#82431) --- static/app/views/profiling/content.tsx | 25 +++++-------------- .../views/profiling/profileSummary/index.tsx | 25 +++++-------------- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/static/app/views/profiling/content.tsx b/static/app/views/profiling/content.tsx index 989cdae0fae4f4..93331450dc658c 100644 --- a/static/app/views/profiling/content.tsx +++ b/static/app/views/profiling/content.tsx @@ -4,7 +4,6 @@ import type {Location} from 'history'; import {Alert} from 'sentry/components/alert'; import {Button, LinkButton} from 'sentry/components/button'; -import SearchBar from 'sentry/components/events/searchBar'; import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton'; import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; @@ -25,7 +24,6 @@ import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {SidebarPanelKey} from 'sentry/components/sidebar/types'; import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import {TabList, Tabs} from 'sentry/components/tabs'; -import {MAX_QUERY_LENGTH} from 'sentry/constants'; import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters'; import {t} from 'sentry/locale'; import SidebarPanelStore from 'sentry/stores/sidebarPanelStore'; @@ -228,23 +226,12 @@ function ProfilingTransactionsContent(props: ProfilingTabContentProps) { - {organization.features.includes('search-query-builder-performance') ? ( - - ) : ( - - )} + {props.shouldShowProfilingOnboardingPanel ? ( diff --git a/static/app/views/profiling/profileSummary/index.tsx b/static/app/views/profiling/profileSummary/index.tsx index e25ab8f06e6d33..7852f6448f2913 100644 --- a/static/app/views/profiling/profileSummary/index.tsx +++ b/static/app/views/profiling/profileSummary/index.tsx @@ -8,7 +8,6 @@ import type {SelectOption} from 'sentry/components/compactSelect/types'; import Count from 'sentry/components/count'; import {DateTime} from 'sentry/components/dateTime'; import ErrorBoundary from 'sentry/components/errorBoundary'; -import SearchBar from 'sentry/components/events/searchBar'; import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton'; import IdBadge from 'sentry/components/idBadge'; import * as Layout from 'sentry/components/layouts/thirds'; @@ -29,7 +28,6 @@ import {SegmentedControl} from 'sentry/components/segmentedControl'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import {TabList, Tabs} from 'sentry/components/tabs'; -import {MAX_QUERY_LENGTH} from 'sentry/constants'; import {IconPanel} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; @@ -231,23 +229,12 @@ function ProfileFilters(props: ProfileFiltersProps) { - {props.organization.features.includes('search-query-builder-performance') ? ( - - ) : ( - - )} + ); } From 8e0d58f35f46455cafa28acc57c755e1b9028240 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 27 Dec 2024 10:38:41 -0800 Subject: [PATCH 501/757] ref(traces): Remove feature flag check for search-query-builder (#82432) --- static/app/views/traces/tracesSearchBar.tsx | 38 ++++----------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/static/app/views/traces/tracesSearchBar.tsx b/static/app/views/traces/tracesSearchBar.tsx index c48e479aa98f4a..f71eb75e2c9454 100644 --- a/static/app/views/traces/tracesSearchBar.tsx +++ b/static/app/views/traces/tracesSearchBar.tsx @@ -1,17 +1,13 @@ import styled from '@emotion/styled'; import {Button} from 'sentry/components/button'; -import SearchBar from 'sentry/components/events/searchBar'; import {SpanSearchQueryBuilder} from 'sentry/components/performance/spanSearchQueryBuilder'; import {IconAdd, IconClose} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import {SavedSearchType} from 'sentry/types/group'; import {trackAnalytics} from 'sentry/utils/analytics'; -import {DiscoverDatasets} from 'sentry/utils/discover/types'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; -import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags'; interface TracesSearchBarProps { handleClearSearch: (index: number) => boolean; @@ -38,35 +34,17 @@ export function TracesSearchBar({ const canAddMoreQueries = queries.length <= 2; const localQueries = queries.length ? queries : ['']; - // Since trace explorer permits cross project searches, - // autocompletion should also be cross projects. - const {data: supportedTags} = useSpanFieldSupportedTags(); - return ( {localQueries.map((query, index) => ( {getSpanName(index)} - {organization.features.includes('search-query-builder-performance') ? ( - handleSearch(index, queryString)} - searchSource="trace-explorer" - /> - ) : ( - handleSearch(index, queryString)} - placeholder={t('Search for span attributes')} - organization={organization} - supportedTags={supportedTags} - dataset={DiscoverDatasets.SPANS_INDEXED} - projectIds={selection.projects} - savedSearchType={SavedSearchType.SPAN} - /> - )} + handleSearch(index, queryString)} + searchSource="trace-explorer" + /> } @@ -128,10 +106,6 @@ const SpanLetter = styled('div')` align-content: center; `; -const StyledSearchBar = styled(SearchBar)` - width: 100%; -`; - const StyledButton = styled(Button)` height: 38px; `; From 6a6858cc1cc993da5704dfdcd5b5a4b395204d85 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 27 Dec 2024 10:59:05 -0800 Subject: [PATCH 502/757] feat(exception-groups): Record analytics for size of exception group trees (#82568) --- .../analytics/workflowAnalyticsEvents.tsx | 2 + static/app/utils/eventExceptionGroup.spec.tsx | 334 ++++++++++++++++++ static/app/utils/eventExceptionGroup.tsx | 87 +++++ static/app/utils/events.tsx | 40 ++- 4 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 static/app/utils/eventExceptionGroup.spec.tsx create mode 100644 static/app/utils/eventExceptionGroup.tsx diff --git a/static/app/utils/analytics/workflowAnalyticsEvents.tsx b/static/app/utils/analytics/workflowAnalyticsEvents.tsx index eef42649cd0066..3b5f7c6bd07e8c 100644 --- a/static/app/utils/analytics/workflowAnalyticsEvents.tsx +++ b/static/app/utils/analytics/workflowAnalyticsEvents.tsx @@ -19,6 +19,8 @@ interface IssueDetailsWithAlert extends CommonGroupAnalyticsData { export type BaseEventAnalyticsParams = { event_id: string; + exception_group_height: number; + exception_group_width: number; has_commit: boolean; has_exception_group: boolean; has_local_variables: boolean; diff --git a/static/app/utils/eventExceptionGroup.spec.tsx b/static/app/utils/eventExceptionGroup.spec.tsx new file mode 100644 index 00000000000000..142afd34171cf6 --- /dev/null +++ b/static/app/utils/eventExceptionGroup.spec.tsx @@ -0,0 +1,334 @@ +import {EventEntryExceptionGroupFixture} from 'sentry-fixture/eventEntryExceptionGroup'; +import {ExceptionValueFixture} from 'sentry-fixture/exceptionValue'; + +import {EntryType} from 'sentry/types/event'; +import { + buildExceptionGroupTree, + getExceptionGroupHeight, + getExceptionGroupWidth, +} from 'sentry/utils/eventExceptionGroup'; + +describe('eventExceptionGroup', function () { + describe('buildExceptionGroupTree', function () { + it('builds the exception group tree', function () { + const exception = EventEntryExceptionGroupFixture(); + + expect(buildExceptionGroupTree(exception)).toEqual([ + { + value: expect.objectContaining({ + type: 'ExceptionGroup 1', + }), + children: [ + { + value: expect.objectContaining({ + type: 'ExceptionGroup 2', + }), + children: [ + { + value: expect.objectContaining({ + type: 'ValueError', + }), + children: [], + }, + ], + }, + { + value: expect.objectContaining({ + type: 'TypeError', + }), + children: [], + }, + ], + }, + ]); + }); + }); + + describe('getExceptionGroupHeight', function () { + it('gets the height of the exception group', function () { + const exception = EventEntryExceptionGroupFixture(); + expect(getExceptionGroupHeight(exception)).toBe(3); + }); + + it('returns 0 with no values', function () { + expect( + getExceptionGroupHeight({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [], + }, + }) + ).toBe(0); + }); + + it('returns 1 with single parent', function () { + expect( + getExceptionGroupHeight({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [ + ExceptionValueFixture({ + mechanism: { + handled: true, + exception_id: 1, + is_exception_group: true, + type: 'ExceptionGroup 1', + }, + }), + ], + }, + }) + ).toBe(1); + }); + + it('returns 2 with a parent and a child', function () { + expect( + getExceptionGroupHeight({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [ + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 2', + is_exception_group: false, + exception_id: 2, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 1', + is_exception_group: true, + exception_id: 1, + }, + }), + ], + }, + }) + ).toBe(2); + }); + + it('returns 2 with a parent and 2 children', function () { + expect( + getExceptionGroupHeight({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [ + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 2', + is_exception_group: false, + exception_id: 2, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 3', + is_exception_group: false, + exception_id: 3, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 1', + is_exception_group: true, + exception_id: 1, + }, + }), + ], + }, + }) + ).toBe(2); + }); + }); + + describe('getExceptionGroupWidth', function () { + it('returns 0 with no values', function () { + expect( + getExceptionGroupWidth({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [], + }, + }) + ).toBe(0); + }); + + it('returns 1 with a single parent', function () { + expect( + getExceptionGroupWidth({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [ + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 1', + is_exception_group: true, + exception_id: 1, + }, + }), + ], + }, + }) + ).toBe(1); + }); + + it('returns 1 with a parent and a child', function () { + expect( + getExceptionGroupWidth({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [ + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 2', + is_exception_group: false, + exception_id: 2, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 1', + is_exception_group: true, + exception_id: 1, + }, + }), + ], + }, + }) + ).toBe(1); + }); + + it('returns 2 with a parent and 2 children', function () { + expect( + getExceptionGroupWidth({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [ + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 2', + is_exception_group: false, + exception_id: 2, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 3', + is_exception_group: false, + exception_id: 3, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 1', + is_exception_group: true, + exception_id: 1, + }, + }), + ], + }, + }) + ).toBe(2); + }); + + it('returns 3 with a parent 3 grandchildren', function () { + expect( + getExceptionGroupWidth({ + type: EntryType.EXCEPTION, + data: { + excOmitted: null, + hasSystemFrames: false, + values: [ + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 4', + is_exception_group: false, + exception_id: 4, + parent_id: 2, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 5', + is_exception_group: false, + exception_id: 5, + parent_id: 2, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 6', + is_exception_group: false, + exception_id: 6, + parent_id: 3, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 2', + is_exception_group: false, + exception_id: 2, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 3', + is_exception_group: false, + exception_id: 3, + parent_id: 1, + }, + }), + ExceptionValueFixture({ + mechanism: { + handled: true, + type: 'ExceptionGroup 1', + is_exception_group: true, + exception_id: 1, + }, + }), + ], + }, + }) + ).toBe(3); + }); + }); +}); diff --git a/static/app/utils/eventExceptionGroup.tsx b/static/app/utils/eventExceptionGroup.tsx new file mode 100644 index 00000000000000..341f9f9567e027 --- /dev/null +++ b/static/app/utils/eventExceptionGroup.tsx @@ -0,0 +1,87 @@ +import type {EntryException, ExceptionValue} from 'sentry/types/event'; +import {defined} from 'sentry/utils'; + +type ExceptionGroupTreeItem = { + children: Array; + value: ExceptionValue; +}; + +function buildExceptionGroupTreeRecursive( + values: ExceptionValue[], + parentId: number | undefined, + visited: Set = new Set() +): ExceptionGroupTreeItem[] { + const tree: ExceptionGroupTreeItem[] = []; + + values.forEach(value => { + if ( + value.mechanism?.parent_id === parentId && + defined(value.mechanism?.exception_id) && + !visited.has(value.mechanism.exception_id) + ) { + visited.add(value.mechanism.exception_id); + tree.push({ + children: buildExceptionGroupTreeRecursive( + values, + value.mechanism.exception_id, + visited + ), + value, + }); + } + }); + + return tree; +} + +export function buildExceptionGroupTree(entry: EntryException) { + const values = entry.data.values || []; + + return buildExceptionGroupTreeRecursive(values, undefined); +} + +function getTreeHeightRecursive(tree: ExceptionGroupTreeItem[], maxHeight: number = 0) { + if (!tree.length) { + return maxHeight; + } + + let maxLevelHeight = maxHeight; + + for (const node of tree) { + const height = 1 + getTreeHeightRecursive(node.children, maxHeight); + maxLevelHeight = Math.max(maxLevelHeight, height); + } + + return Math.max(maxHeight, maxLevelHeight); +} + +function getTreeWidthRecursive( + tree: ExceptionGroupTreeItem[], + depth: number = 0, + widthByDepth: number[] = [] +) { + if (!tree.length) { + return 0; + } + + widthByDepth[depth] = widthByDepth[depth] || 0; + + for (const node of tree) { + widthByDepth[depth] += 1; + getTreeWidthRecursive(node.children, depth + 1, widthByDepth); + } + + return Math.max(...widthByDepth); +} + +export function getExceptionGroupHeight(entry: EntryException) { + const tree = buildExceptionGroupTree(entry); + + return getTreeHeightRecursive(tree); +} + +export function getExceptionGroupWidth(entry: EntryException) { + const tree = buildExceptionGroupTree(entry); + + return getTreeWidthRecursive(tree); +} diff --git a/static/app/utils/events.tsx b/static/app/utils/events.tsx index 50ffc2e58506a2..3800a7c3c0d9b7 100644 --- a/static/app/utils/events.tsx +++ b/static/app/utils/events.tsx @@ -1,3 +1,5 @@ +import * as Sentry from '@sentry/react'; + import {SymbolicatorStatus} from 'sentry/components/events/interfaces/types'; import ConfigStore from 'sentry/stores/configStore'; import type { @@ -20,6 +22,10 @@ import {GroupActivityType, IssueCategory, IssueType} from 'sentry/types/group'; import {defined} from 'sentry/utils'; import type {BaseEventAnalyticsParams} from 'sentry/utils/analytics/workflowAnalyticsEvents'; import {uniq} from 'sentry/utils/array/uniq'; +import { + getExceptionGroupHeight, + getExceptionGroupWidth, +} from 'sentry/utils/eventExceptionGroup'; import {getDaysSinceDatePrecise} from 'sentry/utils/getDaysSinceDate'; import {isMobilePlatform, isNativePlatform} from 'sentry/utils/platform'; import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent'; @@ -269,7 +275,7 @@ function getExceptionFrames(event: Event, inAppOnly: boolean) { /** * Returns all entries of type 'exception' of this event */ -function getExceptionEntries(event: Event) { +export function getExceptionEntries(event: Event) { return (event.entries?.filter(entry => entry.type === EntryType.EXCEPTION) || []) as EntryException[]; } @@ -349,6 +355,36 @@ export function eventHasExceptionGroup(event: Event) { ); } +export function eventExceptionGroupHeight(event: Event) { + try { + const exceptionEntry = getExceptionEntries(event)[0]; + + if (!exceptionEntry) { + return 0; + } + + return getExceptionGroupHeight(exceptionEntry); + } catch (e) { + Sentry.captureException(e); + return 0; + } +} + +export function eventExceptionGroupWidth(event: Event) { + try { + const exceptionEntry = getExceptionEntries(event)[0]; + + if (!exceptionEntry) { + return 0; + } + + return getExceptionGroupWidth(exceptionEntry); + } catch (e) { + Sentry.captureException(e); + return 0; + } +} + export function eventHasGraphQlRequest(event: Event) { const requestEntry = event.entries?.find(entry => entry.type === EntryType.REQUEST) as | EntryRequest @@ -390,6 +426,8 @@ export function getAnalyticsDataForEvent(event?: Event | null): BaseEventAnalyti event_type: event?.type, has_release: !!event?.release, has_exception_group: event ? eventHasExceptionGroup(event) : false, + exception_group_height: event ? eventExceptionGroupHeight(event) : 0, + exception_group_width: event ? eventExceptionGroupWidth(event) : 0, has_graphql_request: event ? eventHasGraphQlRequest(event) : false, has_profile: event ? hasProfile(event) : false, has_source_context: event ? eventHasSourceContext(event) : false, From 4172f7b514973bc5713f5803235d3a4a1dda06b3 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Fri, 27 Dec 2024 11:10:21 -0800 Subject: [PATCH 503/757] test(ui): Prevent importing from .spec files (#82584) --- .eslintrc.js | 5 + .../highlights/editHighlightsModal.spec.tsx | 6 +- .../highlights/highlightsDataSection.spec.tsx | 6 +- .../highlights/highlightsIconSummary.spec.tsx | 6 +- .../events/highlights/testUtils.tsx | 64 +++++++++ .../events/highlights/util.spec.tsx | 65 +-------- .../profile/continuousProfile.spec.tsx | 39 +---- .../profiling/profile/eventedProfile.spec.tsx | 2 +- .../profiling/profile/importProfile.spec.tsx | 3 +- .../profiling/profile/jsSelfProfile.spec.tsx | 2 +- .../utils/profiling/profile/profile.spec.tsx | 35 +---- .../profiling/profile/sampledProfile.spec.tsx | 2 +- .../profile/sentrySampledProfile.spec.tsx | 69 +-------- .../app/utils/profiling/profile/testUtils.tsx | 134 ++++++++++++++++++ 14 files changed, 218 insertions(+), 220 deletions(-) create mode 100644 static/app/components/events/highlights/testUtils.tsx create mode 100644 static/app/utils/profiling/profile/testUtils.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 4d62e1e791359f..29e19b2aff307f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -546,6 +546,11 @@ const appRules = { group: ['sentry/components/devtoolbar/*'], message: 'Do not depend on toolbar internals', }, + { + group: ['*.spec*'], + message: + 'Do not import from test files. This causes tests to be executed multiple times.', + }, ], paths: [ { diff --git a/static/app/components/events/highlights/editHighlightsModal.spec.tsx b/static/app/components/events/highlights/editHighlightsModal.spec.tsx index c1b6cbefe1a73c..fa50b3b3d16269 100644 --- a/static/app/components/events/highlights/editHighlightsModal.spec.tsx +++ b/static/app/components/events/highlights/editHighlightsModal.spec.tsx @@ -14,14 +14,12 @@ import {openModal} from 'sentry/actionCreators/modal'; import EditHighlightsModal, { type EditHighlightsModalProps, } from 'sentry/components/events/highlights/editHighlightsModal'; -import { - TEST_EVENT_CONTEXTS, - TEST_EVENT_TAGS, -} from 'sentry/components/events/highlights/util.spec'; import ModalStore from 'sentry/stores/modalStore'; import type {Project} from 'sentry/types/project'; import * as analytics from 'sentry/utils/analytics'; +import {TEST_EVENT_CONTEXTS, TEST_EVENT_TAGS} from './testUtils'; + describe('EditHighlightsModal', function () { const organization = OrganizationFixture(); const project = ProjectFixture(); diff --git a/static/app/components/events/highlights/highlightsDataSection.spec.tsx b/static/app/components/events/highlights/highlightsDataSection.spec.tsx index 9188e583dc853f..2f56f466900287 100644 --- a/static/app/components/events/highlights/highlightsDataSection.spec.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.spec.tsx @@ -7,13 +7,11 @@ import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary import * as modal from 'sentry/actionCreators/modal'; import HighlightsDataSection from 'sentry/components/events/highlights/highlightsDataSection'; import {EMPTY_HIGHLIGHT_DEFAULT} from 'sentry/components/events/highlights/util'; -import { - TEST_EVENT_CONTEXTS, - TEST_EVENT_TAGS, -} from 'sentry/components/events/highlights/util.spec'; import ProjectsStore from 'sentry/stores/projectsStore'; import * as analytics from 'sentry/utils/analytics'; +import {TEST_EVENT_CONTEXTS, TEST_EVENT_TAGS} from './testUtils'; + describe('HighlightsDataSection', function () { const organization = OrganizationFixture(); const project = ProjectFixture(); diff --git a/static/app/components/events/highlights/highlightsIconSummary.spec.tsx b/static/app/components/events/highlights/highlightsIconSummary.spec.tsx index c710d35ca96a06..cd285ffe66c8be 100644 --- a/static/app/components/events/highlights/highlightsIconSummary.spec.tsx +++ b/static/app/components/events/highlights/highlightsIconSummary.spec.tsx @@ -5,10 +5,8 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {HighlightsIconSummary} from 'sentry/components/events/highlights/highlightsIconSummary'; -import { - TEST_EVENT_CONTEXTS, - TEST_EVENT_TAGS, -} from 'sentry/components/events/highlights/util.spec'; + +import {TEST_EVENT_CONTEXTS, TEST_EVENT_TAGS} from './testUtils'; jest.mock('sentry/components/events/contexts/contextIcon', () => ({ ...jest.requireActual('sentry/components/events/contexts/contextIcon'), diff --git a/static/app/components/events/highlights/testUtils.tsx b/static/app/components/events/highlights/testUtils.tsx new file mode 100644 index 00000000000000..d08dc448e9b411 --- /dev/null +++ b/static/app/components/events/highlights/testUtils.tsx @@ -0,0 +1,64 @@ +export const TEST_EVENT_CONTEXTS = { + keyboard: { + type: 'default', + brand: 'keychron', + percent: 75, + switches: { + form: 'tactile', + brand: 'wuque studios', + }, + }, + client_os: { + type: 'os', + name: 'Mac OS X', + version: '10.15', + }, + runtime: { + type: 'runtime', + name: 'CPython', + version: '3.8.13', + }, +}; + +export const TEST_EVENT_TAGS = [ + { + key: 'browser', + value: 'Chrome 1.2.3', + }, + { + key: 'browser.name', + value: 'Chrome', + }, + { + key: 'device.family', + value: 'Mac', + }, + { + key: 'environment', + value: 'production', + }, + { + key: 'handled', + value: 'no', + }, + { + key: 'level', + value: 'error', + }, + { + key: 'release', + value: '1.8', + }, + { + key: 'runtime', + value: 'CPython 3.8.13', + }, + { + key: 'runtime.name', + value: 'CPython', + }, + { + key: 'url', + value: 'https://example.com', + }, +]; diff --git a/static/app/components/events/highlights/util.spec.tsx b/static/app/components/events/highlights/util.spec.tsx index e802cbb80ade5d..e46784cf47bed7 100644 --- a/static/app/components/events/highlights/util.spec.tsx +++ b/static/app/components/events/highlights/util.spec.tsx @@ -9,70 +9,7 @@ import { getHighlightTagData, } from 'sentry/components/events/highlights/util'; -export const TEST_EVENT_CONTEXTS = { - keyboard: { - type: 'default', - brand: 'keychron', - percent: 75, - switches: { - form: 'tactile', - brand: 'wuque studios', - }, - }, - client_os: { - type: 'os', - name: 'Mac OS X', - version: '10.15', - }, - runtime: { - type: 'runtime', - name: 'CPython', - version: '3.8.13', - }, -}; - -export const TEST_EVENT_TAGS = [ - { - key: 'browser', - value: 'Chrome 1.2.3', - }, - { - key: 'browser.name', - value: 'Chrome', - }, - { - key: 'device.family', - value: 'Mac', - }, - { - key: 'environment', - value: 'production', - }, - { - key: 'handled', - value: 'no', - }, - { - key: 'level', - value: 'error', - }, - { - key: 'release', - value: '1.8', - }, - { - key: 'runtime', - value: 'CPython 3.8.13', - }, - { - key: 'runtime.name', - value: 'CPython', - }, - { - key: 'url', - value: 'https://example.com', - }, -]; +import {TEST_EVENT_CONTEXTS, TEST_EVENT_TAGS} from './testUtils'; describe('getHighlightContextData', function () { it('returns only highlight context data', function () { diff --git a/static/app/utils/profiling/profile/continuousProfile.spec.tsx b/static/app/utils/profiling/profile/continuousProfile.spec.tsx index da3c9e44781e36..29d8aa13818289 100644 --- a/static/app/utils/profiling/profile/continuousProfile.spec.tsx +++ b/static/app/utils/profiling/profile/continuousProfile.spec.tsx @@ -1,44 +1,7 @@ -import merge from 'lodash/merge'; - -import type {DeepPartial} from 'sentry/types/utils'; import {ContinuousProfile} from 'sentry/utils/profiling/profile/continuousProfile'; import {createContinuousProfileFrameIndex} from 'sentry/utils/profiling/profile/utils'; -import {makeTestingBoilerplate} from './profile.spec'; - -export function makeSentryContinuousProfile( - profile?: DeepPartial -): Profiling.SentryContinousProfileChunk { - return merge( - { - chunk_id: 'chunk_id', - environment: '', - project_id: 0, - received: 0, - release: '', - organization_id: 0, - retention_days: 0, - version: '2', - platform: 'node', - profile: { - samples: [ - {timestamp: Date.now() / 1e3, stack_id: 0, thread_id: '0'}, - // 10ms later - {timestamp: Date.now() / 1e3 + 0.01, stack_id: 1, thread_id: '0'}, - ], - frames: [ - {function: 'foo', in_app: true}, - {function: 'bar', in_app: true}, - ], - stacks: [ - [0, 1], - [0, 1], - ], - }, - }, - profile - ) as Profiling.SentryContinousProfileChunk; -} +import {makeSentryContinuousProfile, makeTestingBoilerplate} from './testUtils'; describe('ContinuousProfile', () => { it('imports the base properties', () => { diff --git a/static/app/utils/profiling/profile/eventedProfile.spec.tsx b/static/app/utils/profiling/profile/eventedProfile.spec.tsx index 9af14ed788d1ef..549a3ba48a3f12 100644 --- a/static/app/utils/profiling/profile/eventedProfile.spec.tsx +++ b/static/app/utils/profiling/profile/eventedProfile.spec.tsx @@ -3,7 +3,7 @@ import {createFrameIndex} from 'sentry/utils/profiling/profile/utils'; import {Frame} from '../frame'; -import {firstCallee, makeTestingBoilerplate} from './profile.spec'; +import {firstCallee, makeTestingBoilerplate} from './testUtils'; describe('EventedProfile', () => { it('imports the base properties', () => { diff --git a/static/app/utils/profiling/profile/importProfile.spec.tsx b/static/app/utils/profiling/profile/importProfile.spec.tsx index 8a3e4c57702e88..c6a34414d3deb1 100644 --- a/static/app/utils/profiling/profile/importProfile.spec.tsx +++ b/static/app/utils/profiling/profile/importProfile.spec.tsx @@ -7,9 +7,8 @@ import { import {JSSelfProfile} from 'sentry/utils/profiling/profile/jsSelfProfile'; import {SampledProfile} from 'sentry/utils/profiling/profile/sampledProfile'; -import {makeSentryContinuousProfile} from './continuousProfile.spec'; import {SentrySampledProfile} from './sentrySampledProfile'; -import {makeSentrySampledProfile} from './sentrySampledProfile.spec'; +import {makeSentryContinuousProfile, makeSentrySampledProfile} from './testUtils'; describe('importProfile', () => { it('imports evented profile', () => { diff --git a/static/app/utils/profiling/profile/jsSelfProfile.spec.tsx b/static/app/utils/profiling/profile/jsSelfProfile.spec.tsx index 732402edd1b299..b11b4ff505a14e 100644 --- a/static/app/utils/profiling/profile/jsSelfProfile.spec.tsx +++ b/static/app/utils/profiling/profile/jsSelfProfile.spec.tsx @@ -1,7 +1,7 @@ import {JSSelfProfile} from 'sentry/utils/profiling/profile/jsSelfProfile'; import {createFrameIndex} from 'sentry/utils/profiling/profile/utils'; -import {firstCallee, makeTestingBoilerplate, nthCallee} from './profile.spec'; +import {firstCallee, makeTestingBoilerplate, nthCallee} from './testUtils'; describe('jsSelfProfile', () => { it('imports the base properties', () => { diff --git a/static/app/utils/profiling/profile/profile.spec.tsx b/static/app/utils/profiling/profile/profile.spec.tsx index 1a92cb0103976d..4d37e2467d90bc 100644 --- a/static/app/utils/profiling/profile/profile.spec.tsx +++ b/static/app/utils/profiling/profile/profile.spec.tsx @@ -1,39 +1,8 @@ -import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode'; +import type {CallTreeNode} from 'sentry/utils/profiling/callTreeNode'; import {Frame} from 'sentry/utils/profiling/frame'; import {Profile} from 'sentry/utils/profiling/profile/profile'; -// Test utils to keep the tests code dry -export const f = (name: string, key: number, in_app: boolean = true) => - new Frame({name, key, is_application: in_app}); -export const c = (fr: Frame) => new CallTreeNode(fr, null); -export const firstCallee = (node: CallTreeNode) => node.children[0]; -export const nthCallee = (node: CallTreeNode, n: number) => { - const child = node.children[n]; - if (!child) { - throw new Error('Child not found'); - } - return child; -}; - -export const makeTestingBoilerplate = () => { - const timings: [Frame['name'], string][] = []; - - const openSpy = jest.fn(); - const closeSpy = jest.fn(); - - // We need to wrap the spy fn because they are not allowed to reference external variables - const open = (node, value) => { - timings.push([node.frame.name, 'open']); - openSpy(node, value); - }; - // We need to wrap the spy fn because they are not allowed to reference external variables - const close = (node, val) => { - timings.push([node.frame.name, 'close']); - closeSpy(node, val); - }; - - return {open, close, timings, openSpy, closeSpy}; -}; +import {c, f, makeTestingBoilerplate} from './testUtils'; // Since it's easy to make mistakes or accidentally assign parents to the wrong nodes, this utility fn // will format the stack samples as a tree string so it's more human friendly. diff --git a/static/app/utils/profiling/profile/sampledProfile.spec.tsx b/static/app/utils/profiling/profile/sampledProfile.spec.tsx index b6ab8ce90d5e04..7bdc0dd7a06c13 100644 --- a/static/app/utils/profiling/profile/sampledProfile.spec.tsx +++ b/static/app/utils/profiling/profile/sampledProfile.spec.tsx @@ -3,7 +3,7 @@ import {createFrameIndex} from 'sentry/utils/profiling/profile/utils'; import {Frame} from '../frame'; -import {firstCallee, makeTestingBoilerplate} from './profile.spec'; +import {firstCallee, makeTestingBoilerplate} from './testUtils'; describe('SampledProfile', () => { it('imports the base properties', () => { diff --git a/static/app/utils/profiling/profile/sentrySampledProfile.spec.tsx b/static/app/utils/profiling/profile/sentrySampledProfile.spec.tsx index 8323fc32217460..c58e513645da35 100644 --- a/static/app/utils/profiling/profile/sentrySampledProfile.spec.tsx +++ b/static/app/utils/profiling/profile/sentrySampledProfile.spec.tsx @@ -1,76 +1,9 @@ -import merge from 'lodash/merge'; - -import type {DeepPartial} from 'sentry/types/utils'; - import {Frame} from '../frame'; -import {makeTestingBoilerplate} from './profile.spec'; import {SentrySampledProfile} from './sentrySampledProfile'; +import {makeSentrySampledProfile, makeTestingBoilerplate} from './testUtils'; import {createSentrySampleProfileFrameIndex} from './utils'; -export const makeSentrySampledProfile = ( - profile?: DeepPartial -) => { - return merge( - { - event_id: '1', - version: '1', - os: { - name: 'iOS', - version: '16.0', - build_number: '19H253', - }, - device: { - architecture: 'arm64e', - is_emulator: false, - locale: 'en_US', - manufacturer: 'Apple', - model: 'iPhone14,3', - }, - timestamp: '2022-09-01T09:45:00.000Z', - platform: 'cocoa', - profile: { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: 0, - }, - { - stack_id: 1, - thread_id: '0', - elapsed_since_start_ns: 1000, - }, - ], - frames: [ - { - function: 'main', - instruction_addr: '', - lineno: 1, - colno: 1, - file: 'main.c', - }, - { - function: 'foo', - instruction_addr: '', - lineno: 2, - colno: 2, - file: 'main.c', - }, - ], - stacks: [[1, 0], [0]], - }, - transaction: { - id: '', - name: 'foo', - active_thread_id: 0, - trace_id: '1', - }, - }, - profile - ) as Profiling.SentrySampledProfile; -}; - describe('SentrySampledProfile', () => { it('constructs a profile', () => { const sampledProfile: Profiling.SentrySampledProfile = makeSentrySampledProfile(); diff --git a/static/app/utils/profiling/profile/testUtils.tsx b/static/app/utils/profiling/profile/testUtils.tsx new file mode 100644 index 00000000000000..ac8cd0fed88499 --- /dev/null +++ b/static/app/utils/profiling/profile/testUtils.tsx @@ -0,0 +1,134 @@ +import merge from 'lodash/merge'; + +import type {DeepPartial} from 'sentry/types/utils'; +import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode'; +import {Frame} from 'sentry/utils/profiling/frame'; + +export const f = (name: string, key: number, in_app: boolean = true) => + new Frame({name, key, is_application: in_app}); +export const c = (fr: Frame) => new CallTreeNode(fr, null); +export const firstCallee = (node: CallTreeNode) => node.children[0]; +export const nthCallee = (node: CallTreeNode, n: number) => { + const child = node.children[n]; + if (!child) { + throw new Error('Child not found'); + } + return child; +}; + +export const makeTestingBoilerplate = () => { + const timings: [Frame['name'], string][] = []; + + const openSpy = jest.fn(); + const closeSpy = jest.fn(); + + // We need to wrap the spy fn because they are not allowed to reference external variables + const open = (node: CallTreeNode, value: number) => { + timings.push([node.frame.name, 'open']); + openSpy(node, value); + }; + // We need to wrap the spy fn because they are not allowed to reference external variables + const close = (node: CallTreeNode, val: number) => { + timings.push([node.frame.name, 'close']); + closeSpy(node, val); + }; + + return {open, close, timings, openSpy, closeSpy}; +}; + +export function makeSentryContinuousProfile( + profile?: DeepPartial +): Profiling.SentryContinousProfileChunk { + return merge( + { + chunk_id: 'chunk_id', + environment: '', + project_id: 0, + received: 0, + release: '', + organization_id: 0, + retention_days: 0, + version: '2', + platform: 'node', + profile: { + samples: [ + {timestamp: Date.now() / 1e3, stack_id: 0, thread_id: '0'}, + // 10ms later + {timestamp: Date.now() / 1e3 + 0.01, stack_id: 1, thread_id: '0'}, + ], + frames: [ + {function: 'foo', in_app: true}, + {function: 'bar', in_app: true}, + ], + stacks: [ + [0, 1], + [0, 1], + ], + }, + }, + profile + ) as Profiling.SentryContinousProfileChunk; +} + +export const makeSentrySampledProfile = ( + profile?: DeepPartial +) => { + return merge( + { + event_id: '1', + version: '1', + os: { + name: 'iOS', + version: '16.0', + build_number: '19H253', + }, + device: { + architecture: 'arm64e', + is_emulator: false, + locale: 'en_US', + manufacturer: 'Apple', + model: 'iPhone14,3', + }, + timestamp: '2022-09-01T09:45:00.000Z', + platform: 'cocoa', + profile: { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: 0, + }, + { + stack_id: 1, + thread_id: '0', + elapsed_since_start_ns: 1000, + }, + ], + frames: [ + { + function: 'main', + instruction_addr: '', + lineno: 1, + colno: 1, + file: 'main.c', + }, + { + function: 'foo', + instruction_addr: '', + lineno: 2, + colno: 2, + file: 'main.c', + }, + ], + stacks: [[1, 0], [0]], + }, + transaction: { + id: '', + name: 'foo', + active_thread_id: 0, + trace_id: '1', + }, + }, + profile + ) as Profiling.SentrySampledProfile; +}; From 48c36d953079887d4fdab44332edfde3da5ab5fa Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 27 Dec 2024 14:28:14 -0500 Subject: [PATCH 504/757] test(rr6): Remove a lot of browserHistory from tests (#82586) Instead consistently test that `router.{push,replace}` is called. This works even when the actual implementation is using browserHistory since we assign the same mocks that the router functions use to the browserHistory object. --- .../replays/hooks/useActiveReplayTab.spec.tsx | 11 ++-- .../acceptOrganizationInvite/index.spec.tsx | 13 +++-- .../views/alerts/incidentRedirect.spec.tsx | 3 +- .../rules/issue/details/ruleDetails.spec.tsx | 3 +- .../views/alerts/rules/issue/index.spec.tsx | 7 ++- static/app/views/auth/loginForm.spec.tsx | 8 +-- static/app/views/auth/registerForm.spec.tsx | 8 +-- static/app/views/auth/ssoForm.spec.tsx | 10 ++-- static/app/views/dashboards/detail.spec.tsx | 9 ++-- .../views/dashboards/orgDashboards.spec.tsx | 11 ++-- static/app/views/dashboards/view.spec.tsx | 48 ++++++++--------- static/app/views/discover/homepage.spec.tsx | 5 +- static/app/views/discover/queryList.spec.tsx | 8 +-- static/app/views/discover/results.spec.tsx | 3 +- .../quickContext/actionDropdown.spec.tsx | 16 +++--- .../views/discover/table/tableView.spec.tsx | 31 +++++------ .../components/platformSelector.spec.tsx | 8 +-- .../tables/eventSamplesTable.spec.tsx | 30 +++++++---- .../views/issueDetails/actions/index.spec.tsx | 7 +-- .../issueDetails/groupEventCarousel.spec.tsx | 21 ++++---- .../groupEventDetails.spec.tsx | 11 ++-- .../views/issueDetails/groupEvents.spec.tsx | 3 +- .../groupReplays/groupReplays.spec.tsx | 4 +- .../issueDetails/groupTagValues.spec.tsx | 3 +- static/app/views/issueList/overview.spec.tsx | 34 ++++++------- .../createSampleEventButton.spec.tsx | 9 ++-- static/app/views/performance/content.spec.tsx | 9 ++-- .../views/performance/landing/index.spec.tsx | 7 +-- static/app/views/performance/table.spec.tsx | 7 ++- .../transactionEvents/index.spec.tsx | 3 +- .../transactionOverview/index.spec.tsx | 13 +++-- .../spanDetails/index.spec.tsx | 8 ++- .../transactionTags/index.spec.tsx | 13 +++-- .../views/performance/trends/index.spec.tsx | 17 +++---- .../performance/vitalDetail/index.spec.tsx | 9 ++-- .../detail/header/releaseActions.spec.tsx | 18 ++++--- .../releaseComparisonChart/index.spec.tsx | 3 +- .../organizationCrumb.spec.tsx | 10 ++-- .../organizationMembersList.spec.tsx | 51 +++++++++---------- .../organizationProjects/index.spec.tsx | 9 ++-- .../teamSettings/index.spec.tsx | 19 ++++--- .../projectGeneralSettings/index.spec.tsx | 5 +- 42 files changed, 262 insertions(+), 263 deletions(-) diff --git a/static/app/utils/replays/hooks/useActiveReplayTab.spec.tsx b/static/app/utils/replays/hooks/useActiveReplayTab.spec.tsx index 8ec6b8f95f6744..e2f30938a70594 100644 --- a/static/app/utils/replays/hooks/useActiveReplayTab.spec.tsx +++ b/static/app/utils/replays/hooks/useActiveReplayTab.spec.tsx @@ -5,8 +5,6 @@ import {renderHook} from 'sentry-test/reactTestingLibrary'; import {browserHistory} from 'sentry/utils/browserHistory'; import useActiveReplayTab, {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab'; -const mockPush = jest.mocked(browserHistory.push); - function mockLocation(query: string = '') { window.location.search = qs.stringify({query}); } @@ -14,13 +12,10 @@ function mockLocation(query: string = '') { describe('useActiveReplayTab', () => { beforeEach(() => { mockLocation(); - mockPush.mockReset(); }); it('should use Breadcrumbs as a default', () => { - const {result} = renderHook(useActiveReplayTab, { - initialProps: {}, - }); + const {result} = renderHook(useActiveReplayTab, {initialProps: {}}); expect(result.current.getActiveTab()).toBe(TabKey.BREADCRUMBS); }); @@ -42,7 +37,7 @@ describe('useActiveReplayTab', () => { expect(result.current.getActiveTab()).toBe(TabKey.BREADCRUMBS); result.current.setActiveTab('nEtWoRk'); - expect(mockPush).toHaveBeenLastCalledWith({ + expect(browserHistory.push).toHaveBeenLastCalledWith({ pathname: '/', state: undefined, query: {query: '', t_main: TabKey.NETWORK}, @@ -56,7 +51,7 @@ describe('useActiveReplayTab', () => { expect(result.current.getActiveTab()).toBe(TabKey.BREADCRUMBS); result.current.setActiveTab('foo bar'); - expect(mockPush).toHaveBeenLastCalledWith({ + expect(browserHistory.push).toHaveBeenLastCalledWith({ pathname: '/', query: {query: '', t_main: TabKey.BREADCRUMBS}, }); diff --git a/static/app/views/acceptOrganizationInvite/index.spec.tsx b/static/app/views/acceptOrganizationInvite/index.spec.tsx index e449adabec9cb4..e7f97f1af2bc43 100644 --- a/static/app/views/acceptOrganizationInvite/index.spec.tsx +++ b/static/app/views/acceptOrganizationInvite/index.spec.tsx @@ -1,11 +1,11 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import {logout} from 'sentry/actionCreators/account'; import ConfigStore from 'sentry/stores/configStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import AcceptOrganizationInvite from 'sentry/views/acceptOrganizationInvite'; jest.mock('sentry/actionCreators/account'); @@ -25,6 +25,7 @@ const getJoinButton = () => { }; describe('AcceptOrganizationInvite', function () { + const router = RouterFixture(); const organization = OrganizationFixture({slug: 'org-slug'}); const configState = ConfigStore.getState(); @@ -46,7 +47,8 @@ describe('AcceptOrganizationInvite', function () { + />, + {router} ); const acceptMock = MockApiClient.addMockResponse({ @@ -58,7 +60,7 @@ describe('AcceptOrganizationInvite', function () { await userEvent.click(joinButton!); expect(acceptMock).toHaveBeenCalled(); - expect(browserHistory.replace).toHaveBeenCalledWith('/org-slug/'); + expect(router.replace).toHaveBeenCalledWith('/org-slug/'); }); it('can accept invitation on customer-domains', async function () { @@ -85,7 +87,8 @@ describe('AcceptOrganizationInvite', function () { + />, + {router} ); const acceptMock = MockApiClient.addMockResponse({ @@ -97,7 +100,7 @@ describe('AcceptOrganizationInvite', function () { await userEvent.click(joinButton!); expect(acceptMock).toHaveBeenCalled(); - expect(browserHistory.replace).toHaveBeenCalledWith('/org-slug/'); + expect(router.replace).toHaveBeenCalledWith('/org-slug/'); }); it('renders error message', function () { diff --git a/static/app/views/alerts/incidentRedirect.spec.tsx b/static/app/views/alerts/incidentRedirect.spec.tsx index 4565546145417e..c4bbef77c0782d 100644 --- a/static/app/views/alerts/incidentRedirect.spec.tsx +++ b/static/app/views/alerts/incidentRedirect.spec.tsx @@ -4,7 +4,6 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, waitFor} from 'sentry-test/reactTestingLibrary'; import {trackAnalytics} from 'sentry/utils/analytics'; -import {browserHistory} from 'sentry/utils/browserHistory'; import IncidentRedirect from './incidentRedirect'; @@ -44,7 +43,7 @@ describe('IncidentRedirect', () => { ); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenCalledWith({ + expect(router.replace).toHaveBeenCalledWith({ pathname: '/organizations/org-slug/alerts/rules/details/4/', query: { alert: '123', diff --git a/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx b/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx index 3fd83d0f4b872a..5814a0532b59f7 100644 --- a/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx +++ b/static/app/views/alerts/rules/issue/details/ruleDetails.spec.tsx @@ -9,7 +9,6 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import AlertRuleDetails from './ruleDetails'; @@ -110,7 +109,7 @@ describe('AlertRuleDetails', () => { expect(await screen.findByLabelText('Next')).toBeEnabled(); await userEvent.click(screen.getByLabelText('Next')); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(context.router.push).toHaveBeenCalledWith({ pathname: '/mock-pathname/', query: { cursor: '0:100:0', diff --git a/static/app/views/alerts/rules/issue/index.spec.tsx b/static/app/views/alerts/rules/issue/index.spec.tsx index 1aff61b922759b..f3e329bbb23b5f 100644 --- a/static/app/views/alerts/rules/issue/index.spec.tsx +++ b/static/app/views/alerts/rules/issue/index.spec.tsx @@ -27,7 +27,6 @@ import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks'; import ProjectsStore from 'sentry/stores/projectsStore'; import type {PlainRoute} from 'sentry/types/legacyReactRouter'; import {metric} from 'sentry/utils/analytics'; -import {browserHistory} from 'sentry/utils/browserHistory'; import IssueRuleEditor from 'sentry/views/alerts/rules/issue'; import {permissionAlertText} from 'sentry/views/settings/project/permissionAlert'; import ProjectAlerts from 'sentry/views/settings/projectAlerts'; @@ -226,8 +225,8 @@ describe('IssueRuleEditor', function () { method: 'DELETE', body: {}, }); - createWrapper(); - renderGlobalModal(); + const {router} = createWrapper(); + renderGlobalModal({router}); await userEvent.click(screen.getByLabelText('Delete Rule')); expect( @@ -236,7 +235,7 @@ describe('IssueRuleEditor', function () { await userEvent.click(screen.getByTestId('confirm-button')); await waitFor(() => expect(deleteMock).toHaveBeenCalled()); - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(router.replace).toHaveBeenCalledWith( '/settings/org-slug/projects/project-slug/alerts/' ); }); diff --git a/static/app/views/auth/loginForm.spec.tsx b/static/app/views/auth/loginForm.spec.tsx index 92e29592ac4468..f2d602f1ccf955 100644 --- a/static/app/views/auth/loginForm.spec.tsx +++ b/static/app/views/auth/loginForm.spec.tsx @@ -1,7 +1,8 @@ +import {RouterFixture} from 'sentry-fixture/routerFixture'; + import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import ConfigStore from 'sentry/stores/configStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import LoginForm from 'sentry/views/auth/loginForm'; async function doLogin() { @@ -38,6 +39,7 @@ describe('LoginForm', function () { }); it('handles success', async function () { + const router = RouterFixture(); const userObject = { id: 1, name: 'Joe', @@ -53,7 +55,7 @@ describe('LoginForm', function () { }, }); - render(); + render(, {router}); await doLogin(); expect(mockRequest).toHaveBeenCalledWith( @@ -64,7 +66,7 @@ describe('LoginForm', function () { ); await waitFor(() => expect(ConfigStore.get('user')).toEqual(userObject)); - expect(browserHistory.push).toHaveBeenCalledWith({pathname: '/next/'}); + expect(router.push).toHaveBeenCalledWith({pathname: '/next/'}); }); it('renders login provider buttons', function () { diff --git a/static/app/views/auth/registerForm.spec.tsx b/static/app/views/auth/registerForm.spec.tsx index 29fed77c5a9983..51847ad2b59d86 100644 --- a/static/app/views/auth/registerForm.spec.tsx +++ b/static/app/views/auth/registerForm.spec.tsx @@ -1,7 +1,8 @@ +import {RouterFixture} from 'sentry-fixture/routerFixture'; + import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import ConfigStore from 'sentry/stores/configStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import RegisterForm from 'sentry/views/auth/registerForm'; describe('Register', function () { @@ -52,6 +53,7 @@ describe('Register', function () { }); it('handles success', async function () { + const router = RouterFixture(); const userObject = { id: 1, name: 'Joe', @@ -67,10 +69,10 @@ describe('Register', function () { }, }); - render(); + render(, {router}); await doLogin(mockRequest); await waitFor(() => expect(ConfigStore.get('user')).toEqual(userObject)); - expect(browserHistory.push).toHaveBeenCalledWith({pathname: '/next/'}); + expect(router.push).toHaveBeenCalledWith({pathname: '/next/'}); }); }); diff --git a/static/app/views/auth/ssoForm.spec.tsx b/static/app/views/auth/ssoForm.spec.tsx index cfbe889bca349e..7a5cb3f896e556 100644 --- a/static/app/views/auth/ssoForm.spec.tsx +++ b/static/app/views/auth/ssoForm.spec.tsx @@ -1,6 +1,7 @@ +import {RouterFixture} from 'sentry-fixture/routerFixture'; + import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import SsoForm from 'sentry/views/auth/ssoForm'; describe('SsoForm', function () { @@ -54,6 +55,7 @@ describe('SsoForm', function () { }); it('handles success', async function () { + const router = RouterFixture(); const mockRequest = MockApiClient.addMockResponse({ url: '/auth/sso-locate/', method: 'POST', @@ -63,11 +65,9 @@ describe('SsoForm', function () { }, }); - render(); + render(, {router}); await doSso(mockRequest); - await waitFor(() => - expect(browserHistory.push).toHaveBeenCalledWith({pathname: '/next/'}) - ); + await waitFor(() => expect(router.push).toHaveBeenCalledWith({pathname: '/next/'})); }); }); diff --git a/static/app/views/dashboards/detail.spec.tsx b/static/app/views/dashboards/detail.spec.tsx index 479c778dca97d9..9d0749b86e84a2 100644 --- a/static/app/views/dashboards/detail.spec.tsx +++ b/static/app/views/dashboards/detail.spec.tsx @@ -24,7 +24,6 @@ import ConfigStore from 'sentry/stores/configStore'; import PageFiltersStore from 'sentry/stores/pageFiltersStore'; import ProjectsStore from 'sentry/stores/projectsStore'; import TeamStore from 'sentry/stores/teamStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import CreateDashboard from 'sentry/views/dashboards/create'; import DashboardDetail, { handleUpdateDashboardSplit, @@ -1232,7 +1231,7 @@ describe('Dashboards > Detail', function () { await userEvent.click(document.body); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledWith( + expect(testData.router.push).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ release: '', @@ -1340,7 +1339,7 @@ describe('Dashboards > Detail', function () { await userEvent.click(screen.getByText('Cancel')); screen.getByText('All Releases'); - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(testData.router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ project: undefined, @@ -1550,7 +1549,7 @@ describe('Dashboards > Detail', function () { await userEvent.click(screen.getByText('Cancel')); // release isn't used in the redirect - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(testData.router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: { end: undefined, @@ -1605,7 +1604,7 @@ describe('Dashboards > Detail', function () { await userEvent.click(document.body); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledWith( + expect(testData.router.push).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ release: ['sentry-android-shop@1.2.0'], diff --git a/static/app/views/dashboards/orgDashboards.spec.tsx b/static/app/views/dashboards/orgDashboards.spec.tsx index d40d98ffcd8618..da5bd07180c448 100644 --- a/static/app/views/dashboards/orgDashboards.spec.tsx +++ b/static/app/views/dashboards/orgDashboards.spec.tsx @@ -5,7 +5,6 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import DashboardDetail from 'sentry/views/dashboards/detail'; import OrgDashboards from 'sentry/views/dashboards/orgDashboards'; import {DashboardState} from 'sentry/views/dashboards/types'; @@ -97,7 +96,7 @@ describe('OrgDashboards', () => { ); await waitFor(() => - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(initialData.router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ project: [1, 2], @@ -161,7 +160,7 @@ describe('OrgDashboards', () => { ); await waitFor(() => - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(initialData.router.replace).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ project: [1, 2], @@ -260,7 +259,7 @@ describe('OrgDashboards', () => { {router: initialData.router} ); - expect(browserHistory.replace).not.toHaveBeenCalled(); + expect(initialData.router.replace).not.toHaveBeenCalled(); }); it('does not redirect to add query params if location is cleared manually', async () => { @@ -305,7 +304,7 @@ describe('OrgDashboards', () => { {router: initialData.router} ); - await waitFor(() => expect(browserHistory.replace).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(initialData.router.replace).toHaveBeenCalledTimes(1)); rerender( { ); expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); - expect(browserHistory.replace).toHaveBeenCalledTimes(1); + expect(initialData.router.replace).toHaveBeenCalledTimes(1); }); }); diff --git a/static/app/views/dashboards/view.spec.tsx b/static/app/views/dashboards/view.spec.tsx index b2ae44868ce0cf..ab56e0b661f236 100644 --- a/static/app/views/dashboards/view.spec.tsx +++ b/static/app/views/dashboards/view.spec.tsx @@ -1,35 +1,36 @@ import {Fragment} from 'react'; -import {LocationFixture} from 'sentry-fixture/locationFixture'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {render} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import ViewEditDashboard from 'sentry/views/dashboards/view'; describe('Dashboards > ViewEditDashboard', function () { const initialData = initializeOrg(); it('removes widget params from url and preserves selection params', function () { - const location = { - pathname: '/', - query: { - environment: 'canary', - period: '7d', - project: '11111', - start: null, - end: null, - utc: null, - displayType: 'line', - interval: '5m', - queryConditions: '', - queryFields: 'count()', - queryNames: '', - queryOrderby: '', - title: 'test', - statsPeriod: '7d', + const router = RouterFixture({ + location: { + pathname: '/', + query: { + environment: 'canary', + period: '7d', + project: '11111', + start: null, + end: null, + utc: null, + displayType: 'line', + interval: '5m', + queryConditions: '', + queryFields: 'count()', + queryNames: '', + queryOrderby: '', + title: 'test', + statsPeriod: '7d', + }, }, - }; + }); MockApiClient.addMockResponse({ url: `/organizations/${initialData.organization.slug}/dashboards/1/visit/`, @@ -39,7 +40,7 @@ describe('Dashboards > ViewEditDashboard', function () { render( ViewEditDashboard', function () { routeParams={{}} > - + , + {router} ); - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/', query: { diff --git a/static/app/views/discover/homepage.spec.tsx b/static/app/views/discover/homepage.spec.tsx index 1977c30883eaca..cabb17e506e503 100644 --- a/static/app/views/discover/homepage.spec.tsx +++ b/static/app/views/discover/homepage.spec.tsx @@ -14,7 +14,6 @@ import { import * as pageFilterUtils from 'sentry/components/organizations/pageFilters/persistence'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import EventView from 'sentry/utils/discover/eventView'; import {DEFAULT_EVENT_VIEW} from './data'; @@ -184,7 +183,7 @@ describe('Discover > Homepage', () => { />, {router: initialData.router, organization: initialData.organization} ); - renderGlobalModal(); + renderGlobalModal({router: initialData.router}); await userEvent.click(await screen.findByText('Columns')); @@ -194,7 +193,7 @@ describe('Discover > Homepage', () => { await userEvent.click(within(modal).getByText('event.type')); await userEvent.click(within(modal).getByText('Apply')); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(initialData.router.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/discover/homepage/', query: expect.objectContaining({ diff --git a/static/app/views/discover/queryList.spec.tsx b/static/app/views/discover/queryList.spec.tsx index 040b3f6750b118..f0d3176c67d246 100644 --- a/static/app/views/discover/queryList.spec.tsx +++ b/static/app/views/discover/queryList.spec.tsx @@ -13,7 +13,6 @@ import { } from 'sentry-test/reactTestingLibrary'; import {openAddToDashboardModal} from 'sentry/actionCreators/modal'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {DisplayModes, SavedQueryDatasets} from 'sentry/utils/discover/types'; import {DashboardWidgetSource, DisplayType} from 'sentry/views/dashboards/types'; import QueryList from 'sentry/views/discover/queryList'; @@ -253,7 +252,8 @@ describe('Discover > QueryList', function () { renderPrebuilt={false} onQueryChange={queryChangeMock} location={location} - /> + />, + {router} ); const card = screen.getAllByTestId(/card-*/).at(0)!; @@ -264,7 +264,7 @@ describe('Discover > QueryList', function () { await userEvent.click(withinCard.getByText('Duplicate Query')); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: {}, }); @@ -348,7 +348,7 @@ describe('Discover > QueryList', function () { expect(queryChangeMock).not.toHaveBeenCalled(); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: {cursor: undefined, statsPeriod: '14d'}, }); diff --git a/static/app/views/discover/results.spec.tsx b/static/app/views/discover/results.spec.tsx index c7c38d04bda0c2..8f0b42102ebe21 100644 --- a/static/app/views/discover/results.spec.tsx +++ b/static/app/views/discover/results.spec.tsx @@ -9,7 +9,6 @@ import selectEvent from 'sentry-test/selectEvent'; import * as PageFilterPersistence from 'sentry/components/organizations/pageFilters/persistence'; import ProjectsStore from 'sentry/stores/projectsStore'; import {SavedSearchType} from 'sentry/types/group'; -import {browserHistory} from 'sentry/utils/browserHistory'; import EventView from 'sentry/utils/discover/eventView'; import Results from 'sentry/views/discover/results'; @@ -270,7 +269,7 @@ describe('Results', function () { expect(mockRequests.eventsStatsMock).not.toHaveBeenCalled(); // Should redirect and retain the old query value - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/discover/results/', query: expect.objectContaining({ diff --git a/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx b/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx index 83c04ccc0ef827..880ce515e7df7b 100644 --- a/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx +++ b/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx @@ -1,10 +1,10 @@ import type {Location} from 'history'; import {LocationFixture} from 'sentry-fixture/locationFixture'; import {OrganizationFixture} from 'sentry-fixture/organization'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import type {EventData} from 'sentry/utils/discover/eventView'; import EventView from 'sentry/utils/discover/eventView'; @@ -33,6 +33,8 @@ const mockedLocation = LocationFixture({ }, }); +const mockedRouter = RouterFixture(); + const renderActionDropdown = ( location: Location, eventView: EventView, @@ -51,7 +53,7 @@ const renderActionDropdown = ( value={value} contextValueType={contextValueType} />, - {organization} + {organization, router: mockedRouter} ); }; @@ -122,7 +124,7 @@ describe('Quick Context Actions', function () { await userEvent.click(addAsColumn); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(mockedRouter.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/mock-pathname/', query: expect.objectContaining({ @@ -150,7 +152,7 @@ describe('Quick Context Actions', function () { await userEvent.click(addToFilter); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(mockedRouter.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/mock-pathname/', query: expect.objectContaining({ @@ -180,7 +182,7 @@ describe('Quick Context Actions', function () { await userEvent.click(addToFilter); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(mockedRouter.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/mock-pathname/', query: expect.objectContaining({ @@ -210,7 +212,7 @@ describe('Quick Context Actions', function () { await userEvent.click(showGreaterThanBtn); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(mockedRouter.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/mock-pathname/', query: expect.objectContaining({ @@ -240,7 +242,7 @@ describe('Quick Context Actions', function () { await userEvent.click(showLessThanBtn); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(mockedRouter.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/mock-pathname/', query: expect.objectContaining({ diff --git a/static/app/views/discover/table/tableView.spec.tsx b/static/app/views/discover/table/tableView.spec.tsx index 51ffe0019700e2..6f9c086874ce99 100644 --- a/static/app/views/discover/table/tableView.spec.tsx +++ b/static/app/views/discover/table/tableView.spec.tsx @@ -6,7 +6,6 @@ import {act, render, screen, userEvent, within} from 'sentry-test/reactTestingLi import ProjectsStore from 'sentry/stores/projectsStore'; import TagStore from 'sentry/stores/tagStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import type {TableData} from 'sentry/utils/discover/discoverQuery'; import EventView from 'sentry/utils/discover/eventView'; import {SavedQueryDatasets} from 'sentry/utils/discover/types'; @@ -72,9 +71,6 @@ describe('TableView > CellActions', function () { } beforeEach(function () { - jest.mocked(browserHistory.push).mockReset(); - jest.mocked(browserHistory.replace).mockReset(); - const organization = OrganizationFixture({ features: ['discover-basic'], }); @@ -161,7 +157,7 @@ describe('TableView > CellActions', function () { await openContextMenu(1); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: '!has:title', @@ -178,7 +174,7 @@ describe('TableView > CellActions', function () { await openContextMenu(1); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'tag:value !has:title', @@ -194,7 +190,7 @@ describe('TableView > CellActions', function () { await openContextMenu(1); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'tag:value title:"some title"', @@ -209,7 +205,7 @@ describe('TableView > CellActions', function () { await openContextMenu(1); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'title:"some title"', @@ -225,7 +221,7 @@ describe('TableView > CellActions', function () { screen.getByRole('menuitemradio', {name: 'Exclude from filter'}) ); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: '!title:"some title"', @@ -243,7 +239,7 @@ describe('TableView > CellActions', function () { screen.getByRole('menuitemradio', {name: 'Exclude from filter'}) ); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'tag:value !title:"some title"', @@ -260,7 +256,7 @@ describe('TableView > CellActions', function () { screen.getByRole('menuitemradio', {name: 'Exclude from filter'}) ); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'has:title', @@ -279,7 +275,7 @@ describe('TableView > CellActions', function () { screen.getByRole('menuitemradio', {name: 'Exclude from filter'}) ); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'tag:value has:title', @@ -294,7 +290,7 @@ describe('TableView > CellActions', function () { screen.getByRole('menuitemradio', {name: 'Show values greater than'}) ); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'count():>9', @@ -309,7 +305,7 @@ describe('TableView > CellActions', function () { screen.getByRole('menuitemradio', {name: 'Show values less than'}) ); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'count():<9', @@ -404,7 +400,7 @@ describe('TableView > CellActions', function () { await openContextMenu(5); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Go to release'})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: '/organizations/org-slug/releases/v1.0.2/', query: expect.objectContaining({ environment: eventView.environment, @@ -507,13 +503,14 @@ describe('TableView > CellActions', function () { measurementKeys={null} showTags={false} title="" - /> + />, + {router: initialData.router} ); await userEvent.hover(screen.getByText('444.3 KB')); const buttons = screen.getAllByRole('button'); await userEvent.click(buttons[buttons.length - 1]); await userEvent.click(screen.getByText('Show values less than')); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(initialData.router.push).toHaveBeenCalledWith({ pathname: location.pathname, query: expect.objectContaining({ query: 'p99(measurements.custom.kilobyte):<444300', diff --git a/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx b/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx index 48419efeae2090..4101c136f18a2d 100644 --- a/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/platformSelector.spec.tsx @@ -1,6 +1,7 @@ +import {RouterFixture} from 'sentry-fixture/routerFixture'; + import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import localStorage from 'sentry/utils/localStorage'; import {PlatformSelector} from 'sentry/views/insights/mobile/screenload/components/platformSelector'; @@ -15,10 +16,11 @@ describe('PlatformSelector', function () { }); it('updates url params on click', async function () { - render(); + const router = RouterFixture(); + render(, {router}); await userEvent.click(screen.getByRole('radio', {name: 'iOS'})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/mock-pathname/', query: { platform: 'iOS', diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx index 8c8910fc6a4fd3..1f25fbfbb3109d 100644 --- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx +++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx @@ -1,17 +1,20 @@ import {LocationFixture} from 'sentry-fixture/locationFixture'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {NewQuery} from 'sentry/types/organization'; -import {browserHistory} from 'sentry/utils/browserHistory'; import EventView from 'sentry/utils/discover/eventView'; import {EventSamplesTable} from 'sentry/views/insights/mobile/screenload/components/tables/eventSamplesTable'; describe('EventSamplesTable', function () { + let mockRouter: InjectedRouter; let mockLocation: ReturnType; let mockQuery: NewQuery; let mockEventView: EventView; beforeEach(function () { + mockRouter = RouterFixture(); mockLocation = LocationFixture({ query: { statsPeriod: '99d', @@ -52,7 +55,8 @@ describe('EventSamplesTable', function () { kind: 'desc', }} sortKey="" - /> + />, + {router: mockRouter} ); expect(screen.getByText('Readable Column Name')).toBeInTheDocument(); @@ -82,7 +86,8 @@ describe('EventSamplesTable', function () { }} sortKey="" data={{data: [{id: '1', 'transaction.id': 'abc'}], meta: {}}} - /> + />, + {router: mockRouter} ); // Test only one column to isolate event ID @@ -118,7 +123,8 @@ describe('EventSamplesTable', function () { data: [{id: '1', 'profile.id': 'abc', 'project.name': 'project'}], meta: {fields: {'profile.id': 'string', 'project.name': 'string'}}, }} - /> + />, + {router: mockRouter} ); // Test only one column to isolate profile column @@ -151,13 +157,14 @@ describe('EventSamplesTable', function () { }} sortKey="" data={{data: [{id: '1', 'transaction.id': 'abc'}], meta: {}}} - /> + />, + {router: mockRouter} ); expect(screen.getByRole('button', {name: /device class all/i})).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: /device class all/i})); await userEvent.click(screen.getByText('Medium')); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(mockRouter.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/mock-pathname/', query: expect.objectContaining({ @@ -187,11 +194,12 @@ describe('EventSamplesTable', function () { sortKey="" data={{data: [{id: '1', 'transaction.id': 'abc'}], meta: {}}} pageLinks={pageLinks} - /> + />, + {router: mockRouter} ); expect(screen.getByRole('button', {name: 'Next'})).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: 'Next'})); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(mockRouter.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/mock-pathname/', query: expect.objectContaining({ @@ -225,7 +233,8 @@ describe('EventSamplesTable', function () { }} sortKey="customSortKey" data={{data: [{id: '1', 'transaction.id': 'abc', duration: 'def'}], meta: {}}} - /> + />, + {router: mockRouter} ); // Ascending sort in transaction ID because the default is descending @@ -262,7 +271,8 @@ describe('EventSamplesTable', function () { }} sortKey="customSortKey" data={{data: [{id: '1', 'transaction.id': 'abc', duration: 'def'}], meta: {}}} - /> + />, + {router: mockRouter} ); // Although ID is queried for, because it's not defined in the map diff --git a/static/app/views/issueDetails/actions/index.spec.tsx b/static/app/views/issueDetails/actions/index.spec.tsx index 49c49c1e1fe478..8b5738ba955a7a 100644 --- a/static/app/views/issueDetails/actions/index.spec.tsx +++ b/static/app/views/issueDetails/actions/index.spec.tsx @@ -3,6 +3,7 @@ import {EventStacktraceExceptionFixture} from 'sentry-fixture/eventStacktraceExc import {GroupFixture} from 'sentry-fixture/group'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import { render, @@ -17,7 +18,6 @@ import ConfigStore from 'sentry/stores/configStore'; import ModalStore from 'sentry/stores/modalStore'; import {GroupStatus, IssueCategory} from 'sentry/types/group'; import * as analytics from 'sentry/utils/analytics'; -import {browserHistory} from 'sentry/utils/browserHistory'; import GroupActions from 'sentry/views/issueDetails/actions'; const project = ProjectFixture({ @@ -215,6 +215,7 @@ describe('GroupActions', function () { describe('delete', function () { it('opens delete confirm modal from more actions dropdown', async () => { + const router = RouterFixture(); const org = OrganizationFixture({ ...organization, access: [...organization.access, 'event:admin'], @@ -240,7 +241,7 @@ describe('GroupActions', function () { event={null} /> , - {organization: org} + {router, organization: org} ); await userEvent.click(screen.getByLabelText('More Actions')); @@ -254,7 +255,7 @@ describe('GroupActions', function () { await userEvent.click(within(modal).getByRole('button', {name: 'Delete'})); expect(deleteMock).toHaveBeenCalled(); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: `/organizations/${org.slug}/issues/`, query: {project: project.id}, }); diff --git a/static/app/views/issueDetails/groupEventCarousel.spec.tsx b/static/app/views/issueDetails/groupEventCarousel.spec.tsx index 8f84138d962329..a21544b1bfa8f0 100644 --- a/static/app/views/issueDetails/groupEventCarousel.spec.tsx +++ b/static/app/views/issueDetails/groupEventCarousel.spec.tsx @@ -2,16 +2,18 @@ import {ConfigFixture} from 'sentry-fixture/config'; import {EventFixture} from 'sentry-fixture/event'; import {GroupFixture} from 'sentry-fixture/group'; import {OrganizationFixture} from 'sentry-fixture/organization'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {UserFixture} from 'sentry-fixture/user'; import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; import ConfigStore from 'sentry/stores/configStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import * as useMedia from 'sentry/utils/useMedia'; import {GroupEventCarousel} from 'sentry/views/issueDetails/groupEventCarousel'; describe('GroupEventCarousel', () => { + const router = RouterFixture(); + const testEvent = EventFixture({ id: 'event-id', size: 7, @@ -66,12 +68,12 @@ describe('GroupEventCarousel', () => { it('can navigate to the oldest event', async () => { jest.spyOn(useMedia, 'default').mockReturnValue(true); - render(); + render(, {router}); await userEvent.click(screen.getByRole('button', {name: /recommended/i})); await userEvent.click(screen.getByRole('option', {name: /oldest/i})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/organizations/org-slug/issues/group-id/events/oldest/', query: {referrer: 'oldest-event'}, }); @@ -80,30 +82,27 @@ describe('GroupEventCarousel', () => { it('can navigate to the latest event', async () => { jest.spyOn(useMedia, 'default').mockReturnValue(true); - render(); + render(, {router}); await userEvent.click(screen.getByRole('button', {name: /recommended/i})); await userEvent.click(screen.getByRole('option', {name: /latest/i})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/organizations/org-slug/issues/group-id/events/latest/', query: {referrer: 'latest-event'}, }); }); it('can navigate to the recommended event', async () => { + const newRouter = RouterFixture({params: {eventId: 'latest'}}); jest.spyOn(useMedia, 'default').mockReturnValue(true); - render(, { - router: { - params: {eventId: 'latest'}, - }, - }); + render(, {router: newRouter}); await userEvent.click(screen.getByRole('button', {name: /latest/i})); await userEvent.click(screen.getByRole('option', {name: /recommended/i})); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(newRouter.push).toHaveBeenCalledWith({ pathname: '/organizations/org-slug/issues/group-id/events/recommended/', query: {referrer: 'recommended-event'}, }); diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx index 5239843a3fe155..f1462ab0dcb59b 100644 --- a/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx +++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx @@ -19,7 +19,6 @@ import {IssueCategory, IssueType} from 'sentry/types/group'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; -import {browserHistory} from 'sentry/utils/browserHistory'; import type {QuickTraceEvent} from 'sentry/utils/performance/quickTrace/types'; import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails'; @@ -346,7 +345,6 @@ describe('groupEventDetails', () => { afterEach(function () { MockApiClient.clearMockResponses(); - jest.mocked(browserHistory.replace).mockClear(); }); it('redirects on switching to an invalid environment selection for event', async function () { @@ -364,12 +362,12 @@ describe('groupEventDetails', () => { router: props.router, }); expect(await screen.findByTestId('group-event-details')).toBeInTheDocument(); - expect(browserHistory.replace).not.toHaveBeenCalled(); + expect(props.router.replace).not.toHaveBeenCalled(); props.router.location.query.environment = ['prod']; rerender(); - await waitFor(() => expect(browserHistory.replace).toHaveBeenCalled()); + await waitFor(() => expect(props.router.replace).toHaveBeenCalled()); }); it('does not redirect when switching to a valid environment selection for event', async function () { @@ -381,13 +379,13 @@ describe('groupEventDetails', () => { router: props.router, }); - expect(browserHistory.replace).not.toHaveBeenCalled(); + expect(props.router.replace).not.toHaveBeenCalled(); props.router.location.query.environment = []; rerender(); expect(await screen.findByTestId('group-event-details')).toBeInTheDocument(); - expect(browserHistory.replace).not.toHaveBeenCalled(); + expect(props.router.replace).not.toHaveBeenCalled(); }); it('displays error on event error', async function () { @@ -509,7 +507,6 @@ describe('EventCause', () => { afterEach(function () { MockApiClient.clearMockResponses(); - jest.mocked(browserHistory.replace).mockClear(); }); it('renders suspect commit', async function () { diff --git a/static/app/views/issueDetails/groupEvents.spec.tsx b/static/app/views/issueDetails/groupEvents.spec.tsx index 83592306050fac..38caab2ae571b9 100644 --- a/static/app/views/issueDetails/groupEvents.spec.tsx +++ b/static/app/views/issueDetails/groupEvents.spec.tsx @@ -13,7 +13,6 @@ import { import {type Group, IssueCategory} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; -import {browserHistory} from 'sentry/utils/browserHistory'; import GroupEvents from 'sentry/views/issueDetails/groupEvents'; describe('groupEvents', () => { @@ -143,7 +142,7 @@ describe('groupEvents', () => { await userEvent.keyboard('{enter}'); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( expect.objectContaining({ query: {query: 'foo'}, }) diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx index 47c0bd83aec60e..fc8c53e40cba08 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx @@ -10,7 +10,6 @@ import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrar import {resetMockDate, setMockDate} from 'sentry-test/utils'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import useLoadReplayReader from 'sentry/utils/replays/hooks/useLoadReplayReader'; import ReplayReader from 'sentry/utils/replays/replayReader'; import GroupReplays from 'sentry/views/issueDetails/groupReplays'; @@ -573,14 +572,13 @@ describe('GroupReplays', () => { ); }); - const mockReplace = jest.mocked(browserHistory.replace); const replayPlayPlause = ( await screen.findAllByTestId('replay-table-play-button') )[0]; await userEvent.click(replayPlayPlause); await waitFor(() => - expect(mockReplace).toHaveBeenCalledWith( + expect(router.replace).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/replays/', query: { diff --git a/static/app/views/issueDetails/groupTagValues.spec.tsx b/static/app/views/issueDetails/groupTagValues.spec.tsx index a25096c8ac0d75..4bb98d37758495 100644 --- a/static/app/views/issueDetails/groupTagValues.spec.tsx +++ b/static/app/views/issueDetails/groupTagValues.spec.tsx @@ -13,7 +13,6 @@ import { } from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {GroupTagValues} from 'sentry/views/issueDetails/groupTagValues'; describe('GroupTagValues', () => { @@ -92,7 +91,7 @@ describe('GroupTagValues', () => { // Clicking next button loads page with query param ?cursor=0:100:0 await userEvent.click(screen.getByRole('button', {name: 'Next'})); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( expect.objectContaining({query: expect.objectContaining({cursor: '0:100:0'})}) ); }); diff --git a/static/app/views/issueList/overview.spec.tsx b/static/app/views/issueList/overview.spec.tsx index 15dab03ff4bce0..9a0d90129b3810 100644 --- a/static/app/views/issueList/overview.spec.tsx +++ b/static/app/views/issueList/overview.spec.tsx @@ -5,6 +5,7 @@ import {LocationFixture} from 'sentry-fixture/locationFixture'; import {MemberFixture} from 'sentry-fixture/member'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {SearchFixture} from 'sentry-fixture/search'; import {TagsFixture} from 'sentry-fixture/tags'; @@ -24,7 +25,6 @@ import {DEFAULT_QUERY} from 'sentry/constants'; import ProjectsStore from 'sentry/stores/projectsStore'; import TagStore from 'sentry/stores/tagStore'; import {SavedSearchVisibility} from 'sentry/types/group'; -import {browserHistory} from 'sentry/utils/browserHistory'; import localStorageWrapper from 'sentry/utils/localStorage'; import * as parseLinkHeader from 'sentry/utils/parseLinkHeader'; import IssueListWithStores, {IssueListOverview} from 'sentry/views/issueList/overview'; @@ -486,7 +486,7 @@ describe('IssueList', function () { await userEvent.click(screen.getByRole('button', {name: /custom search/i})); await userEvent.click(screen.getByRole('button', {name: localSavedSearch.name})); - expect(browserHistory.push).toHaveBeenLastCalledWith( + expect(router.push).toHaveBeenLastCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/issues/searches/789/', }) @@ -519,7 +519,7 @@ describe('IssueList', function () { await userEvent.click(getSearchInput()); await userEvent.keyboard('dogs{Enter}'); - expect(browserHistory.push).toHaveBeenLastCalledWith( + expect(router.push).toHaveBeenLastCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/issues/', query: { @@ -560,7 +560,7 @@ describe('IssueList', function () { await userEvent.paste('assigned:me level:fatal'); await userEvent.keyboard('{Enter}'); - expect(browserHistory.push as jest.Mock).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ query: 'assigned:me level:fatal', @@ -590,7 +590,7 @@ describe('IssueList', function () { await waitFor(() => { expect(createPin).toHaveBeenCalled(); - expect(browserHistory.replace).toHaveBeenLastCalledWith( + expect(router.replace).toHaveBeenLastCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/issues/searches/666/', query: { @@ -620,9 +620,9 @@ describe('IssueList', function () { method: 'DELETE', }); - const routerWithSavedSearch = { + const routerWithSavedSearch = RouterFixture({ params: {searchId: pinnedSearch.id}, - }; + }); render(, { router: routerWithSavedSearch, @@ -639,7 +639,7 @@ describe('IssueList', function () { }); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenLastCalledWith( + expect(routerWithSavedSearch.replace).toHaveBeenLastCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/issues/', }) @@ -671,7 +671,7 @@ describe('IssueList', function () { isPinned: true, }, }); - const routerWithSavedSearch = {params: {searchId: '789'}}; + const routerWithSavedSearch = RouterFixture({params: {searchId: '789'}}); render(, { router: routerWithSavedSearch, @@ -685,7 +685,7 @@ describe('IssueList', function () { await waitFor(() => { expect(createPin).toHaveBeenCalled(); - expect(browserHistory.replace).toHaveBeenLastCalledWith( + expect(routerWithSavedSearch.replace).toHaveBeenLastCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/issues/searches/789/', }) @@ -747,7 +747,7 @@ describe('IssueList', function () { await waitFor(() => { expect(createPin).toHaveBeenCalled(); - expect(browserHistory.replace).toHaveBeenLastCalledWith( + expect(newRouter.replace).toHaveBeenLastCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/issues/searches/666/', query: expect.objectContaining({ @@ -816,7 +816,7 @@ describe('IssueList', function () { await waitFor(() => { expect(deletePin).toHaveBeenCalled(); - expect(browserHistory.replace).toHaveBeenLastCalledWith( + expect(newRouter.replace).toHaveBeenLastCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/issues/', query: expect.objectContaining({ @@ -863,7 +863,7 @@ describe('IssueList', function () { }; await waitFor(() => { - expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs); + expect(router.push).toHaveBeenLastCalledWith(pushArgs); }); rerender(); @@ -887,7 +887,7 @@ describe('IssueList', function () { }; await waitFor(() => { - expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs); + expect(router.push).toHaveBeenLastCalledWith(pushArgs); }); rerender(); @@ -909,7 +909,7 @@ describe('IssueList', function () { }; await waitFor(() => { - expect(browserHistory.push).toHaveBeenLastCalledWith(pushArgs); + expect(router.push).toHaveBeenLastCalledWith(pushArgs); }); rerender(); @@ -919,7 +919,7 @@ describe('IssueList', function () { await waitFor(() => { // cursor is undefined because "prev" cursor is === initial "next" cursor - expect(browserHistory.push).toHaveBeenLastCalledWith({ + expect(router.push).toHaveBeenLastCalledWith({ pathname: '/organizations/org-slug/issues/', query: { cursor: undefined, @@ -955,7 +955,7 @@ describe('IssueList', function () { await userEvent.keyboard('{enter}'); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/organizations/org-slug/issues/', query: { environment: [], diff --git a/static/app/views/onboarding/createSampleEventButton.spec.tsx b/static/app/views/onboarding/createSampleEventButton.spec.tsx index 27b6c353f498c3..cf0619200d2728 100644 --- a/static/app/views/onboarding/createSampleEventButton.spec.tsx +++ b/static/app/views/onboarding/createSampleEventButton.spec.tsx @@ -1,17 +1,18 @@ import * as Sentry from '@sentry/react'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import {trackAnalytics} from 'sentry/utils/analytics'; -import {browserHistory} from 'sentry/utils/browserHistory'; import CreateSampleEventButton from 'sentry/views/onboarding/createSampleEventButton'; jest.useFakeTimers(); jest.mock('sentry/utils/analytics'); describe('CreateSampleEventButton', function () { + const router = RouterFixture(); const org = OrganizationFixture(); const project = ProjectFixture(); const groupID = '123'; @@ -25,7 +26,7 @@ describe('CreateSampleEventButton', function () { > {createSampleText} , - {organization: org} + {organization: org, router} ); } @@ -66,7 +67,7 @@ describe('CreateSampleEventButton', function () { // Wait for the api request and latestEventAvailable to resolve expect(sampleButton).toBeEnabled(); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( `/organizations/${org.slug}/issues/${groupID}/?project=${project.id}&referrer=sample-error` ); }); @@ -107,7 +108,7 @@ describe('CreateSampleEventButton', function () { jest.runAllTimers(); await waitFor(() => expect(latestIssueRequest).toHaveBeenCalled()); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( `/organizations/${org.slug}/issues/${groupID}/?project=${project.id}&referrer=sample-error` ); diff --git a/static/app/views/performance/content.spec.tsx b/static/app/views/performance/content.spec.tsx index ab98d0b077c724..66b7d52882542f 100644 --- a/static/app/views/performance/content.spec.tsx +++ b/static/app/views/performance/content.spec.tsx @@ -10,7 +10,6 @@ import ProjectsStore from 'sentry/stores/projectsStore'; import TeamStore from 'sentry/stores/teamStore'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {Project} from 'sentry/types/project'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import PerformanceContent from 'sentry/views/performance/content'; import {DEFAULT_MAX_DURATION} from 'sentry/views/performance/trends/utils'; @@ -372,7 +371,7 @@ describe('Performance > Content', function () { expect(pageFilters.updateDateTime).toHaveBeenCalledTimes(0); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/performance/trends/', query: { @@ -395,7 +394,7 @@ describe('Performance > Content', function () { }); expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument(); - expect(browserHistory.push).toHaveBeenCalledTimes(0); + expect(router.push).toHaveBeenCalledTimes(0); }); it('Default page (transactions) with trends feature will not update filters if none are set', async function () { @@ -405,7 +404,7 @@ describe('Performance > Content', function () { router, }); expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument(); - expect(browserHistory.push).toHaveBeenCalledTimes(0); + expect(router.push).toHaveBeenCalledTimes(0); }); it('Tags are replaced with trends default query if navigating to trends', async function () { @@ -419,7 +418,7 @@ describe('Performance > Content', function () { await userEvent.click(trendsLinks[0]); expect(await screen.findByTestId('performance-landing-v3')).toBeInTheDocument(); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/performance/trends/', query: { diff --git a/static/app/views/performance/landing/index.spec.tsx b/static/app/views/performance/landing/index.spec.tsx index 65ab8c3a39f8d4..6dbfe3549a96e9 100644 --- a/static/app/views/performance/landing/index.spec.tsx +++ b/static/app/views/performance/landing/index.spec.tsx @@ -1,4 +1,5 @@ import {ProjectFixture} from 'sentry-fixture/project'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock'; import {initializeData} from 'sentry-test/performance/initializePerformanceData'; @@ -13,7 +14,6 @@ import { } from 'sentry-test/reactTestingLibrary'; import TeamStore from 'sentry/stores/teamStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality'; import {QueryClientProvider} from 'sentry/utils/queryClient'; import {OrganizationContext} from 'sentry/views/organizationContext'; @@ -244,15 +244,16 @@ describe('Performance > Landing > Index', function () { }); it('Can switch between landing displays', async function () { + const router = RouterFixture(); const data = initializeData({ query: {landingDisplay: LandingDisplayField.FRONTEND_OTHER, abc: '123'}, }); - wrapper = render(); + wrapper = render(, {router}); expect(screen.getByTestId('frontend-other-view')).toBeInTheDocument(); await userEvent.click(screen.getByRole('tab', {name: 'All Transactions'})); - expect(browserHistory.push).toHaveBeenNthCalledWith( + expect(router.push).toHaveBeenNthCalledWith( 1, expect.objectContaining({ pathname: data.location.pathname, diff --git a/static/app/views/performance/table.spec.tsx b/static/app/views/performance/table.spec.tsx index eb805c688d2054..240a892660f8e7 100644 --- a/static/app/views/performance/table.spec.tsx +++ b/static/app/views/performance/table.spec.tsx @@ -5,7 +5,6 @@ import {initializeData as _initializeData} from 'sentry-test/performance/initial import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import EventView from 'sentry/utils/discover/eventView'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; @@ -241,11 +240,11 @@ describe('Performance > Table', function () { expect(transactionCellTrigger).toBeInTheDocument(); await userEvent.click(transactionCellTrigger); - expect(browserHistory.push).toHaveBeenCalledTimes(0); + expect(data.router.push).toHaveBeenCalledTimes(0); await userEvent.click(screen.getByRole('menuitemradio', {name: 'Add to filter'})); - expect(browserHistory.push).toHaveBeenCalledTimes(1); - expect(browserHistory.push).toHaveBeenNthCalledWith(1, { + expect(data.router.push).toHaveBeenCalledTimes(1); + expect(data.router.push).toHaveBeenNthCalledWith(1, { pathname: undefined, query: expect.objectContaining({ query: 'transaction:/apple/cart', diff --git a/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx index 101bec6af6406f..068d1af21c1486 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx @@ -5,7 +5,6 @@ import {initializeData as _initializeData} from 'sentry-test/performance/initial import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {useLocation} from 'sentry/utils/useLocation'; import TransactionEvents from 'sentry/views/performance/transactionSummary/transactionEvents'; @@ -200,7 +199,7 @@ describe('Performance > Transaction Summary > Transaction Events > Index', () => await userEvent.click(p50); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(data.router.push).toHaveBeenCalledWith( expect.objectContaining({query: expect.objectContaining({showTransactions: 'p50'})}) ); }); diff --git a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx index e9de9155ad6007..e3be63ac0e6845 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx @@ -17,7 +17,6 @@ import ProjectsStore from 'sentry/stores/projectsStore'; import TeamStore from 'sentry/stores/teamStore'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {Project} from 'sentry/types/project'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality'; import { @@ -836,11 +835,11 @@ describe('Performance > TransactionSummary', function () { await userEvent.keyboard('{enter}'); await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledTimes(1); }); // Check the navigation. - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/', query: { transaction: '/performance', @@ -912,7 +911,7 @@ describe('Performance > TransactionSummary', function () { await userEvent.click(screen.getAllByText('Slow Transactions (p95)')[1]); // Check the navigation. - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/', query: { transaction: '/performance', @@ -948,7 +947,7 @@ describe('Performance > TransactionSummary', function () { await userEvent.click(await findByLabelText(pagination, 'Next')); // Check the navigation. - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/', query: { transaction: '/performance', @@ -1060,8 +1059,8 @@ describe('Performance > TransactionSummary', function () { await userEvent.click(screen.getByTestId('status-ok')); - expect(browserHistory.push).toHaveBeenCalledTimes(1); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ query: expect.stringContaining('transaction.status:ok'), diff --git a/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx index d6fe3e511bf27c..c0245c3b68cda2 100644 --- a/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionSpans/spanDetails/index.spec.tsx @@ -12,7 +12,6 @@ import { } from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import SpanDetails from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails'; import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils'; @@ -38,7 +37,6 @@ describe('Performance > Transaction Spans > Span Summary', function () { afterEach(function () { MockApiClient.clearMockResponses(); ProjectsStore.reset(); - jest.mocked(browserHistory.push).mockReset(); }); describe('Without Span Data', function () { @@ -445,7 +443,7 @@ describe('Performance > Transaction Spans > Span Summary', function () { name: /reset view/i, }); await userEvent.click(resetButton); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(data.router.push).toHaveBeenCalledWith( expect.not.objectContaining({min: expect.any(String), max: expect.any(String)}) ); }); @@ -465,7 +463,7 @@ describe('Performance > Transaction Spans > Span Summary', function () { await userEvent.click(searchBarNode); await userEvent.paste('count():>3'); expect(searchBarNode).toHaveTextContent('count():>3'); - expect(browserHistory.push).not.toHaveBeenCalled(); + expect(data.router.push).not.toHaveBeenCalled(); }); it('renders a display toggle that changes a chart view between timeseries and histogram by pushing it to the browser history', async function () { @@ -506,7 +504,7 @@ describe('Performance > Transaction Spans > Span Summary', function () { }) ); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(data.router.push).toHaveBeenCalledWith( expect.objectContaining({ query: { display: 'histogram', diff --git a/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx index 677832e5610a8c..a8fc1995c0fc1f 100644 --- a/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx @@ -6,7 +6,6 @@ import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingL import selectEvent from 'sentry-test/selectEvent'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {useLocation} from 'sentry/utils/useLocation'; import TransactionTags from 'sentry/views/performance/transactionSummary/transactionTags'; @@ -184,7 +183,7 @@ describe('Performance > Transaction Tags', function () { expect(screen.getByText('Heat Map')).toBeInTheDocument(); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenCalledWith({ + expect(router.replace).toHaveBeenCalledWith({ query: { project: '1', statsPeriod: '14d', @@ -211,7 +210,7 @@ describe('Performance > Transaction Tags', function () { }); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenCalledWith({ + expect(router.replace).toHaveBeenCalledWith({ query: { project: '1', statsPeriod: '14d', @@ -250,7 +249,7 @@ describe('Performance > Transaction Tags', function () { }); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenCalledWith({ + expect(router.replace).toHaveBeenCalledWith({ query: { project: '1', statsPeriod: '14d', @@ -312,7 +311,7 @@ describe('Performance > Transaction Tags', function () { expect(await screen.findByText('Suspect Tags')).toBeInTheDocument(); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenCalledWith({ + expect(router.replace).toHaveBeenCalledWith({ query: { project: '1', statsPeriod: '14d', @@ -332,7 +331,7 @@ describe('Performance > Transaction Tags', function () { await userEvent.click(screen.getByLabelText('Next')); await waitFor(() => - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: '/organizations/org-slug/performance/summary/tags/', query: { project: '1', @@ -348,7 +347,7 @@ describe('Performance > Transaction Tags', function () { await userEvent.click(screen.getByRole('radio', {name: 'effectiveConnectionType'})); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenCalledWith({ + expect(router.replace).toHaveBeenCalledWith({ query: { project: '1', statsPeriod: '14d', diff --git a/static/app/views/performance/trends/index.spec.tsx b/static/app/views/performance/trends/index.spec.tsx index f0a69a618980ff..72beaf1a9efcf0 100644 --- a/static/app/views/performance/trends/index.spec.tsx +++ b/static/app/views/performance/trends/index.spec.tsx @@ -17,7 +17,6 @@ import { import PageFiltersStore from 'sentry/stores/pageFiltersStore'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {WebVital} from 'sentry/utils/fields'; import {useLocation} from 'sentry/utils/useLocation'; import TrendsIndex from 'sentry/views/performance/trends/'; @@ -359,7 +358,7 @@ describe('Performance > Trends', function () { const menuAction = within(firstTransaction).getAllByRole('menuitemradio')[2]; await clickEl(menuAction); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(data.router.push).toHaveBeenCalledWith({ pathname: '/trends/', query: expect.objectContaining({ project: expect.anything(), @@ -384,7 +383,7 @@ describe('Performance > Trends', function () { enterSearch(input, 'transaction.duration:>9000'); await waitFor(() => - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(data.router.push).toHaveBeenCalledWith({ pathname: '/trends/', query: expect.objectContaining({ project: ['1'], @@ -423,7 +422,7 @@ describe('Performance > Trends', function () { const menuAction = within(firstTransaction).getAllByRole('menuitemradio')[0]; await clickEl(menuAction); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(data.router.push).toHaveBeenCalledWith({ pathname: '/trends/', query: expect.objectContaining({ project: expect.anything(), @@ -459,7 +458,7 @@ describe('Performance > Trends', function () { const menuAction = within(firstTransaction).getAllByRole('menuitemradio')[1]; await clickEl(menuAction); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(data.router.push).toHaveBeenCalledWith({ pathname: '/trends/', query: expect.objectContaining({ project: expect.anything(), @@ -489,7 +488,7 @@ describe('Performance > Trends', function () { const option = screen.getByRole('option', {name: trendFunction.label}); await clickEl(option); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(data.router.push).toHaveBeenCalledWith({ pathname: '/trends/', query: expect.objectContaining({ regressionCursor: undefined, @@ -569,7 +568,7 @@ describe('Performance > Trends', function () { const option = screen.getByRole('option', {name: parameter.label}); await clickEl(option); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(data.router.push).toHaveBeenCalledWith({ pathname: '/trends/', query: expect.objectContaining({ trendParameter: parameter.label, @@ -718,7 +717,7 @@ describe('Performance > Trends', function () { ); await waitFor(() => - expect(browserHistory.push).toHaveBeenNthCalledWith( + expect(data.router.push).toHaveBeenNthCalledWith( 1, expect.objectContaining({ query: { @@ -746,7 +745,7 @@ describe('Performance > Trends', function () { } ); - jest.mocked(browserHistory.push).mockReset(); + jest.mocked(data.router.push).mockReset(); const byTransactionLink = await screen.findByTestId('breadcrumb-link'); diff --git a/static/app/views/performance/vitalDetail/index.spec.tsx b/static/app/views/performance/vitalDetail/index.spec.tsx index 16868b06aaedc7..69c97b01369288 100644 --- a/static/app/views/performance/vitalDetail/index.spec.tsx +++ b/static/app/views/performance/vitalDetail/index.spec.tsx @@ -14,7 +14,6 @@ import {textWithMarkupMatcher} from 'sentry-test/utils'; import ProjectsStore from 'sentry/stores/projectsStore'; import TeamStore from 'sentry/stores/teamStore'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {WebVital} from 'sentry/utils/fields'; import {Browser} from 'sentry/utils/performance/vitals/constants'; import {DEFAULT_STATS_PERIOD} from 'sentry/views/performance/data'; @@ -268,10 +267,10 @@ describe('Performance > VitalDetail', function () { // Check the navigation. await waitFor(() => { - expect(browserHistory.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledTimes(1); }); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ pathname: undefined, query: { project: '1', @@ -396,8 +395,8 @@ describe('Performance > VitalDetail', function () { expect(menuItem).toBeInTheDocument(); await userEvent.click(menuItem); - expect(browserHistory.push).toHaveBeenCalledTimes(1); - expect(browserHistory.push).toHaveBeenCalledWith({ + expect(newRouter.push).toHaveBeenCalledTimes(1); + expect(newRouter.push).toHaveBeenCalledWith({ pathname: undefined, query: { project: 1, diff --git a/static/app/views/releases/detail/header/releaseActions.spec.tsx b/static/app/views/releases/detail/header/releaseActions.spec.tsx index 9e91b32c6e475f..eccd68f42df2bb 100644 --- a/static/app/views/releases/detail/header/releaseActions.spec.tsx +++ b/static/app/views/releases/detail/header/releaseActions.spec.tsx @@ -6,6 +6,7 @@ import {ReleaseFixture} from 'sentry-fixture/release'; import {ReleaseMetaFixture} from 'sentry-fixture/releaseMeta'; import {ReleaseProjectFixture} from 'sentry-fixture/releaseProject'; import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import { render, @@ -17,10 +18,10 @@ import { import type {ReleaseProject} from 'sentry/types/release'; import {ReleaseStatus} from 'sentry/types/release'; -import {browserHistory} from 'sentry/utils/browserHistory'; import ReleaseActions from 'sentry/views/releases/detail/header/releaseActions'; describe('ReleaseActions', function () { + const router = RouterFixture(); const organization = OrganizationFixture(); const project1 = ReleaseProjectFixture({ @@ -70,9 +71,10 @@ describe('ReleaseActions', function () { refetchData={jest.fn()} releaseMeta={{...ReleaseMetaFixture(), projects: release.projects}} location={location} - /> + />, + {router} ); - renderGlobalModal(); + renderGlobalModal({router}); await userEvent.click(screen.getByLabelText('Actions')); @@ -101,7 +103,7 @@ describe('ReleaseActions', function () { }) ); await waitFor(() => - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( `/organizations/${organization.slug}/releases/` ) ); @@ -119,9 +121,10 @@ describe('ReleaseActions', function () { refetchData={refetchDataMock} releaseMeta={{...ReleaseMetaFixture(), projects: release.projects}} location={location} - /> + />, + {router} ); - renderGlobalModal(); + renderGlobalModal({router}); await userEvent.click(screen.getByLabelText('Actions')); @@ -162,7 +165,8 @@ describe('ReleaseActions', function () { refetchData={jest.fn()} releaseMeta={{...ReleaseMetaFixture(), projects: release.projects}} location={location} - /> + />, + {router} ); expect(screen.getByLabelText('Oldest')).toHaveAttribute( diff --git a/static/app/views/releases/detail/overview/releaseComparisonChart/index.spec.tsx b/static/app/views/releases/detail/overview/releaseComparisonChart/index.spec.tsx index 75eb7e343f1949..9dff0ca7d42027 100644 --- a/static/app/views/releases/detail/overview/releaseComparisonChart/index.spec.tsx +++ b/static/app/views/releases/detail/overview/releaseComparisonChart/index.spec.tsx @@ -8,7 +8,6 @@ import {initializeOrg} from 'sentry-test/initializeOrg'; import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import type {ReleaseProject} from 'sentry/types/release'; -import {browserHistory} from 'sentry/utils/browserHistory'; import ReleaseComparisonChart from 'sentry/views/releases/detail/overview/releaseComparisonChart'; describe('Releases > Detail > Overview > ReleaseComparison', () => { @@ -81,7 +80,7 @@ describe('Releases > Detail > Overview > ReleaseComparison', () => { await userEvent.click(screen.getByLabelText(/crash free user rate/i)); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(router.push).toHaveBeenCalledWith( expect.objectContaining({query: {chart: 'crashFreeUsers'}}) ); diff --git a/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.spec.tsx b/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.spec.tsx index 7f644ef38c8bc5..930b2f813c7868 100644 --- a/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.spec.tsx +++ b/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.spec.tsx @@ -6,7 +6,6 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import ConfigStore from 'sentry/stores/configStore'; import OrganizationsStore from 'sentry/stores/organizationsStore'; import type {Config} from 'sentry/types/system'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {OrganizationCrumb} from './organizationCrumb'; import type {RouteWithName} from './types'; @@ -29,7 +28,6 @@ describe('OrganizationCrumb', function () { OrganizationsStore.load(organizations); initialData = ConfigStore.getState(); - jest.mocked(browserHistory.push).mockReset(); jest.mocked(window.location.assign).mockReset(); }); @@ -67,7 +65,7 @@ describe('OrganizationCrumb', function () { renderComponent({routes, route}); await switchOrganization(); - expect(browserHistory.push).toHaveBeenCalledWith('/settings/org-slug2/'); + expect(router.push).toHaveBeenCalledWith('/settings/org-slug2/'); }); it('switches organizations while on API Keys Details route', async function () { @@ -88,7 +86,7 @@ describe('OrganizationCrumb', function () { renderComponent({routes, route}); await switchOrganization(); - expect(browserHistory.push).toHaveBeenCalledWith('/settings/org-slug2/api-keys/'); + expect(router.push).toHaveBeenCalledWith('/settings/org-slug2/api-keys/'); }); it('switches organizations while on API Keys List route', async function () { @@ -108,7 +106,7 @@ describe('OrganizationCrumb', function () { renderComponent({routes, route}); await switchOrganization(); - expect(browserHistory.push).toHaveBeenCalledWith('/settings/org-slug2/api-keys/'); + expect(router.push).toHaveBeenCalledWith('/settings/org-slug2/api-keys/'); }); it('switches organizations while in Project Client Keys Details route', async function () { @@ -130,7 +128,7 @@ describe('OrganizationCrumb', function () { }); await switchOrganization(); - expect(browserHistory.push).toHaveBeenCalledWith('/settings/org-slug2/'); + expect(router.push).toHaveBeenCalledWith('/settings/org-slug2/'); }); it('switches organizations for child route with customer domains', async function () { diff --git a/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx b/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx index bc2f1c04720660..934b083cff7f81 100644 --- a/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx +++ b/static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx @@ -21,7 +21,6 @@ import ConfigStore from 'sentry/stores/configStore'; import ModalStore from 'sentry/stores/modalStore'; import OrganizationsStore from 'sentry/stores/organizationsStore'; import {trackAnalytics} from 'sentry/utils/analytics'; -import {browserHistory} from 'sentry/utils/browserHistory'; import OrganizationMembersList from 'sentry/views/settings/organizationMembers/organizationMembersList'; jest.mock('sentry/utils/analytics'); @@ -165,8 +164,8 @@ describe('OrganizationMembersList', function () { method: 'DELETE', }); - render(, {organization}); - renderGlobalModal(); + render(, {organization, router}); + renderGlobalModal({router}); // The organization member row expect(await screen.findByTestId(members[0].email)).toBeInTheDocument(); @@ -191,7 +190,7 @@ describe('OrganizationMembersList', function () { }); render(, {organization, router}); - renderGlobalModal(); + renderGlobalModal({router}); // The organization member row expect(await screen.findByTestId(members[0].email)).toBeInTheDocument(); @@ -215,7 +214,7 @@ describe('OrganizationMembersList', function () { }); render(, {organization, router}); - renderGlobalModal(); + renderGlobalModal({router}); await userEvent.click(await screen.findByRole('button', {name: 'Leave'})); await userEvent.click(await screen.findByRole('button', {name: 'Confirm'})); @@ -223,8 +222,8 @@ describe('OrganizationMembersList', function () { await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled()); expect(deleteMock).toHaveBeenCalled(); - expect(browserHistory.push).toHaveBeenCalledTimes(1); - expect(browserHistory.push).toHaveBeenCalledWith('/organizations/new/'); + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith('/organizations/new/'); }); it('can redirect to remaining org after leaving', async function () { @@ -242,7 +241,7 @@ describe('OrganizationMembersList', function () { OrganizationsStore.addOrReplace(secondOrg); render(, {organization, router}); - renderGlobalModal(); + renderGlobalModal({router}); await userEvent.click(await screen.findByRole('button', {name: 'Leave'})); await userEvent.click(screen.getByTestId('confirm-button')); @@ -250,8 +249,8 @@ describe('OrganizationMembersList', function () { await waitFor(() => expect(addSuccessMessage).toHaveBeenCalled()); expect(deleteMock).toHaveBeenCalled(); - expect(browserHistory.push).toHaveBeenCalledTimes(1); - expect(browserHistory.push).toHaveBeenCalledWith('/organizations/org-two/issues/'); + expect(router.push).toHaveBeenCalledTimes(1); + expect(router.push).toHaveBeenCalledWith('/organizations/org-two/issues/'); expect(OrganizationsStore.getAll()).toEqual([secondOrg]); }); @@ -262,8 +261,8 @@ describe('OrganizationMembersList', function () { statusCode: 500, }); - render(, {organization}); - renderGlobalModal(); + render(, {organization, router}); + renderGlobalModal({router}); await userEvent.click(await screen.findByRole('button', {name: 'Leave'})); await userEvent.click(await screen.findByRole('button', {name: 'Confirm'})); @@ -474,13 +473,13 @@ describe('OrganizationMembersList', function () { method: 'PUT', }); - render(, {organization}); + render(, {organization, router}); expect(await screen.findByText('Pending Members')).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: 'Approve'})); - renderGlobalModal(); + renderGlobalModal({router}); await userEvent.click(screen.getByTestId('confirm-button')); expect(screen.queryByText('Pending Members')).not.toBeInTheDocument(); @@ -544,7 +543,7 @@ describe('OrganizationMembersList', function () { method: 'PUT', }); - render(, {organization: org}); + render(, {organization: org, router}); expect(await screen.findByText('Pending Members')).toBeInTheDocument(); await selectEvent.select(screen.getByRole('textbox', {name: 'Role: Member'}), [ @@ -553,7 +552,7 @@ describe('OrganizationMembersList', function () { await userEvent.click(screen.getByRole('button', {name: 'Approve'})); - renderGlobalModal(); + renderGlobalModal({router}); await userEvent.click(screen.getByTestId('confirm-button')); expect(updateWithApprove).toHaveBeenCalledWith( @@ -574,8 +573,8 @@ describe('OrganizationMembersList', function () { }, }); - render(, {organization: inviteOrg}); - renderGlobalModal(); + render(, {organization: inviteOrg, router}); + renderGlobalModal({router}); await userEvent.click(await screen.findByRole('button', {name: 'Invite Members'})); expect(screen.getByRole('dialog')).toBeInTheDocument(); @@ -591,8 +590,8 @@ describe('OrganizationMembersList', function () { }, }); - render(, {organization: org}); - renderGlobalModal(); + render(, {organization: org, router}); + renderGlobalModal({router}); expect(await screen.findByRole('button', {name: 'Invite Members'})).toBeDisabled(); }); @@ -608,8 +607,8 @@ describe('OrganizationMembersList', function () { requiresSso: true, }); - render(, {organization: org}); - renderGlobalModal(); + render(, {organization: org, router}); + renderGlobalModal({router}); await userEvent.click(screen.getByRole('button', {name: 'Invite Members'})); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); @@ -625,8 +624,8 @@ describe('OrganizationMembersList', function () { }, }); - render(, {organization: org}); - renderGlobalModal(); + render(, {organization: org, router}); + renderGlobalModal({router}); await userEvent.click(await screen.findByRole('button', {name: 'Invite Members'})); expect(screen.getByRole('dialog')).toBeInTheDocument(); @@ -643,8 +642,8 @@ describe('OrganizationMembersList', function () { method: 'GET', body: {}, }); - render(, {organization}); - renderGlobalModal(); + render(, {organization, router}); + renderGlobalModal({router}); expect(await screen.findByText('Members')).toBeInTheDocument(); expect(screen.getByText(member.name)).toBeInTheDocument(); diff --git a/static/app/views/settings/organizationProjects/index.spec.tsx b/static/app/views/settings/organizationProjects/index.spec.tsx index a0cd7f770fbf41..a8b968dea3c909 100644 --- a/static/app/views/settings/organizationProjects/index.spec.tsx +++ b/static/app/views/settings/organizationProjects/index.spec.tsx @@ -1,8 +1,8 @@ import {ProjectFixture} from 'sentry-fixture/project'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; -import {browserHistory} from 'sentry/utils/browserHistory'; import OrganizationProjectsContainer from 'sentry/views/settings/organizationProjects'; describe('OrganizationProjects', function () { @@ -10,6 +10,7 @@ describe('OrganizationProjects', function () { let statsGetMock: jest.Mock; let projectsPutMock: jest.Mock; const project = ProjectFixture(); + const router = RouterFixture(); beforeEach(function () { projectsGetMock = MockApiClient.addMockResponse({ @@ -33,7 +34,7 @@ describe('OrganizationProjects', function () { }); it('should render the projects in the store', async function () { - render(); + render(, {router}); expect(await screen.findByText('project-slug')).toBeInTheDocument(); @@ -62,7 +63,7 @@ describe('OrganizationProjects', function () { }); it('should search organization projects', async function () { - render(); + render(, {router}); expect(await screen.findByText('project-slug')).toBeInTheDocument(); @@ -70,7 +71,7 @@ describe('OrganizationProjects', function () { await userEvent.type(searchBox, 'random'); await waitFor(() => { - expect(browserHistory.replace).toHaveBeenLastCalledWith({ + expect(router.replace).toHaveBeenLastCalledWith({ pathname: '/mock-pathname/', query: {query: 'random'}, }); diff --git a/static/app/views/settings/organizationTeams/teamSettings/index.spec.tsx b/static/app/views/settings/organizationTeams/teamSettings/index.spec.tsx index 995ff92a34810f..b60d20de9002c5 100644 --- a/static/app/views/settings/organizationTeams/teamSettings/index.spec.tsx +++ b/static/app/views/settings/organizationTeams/teamSettings/index.spec.tsx @@ -11,11 +11,10 @@ import { } from 'sentry-test/reactTestingLibrary'; import TeamStore from 'sentry/stores/teamStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import TeamSettings from 'sentry/views/settings/organizationTeams/teamSettings'; describe('TeamSettings', function () { - const {routerProps} = initializeOrg(); + const {router, routerProps} = initializeOrg(); beforeEach(function () { TeamStore.reset(); @@ -37,7 +36,9 @@ describe('TeamSettings', function () { }, }); - render(); + render(, { + router, + }); const input = screen.getByRole('textbox', {name: 'Team Slug'}); @@ -56,7 +57,7 @@ describe('TeamSettings', function () { ); await waitFor(() => - expect(browserHistory.replace).toHaveBeenCalledWith( + expect(router.replace).toHaveBeenCalledWith( '/settings/org-slug/teams/new-slug/settings/' ) ); @@ -68,6 +69,7 @@ describe('TeamSettings', function () { render(, { organization, + router, }); expect(screen.getByTestId('button-remove-team')).toBeDisabled(); @@ -81,13 +83,15 @@ describe('TeamSettings', function () { }); TeamStore.loadInitialData([team]); - render(); + render(, { + router, + }); // Click "Remove Team button await userEvent.click(screen.getByRole('button', {name: 'Remove Team'})); // Wait for modal - renderGlobalModal(); + renderGlobalModal({router}); await userEvent.click(screen.getByTestId('confirm-button')); expect(deleteMock).toHaveBeenCalledWith( @@ -98,7 +102,7 @@ describe('TeamSettings', function () { ); await waitFor(() => - expect(browserHistory.replace).toHaveBeenCalledWith('/settings/org-slug/teams/') + expect(router.replace).toHaveBeenCalledWith('/settings/org-slug/teams/') ); expect(TeamStore.getAll()).toEqual([]); @@ -110,6 +114,7 @@ describe('TeamSettings', function () { render(, { organization, + router, }); expect( diff --git a/static/app/views/settings/projectGeneralSettings/index.spec.tsx b/static/app/views/settings/projectGeneralSettings/index.spec.tsx index 2aa4e099777df6..7a6dd9de0728fa 100644 --- a/static/app/views/settings/projectGeneralSettings/index.spec.tsx +++ b/static/app/views/settings/projectGeneralSettings/index.spec.tsx @@ -17,7 +17,6 @@ import selectEvent from 'sentry-test/selectEvent'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {removePageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence'; import ProjectsStore from 'sentry/stores/projectsStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; import ProjectContextProvider from 'sentry/views/projects/projectContext'; import ProjectGeneralSettings from 'sentry/views/settings/projectGeneralSettings'; @@ -298,7 +297,7 @@ describe('projectGeneralSettings', function () { params={params} /> , - {organization} + {organization, router} ); await userEvent.type( @@ -313,7 +312,7 @@ describe('projectGeneralSettings', function () { await userEvent.click(screen.getByRole('button', {name: 'Save'})); // Redirects the user - await waitFor(() => expect(browserHistory.replace).toHaveBeenCalled()); + await waitFor(() => expect(router.replace).toHaveBeenCalled()); expect(ProjectsStore.getById('2')!.slug).toBe('new-project'); }); From 16d4d44f12ff76e04220425f65bad7429cdaf2d1 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 27 Dec 2024 14:44:00 -0500 Subject: [PATCH 505/757] test(js): Remove browserHistory from organization actionCreators test (#82592) As far as I can tell browserHistory.replace would never even be called from calling fetchOrganizations that this test is testing. --- static/app/actionCreators/organizations.spec.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/app/actionCreators/organizations.spec.tsx b/static/app/actionCreators/organizations.spec.tsx index deceb5f403dd28..822f1bdbaae26d 100644 --- a/static/app/actionCreators/organizations.spec.tsx +++ b/static/app/actionCreators/organizations.spec.tsx @@ -2,7 +2,6 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {fetchOrganizations} from 'sentry/actionCreators/organizations'; import ConfigStore from 'sentry/stores/configStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; describe('fetchOrganizations', function () { const api = new MockApiClient(); @@ -76,6 +75,5 @@ describe('fetchOrganizations', function () { expect(usMock).toHaveBeenCalledTimes(1); expect(deMock).toHaveBeenCalledTimes(1); expect(window.location.reload).not.toHaveBeenCalled(); - expect(browserHistory.replace).not.toHaveBeenCalled(); }); }); From be0f4da5a3d3c7d89d5cb07cf91104244a59afb7 Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:28:52 -0500 Subject: [PATCH 506/757] feat(insights): Removes unused webvitals function query and code (#82485) The frontend doesn't actually use the `weighted_performance_score` function anymore. Removes all calls and references from frontend code. --- .../performanceScoreBreakdownChart.spec.tsx | 5 --- .../charts/performanceScoreBreakdownChart.tsx | 16 ++++----- ...eProjectWebVitalsScoresTimeseriesQuery.tsx | 34 ++----------------- .../applyStaticWeightsToTimeseries.spec.tsx | 15 +++----- .../utils/applyStaticWeightsToTimeseries.tsx | 15 +++----- 5 files changed, 19 insertions(+), 66 deletions(-) diff --git a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx index c9773ba36c12ad..d34b0cabee99f8 100644 --- a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx @@ -72,11 +72,6 @@ describe('PerformanceScoreBreakdownChart', function () { method: 'GET', query: expect.objectContaining({ yAxis: [ - 'weighted_performance_score(measurements.score.lcp)', - 'weighted_performance_score(measurements.score.fcp)', - 'weighted_performance_score(measurements.score.cls)', - 'weighted_performance_score(measurements.score.inp)', - 'weighted_performance_score(measurements.score.ttfb)', 'performance_score(measurements.score.lcp)', 'performance_score(measurements.score.fcp)', 'performance_score(measurements.score.cls)', diff --git a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx index 6ae6914104ce00..24f863ff4e62d6 100644 --- a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx +++ b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx @@ -72,13 +72,13 @@ export function PerformanceScoreBreakdownChart({ chartSeriesOrder ); - const unweightedTimeseries = formatTimeSeriesResultsToChartData( + const timeseries = formatTimeSeriesResultsToChartData( { - lcp: timeseriesData.unweightedLcp, - fcp: timeseriesData.unweightedFcp, - cls: timeseriesData.unweightedCls, - ttfb: timeseriesData.unweightedTtfb, - inp: timeseriesData.unweightedInp, + lcp: timeseriesData.lcp, + fcp: timeseriesData.fcp, + cls: timeseriesData.cls, + ttfb: timeseriesData.ttfb, + inp: timeseriesData.inp, total: timeseriesData.total, }, segmentColors, @@ -128,10 +128,10 @@ export function PerformanceScoreBreakdownChart({ }, valueFormatter: (_value, _label, seriesParams: any) => { const timestamp = seriesParams?.data[0]; - const unweightedValue = unweightedTimeseries + const value = timeseries .find(series => series.seriesName === seriesParams?.seriesName) ?.data.find(dataPoint => dataPoint.name === timestamp)?.value; - return `${unweightedValue}`; + return `${value}`; }, }} /> diff --git a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresTimeseriesQuery.tsx b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresTimeseriesQuery.tsx index 346c69c2ce0583..343f9758a590fe 100644 --- a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresTimeseriesQuery.tsx +++ b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresTimeseriesQuery.tsx @@ -32,14 +32,6 @@ export type WebVitalsScoreBreakdown = { ttfb: SeriesDataUnit[]; }; -export type UnweightedWebVitalsScoreBreakdown = { - unweightedCls: SeriesDataUnit[]; - unweightedFcp: SeriesDataUnit[]; - unweightedInp: SeriesDataUnit[]; - unweightedLcp: SeriesDataUnit[]; - unweightedTtfb: SeriesDataUnit[]; -}; - export const useProjectWebVitalsScoresTimeseriesQuery = ({ transaction, tag, @@ -66,11 +58,6 @@ export const useProjectWebVitalsScoresTimeseriesQuery = ({ const projectTimeSeriesEventView = EventView.fromNewQueryWithPageFilters( { yAxis: [ - 'weighted_performance_score(measurements.score.lcp)', - 'weighted_performance_score(measurements.score.fcp)', - 'weighted_performance_score(measurements.score.cls)', - 'weighted_performance_score(measurements.score.inp)', - 'weighted_performance_score(measurements.score.ttfb)', 'performance_score(measurements.score.lcp)', 'performance_score(measurements.score.fcp)', 'performance_score(measurements.score.cls)', @@ -115,36 +102,19 @@ export const useProjectWebVitalsScoresTimeseriesQuery = ({ referrer: 'api.performance.browser.web-vitals.timeseries-scores', }); - const data: WebVitalsScoreBreakdown & UnweightedWebVitalsScoreBreakdown = { + const data: WebVitalsScoreBreakdown = { lcp: [], fcp: [], cls: [], ttfb: [], inp: [], total: [], - unweightedCls: [], - unweightedFcp: [], - unweightedInp: [], - unweightedLcp: [], - unweightedTtfb: [], }; - result?.data?.['weighted_performance_score(measurements.score.lcp)']?.data.forEach( + result?.data?.['performance_score(measurements.score.lcp)']?.data.forEach( (interval, index) => { - // Weighted data ['lcp', 'fcp', 'cls', 'ttfb', 'inp'].forEach(webVital => { data[webVital].push({ - value: - result?.data?.[`weighted_performance_score(measurements.score.${webVital})`] - ?.data[index][1][0].count * 100, - name: interval[0] * 1000, - }); - }); - // Unweighted data - ['lcp', 'fcp', 'cls', 'ttfb', 'inp'].forEach(webVital => { - // Capitalize first letter of webVital - const capitalizedWebVital = webVital.charAt(0).toUpperCase() + webVital.slice(1); - data[`unweighted${capitalizedWebVital}`].push({ value: result?.data?.[`performance_score(measurements.score.${webVital})`]?.data[ index diff --git a/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.spec.tsx b/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.spec.tsx index 8bbe2c6125cf8f..6da9a7a64ebe25 100644 --- a/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.spec.tsx +++ b/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.spec.tsx @@ -3,28 +3,23 @@ import {applyStaticWeightsToTimeseries} from 'sentry/views/insights/browser/webV describe('applyStaticWeightsToTimeseries', function () { it('updates timeseries scores with static weighing', function () { const timeseriesData = { - lcp: [], - fcp: [], - cls: [], - ttfb: [], - inp: [], - unweightedLcp: [ + lcp: [ {name: '2024-07-01T00:00:00.000Z', value: 90}, {name: '2024-07-02T00:00:00.000Z', value: 40}, ], - unweightedFcp: [ + fcp: [ {name: '2024-07-01T00:00:00.000Z', value: 30}, {name: '2024-07-02T00:00:00.000Z', value: 20}, ], - unweightedCls: [ + cls: [ {name: '2024-07-01T00:00:00.000Z', value: 10}, {name: '2024-07-02T00:00:00.000Z', value: 90}, ], - unweightedTtfb: [ + ttfb: [ {name: '2024-07-01T00:00:00.000Z', value: 22}, {name: '2024-07-02T00:00:00.000Z', value: 43}, ], - unweightedInp: [ + inp: [ {name: '2024-07-01T00:00:00.000Z', value: 100}, {name: '2024-07-02T00:00:00.000Z', value: 0}, ], diff --git a/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.tsx b/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.tsx index 234c2137f9f040..ca67b1e34c81a0 100644 --- a/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.tsx +++ b/static/app/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries.tsx @@ -1,23 +1,16 @@ -import type { - UnweightedWebVitalsScoreBreakdown, - WebVitalsScoreBreakdown, -} from 'sentry/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresTimeseriesQuery'; +import type {WebVitalsScoreBreakdown} from 'sentry/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresTimeseriesQuery'; import {PERFORMANCE_SCORE_WEIGHTS} from 'sentry/views/insights/browser/webVitals/utils/scoreThresholds'; // Returns a weighed score timeseries with each interval calculated from applying hardcoded weights to unweighted scores -export function applyStaticWeightsToTimeseries( - timeseriesData: WebVitalsScoreBreakdown & UnweightedWebVitalsScoreBreakdown -) { +export function applyStaticWeightsToTimeseries(timeseriesData: WebVitalsScoreBreakdown) { return { ...Object.keys(PERFORMANCE_SCORE_WEIGHTS).reduce((acc, webVital) => { - acc[webVital] = timeseriesData[ - `unweighted${webVital.charAt(0).toUpperCase()}${webVital.slice(1)}` - ].map(({name, value}) => ({ + acc[webVital] = timeseriesData[webVital].map(({name, value}) => ({ name, value: value * PERFORMANCE_SCORE_WEIGHTS[webVital] * 0.01, })); return acc; }, {}), total: timeseriesData.total, - } as WebVitalsScoreBreakdown & UnweightedWebVitalsScoreBreakdown; + } as WebVitalsScoreBreakdown; } From 5f1058e440a8058d125da27923b8a530fd660305 Mon Sep 17 00:00:00 2001 From: mia hsu <55610339+ameliahsu@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:08:21 -0800 Subject: [PATCH 507/757] feat(workflow-engine): add `EveryEventConditionHandler` (#82591) add `EveryEventConditionHandler` --- .../handlers/condition/__init__.py | 2 + .../condition/group_event_handlers.py | 7 ++++ .../issue_alert_conditions.py | 13 ++++++ .../workflow_engine/models/data_condition.py | 1 + .../condition/test_group_event_handlers.py | 42 +++++++++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 tests/sentry/workflow_engine/handlers/condition/test_group_event_handlers.py diff --git a/src/sentry/workflow_engine/handlers/condition/__init__.py b/src/sentry/workflow_engine/handlers/condition/__init__.py index 2168a6ac24cd87..1c938bdc58769a 100644 --- a/src/sentry/workflow_engine/handlers/condition/__init__.py +++ b/src/sentry/workflow_engine/handlers/condition/__init__.py @@ -1,6 +1,7 @@ __all__ = [ "EventCreatedByDetectorConditionHandler", "EventSeenCountConditionHandler", + "EveryEventConditionHandler", "ReappearedEventConditionHandler", "RegressionEventConditionHandler", ] @@ -8,5 +9,6 @@ from .group_event_handlers import ( EventCreatedByDetectorConditionHandler, EventSeenCountConditionHandler, + EveryEventConditionHandler, ) from .group_state_handlers import ReappearedEventConditionHandler, RegressionEventConditionHandler diff --git a/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py b/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py index 2bc9422c544595..c0c0abbd26cdcc 100644 --- a/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py +++ b/src/sentry/workflow_engine/handlers/condition/group_event_handlers.py @@ -16,6 +16,13 @@ def evaluate_value(job: WorkflowJob, comparison: Any) -> bool: return event.occurrence.evidence_data.get("detector_id", None) == comparison +@condition_handler_registry.register(Condition.EVERY_EVENT) +class EveryEventConditionHandler(DataConditionHandler[WorkflowJob]): + @staticmethod + def evaluate_value(job: WorkflowJob, comparison: Any) -> bool: + return True + + @condition_handler_registry.register(Condition.EVENT_SEEN_COUNT) class EventSeenCountConditionHandler(DataConditionHandler[WorkflowJob]): @staticmethod diff --git a/src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py b/src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py index 36dfb5007c1abd..08f6242d52b4ba 100644 --- a/src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py +++ b/src/sentry/workflow_engine/migration_helpers/issue_alert_conditions.py @@ -1,6 +1,7 @@ from collections.abc import Callable from typing import Any +from sentry.rules.conditions.every_event import EveryEventCondition from sentry.rules.conditions.reappeared_event import ReappearedEventCondition from sentry.rules.conditions.regression_event import RegressionEventCondition from sentry.utils.registry import Registry @@ -39,3 +40,15 @@ def create_regressed_event_data_condition( condition_result=True, condition_group=dcg, ) + + +@data_condition_translator_registry.register(EveryEventCondition.id) +def create_every_event_data_condition( + data: dict[str, Any], dcg: DataConditionGroup +) -> DataCondition: + return DataCondition.objects.create( + type=Condition.EVERY_EVENT, + comparison=True, + condition_result=True, + condition_group=dcg, + ) diff --git a/src/sentry/workflow_engine/models/data_condition.py b/src/sentry/workflow_engine/models/data_condition.py index e9ef394cff23a1..975b677c294b43 100644 --- a/src/sentry/workflow_engine/models/data_condition.py +++ b/src/sentry/workflow_engine/models/data_condition.py @@ -22,6 +22,7 @@ class Condition(models.TextChoices): NOT_EQUAL = "ne" EVENT_CREATED_BY_DETECTOR = "event_created_by_detector" EVENT_SEEN_COUNT = "event_seen_count" + EVERY_EVENT = "every_event" REGRESSION_EVENT = "regression_event" REAPPEARED_EVENT = "reappeared_event" diff --git a/tests/sentry/workflow_engine/handlers/condition/test_group_event_handlers.py b/tests/sentry/workflow_engine/handlers/condition/test_group_event_handlers.py new file mode 100644 index 00000000000000..d7e050daa56145 --- /dev/null +++ b/tests/sentry/workflow_engine/handlers/condition/test_group_event_handlers.py @@ -0,0 +1,42 @@ +from sentry.eventstream.base import GroupState +from sentry.rules.conditions.every_event import EveryEventCondition +from sentry.workflow_engine.models.data_condition import Condition +from sentry.workflow_engine.types import WorkflowJob +from tests.sentry.workflow_engine.handlers.condition.test_base import ConditionTestCase + + +class TestEveryEventCondition(ConditionTestCase): + condition = Condition.EVERY_EVENT + rule_cls = EveryEventCondition + payload = {"id": EveryEventCondition.id} + + def test(self): + job = WorkflowJob( + { + "event": self.group_event, + "group_state": GroupState( + { + "id": 1, + "is_regression": False, + "is_new": False, + "is_new_group_environment": False, + } + ), + } + ) + dc = self.create_data_condition( + type=self.condition, + comparison=True, + condition_result=True, + ) + + self.assert_passes(dc, job) + + def test_dual_write(self): + dcg = self.create_data_condition_group() + dc = self.translate_to_data_condition(self.payload, dcg) + + assert dc.type == self.condition + assert dc.comparison is True + assert dc.condition_result is True + assert dc.condition_group == dcg From 0f27f6a852b746322f9d53561f19ef7247ae3245 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 27 Dec 2024 16:13:56 -0500 Subject: [PATCH 508/757] ref(browserHistory): Remove from vitalsChart (#82603) --- .../transactionOverview/vitalsChart/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx b/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx index 95d762ce803df5..1d2e6f9faa3a06 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx @@ -9,12 +9,12 @@ import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilte import QuestionTooltip from 'sentry/components/questionTooltip'; import {t} from 'sentry/locale'; import type {OrganizationSummary} from 'sentry/types/organization'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {getUtcToLocalDateObject} from 'sentry/utils/dates'; import {getAggregateArg, getMeasurementSlug} from 'sentry/utils/discover/fields'; import {WebVital} from 'sentry/utils/fields'; import useApi from 'sentry/utils/useApi'; import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; import type {ViewProps} from '../../../types'; @@ -39,6 +39,7 @@ function VitalsChart({ end: propsEnd, queryExtras, }: Props) { + const navigate = useNavigate(); const location = useLocation(); const api = useApi(); const theme = useTheme(); @@ -58,7 +59,7 @@ function VitalsChart({ unselectedSeries: unselected, }, }; - browserHistory.push(to); + navigate(to); }; const vitals = [WebVital.FCP, WebVital.LCP, WebVital.FID, WebVital.CLS]; From 2da8db18ca4311d8992d41b8c8fb0794abbaa02c Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 27 Dec 2024 16:20:24 -0500 Subject: [PATCH 509/757] ref(browserHistory): Remove from spanSummaryTable (#82604) --- .../transactionSpans/spanSummary/spanSummaryTable.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx b/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx index db0b342d95959e..7cb906e18cf260 100644 --- a/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable.tsx @@ -14,7 +14,6 @@ import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; -import {browserHistory} from 'sentry/utils/browserHistory'; import EventView, {type MetaType} from 'sentry/utils/discover/eventView'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {ColumnType} from 'sentry/utils/discover/fields'; @@ -200,7 +199,7 @@ export default function SpanSummaryTable(props: Props) { }) ?? []; const handleCursor: CursorHandler = (cursor, pathname, query) => { - browserHistory.push({ + navigate({ pathname, query: {...query, [QueryParameterNames.SPANS_CURSOR]: cursor}, }); From d5e6ef74c03b9a6be8acc648d2b8c11fc9ae6f79 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Fri, 27 Dec 2024 16:21:40 -0500 Subject: [PATCH 510/757] ref(js): Remove browserHistory from discover utils (#82594) --- static/app/utils/useNavigate.tsx | 2 +- static/app/views/discover/table/tableView.tsx | 3 +++ static/app/views/discover/utils.spec.tsx | 9 ++++++--- static/app/views/discover/utils.tsx | 7 ++++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/static/app/utils/useNavigate.tsx b/static/app/utils/useNavigate.tsx index 2145b973b88faf..2ec76e16943f89 100644 --- a/static/app/utils/useNavigate.tsx +++ b/static/app/utils/useNavigate.tsx @@ -12,7 +12,7 @@ type NavigateOptions = { state?: any; }; -interface ReactRouter3Navigate { +export interface ReactRouter3Navigate { (to: LocationDescriptor, options?: NavigateOptions): void; (delta: number): void; } diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index fedbd5f945a513..d093e695d27a87 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -49,6 +49,7 @@ import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {decodeList} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; +import {useNavigate} from 'sentry/utils/useNavigate'; import useProjects from 'sentry/utils/useProjects'; import {useRoutes} from 'sentry/utils/useRoutes'; import {appendQueryDatasetParam, hasDatasetSelector} from 'sentry/views/dashboards/utils'; @@ -109,6 +110,7 @@ export type TableViewProps = { function TableView(props: TableViewProps) { const {projects} = useProjects(); const routes = useRoutes(); + const navigate = useNavigate(); const replayLinkGenerator = generateReplayLink(routes); /** @@ -124,6 +126,7 @@ function TableView(props: TableViewProps) { const nextEventView = eventView.withResizedColumn(columnIndex, newWidth); pushEventViewToLocation({ + navigate, location, nextEventView, extraQuery: { diff --git a/static/app/views/discover/utils.spec.tsx b/static/app/views/discover/utils.spec.tsx index 50d62fcad067bd..55e92544f69a6a 100644 --- a/static/app/views/discover/utils.spec.tsx +++ b/static/app/views/discover/utils.spec.tsx @@ -4,7 +4,6 @@ import {LocationFixture} from 'sentry-fixture/locationFixture'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; -import {browserHistory} from 'sentry/utils/browserHistory'; import type {EventViewOptions} from 'sentry/utils/discover/eventView'; import EventView from 'sentry/utils/discover/eventView'; import {DisplayType} from 'sentry/views/dashboards/types'; @@ -252,14 +251,16 @@ describe('pushEventViewToLocation', function () { }); it('correct query string object pushed to history', function () { + const navigate = jest.fn(); const eventView = new EventView({...baseView, ...state}); pushEventViewToLocation({ + navigate, location, nextEventView: eventView, }); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(navigate).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ id: '1234', @@ -280,9 +281,11 @@ describe('pushEventViewToLocation', function () { }); it('extra query params', function () { + const navigate = jest.fn(); const eventView = new EventView({...baseView, ...state}); pushEventViewToLocation({ + navigate, location, nextEventView: eventView, extraQuery: { @@ -290,7 +293,7 @@ describe('pushEventViewToLocation', function () { }, }); - expect(browserHistory.push).toHaveBeenCalledWith( + expect(navigate).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ id: '1234', diff --git a/static/app/views/discover/utils.tsx b/static/app/views/discover/utils.tsx index 2ff8263c799660..a8b9ebfa3f01bf 100644 --- a/static/app/views/discover/utils.tsx +++ b/static/app/views/discover/utils.tsx @@ -15,7 +15,6 @@ import type { OrganizationSummary, } from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; -import {browserHistory} from 'sentry/utils/browserHistory'; import {getUtcDateString} from 'sentry/utils/dates'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; import type {EventData} from 'sentry/utils/discover/eventView'; @@ -48,6 +47,7 @@ import {getTitle} from 'sentry/utils/events'; import {DISCOVER_FIELDS, FieldValueType, getFieldDefinition} from 'sentry/utils/fields'; import localStorage from 'sentry/utils/localStorage'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import type {ReactRouter3Navigate} from 'sentry/utils/useNavigate'; import { DashboardWidgetSource, @@ -124,14 +124,15 @@ export function decodeColumnOrder(fields: Readonly): TableColumn Date: Fri, 27 Dec 2024 16:21:55 -0500 Subject: [PATCH 511/757] test(js): Remove browserHistory from relocation.spec (#82595) --- .../app/views/relocation/relocation.spec.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/static/app/views/relocation/relocation.spec.tsx b/static/app/views/relocation/relocation.spec.tsx index 5f354a53745799..8852e5e941f73a 100644 --- a/static/app/views/relocation/relocation.spec.tsx +++ b/static/app/views/relocation/relocation.spec.tsx @@ -9,7 +9,7 @@ import { import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import ConfigStore from 'sentry/stores/configStore'; -import {browserHistory} from 'sentry/utils/browserHistory'; +import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import Relocation from 'sentry/views/relocation/relocation'; jest.mock('sentry/actionCreators/indicator'); @@ -38,6 +38,7 @@ const fakeRegions: {[key: string]: FakeRegion} = { }; describe('Relocation', function () { + let router: InjectedRouter; let fetchExistingRelocations: jest.Mock; let fetchPublicKeys: jest.Mock; @@ -86,11 +87,12 @@ describe('Relocation', function () { step, }; - const {router, routerProps, organization} = initializeOrg({ + const {routerProps, organization, ...rest} = initializeOrg({ router: { params: routeParams, }, }); + router = rest.router; return render(, { router, @@ -144,7 +146,7 @@ describe('Relocation', function () { await waitFor(() => expect(fetchExistingRelocations).toHaveBeenCalledTimes(2)); await waitFor(() => expect(fetchPublicKeys).toHaveBeenCalledTimes(2)); - expect(browserHistory.push).toHaveBeenCalledWith('/relocation/in-progress/'); + expect(router.push).toHaveBeenCalledWith('/relocation/in-progress/'); }); it('should prevent user from going to the next step if no org slugs or region are entered', async function () { @@ -365,7 +367,7 @@ describe('Relocation', function () { await waitFor(() => expect(fetchExistingRelocations).toHaveBeenCalledTimes(2)); await waitFor(() => expect(fetchPublicKeys).toHaveBeenCalledTimes(2)); - expect(browserHistory.push).toHaveBeenCalledWith('/relocation/get-started/'); + expect(router.push).toHaveBeenCalledWith('/relocation/get-started/'); }); }); @@ -394,7 +396,7 @@ describe('Relocation', function () { await waitFor(() => expect(fetchExistingRelocations).toHaveBeenCalledTimes(2)); await waitFor(() => expect(fetchPublicKeys).toHaveBeenCalledTimes(2)); - expect(browserHistory.push).toHaveBeenCalledWith('/relocation/get-started/'); + expect(router.push).toHaveBeenCalledWith('/relocation/get-started/'); }); }); @@ -591,7 +593,7 @@ describe('Relocation', function () { await waitFor(() => expect(fetchExistingRelocations).toHaveBeenCalledTimes(2)); await waitFor(() => expect(fetchPublicKeys).toHaveBeenCalledTimes(2)); - expect(browserHistory.push).toHaveBeenCalledWith('/relocation/get-started/'); + expect(router.push).toHaveBeenCalledWith('/relocation/get-started/'); }); }); @@ -625,7 +627,7 @@ describe('Relocation', function () { await waitFor(() => expect(fetchExistingRelocations).toHaveBeenCalledTimes(2)); await waitFor(() => expect(fetchPublicKeys).toHaveBeenCalledTimes(2)); - expect(browserHistory.push).toHaveBeenCalledWith('/relocation/get-started/'); + expect(router.push).toHaveBeenCalledWith('/relocation/get-started/'); }); it('redirects to `get-started` page if there is no active relocation', async function () { @@ -650,7 +652,7 @@ describe('Relocation', function () { await waitFor(() => expect(fetchExistingRelocations).toHaveBeenCalledTimes(2)); await waitFor(() => expect(fetchPublicKeys).toHaveBeenCalledTimes(2)); - expect(browserHistory.push).toHaveBeenCalledWith('/relocation/get-started/'); + expect(router.push).toHaveBeenCalledWith('/relocation/get-started/'); }); }); }); From 1710c08991c9f4000c1344c6ba6b8432e0f7b2ad Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 27 Dec 2024 17:15:21 -0500 Subject: [PATCH 512/757] chore(explore): Add tag for visiting explore (#82632) --- static/app/views/explore/content.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static/app/views/explore/content.tsx b/static/app/views/explore/content.tsx index d574f59eff9ccc..5aa80505da6909 100644 --- a/static/app/views/explore/content.tsx +++ b/static/app/views/explore/content.tsx @@ -1,5 +1,6 @@ import {useCallback, useState} from 'react'; import styled from '@emotion/styled'; +import * as Sentry from '@sentry/react'; import type {Location} from 'history'; import Feature from 'sentry/components/acl/feature'; @@ -196,6 +197,8 @@ function ExploreTagsProvider({children}) { } export function ExploreContent(props: ExploreContentProps) { + Sentry.setTag('explore.visited', 'yes'); + return ( From 29ca691148c15c271a7a37cf8e88d44235dc72a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vjeran=20Grozdani=C4=87?= Date: Mon, 30 Dec 2024 09:21:36 +0100 Subject: [PATCH 513/757] feat(tempest): UI for tempest screenshots project option (#82522) UI form for managing setting if Tempest should fetch screenshots image API part: https://github.com/getsentry/sentry/pull/82520 Part of https://github.com/getsentry/team-gdx/issues/53 --------- Signed-off-by: Vjeran Grozdanic --- .../views/settings/project/tempest/index.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/static/app/views/settings/project/tempest/index.tsx b/static/app/views/settings/project/tempest/index.tsx index c827715c7f9169..6df345f849c991 100644 --- a/static/app/views/settings/project/tempest/index.tsx +++ b/static/app/views/settings/project/tempest/index.tsx @@ -3,6 +3,8 @@ import {Fragment} from 'react'; import {openAddTempestCredentialsModal} from 'sentry/actionCreators/modal'; import Alert from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; +import Form from 'sentry/components/forms/form'; +import JsonForm from 'sentry/components/forms/jsonForm'; import {PanelTable} from 'sentry/components/panels/panelTable'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {Tooltip} from 'sentry/components/tooltip'; @@ -40,6 +42,32 @@ export default function TempestSettings({organization, project}: Props) { action={addNewCredentials(hasWriteAccess, organization, project)} /> +
      + + + Date: Mon, 30 Dec 2024 09:10:09 -0500 Subject: [PATCH 514/757] ref: delete unused MetricsKeyIndexer model pt 2 (#82556) --- migrations_lockfile.txt | 2 +- .../0804_delete_metrics_key_indexer_pt2.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/sentry/migrations/0804_delete_metrics_key_indexer_pt2.py diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 68a5f78f741f7f..8110b79486af6a 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -15,7 +15,7 @@ remote_subscriptions: 0003_drop_remote_subscription replays: 0004_index_together -sentry: 0803_delete_unused_metricskeyindexer_pt1 +sentry: 0804_delete_metrics_key_indexer_pt2 social_auth: 0002_default_auto_field diff --git a/src/sentry/migrations/0804_delete_metrics_key_indexer_pt2.py b/src/sentry/migrations/0804_delete_metrics_key_indexer_pt2.py new file mode 100644 index 00000000000000..c863a8ca7f503e --- /dev/null +++ b/src/sentry/migrations/0804_delete_metrics_key_indexer_pt2.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.4 on 2024-12-26 14:38 + +from sentry.new_migrations.migrations import CheckedMigration +from sentry.new_migrations.monkey.models import SafeDeleteModel +from sentry.new_migrations.monkey.state import DeletionAction + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("sentry", "0803_delete_unused_metricskeyindexer_pt1"), + ] + + operations = [SafeDeleteModel(name="MetricsKeyIndexer", deletion_action=DeletionAction.DELETE)] From 12c6622fe42428e8727a23373e3a527f3683025e Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:05:44 -0500 Subject: [PATCH 515/757] chore(insights): Removes unused backend webvitals functions (#82486) The sentry frontend no longer uses the `weighted_performance_score` function. Removes the function from the sentry backend in the discover and metrics datasets --- src/sentry/search/events/datasets/discover.py | 62 +------- src/sentry/search/events/datasets/metrics.py | 129 ---------------- .../api/endpoints/test_organization_events.py | 42 ----- .../endpoints/test_organization_events_mep.py | 143 ------------------ 4 files changed, 1 insertion(+), 375 deletions(-) diff --git a/src/sentry/search/events/datasets/discover.py b/src/sentry/search/events/datasets/discover.py index 9178ae61de05cd..dafc83183440df 100644 --- a/src/sentry/search/events/datasets/discover.py +++ b/src/sentry/search/events/datasets/discover.py @@ -993,14 +993,6 @@ def function_converter(self) -> Mapping[str, SnQLFunction]: snql_aggregate=self._resolve_web_vital_score_function, default_result_type="number", ), - SnQLFunction( - "weighted_performance_score", - required_args=[ - NumericColumn("column"), - ], - snql_aggregate=self._resolve_weighted_web_vital_score_function, - default_result_type="number", - ), SnQLFunction( "opportunity_score", required_args=[ @@ -1710,58 +1702,6 @@ def _resolve_web_vital_score_function( alias, ) - def _resolve_weighted_web_vital_score_function( - self, - args: Mapping[str, Column], - alias: str, - ) -> SelectType: - column = args["column"] - if column.key not in [ - "score.lcp", - "score.fcp", - "score.fid", - "score.cls", - "score.ttfb", - ]: - raise InvalidSearchQuery( - "weighted_performance_score only supports performance score measurements" - ) - total_score_column = self.builder.column("measurements.score.total") - return Function( - "greatest", - [ - Function( - "least", - [ - Function( - "divide", - [ - Function( - "sum", - [column], - ), - Function( - "countIf", - [ - Function( - "greaterOrEquals", - [ - total_score_column, - 0, - ], - ) - ], - ), - ], - ), - 1.0, - ], - ), - 0.0, - ], - alias, - ) - def _resolve_web_vital_opportunity_score_function( self, args: Mapping[str, Column], @@ -1777,7 +1717,7 @@ def _resolve_web_vital_opportunity_score_function( "score.total", ]: raise InvalidSearchQuery( - "weighted_performance_score only supports performance score measurements" + "opportunity_score only supports performance score measurements" ) weight_column = ( diff --git a/src/sentry/search/events/datasets/metrics.py b/src/sentry/search/events/datasets/metrics.py index f071a2a041cd21..ee851f009946de 100644 --- a/src/sentry/search/events/datasets/metrics.py +++ b/src/sentry/search/events/datasets/metrics.py @@ -643,26 +643,6 @@ def function_converter(self) -> Mapping[str, fields.MetricsFunction]: snql_distribution=self._resolve_web_vital_score_function, default_result_type="number", ), - fields.MetricsFunction( - "weighted_performance_score", - required_args=[ - fields.MetricArg( - "column", - allowed_columns=[ - "measurements.score.fcp", - "measurements.score.lcp", - "measurements.score.fid", - "measurements.score.inp", - "measurements.score.cls", - "measurements.score.ttfb", - ], - allow_custom_measurements=False, - ) - ], - calculated_args=[resolve_metric_id], - snql_distribution=self._resolve_weighted_web_vital_score_function, - default_result_type="number", - ), fields.MetricsFunction( "opportunity_score", required_args=[ @@ -1625,115 +1605,6 @@ def _resolve_web_vital_score_function( alias, ) - def _resolve_weighted_web_vital_score_function( - self, - args: Mapping[str, str | Column | SelectType | int | float], - alias: str, - ) -> SelectType: - column = args["column"] - metric_id = args["metric_id"] - - if column not in [ - "measurements.score.lcp", - "measurements.score.fcp", - "measurements.score.fid", - "measurements.score.inp", - "measurements.score.cls", - "measurements.score.ttfb", - ]: - raise InvalidSearchQuery("performance_score only supports measurements") - - return Function( - "greatest", - [ - Function( - "least", - [ - Function( - "if", - [ - Function( - "and", - [ - Function( - "greater", - [ - Function( - "sumIf", - [ - Column("value"), - Function( - "equals", - [Column("metric_id"), metric_id], - ), - ], - ), - 0, - ], - ), - Function( - "greater", - [ - Function( - "countIf", - [ - Column("value"), - Function( - "equals", - [ - Column("metric_id"), - self.resolve_metric( - "measurements.score.total" - ), - ], - ), - ], - ), - 0, - ], - ), - ], - ), - Function( - "divide", - [ - Function( - "sumIf", - [ - Column("value"), - Function( - "equals", [Column("metric_id"), metric_id] - ), - ], - ), - Function( - "countIf", - [ - Column("value"), - Function( - "equals", - [ - Column("metric_id"), - self.resolve_metric( - "measurements.score.total" - ), - ], - ), - ], - ), - ], - ), - 0.0, - ], - ), - 1.0, - ], - ), - 0.0, - ], - alias, - ) - def _resolve_web_vital_opportunity_score_function( self, args: Mapping[str, str | Column | SelectType | int | float], diff --git a/tests/snuba/api/endpoints/test_organization_events.py b/tests/snuba/api/endpoints/test_organization_events.py index 233ebbb697caa7..8eb9c68151b429 100644 --- a/tests/snuba/api/endpoints/test_organization_events.py +++ b/tests/snuba/api/endpoints/test_organization_events.py @@ -6627,35 +6627,6 @@ def test_performance_score(self): "performance_score(measurements.score.lcp)": 0.7923076923076923, } - def test_weighted_performance_score(self): - self.transaction_data["measurements"] = { - "score.lcp": {"value": 0.03}, - "score.weight.lcp": {"value": 0.3}, - "score.total": {"value": 0.03}, - } - self.store_event(self.transaction_data, self.project.id) - self.transaction_data["measurements"] = { - "score.lcp": {"value": 1.0}, - "score.weight.lcp": {"value": 1.0}, - "score.total": {"value": 1.0}, - } - self.store_event(self.transaction_data, self.project.id) - self.transaction_data["measurements"] = { - "score.total": {"value": 0.0}, - } - self.store_event(self.transaction_data, self.project.id) - query = { - "field": [ - "weighted_performance_score(measurements.score.lcp)", - ] - } - response = self.do_request(query) - assert response.status_code == 200, response.content - assert len(response.data["data"]) == 1 - assert response.data["data"][0] == { - "weighted_performance_score(measurements.score.lcp)": 0.3433333333333333, - } - def test_invalid_performance_score_column(self): self.transaction_data["measurements"] = { "score.total": {"value": 0.0}, @@ -6669,19 +6640,6 @@ def test_invalid_performance_score_column(self): response = self.do_request(query) assert response.status_code == 400, response.content - def test_invalid_weighted_performance_score_column(self): - self.transaction_data["measurements"] = { - "score.total": {"value": 0.0}, - } - self.store_event(self.transaction_data, self.project.id) - query = { - "field": [ - "weighted_performance_score(measurements.score.fp)", - ] - } - response = self.do_request(query) - assert response.status_code == 400, response.content - def test_all_events_fields(self): user_data = { "id": self.user.id, diff --git a/tests/snuba/api/endpoints/test_organization_events_mep.py b/tests/snuba/api/endpoints/test_organization_events_mep.py index 4e77c88c21394f..c87441e06a22dc 100644 --- a/tests/snuba/api/endpoints/test_organization_events_mep.py +++ b/tests/snuba/api/endpoints/test_organization_events_mep.py @@ -2815,87 +2815,6 @@ def test_performance_score_boundaries(self): assert meta["isMetricsData"] assert field_meta["performance_score(measurements.score.ttfb)"] == "number" - def test_weighted_performance_score(self): - self.store_transaction_metric( - 0.03, - metric="measurements.score.ttfb", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - self.store_transaction_metric( - 0.30, - metric="measurements.score.weight.ttfb", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - self.store_transaction_metric( - 0.03, - metric="measurements.score.total", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - - self.store_transaction_metric( - 1.00, - metric="measurements.score.ttfb", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - self.store_transaction_metric( - 1.00, - metric="measurements.score.weight.ttfb", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - self.store_transaction_metric( - 1.00, - metric="measurements.score.total", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - - self.store_transaction_metric( - 0.80, - metric="measurements.score.inp", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - self.store_transaction_metric( - 1.00, - metric="measurements.score.weight.inp", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - self.store_transaction_metric( - 0.80, - metric="measurements.score.total", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - - response = self.do_request( - { - "field": [ - "transaction", - "weighted_performance_score(measurements.score.ttfb)", - "weighted_performance_score(measurements.score.inp)", - ], - "query": "event.type:transaction", - "dataset": "metrics", - "per_page": 50, - } - ) - assert response.status_code == 200, response.content - assert len(response.data["data"]) == 1 - data = response.data["data"] - meta = response.data["meta"] - field_meta = meta["fields"] - - assert data[0]["weighted_performance_score(measurements.score.ttfb)"] == 0.3433333333333333 - assert data[0]["weighted_performance_score(measurements.score.inp)"] == 0.26666666666666666 - assert meta["isMetricsData"] - assert field_meta["weighted_performance_score(measurements.score.ttfb)"] == "number" - def test_invalid_performance_score_column(self): self.store_transaction_metric( 0.03, @@ -2917,56 +2836,6 @@ def test_invalid_performance_score_column(self): ) assert response.status_code == 400, response.content - def test_invalid_weighted_performance_score_column(self): - self.store_transaction_metric( - 0.03, - metric="measurements.score.total", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - - response = self.do_request( - { - "field": [ - "transaction", - "weighted_performance_score(measurements.score.fp)", - ], - "query": "event.type:transaction", - "dataset": "metrics", - "per_page": 50, - } - ) - assert response.status_code == 400, response.content - - def test_no_weighted_performance_score_column(self): - self.store_transaction_metric( - 0.0, - metric="measurements.score.ttfb", - tags={"transaction": "foo_transaction"}, - timestamp=self.min_ago, - ) - response = self.do_request( - { - "field": [ - "transaction", - "weighted_performance_score(measurements.score.ttfb)", - ], - "query": "event.type:transaction", - "dataset": "metrics", - "per_page": 50, - } - ) - - assert response.status_code == 200, response.content - assert len(response.data["data"]) == 1 - data = response.data["data"] - meta = response.data["meta"] - field_meta = meta["fields"] - - assert data[0]["weighted_performance_score(measurements.score.ttfb)"] == 0.0 - assert meta["isMetricsData"] - assert field_meta["weighted_performance_score(measurements.score.ttfb)"] == "number" - def test_opportunity_score(self): self.store_transaction_metric( 0.03, @@ -4161,22 +4030,10 @@ def test_performance_score_boundaries(self): def test_total_performance_score(self): super().test_total_performance_score() - @pytest.mark.xfail(reason="Not implemented") - def test_weighted_performance_score(self): - super().test_weighted_performance_score() - @pytest.mark.xfail(reason="Not implemented") def test_invalid_performance_score_column(self): super().test_invalid_performance_score_column() - @pytest.mark.xfail(reason="Not implemented") - def test_invalid_weighted_performance_score_column(self): - super().test_invalid_weighted_performance_score_column() - - @pytest.mark.xfail(reason="Not implemented") - def test_no_weighted_performance_score_column(self): - super().test_invalid_weighted_performance_score_column() - @pytest.mark.xfail(reason="Not implemented") def test_opportunity_score(self): super().test_opportunity_score() From 40077d399d816dc68aca18d39eec3e3b885e22b3 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:23:20 -0500 Subject: [PATCH 516/757] ref: improve typing of OperationsConfiguration (#82386) --- .../querying/metadata/metrics.py | 10 ++---- .../sentry_metrics/querying/metadata/utils.py | 15 ++++++--- src/sentry/snuba/metrics/naming_layer/mri.py | 32 +++++++++---------- src/sentry/snuba/metrics/utils.py | 4 +-- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/sentry/sentry_metrics/querying/metadata/metrics.py b/src/sentry/sentry_metrics/querying/metadata/metrics.py index e09e828ca2572a..09ba8d840968ca 100644 --- a/src/sentry/sentry_metrics/querying/metadata/metrics.py +++ b/src/sentry/sentry_metrics/querying/metadata/metrics.py @@ -18,13 +18,7 @@ from sentry.snuba.metrics import parse_mri from sentry.snuba.metrics.datasource import get_metrics_blocking_state_of_projects from sentry.snuba.metrics.naming_layer.mri import ParsedMRI, get_available_operations -from sentry.snuba.metrics.utils import ( - BlockedMetric, - MetricMeta, - MetricOperationType, - MetricType, - MetricUnit, -) +from sentry.snuba.metrics.utils import BlockedMetric, MetricMeta, MetricType, MetricUnit from sentry.snuba.metrics_layer.query import fetch_metric_mris @@ -152,7 +146,7 @@ def _build_metric_meta( name=parsed_mri.name, unit=cast(MetricUnit, parsed_mri.unit), mri=parsed_mri.mri_string, - operations=cast(Sequence[MetricOperationType], available_operations), + operations=available_operations, projectIds=project_ids, blockingStatus=blocking_status, ) diff --git a/src/sentry/sentry_metrics/querying/metadata/utils.py b/src/sentry/sentry_metrics/querying/metadata/utils.py index 6b9c8941647389..2a3e5520a374e4 100644 --- a/src/sentry/sentry_metrics/querying/metadata/utils.py +++ b/src/sentry/sentry_metrics/querying/metadata/utils.py @@ -1,7 +1,10 @@ +from __future__ import annotations + from sentry.snuba.metrics import get_mri from sentry.snuba.metrics.naming_layer.mri import is_mri +from sentry.snuba.metrics.utils import MetricOperationType -METRICS_API_HIDDEN_OPERATIONS = { +METRICS_API_HIDDEN_OPERATIONS: dict[str, list[MetricOperationType]] = { "sentry:metrics_activate_percentiles": [ "p50", "p75", @@ -12,17 +15,21 @@ "sentry:metrics_activate_last_for_gauges": ["last"], } -NON_QUERYABLE_METRIC_OPERATIONS = ["histogram", "min_timestamp", "max_timestamp"] +NON_QUERYABLE_METRIC_OPERATIONS: list[MetricOperationType] = [ + "histogram", + "min_timestamp", + "max_timestamp", +] class OperationsConfiguration: def __init__(self): self.hidden_operations = set() - def hide_operations(self, operations: list[str]) -> None: + def hide_operations(self, operations: list[MetricOperationType]) -> None: self.hidden_operations.update(operations) - def get_hidden_operations(self): + def get_hidden_operations(self) -> list[MetricOperationType]: return list(self.hidden_operations) diff --git a/src/sentry/snuba/metrics/naming_layer/mri.py b/src/sentry/snuba/metrics/naming_layer/mri.py index 49d7070d882361..284a1e34d3daa3 100644 --- a/src/sentry/snuba/metrics/naming_layer/mri.py +++ b/src/sentry/snuba/metrics/naming_layer/mri.py @@ -33,7 +33,6 @@ ) import re -from collections.abc import Sequence from dataclasses import dataclass from enum import Enum from typing import cast @@ -42,12 +41,12 @@ from sentry.exceptions import InvalidParams from sentry.sentry_metrics.use_case_id_registry import UseCaseID -from sentry.snuba.dataset import EntityKey from sentry.snuba.metrics.units import format_value_using_unit_and_op from sentry.snuba.metrics.utils import ( AVAILABLE_GENERIC_OPERATIONS, AVAILABLE_OPERATIONS, OP_REGEX, + MetricEntity, MetricOperationType, MetricUnit, ) @@ -335,28 +334,27 @@ def is_custom_measurement(parsed_mri: ParsedMRI) -> bool: ) -def get_entity_key_from_entity_type(entity_type: str, generic_metrics: bool) -> EntityKey: - entity_name_suffixes = { - "c": "counters", - "s": "sets", - "d": "distributions", - "g": "gauges", - } - - if generic_metrics: - return EntityKey(f"generic_metrics_{entity_name_suffixes[entity_type]}") - else: - return EntityKey(f"metrics_{entity_name_suffixes[entity_type]}") +_ENTITY_KEY_MAPPING_GENERIC: dict[str, MetricEntity] = { + "c": "generic_metrics_counters", + "s": "generic_metrics_sets", + "d": "generic_metrics_distributions", + "g": "generic_metrics_gauges", +} +_ENTITY_KEY_MAPPING_NON_GENERIC: dict[str, MetricEntity] = { + "c": "metrics_counters", + "s": "metrics_sets", + "d": "metrics_distributions", +} -def get_available_operations(parsed_mri: ParsedMRI) -> Sequence[str]: +def get_available_operations(parsed_mri: ParsedMRI) -> list[MetricOperationType]: if parsed_mri.entity == "e": return [] elif parsed_mri.namespace == "sessions": - entity_key = get_entity_key_from_entity_type(parsed_mri.entity, False).value + entity_key = _ENTITY_KEY_MAPPING_NON_GENERIC[parsed_mri.entity] return AVAILABLE_OPERATIONS[entity_key] else: - entity_key = get_entity_key_from_entity_type(parsed_mri.entity, True).value + entity_key = _ENTITY_KEY_MAPPING_GENERIC[parsed_mri.entity] return AVAILABLE_GENERIC_OPERATIONS[entity_key] diff --git a/src/sentry/snuba/metrics/utils.py b/src/sentry/snuba/metrics/utils.py index 3655b538494684..38af21944437ee 100644 --- a/src/sentry/snuba/metrics/utils.py +++ b/src/sentry/snuba/metrics/utils.py @@ -141,7 +141,7 @@ "generic_metrics_gauges", ] -OP_TO_SNUBA_FUNCTION = { +OP_TO_SNUBA_FUNCTION: dict[MetricEntity, dict[MetricOperationType, str]] = { "metrics_counters": { "sum": "sumIf", "min_timestamp": "minIf", @@ -169,7 +169,7 @@ "max_timestamp": "maxIf", }, } -GENERIC_OP_TO_SNUBA_FUNCTION = { +GENERIC_OP_TO_SNUBA_FUNCTION: dict[MetricEntity, dict[MetricOperationType, str]] = { "generic_metrics_counters": OP_TO_SNUBA_FUNCTION["metrics_counters"], "generic_metrics_distributions": OP_TO_SNUBA_FUNCTION["metrics_distributions"], "generic_metrics_sets": OP_TO_SNUBA_FUNCTION["metrics_sets"], From 21c25382f4f5894429720aff9843810c6f4dc477 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:23:33 -0500 Subject: [PATCH 517/757] ref: remove unused projects:minidump feature (#82647) this feature was launched long ago: 91bbe216e9be4575fc321230bd73b73ec5186acc --- src/sentry/features/temporary.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 2bd72c5c5085c1..c46aa8b35c5f56 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -564,9 +564,6 @@ def register_temporary_features(manager: FeatureManager): manager.add("projects:first-event-severity-calculation", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False) # Enable escalation detection for new issues manager.add("projects:first-event-severity-new-escalation", ProjectFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=False) - # Enable functionality for attaching minidumps to events and displaying - # them in the group UI. - manager.add("projects:minidump", ProjectFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=True) # Enable alternative version of group creation that is supposed to be less racy. manager.add("projects:race-free-group-creation", ProjectFeature, FeatureHandlerStrategy.INTERNAL, default=True, api_expose=False) # Enable similarity embeddings API call From 556d0745c4a17c207dd7c052b28f93283b0d7e05 Mon Sep 17 00:00:00 2001 From: "Armen Zambrano G." <44410+armenzg@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:26:56 -0500 Subject: [PATCH 518/757] chore(escalating): Run once a day (#82473) The original design was to run this once a week. Six hours was a temporary step while doing development. Let's move it to only once daily and see if anything degrades. This will reduce unnecessary load on Snuba calls. --- src/sentry/conf/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index ca5144bbd0527c..2cda92b1f512c8 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -1232,8 +1232,8 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: }, "weekly-escalating-forecast": { "task": "sentry.tasks.weekly_escalating_forecast.run_escalating_forecast", - # Run every 6 hours - "schedule": crontab(minute="0", hour="*/6"), + # Run once a day at 00:00 + "schedule": crontab(minute="0", hour="0"), "options": {"expires": 60 * 60 * 3}, }, "schedule_auto_transition_to_ongoing": { From cc70ff1e50c19478a6dc9d3552bc32a6df3c2c66 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Mon, 30 Dec 2024 11:57:22 -0500 Subject: [PATCH 519/757] chore: Add few more allowed referrers to events endpoint (#82597) Co-authored-by: George Gritsouk <989898+gggritso@users.noreply.github.com> --- src/sentry/api/endpoints/organization_events.py | 5 ++++- src/sentry/snuba/referrer.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sentry/api/endpoints/organization_events.py b/src/sentry/api/endpoints/organization_events.py index 74752d7f9505ed..0e7952cc8318de 100644 --- a/src/sentry/api/endpoints/organization_events.py +++ b/src/sentry/api/endpoints/organization_events.py @@ -62,6 +62,7 @@ class DiscoverDatasetSplitException(Exception): Referrer.API_DASHBOARDS_BIGNUMBERWIDGET.value, Referrer.API_DISCOVER_TRANSACTIONS_LIST.value, Referrer.API_DISCOVER_QUERY_TABLE.value, + Referrer.API_INSIGHTS_USER_GEO_SUBREGION_SELECTOR.value, Referrer.API_PERFORMANCE_BROWSER_RESOURCE_MAIN_TABLE.value, Referrer.API_PERFORMANCE_BROWSER_RESOURCES_PAGE_SELECTOR.value, Referrer.API_PERFORMANCE_BROWSER_WEB_VITALS_PROJECT.value, @@ -167,7 +168,9 @@ class DiscoverDatasetSplitException(Exception): Referrer.API_PERFORMANCE_MOBILE_UI_METRICS_RIBBON.value, Referrer.API_PERFORMANCE_SPAN_SUMMARY_HEADER_DATA.value, Referrer.API_PERFORMANCE_SPAN_SUMMARY_TABLE.value, - Referrer.API_EXPLORE_SPANS_SAMPLES_TABLE, + Referrer.API_EXPLORE_SPANS_SAMPLES_TABLE.value, + Referrer.ISSUE_DETAILS_STREAMLINE_GRAPH.value, + Referrer.ISSUE_DETAILS_STREAMLINE_LIST.value, } API_TOKEN_REFERRER = Referrer.API_AUTH_TOKEN_EVENTS.value diff --git a/src/sentry/snuba/referrer.py b/src/sentry/snuba/referrer.py index 667d39232b72dc..a4f1cf20319c04 100644 --- a/src/sentry/snuba/referrer.py +++ b/src/sentry/snuba/referrer.py @@ -107,6 +107,7 @@ class Referrer(Enum): ) API_GROUP_HASHES_LEVELS_GET_LEVELS_OVERVIEW = "api.group_hashes_levels.get_levels_overview" API_GROUP_HASHES = "api.group-hashes" + API_INSIGHTS_USER_GEO_SUBREGION_SELECTOR = "api.insights.user-geo-subregion-selector" API_ISSUES_ISSUE_EVENTS = "api.issues.issue_events" API_ISSUES_RELATED_ISSUES = "api.issues.related_issues" API_METRICS_TOTALS = "api.metrics.totals" @@ -716,6 +717,8 @@ class Referrer(Enum): INCIDENTS_GET_INCIDENT_AGGREGATES_PRIMARY = "incidents.get_incident_aggregates.primary" INCIDENTS_GET_INCIDENT_AGGREGATES = "incidents.get_incident_aggregates" IS_ESCALATING_GROUP = "sentry.issues.escalating.is_escalating" + ISSUE_DETAILS_STREAMLINE_GRAPH = "issue_details.streamline_graph" + ISSUE_DETAILS_STREAMLINE_LIST = "issue_details.streamline_list" METRIC_EXTRACTION_CARDINALITY_CHECK = "metric_extraction.cardinality_check" OUTCOMES_TIMESERIES = "outcomes.timeseries" OUTCOMES_TOTALS = "outcomes.totals" From 941888b40bbf9185b00ee031ffd7f6cb28db3a24 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:12:10 -0500 Subject: [PATCH 520/757] feat(dashboards): Pass `AreaChart` series meta alongside the data (#82653) Exactly the same thing as https://github.com/getsentry/sentry/pull/82047 but for `AreaChartWidget` --- .../areaChartWidget/areaChartWidget.spec.tsx | 8 ----- .../areaChartWidget.stories.tsx | 30 ------------------- .../areaChartWidget/areaChartWidget.tsx | 1 - .../areaChartWidgetVisualization.tsx | 8 ++--- .../sampleLatencyTimeSeries.json | 8 +++++ .../sampleSpanDurationTimeSeries.json | 8 +++++ 6 files changed, 19 insertions(+), 44 deletions(-) diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx index b17340231ad56a..9d84891002ff77 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx @@ -12,14 +12,6 @@ describe('AreaChartWidget', () => { title="eps()" description="Number of events per second" timeseries={[sampleLatencyTimeSeries, sampleSpanDurationTimeSeries]} - meta={{ - fields: { - 'eps()': 'rate', - }, - units: { - 'eps()': '1/second', - }, - }} /> ); }); diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx index 744d6f7d0e2793..5cf50f27a14d95 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx @@ -66,16 +66,6 @@ export default storyBook(AreaChartWidget, story => { title="Duration Breakdown" description="Explains what proportion of total duration is taken up by latency vs. span duration" timeseries={[latencyTimeSeries, spanDurationTimeSeries]} - meta={{ - fields: { - 'avg(latency)': 'duration', - 'avg(span.duration)': 'duration', - }, - units: { - 'avg(latency)': 'millisecond', - 'avg(span.duration)': 'millisecond', - }, - }} /> @@ -135,16 +125,6 @@ export default storyBook(AreaChartWidget, story => { {...sampleSpanDurationTimeSeries, color: theme.warning}, ]} - meta={{ - fields: { - 'avg(latency)': 'duration', - 'avg(span.duration)': 'duration', - }, - units: { - 'avg(latency)': 'millisecond', - 'avg(span.duration)': 'millisecond', - }, - }} /> @@ -175,16 +155,6 @@ export default storyBook(AreaChartWidget, story => { diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx index 71d3b76c4f4826..abc1f1b60eebc1 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx @@ -55,7 +55,6 @@ export function AreaChartWidget(props: AreaChartWidgetProps) { )} diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx index 8c33bc93879f6f..6b775819bff0be 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx @@ -23,11 +23,10 @@ import {useWidgetSyncContext} from '../../contexts/widgetSyncContext'; import {formatTooltipValue} from '../common/formatTooltipValue'; import {formatYAxisValue} from '../common/formatYAxisValue'; import {ReleaseSeries} from '../common/releaseSeries'; -import type {Meta, Release, TimeseriesData} from '../common/types'; +import type {Release, TimeseriesData} from '../common/types'; export interface AreaChartWidgetVisualizationProps { timeseries: TimeseriesData[]; - meta?: Meta; releases?: Release[]; } @@ -37,7 +36,6 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization const pageFilters = usePageFilters(); const {start, end, period, utc} = pageFilters.selection.datetime; - const {meta} = props; const theme = useTheme(); const organization = useOrganization(); @@ -69,8 +67,8 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization // TODO: Raise error if attempting to plot series of different types or units const firstSeriesField = firstSeries?.field; - const type = meta?.fields?.[firstSeriesField] ?? 'number'; - const unit = meta?.units?.[firstSeriesField] ?? undefined; + const type = firstSeries?.meta?.fields?.[firstSeriesField] ?? 'number'; + const unit = firstSeries?.meta?.units?.[firstSeriesField] ?? undefined; const formatter: TooltipFormatterCallback = ( params, diff --git a/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json b/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json index a6fd6ffaee59e2..93a6f8851acdd0 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json +++ b/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json @@ -1,5 +1,13 @@ { "field": "avg(latency)", + "meta": { + "fields": { + "avg(latency)": "duration" + }, + "units": { + "avg(latency)": "millisecond" + } + }, "data": [ { "timestamp": "2024-12-09T22:00:00Z", diff --git a/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json b/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json index 815c94d3eeac81..278ec199649563 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json +++ b/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json @@ -1,5 +1,13 @@ { "field": "avg(span.duration)", + "meta": { + "fields": { + "avg(span.duration)": "duration" + }, + "units": { + "avg(span.duration)": "millisecond" + } + }, "data": [ { "timestamp": "2024-12-09T22:00:00Z", From bcb9d9d444652d93ff6fcf30b425195fc5fe41cb Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:12:49 -0500 Subject: [PATCH 521/757] fix(insights): update to handle when there are no value segments provided to the score ring (#82649) Slight update to PerformanceScoreRing to better handle when no segments/values are passed to the component. --- .../components/performanceScoreRing.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/static/app/views/insights/browser/webVitals/components/performanceScoreRing.tsx b/static/app/views/insights/browser/webVitals/components/performanceScoreRing.tsx index 765cdd70e3a7b6..c4cc513ebd7655 100644 --- a/static/app/views/insights/browser/webVitals/components/performanceScoreRing.tsx +++ b/static/app/views/insights/browser/webVitals/components/performanceScoreRing.tsx @@ -76,14 +76,17 @@ function PerformanceScoreRing({ const ringSegmentPadding = values.length > 1 ? PADDING : 0; // TODO: Hacky way to add padding to ring segments. Should clean this up so it's more accurate to the value. // This only mostly works because we expect values to be somewhere between 0 and 100. - const maxOffset = - (1 - Math.max(maxValue - ringSegmentPadding, 0) / sumMaxValues) * circumference; - const progressOffset = - (1 - Math.max(boundedValue - ringSegmentPadding, 0) / sumMaxValues) * - circumference; + const maxOffset = sumMaxValues + ? (1 - Math.max(maxValue - ringSegmentPadding, 0) / sumMaxValues) * circumference + : 0; + const progressOffset = sumMaxValues + ? (1 - Math.max(boundedValue - ringSegmentPadding, 0) / sumMaxValues) * + circumference + : 0; const rotate = currentRotate; - currentRotate += (360 * maxValue) / sumMaxValues; - + if (sumMaxValues) { + currentRotate += (360 * maxValue) / sumMaxValues; + } const cx = radius + barWidth / 2; return [ From d179224c31127efdff003154e8d0b46a2e0d6f0f Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:15:28 -0500 Subject: [PATCH 522/757] chore(insights): Add and update types to web vitals code (#82648) More typing for webvitals code. Also removes `Weights` from `ProjectScore` type since it is no longer being used --- .../components/charts/performanceScoreChart.tsx | 2 +- .../webVitals/components/webVitalMeters.spec.tsx | 5 ----- .../calculatePerformanceScoreFromStored.tsx | 9 +-------- .../app/views/insights/browser/webVitals/types.tsx | 12 +----------- .../browser/webVitals/utils/scoreThresholds.tsx | 8 +++++--- 5 files changed, 8 insertions(+), 28 deletions(-) diff --git a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreChart.tsx b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreChart.tsx index 2786d92b71ecd5..2ef901f1d240a3 100644 --- a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreChart.tsx +++ b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreChart.tsx @@ -28,7 +28,7 @@ type Props = { webVital?: WebVitals | null; }; -export const ORDER = ['lcp', 'fcp', 'inp', 'cls', 'ttfb']; +export const ORDER: WebVitals[] = ['lcp', 'fcp', 'inp', 'cls', 'ttfb']; export function PerformanceScoreChart({ projectScore, diff --git a/static/app/views/insights/browser/webVitals/components/webVitalMeters.spec.tsx b/static/app/views/insights/browser/webVitals/components/webVitalMeters.spec.tsx index c8398c218cdfbe..f33de950326d46 100644 --- a/static/app/views/insights/browser/webVitals/components/webVitalMeters.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/webVitalMeters.spec.tsx @@ -14,11 +14,6 @@ jest.mock('sentry/utils/usePageFilters'); describe('WebVitalMeters', function () { const organization = OrganizationFixture(); const projectScore: ProjectScore = { - lcpWeight: 30, - fcpWeight: 20, - clsWeight: 15, - ttfbWeight: 10, - inpWeight: 10, lcpScore: 100, fcpScore: 100, clsScore: 100, diff --git a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/calculatePerformanceScoreFromStored.tsx b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/calculatePerformanceScoreFromStored.tsx index efc5ec6c65f91f..5c3c580a0ae59e 100644 --- a/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/calculatePerformanceScoreFromStored.tsx +++ b/static/app/views/insights/browser/webVitals/queries/storedScoreQueries/calculatePerformanceScoreFromStored.tsx @@ -3,7 +3,6 @@ import type { ProjectScore, WebVitals, } from 'sentry/views/insights/browser/webVitals/types'; -import {PERFORMANCE_SCORE_WEIGHTS} from 'sentry/views/insights/browser/webVitals/utils/scoreThresholds'; export const calculatePerformanceScoreFromStoredTableDataRow = ( data?: TableDataRow @@ -47,13 +46,7 @@ function hasWebVitalScore(data: TableDataRow, webVital: WebVitals): boolean { export function getWebVitalScores(data?: TableDataRow): ProjectScore { if (!data) { - return { - lcpWeight: PERFORMANCE_SCORE_WEIGHTS.lcp, - fcpWeight: PERFORMANCE_SCORE_WEIGHTS.fcp, - clsWeight: PERFORMANCE_SCORE_WEIGHTS.cls, - ttfbWeight: PERFORMANCE_SCORE_WEIGHTS.ttfb, - inpWeight: PERFORMANCE_SCORE_WEIGHTS.inp, - }; + return {}; } const hasLcp = hasWebVitalScore(data, 'lcp'); diff --git a/static/app/views/insights/browser/webVitals/types.tsx b/static/app/views/insights/browser/webVitals/types.tsx index 5c6069d6dc8c87..45bbe7c8f1cd63 100644 --- a/static/app/views/insights/browser/webVitals/types.tsx +++ b/static/app/views/insights/browser/webVitals/types.tsx @@ -40,8 +40,6 @@ type Score = { ttfbScore: number; }; -export type ScoreWithWeightsAndOpportunity = Score & Weight & Opportunity; - export type InteractionSpanSampleRow = { [SpanIndexedField.INP]: number; 'profile.id': string; @@ -58,19 +56,11 @@ export type InteractionSpanSampleRowWithScore = InteractionSpanSampleRow & { totalScore: number; }; -export type Weight = { - clsWeight: number; - fcpWeight: number; - inpWeight: number; - lcpWeight: number; - ttfbWeight: number; -}; - export type Opportunity = { opportunity: number; }; -export type ProjectScore = Partial & Weight; +export type ProjectScore = Partial; export type RowWithScoreAndOpportunity = Row & Score & Opportunity; diff --git a/static/app/views/insights/browser/webVitals/utils/scoreThresholds.tsx b/static/app/views/insights/browser/webVitals/utils/scoreThresholds.tsx index 6d40d25ab79007..c1f4dbec64eac7 100644 --- a/static/app/views/insights/browser/webVitals/utils/scoreThresholds.tsx +++ b/static/app/views/insights/browser/webVitals/utils/scoreThresholds.tsx @@ -1,4 +1,6 @@ -export const PERFORMANCE_SCORE_WEIGHTS = { +import type {WebVitals} from 'sentry/views/insights/browser/webVitals/types'; + +export const PERFORMANCE_SCORE_WEIGHTS: Record = { lcp: 30, fcp: 15, inp: 30, @@ -6,7 +8,7 @@ export const PERFORMANCE_SCORE_WEIGHTS = { ttfb: 10, }; -export const PERFORMANCE_SCORE_MEDIANS = { +export const PERFORMANCE_SCORE_MEDIANS: Record = { lcp: 2400, fcp: 1600, cls: 0.25, @@ -14,7 +16,7 @@ export const PERFORMANCE_SCORE_MEDIANS = { inp: 500, }; -export const PERFORMANCE_SCORE_P90S = { +export const PERFORMANCE_SCORE_P90S: Record = { lcp: 1200, fcp: 900, cls: 0.1, From a956f66cc1070a5f4e1933a3b67f48dd24e56417 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 09:29:56 -0800 Subject: [PATCH 523/757] feat(ui): Remove findDOMNode from exports (#82640) --- static/app/bootstrap/exportGlobals.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/static/app/bootstrap/exportGlobals.tsx b/static/app/bootstrap/exportGlobals.tsx index 7b62c39158c546..a1941b55e789cb 100644 --- a/static/app/bootstrap/exportGlobals.tsx +++ b/static/app/bootstrap/exportGlobals.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import {findDOMNode} from 'react-dom'; import {createRoot} from 'react-dom/client'; import * as Sentry from '@sentry/react'; import moment from 'moment-timezone'; @@ -12,7 +11,7 @@ const globals = { React, Sentry, moment, - ReactDOM: {findDOMNode, createRoot}, + ReactDOM: {createRoot}, // django templates make use of these globals SentryApp: {}, From fe7ebdc8236dfddd2aa07a5a638df043097c675d Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 09:35:20 -0800 Subject: [PATCH 524/757] ref(ui): Remove unused select owners component (#82639) --- .../project/projectOwnership/selectOwners.tsx | 334 ------------------ 1 file changed, 334 deletions(-) delete mode 100644 static/app/views/settings/project/projectOwnership/selectOwners.tsx diff --git a/static/app/views/settings/project/projectOwnership/selectOwners.tsx b/static/app/views/settings/project/projectOwnership/selectOwners.tsx deleted file mode 100644 index b1c633c6675b3b..00000000000000 --- a/static/app/views/settings/project/projectOwnership/selectOwners.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import {Component, createRef} from 'react'; -import {findDOMNode} from 'react-dom'; -import type {MultiValueProps} from 'react-select'; -import styled from '@emotion/styled'; -import debounce from 'lodash/debounce'; -import isEqual from 'lodash/isEqual'; - -import {addTeamToProject} from 'sentry/actionCreators/projects'; -import type {Client} from 'sentry/api'; -import ActorAvatar from 'sentry/components/avatar/actorAvatar'; -import {Button} from 'sentry/components/button'; -import SelectControl from 'sentry/components/forms/controls/selectControl'; -import IdBadge from 'sentry/components/idBadge'; -import {Tooltip} from 'sentry/components/tooltip'; -import {IconAdd} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import MemberListStore from 'sentry/stores/memberListStore'; -import ProjectsStore from 'sentry/stores/projectsStore'; -import TeamStore from 'sentry/stores/teamStore'; -import {space} from 'sentry/styles/space'; -import type {Actor} from 'sentry/types/core'; -import type {Member, Organization, Team} from 'sentry/types/organization'; -import type {Project} from 'sentry/types/project'; -import type {User} from 'sentry/types/user'; -import {buildTeamId, buildUserId} from 'sentry/utils'; -import withApi from 'sentry/utils/withApi'; -import withProjects from 'sentry/utils/withProjects'; - -export type Owner = { - actor: Actor; - label: React.ReactNode; - searchKey: string; - value: string; - disabled?: boolean; -}; - -function ValueComponent({data, removeProps}: MultiValueProps) { - return ( - - - - ); -} - -const getSearchKeyForUser = (user: User) => - `${user.email?.toLowerCase()} ${user.name?.toLowerCase()}`; - -type Props = { - api: Client; - disabled: boolean; - onChange: (owners: Owner[]) => void; - organization: Organization; - project: Project; - projects: Project[]; - value: any; - onInputChange?: (text: string) => void; -}; - -type State = { - inputValue: string; - loading: boolean; -}; - -class SelectOwners extends Component { - state: State = { - loading: false, - inputValue: '', - }; - - componentDidUpdate(prevProps: Props) { - // Once a team has been added to the project the menu can be closed. - if (!isEqual(this.props.projects, prevProps.projects)) { - this.closeSelectMenu(); - } - } - - private selectRef = createRef(); - - renderUserBadge = (user: User) => ( - - ); - - createMentionableUser = (user: User): Owner => ({ - value: buildUserId(user.id), - label: this.renderUserBadge(user), - searchKey: getSearchKeyForUser(user), - actor: { - type: 'user' as const, - id: user.id, - name: user.name, - }, - }); - - createUnmentionableUser = ({user}): Owner => ({ - ...this.createMentionableUser(user), - disabled: true, - label: ( - - - {this.renderUserBadge(user)} - - - ), - }); - - createMentionableTeam = (team: Team): Owner => ({ - value: buildTeamId(team.id), - label: , - searchKey: `#${team.slug}`, - actor: { - type: 'team' as const, - id: team.id, - name: team.slug, - }, - }); - - createUnmentionableTeam = (team: Team): Owner => { - const {organization} = this.props; - const canAddTeam = organization.access.includes('project:write'); - - return { - ...this.createMentionableTeam(team), - disabled: true, - label: ( - - - - - - - - } - aria-label={t('Add %s to project', `#${team.slug}`)} - /> - - - ), - }; - }; - - getMentionableUsers() { - return MemberListStore.getAll().map(this.createMentionableUser); - } - - getMentionableTeams() { - const {project} = this.props; - const projectData = ProjectsStore.getBySlug(project.slug); - - if (!projectData) { - return []; - } - - return projectData.teams.map(this.createMentionableTeam); - } - - /** - * Get list of teams that are not in the current project, for use in `MultiSelectMenu` - */ - getTeamsNotInProject(teamsInProject: Owner[] = []) { - const teams = TeamStore.getAll() || []; - const excludedTeamIds = teamsInProject.map(({actor}) => actor.id); - - return teams - .filter(team => !excludedTeamIds.includes(team.id)) - .map(this.createUnmentionableTeam); - } - - /** - * Closes the select menu by blurring input if possible since that seems to be the only - * way to close it. - */ - closeSelectMenu() { - // Close select menu - if (this.selectRef.current) { - // eslint-disable-next-line react/no-find-dom-node - const node = findDOMNode(this.selectRef.current); - const input: HTMLInputElement | null = (node as Element)?.querySelector( - '.Select-input input' - ); - if (input) { - // I don't think there's another way to close `react-select` - input.blur(); - } - } - } - - async handleAddTeamToProject(team) { - const {api, organization, project, value} = this.props; - // Copy old value - const oldValue = [...value]; - - // Optimistic update - this.props.onChange([...this.props.value, this.createMentionableTeam(team)]); - - try { - // Try to add team to project - // Note: we can't close select menu here because we have to wait for ProjectsStore to update first - // The reason for this is because we have little control over `react-select`'s `AsyncSelect` - // We can't control when `handleLoadOptions` gets called, but it gets called when select closes, so - // wait for store to update before closing the menu. Otherwise, we'll have stale items in the select menu - await addTeamToProject(api, organization.slug, project.slug, team); - } catch (err) { - // Unable to add team to project, revert select menu value - this.props.onChange(oldValue); - this.closeSelectMenu(); - } - } - - handleChange = (newValue: Owner[]) => { - this.props.onChange(newValue); - }; - - handleInputChange = (inputValue: string) => { - this.setState({inputValue}); - - if (this.props.onInputChange) { - this.props.onInputChange(inputValue); - } - }; - - queryMembers = debounce((query, cb) => { - const {api, organization} = this.props; - - // Because this function is debounced, the component can potentially be - // unmounted before this fires, in which case, `this.api` is null - if (!api) { - return null; - } - - return api - .requestPromise(`/organizations/${organization.slug}/members/`, { - query: {query}, - }) - .then( - (data: Member[]) => cb(null, data), - err => cb(err) - ); - }, 250); - - handleLoadOptions = () => { - const usersInProject = this.getMentionableUsers(); - const teamsInProject = this.getMentionableTeams(); - const teamsNotInProject = this.getTeamsNotInProject(teamsInProject); - const usersInProjectById = usersInProject.map(({actor}) => actor.id); - - // Return a promise for `react-select` - return new Promise((resolve, reject) => { - this.queryMembers(this.state.inputValue, (err, result) => { - if (err) { - reject(err); - } else { - resolve(result); - } - }); - }) - .then(members => - // Be careful here as we actually want the `users` object, otherwise it means user - // has not registered for sentry yet, but has been invited - members - ? (members as Member[]) - .filter(({user}) => user && !usersInProjectById.includes(user.id)) - .map(this.createUnmentionableUser) - : [] - ) - .then(members => { - return [...usersInProject, ...teamsInProject, ...teamsNotInProject, ...members]; - }); - }; - - render() { - return ( - - option.data.searchKey.indexOf(filterText) > -1 - } - ref={this.selectRef} - loadOptions={this.handleLoadOptions} - defaultOptions - async - clearable - disabled={this.props.disabled} - cache={false} - aria-label={t('Rule owner')} - placeholder={t('Owners')} - components={{ - MultiValue: ValueComponent, - }} - onInputChange={this.handleInputChange} - onChange={this.handleChange} - value={this.props.value} - css={{width: 300}} - /> - ); - } -} - -export default withApi(withProjects(SelectOwners)); - -const Container = styled('div')` - display: flex; - justify-content: space-between; -`; - -const DisabledLabel = styled('div')` - opacity: 0.5; - overflow: hidden; /* Needed so that "Add to team" button can fit */ -`; - -const AddToProjectButton = styled(Button)` - flex-shrink: 0; -`; - -const ValueWrapper = styled('a')` - margin-right: ${space(0.5)}; -`; From 9d001c9dbff92400a96d1b48a3b0eaeee0ea1690 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 30 Dec 2024 09:43:24 -0800 Subject: [PATCH 525/757] fix(test): Wait for item to appear in test to fix CI (#82659) --- static/app/components/performance/searchBar.spec.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/static/app/components/performance/searchBar.spec.tsx b/static/app/components/performance/searchBar.spec.tsx index 61b618cf1f32c1..b3f772c23bd4b6 100644 --- a/static/app/components/performance/searchBar.spec.tsx +++ b/static/app/components/performance/searchBar.spec.tsx @@ -157,10 +157,11 @@ describe('SearchBar', () => { render(); - await userEvent.type( - screen.getByRole('textbox'), - 'GET /my-endpoint{ArrowDown}{Enter}' - ); + await userEvent.type(screen.getByRole('textbox'), 'GET /my-endpoint'); + + await screen.findByText('GET /my-endpoint'); + + await userEvent.keyboard('{ArrowDown}{Enter}'); expect(onSearch).toHaveBeenCalledTimes(1); expect(onSearch).toHaveBeenCalledWith('transaction:"GET /my-endpoint"'); From b43c2a8352bece6f7ade1f6dab9dcf694744f11d Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 09:45:31 -0800 Subject: [PATCH 526/757] feat(react19): Rm findDomNode from feedback username (#82636) --- .../feedbackItemUsername.spec.tsx | 30 ++++++++++++++++++- .../feedbackItem/feedbackItemUsername.tsx | 19 ++++-------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/static/app/components/feedback/feedbackItem/feedbackItemUsername.spec.tsx b/static/app/components/feedback/feedbackItem/feedbackItemUsername.spec.tsx index 48bd65fecddca3..4fa250b847f717 100644 --- a/static/app/components/feedback/feedbackItem/feedbackItemUsername.spec.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackItemUsername.spec.tsx @@ -1,10 +1,18 @@ import {FeedbackIssueFixture} from 'sentry-fixture/feedbackIssue'; -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import FeedbackItemUsername from 'sentry/components/feedback/feedbackItem/feedbackItemUsername'; describe('FeedbackItemUsername', () => { + beforeEach(() => { + Object.assign(navigator, { + clipboard: { + writeText: jest.fn().mockResolvedValue(''), + }, + }); + }); + it('should fallback to "Anonymous User" when no name/contact_email exist', () => { const issue = FeedbackIssueFixture({ metadata: { @@ -88,4 +96,24 @@ describe('FeedbackItemUsername', () => { expect.stringContaining('mailto:foo@bar.com') ); }); + + it('should copy text and select it on click', async () => { + const issue = FeedbackIssueFixture({ + metadata: { + name: 'Foo Bar', + contact_email: 'foo@bar.com', + }, + }); + render(); + + const username = screen.getByText('Foo Bar'); + + await userEvent.click(username); + + await waitFor(() => { + expect(window.getSelection()?.toString()).toBe('Foo Bar•foo@bar.com'); + }); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Foo Bar '); + }); }); diff --git a/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx b/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx index 6f4abca146d595..c65bbf02bcf47c 100644 --- a/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx @@ -1,5 +1,4 @@ -import {type CSSProperties, Fragment, useCallback, useRef} from 'react'; -import {findDOMNode} from 'react-dom'; +import {type CSSProperties, Fragment, useCallback, useId} from 'react'; import styled from '@emotion/styled'; import {LinkButton} from 'sentry/components/button'; @@ -29,22 +28,16 @@ export default function FeedbackItemUsername({className, feedbackIssue, style}: const user = name && email && !isSameNameAndEmail ? `${name} <${email}>` : nameOrEmail; - const userNodeRef = useRef(null); + const userNodeId = useId(); const handleSelectText = useCallback(() => { - if (!userNodeRef.current) { - return; - } - - // We use findDOMNode here because `this.userNodeRef` is not a dom node, - // it's a ref to AutoSelectText - const node = findDOMNode(userNodeRef.current); // eslint-disable-line react/no-find-dom-node - if (!node || !(node instanceof HTMLElement)) { + const node = document.getElementById(userNodeId); + if (!node) { return; } selectText(node); - }, []); + }, [userNodeId]); const {onClick: handleCopyToClipboard} = useCopyToClipboard({ text: user ?? '', @@ -65,6 +58,7 @@ export default function FeedbackItemUsername({className, feedbackIssue, style}: {isSameNameAndEmail ? ( {name ?? email} From bd67bd445cfea5fe0629093a430eaa4d5f83da9e Mon Sep 17 00:00:00 2001 From: "Armen Zambrano G." <44410+armenzg@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:45:36 -0500 Subject: [PATCH 527/757] typing: activity model (#82658) --- pyproject.toml | 1 + src/sentry/models/activity.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 258cc7d056c922..35e10994f44637 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -427,6 +427,7 @@ module = [ "sentry.lang.java.processing", "sentry.llm.*", "sentry.migrations.*", + "sentry.models.activity", "sentry.models.event", "sentry.models.eventattachment", "sentry.models.groupsubscription", diff --git a/src/sentry/models/activity.py b/src/sentry/models/activity.py index ae5e30d012c2d6..363c6cd7dd7f1a 100644 --- a/src/sentry/models/activity.py +++ b/src/sentry/models/activity.py @@ -128,10 +128,10 @@ class Meta: __repr__ = sane_repr("project_id", "group_id", "event_id", "user_id", "type", "ident") @staticmethod - def get_version_ident(version): + def get_version_ident(version: str | None) -> str: return (version or "")[:64] - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) from sentry.models.release import Release @@ -143,7 +143,7 @@ def __init__(self, *args, **kwargs): if self.type == ActivityType.ASSIGNED.value: self.data["assignee"] = str(self.data["assignee"]) - def save(self, *args, **kwargs): + def save(self, *args: Any, **kwargs: Any) -> None: created = bool(not self.id) super().save(*args, **kwargs) @@ -178,8 +178,8 @@ def save(self, *args, **kwargs): sender=Group, instance=self.group, created=True, update_fields=["num_comments"] ) - def delete(self, *args, **kwargs): - super().delete(*args, **kwargs) + def delete(self, *args: Any, **kwargs: Any) -> tuple[int, dict[str, int]]: + result = super().delete(*args, **kwargs) # HACK: support Group.num_comments if self.type == ActivityType.NOTE.value and self.group is not None: @@ -191,7 +191,9 @@ def delete(self, *args, **kwargs): sender=Group, instance=self.group, created=True, update_fields=["num_comments"] ) - def send_notification(self): + return result + + def send_notification(self) -> None: if self.group: group_type = get_group_type_by_type_id(self.group.type) has_status_change_notifications = group_type.enable_status_change_workflow_notifications From 8bca2e420240a645bf23d73b7bb135650d03fc4a Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:49:39 -0500 Subject: [PATCH 528/757] fix(dashboards): Turn off axis animation for `LineChartWidget` and `AreaChartWidget` (#82657) Otherwise the numbers float around awkwardly when series are turned on and off. --- .../widgets/areaChartWidget/areaChartWidgetVisualization.tsx | 2 ++ .../widgets/lineChartWidget/lineChartWidgetVisualization.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx index 6b775819bff0be..2bd15df6025ba3 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx @@ -178,6 +178,7 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization formatter, }} xAxis={{ + animation: false, axisLabel: { padding: [0, 10, 0, 10], width: 60, @@ -185,6 +186,7 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization splitNumber: 0, }} yAxis={{ + animation: false, axisLabel: { formatter(value: number) { return formatYAxisValue(value, type, unit); diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx index d99f870851ded0..7240f1b7351c0c 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx @@ -213,6 +213,7 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization formatter, }} xAxis={{ + animation: false, axisLabel: { padding: [0, 10, 0, 10], width: 60, @@ -220,6 +221,7 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization splitNumber: 0, }} yAxis={{ + animation: false, axisLabel: { formatter(value: number) { return formatYAxisValue(value, type, unit); From f53592140f18dc01446c7c3fa3c8d1f8fbbc46b1 Mon Sep 17 00:00:00 2001 From: Cathy Teng <70817427+cathteng@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:09:29 -0800 Subject: [PATCH 529/757] ref(scm): introduce SCM webhook ABC (#82563) --- src/sentry/integrations/github/webhook.py | 111 ++++++++---------- .../integrations/github_enterprise/webhook.py | 45 +++---- .../source_code_management/webhook.py | 26 ++++ 3 files changed, 90 insertions(+), 92 deletions(-) create mode 100644 src/sentry/integrations/source_code_management/webhook.py diff --git a/src/sentry/integrations/github/webhook.py b/src/sentry/integrations/github/webhook.py index d5066d0e908673..24cd6fdfe02f9d 100644 --- a/src/sentry/integrations/github/webhook.py +++ b/src/sentry/integrations/github/webhook.py @@ -26,12 +26,10 @@ from sentry.integrations.base import IntegrationDomain from sentry.integrations.github.tasks.open_pr_comment import open_pr_comment_workflow from sentry.integrations.pipeline import ensure_integration -from sentry.integrations.services.integration.model import ( - RpcIntegration, - RpcOrganizationIntegration, -) +from sentry.integrations.services.integration.model import RpcIntegration from sentry.integrations.services.integration.service import integration_service from sentry.integrations.services.repository.service import repository_service +from sentry.integrations.source_code_management.webhook import SCMWebhook from sentry.integrations.utils.metrics import IntegrationWebhookEvent, IntegrationWebhookEventType from sentry.integrations.utils.scope import clear_tags_and_context from sentry.models.commit import Commit @@ -73,31 +71,21 @@ def get_file_language(filename: str) -> str | None: return language -class Webhook(ABC): +class GitHubWebhook(SCMWebhook, ABC): """ Base class for GitHub webhooks handled in region silos. """ - provider = "github" - @property - @abstractmethod - def event_type(self) -> IntegrationWebhookEventType: - raise NotImplementedError + def provider(self) -> str: + return "github" @abstractmethod - def _handle( - self, - integration: RpcIntegration, - event: Mapping[str, Any], - organization: Organization, - repo: Repository, - host: str | None = None, - ) -> None: - raise NotImplementedError + def _handle(self, integration: RpcIntegration, event: Mapping[str, Any], **kwargs) -> None: + pass - def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None: - external_id = get_github_external_id(event=event, host=host) + def __call__(self, event: Mapping[str, Any], **kwargs) -> None: + external_id = get_github_external_id(event=event, host=kwargs.get("host")) result = integration_service.organization_contexts( external_id=external_id, provider=self.provider @@ -166,7 +154,12 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None: for repo in repos.exclude(status=ObjectStatus.HIDDEN): self.update_repo_data(repo, event) - self._handle(integration, event, orgs[repo.organization_id], repo) + self._handle( + integration=integration, + event=event, + organization=orgs[repo.organization_id], + repo=repo, + ) def update_repo_data(self, repo: Repository, event: Mapping[str, Any]) -> None: """ @@ -208,21 +201,28 @@ def update_repo_data(self, repo: Repository, event: Mapping[str, Any]) -> None: ) pass + def is_anonymous_email(self, email: str) -> bool: + return email[-25:] == "@users.noreply.github.com" + + def get_external_id(self, username: str) -> str: + return f"github:{username}" -class InstallationEventWebhook: + def get_idp_external_id(self, integration: RpcIntegration, host: str | None = None) -> str: + return options.get("github-app.id") + + +class InstallationEventWebhook(GitHubWebhook): """ Unlike other GitHub webhooks, installation webhooks are handled in control silo. https://developer.github.com/v3/activity/events/types/#installationevent """ - provider = "github" - @property def event_type(self) -> IntegrationWebhookEventType: return IntegrationWebhookEventType.INSTALLATION - def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None: + def __call__(self, event: Mapping[str, Any], **kwargs) -> None: installation = event["installation"] if not installation: @@ -241,7 +241,7 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None: if event["action"] == "deleted": external_id = event["installation"]["id"] - if host: + if host := kwargs.get("host"): external_id = "{}:{}".format(host, event["installation"]["id"]) result = integration_service.organization_contexts( provider=self.provider, @@ -251,7 +251,7 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None: org_integrations = result.organization_integrations if integration is not None: - self._handle_delete(event, integration, org_integrations) + self._handle(integration, event, org_integrations=org_integrations) else: # It seems possible for the GH or GHE app to be installed on their # end, but the integration to not exist. Possibly from deleting in @@ -267,13 +267,13 @@ def __call__(self, event: Mapping[str, Any], host: str | None = None) -> None: ) logger.error("Installation is missing.") - def _handle_delete( + def _handle( self, - event: Mapping[str, Any], integration: RpcIntegration, - org_integrations: list[RpcOrganizationIntegration], + event: Mapping[str, Any], + **kwargs, ) -> None: - org_ids = {oi.organization_id for oi in org_integrations} + org_ids = {oi.organization_id for oi in kwargs.get("org_integrations", [])} logger.info( "InstallationEventWebhook._handle_delete", @@ -294,22 +294,13 @@ def _handle_delete( ) -class PushEventWebhook(Webhook): +class PushEventWebhook(GitHubWebhook): """https://developer.github.com/v3/activity/events/types/#pushevent""" @property def event_type(self) -> IntegrationWebhookEventType: return IntegrationWebhookEventType.PUSH - def is_anonymous_email(self, email: str) -> bool: - return email[-25:] == "@users.noreply.github.com" - - def get_external_id(self, username: str) -> str: - return f"github:{username}" - - def get_idp_external_id(self, integration: RpcIntegration, host: str | None = None) -> str: - return options.get("github-app.id") - def should_ignore_commit(self, commit: Mapping[str, Any]) -> bool: return GitHubRepositoryProvider.should_ignore_commit(commit["message"]) @@ -317,11 +308,12 @@ def _handle( self, integration: RpcIntegration, event: Mapping[str, Any], - organization: Organization, - repo: Repository, - host: str | None = None, + **kwargs, ) -> None: authors = {} + if not ((organization := kwargs.get("organization")) and (repo := kwargs.get("repo"))): + raise ValueError("Missing organization and repo") + client = integration.get_installation(organization_id=organization.id).get_client() gh_username_cache: MutableMapping[str, str | None] = {} @@ -373,7 +365,7 @@ def _handle( "identity_ext_id": gh_user["id"], "provider_type": self.provider, "provider_ext_id": self.get_idp_external_id( - integration, host + integration, kwargs.get("host") ), } ) @@ -474,29 +466,18 @@ def _handle( repo.save() -class PullRequestEventWebhook(Webhook): +class PullRequestEventWebhook(GitHubWebhook): """https://developer.github.com/v3/activity/events/types/#pullrequestevent""" @property def event_type(self) -> IntegrationWebhookEventType: return IntegrationWebhookEventType.PULL_REQUEST - def is_anonymous_email(self, email: str) -> bool: - return email[-25:] == "@users.noreply.github.com" - - def get_external_id(self, username: str) -> str: - return f"github:{username}" - - def get_idp_external_id(self, integration: RpcIntegration, host: str | None = None) -> str: - return options.get("github-app.id") - def _handle( self, integration: RpcIntegration, event: Mapping[str, Any], - organization: Organization, - repo: Repository, - host: str | None = None, + **kwargs, ) -> None: pull_request = event["pull_request"] number = pull_request["number"] @@ -522,6 +503,10 @@ def _handle( merge_commit_sha = pull_request["merge_commit_sha"] if pull_request["merged"] else None author_email = "{}@localhost".format(user["login"][:65]) + + if not ((organization := kwargs.get("organization")) and (repo := kwargs.get("repo"))): + raise ValueError("Missing organization and repo") + try: commit_author = CommitAuthor.objects.get( external_id=self.get_external_id(user["login"]), organization_id=organization.id @@ -533,7 +518,7 @@ def _handle( filter={ "identity_ext_id": user["id"], "provider_type": self.provider, - "provider_ext_id": self.get_idp_external_id(integration, host), + "provider_ext_id": self.get_idp_external_id(integration, kwargs.get("host")), } ) if identity is not None: @@ -612,13 +597,13 @@ class GitHubIntegrationsWebhookEndpoint(Endpoint): "POST": ApiPublishStatus.PRIVATE, } - _handlers: dict[str, type[Webhook] | type[InstallationEventWebhook]] = { + _handlers: dict[str, type[GitHubWebhook]] = { "push": PushEventWebhook, "pull_request": PullRequestEventWebhook, "installation": InstallationEventWebhook, } - def get_handler(self, event_type: str) -> type[Webhook] | type[InstallationEventWebhook] | None: + def get_handler(self, event_type: str) -> type[GitHubWebhook] | None: return self._handlers.get(event_type) def is_valid_signature(self, method: str, body: bytes, secret: str, signature: str) -> bool: @@ -699,7 +684,7 @@ def handle(self, request: HttpRequest) -> HttpResponse: with IntegrationWebhookEvent( interaction_type=event_handler.event_type, domain=IntegrationDomain.SOURCE_CODE_MANAGEMENT, - provider_key="github", + provider_key=event_handler.provider, ).capture(): event_handler(event) return HttpResponse(status=204) diff --git a/src/sentry/integrations/github_enterprise/webhook.py b/src/sentry/integrations/github_enterprise/webhook.py index 230ab9e069434f..8550214d531ff7 100644 --- a/src/sentry/integrations/github_enterprise/webhook.py +++ b/src/sentry/integrations/github_enterprise/webhook.py @@ -18,10 +18,10 @@ from sentry.constants import ObjectStatus from sentry.integrations.base import IntegrationDomain from sentry.integrations.github.webhook import ( + GitHubWebhook, InstallationEventWebhook, PullRequestEventWebhook, PushEventWebhook, - Webhook, get_github_external_id, ) from sentry.integrations.utils.metrics import IntegrationWebhookEvent @@ -29,8 +29,6 @@ from sentry.utils import metrics from sentry.utils.sdk import Scope -from .repository import GitHubEnterpriseRepositoryProvider - logger = logging.getLogger("sentry.webhooks") from sentry.api.base import Endpoint, region_silo_endpoint from sentry.integrations.services.integration import integration_service @@ -89,16 +87,10 @@ def get_installation_metadata(event, host): return integration.metadata["installation"] -class GitHubEnterpriseInstallationEventWebhook(InstallationEventWebhook): - provider = "github_enterprise" - - -class GitHubEnterprisePushEventWebhook(PushEventWebhook): - provider = "github_enterprise" - - # https://developer.github.com/v3/activity/events/types/#pushevent - def is_anonymous_email(self, email: str) -> bool: - return email[-25:] == "@users.noreply.github.com" +class GitHubEnterpriseWebhook: + @property + def provider(self) -> str: + return "github_enterprise" def get_external_id(self, username: str) -> str: return f"github_enterprise:{username}" @@ -106,29 +98,24 @@ def get_external_id(self, username: str) -> str: def get_idp_external_id(self, integration: RpcIntegration, host: str | None = None) -> str: return "{}:{}".format(host, integration.metadata["installation"]["id"]) - def should_ignore_commit(self, commit): - return GitHubEnterpriseRepositoryProvider.should_ignore_commit(commit["message"]) +class GitHubEnterpriseInstallationEventWebhook(GitHubEnterpriseWebhook, InstallationEventWebhook): + pass -class GitHubEnterprisePullRequestEventWebhook(PullRequestEventWebhook): - provider = "github_enterprise" - # https://developer.github.com/v3/activity/events/types/#pullrequestevent - def is_anonymous_email(self, email: str) -> bool: - return email[-25:] == "@users.noreply.github.com" +class GitHubEnterprisePushEventWebhook(GitHubEnterpriseWebhook, PushEventWebhook): + pass - def get_external_id(self, username: str) -> str: - return f"github_enterprise:{username}" - def get_idp_external_id(self, integration: RpcIntegration, host: str | None = None) -> str: - return "{}:{}".format(host, integration.metadata["installation"]["id"]) +class GitHubEnterprisePullRequestEventWebhook(GitHubEnterpriseWebhook, PullRequestEventWebhook): + pass class GitHubEnterpriseWebhookBase(Endpoint): authentication_classes = () permission_classes = () - _handlers: dict[str, type[InstallationEventWebhook] | type[Webhook]] = {} + _handlers: dict[str, type[GitHubWebhook]] = {} # https://developer.github.com/webhooks/ def get_handler(self, event_type): @@ -163,7 +150,7 @@ def get_secret(self, event, host): else: return None - def handle(self, request: HttpRequest) -> HttpResponse: + def _handle(self, request: HttpRequest) -> HttpResponse: clear_tags_and_context() scope = Scope.get_isolation_scope() @@ -301,9 +288,9 @@ def handle(self, request: HttpRequest) -> HttpResponse: with IntegrationWebhookEvent( interaction_type=event_handler.event_type, domain=IntegrationDomain.SOURCE_CODE_MANAGEMENT, - provider_key="github-enterprise", + provider_key=event_handler.provider, ).capture(): - event_handler(event, host) + event_handler(event, host=host) return HttpResponse(status=204) @@ -329,4 +316,4 @@ def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @method_decorator(csrf_exempt) def post(self, request: HttpRequest) -> HttpResponse: - return self.handle(request) + return self._handle(request) diff --git a/src/sentry/integrations/source_code_management/webhook.py b/src/sentry/integrations/source_code_management/webhook.py new file mode 100644 index 00000000000000..bc0eef1ae39cf9 --- /dev/null +++ b/src/sentry/integrations/source_code_management/webhook.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod +from collections.abc import Mapping +from typing import Any + +from sentry.integrations.utils.metrics import IntegrationWebhookEventType +from sentry.models.repository import Repository + + +class SCMWebhook(ABC): + @property + @abstractmethod + def provider(self) -> str: + raise NotImplementedError + + @property + @abstractmethod + def event_type(self) -> IntegrationWebhookEventType: + raise NotImplementedError + + @abstractmethod + def __call__(self, event: Mapping[str, Any], **kwargs) -> None: + raise NotImplementedError + + @abstractmethod + def update_repo_data(self, repo: Repository, event: Mapping[str, Any]) -> None: + raise NotImplementedError From 8cb13348bf840d69859a1905d31ca552ed9b26a1 Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:17:55 -0500 Subject: [PATCH 530/757] fix(alerts): updates span alerts image (#82665) Updates the span alerts image in the alert creation wizard to use to throughput graphic, instead of the old metrics code snippet. --- static/app/views/alerts/wizard/panelContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/alerts/wizard/panelContent.tsx b/static/app/views/alerts/wizard/panelContent.tsx index 24042382121c15..c6ccc7447d64bf 100644 --- a/static/app/views/alerts/wizard/panelContent.tsx +++ b/static/app/views/alerts/wizard/panelContent.tsx @@ -192,6 +192,6 @@ export const AlertWizardPanelContent: Record = { t('When your average time in queue exceeds 100ms.'), t('When your app runs more than 1000 queries in a minute.'), ], - illustration: diagramCustomMetrics, + illustration: diagramThroughput, }, }; From bf6c23892906541614d01b59a695d35db94d7c98 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:29:14 -0500 Subject: [PATCH 531/757] feat(dashboards): Add "Aliases" support to `LineChartWidget` and `AreaChartWidget` (#82652) Passes through some name formatting. EZ. Code re-use between `LineChartWidget` and `AreaChartWidget` is already getting uncomfortable. --- .../widgets/areaChartWidget/areaChartWidget.tsx | 1 + .../areaChartWidgetVisualization.tsx | 15 ++++++++++++--- .../app/views/dashboards/widgets/common/types.tsx | 2 ++ .../lineChartWidget/lineChartWidget.stories.tsx | 4 ++++ .../widgets/lineChartWidget/lineChartWidget.tsx | 1 + .../lineChartWidgetVisualization.tsx | 15 ++++++++++++--- 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx index abc1f1b60eebc1..e06d015f465e06 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx @@ -55,6 +55,7 @@ export function AreaChartWidget(props: AreaChartWidgetProps) { )} diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx index 2bd15df6025ba3..cfeb77a29ec6c4 100644 --- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx @@ -23,10 +23,11 @@ import {useWidgetSyncContext} from '../../contexts/widgetSyncContext'; import {formatTooltipValue} from '../common/formatTooltipValue'; import {formatYAxisValue} from '../common/formatYAxisValue'; import {ReleaseSeries} from '../common/releaseSeries'; -import type {Release, TimeseriesData} from '../common/types'; +import type {Aliases, Release, TimeseriesData} from '../common/types'; export interface AreaChartWidgetVisualizationProps { timeseries: TimeseriesData[]; + aliases?: Aliases; releases?: Release[]; } @@ -56,6 +57,10 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization releaseSeries = ReleaseSeries(theme, props.releases, onClick, utc ?? false); } + const formatSeriesName: (string) => string = name => { + return props.aliases?.[name] ?? name; + }; + const chartZoomProps = useChartZoom({ saveOnZoom: true, }); @@ -70,7 +75,7 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization const type = firstSeries?.meta?.fields?.[firstSeriesField] ?? 'number'; const unit = firstSeries?.meta?.units?.[firstSeriesField] ?? undefined; - const formatter: TooltipFormatterCallback = ( + const formatTooltip: TooltipFormatterCallback = ( params, asyncTicket ) => { @@ -110,6 +115,7 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization valueFormatter: value => { return formatTooltipValue(value, type, unit); }, + nameFormatter: formatSeriesName, truncate: true, utc: utc ?? false, })(deDupedParams, asyncTicket); @@ -167,6 +173,9 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization ? { top: 0, left: 0, + formatter(name: string) { + return formatSeriesName(name); + }, } : undefined } @@ -175,7 +184,7 @@ export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualization axisPointer: { type: 'cross', }, - formatter, + formatter: formatTooltip, }} xAxis={{ animation: false, diff --git a/static/app/views/dashboards/widgets/common/types.tsx b/static/app/views/dashboards/widgets/common/types.tsx index f85d06f7ffd19b..22d9c7e10af983 100644 --- a/static/app/views/dashboards/widgets/common/types.tsx +++ b/static/app/views/dashboards/widgets/common/types.tsx @@ -34,3 +34,5 @@ export type Release = { timestamp: string; version: string; }; + +export type Aliases = Record; diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx index 2789dc918cabb6..bf3430a3ceac8b 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx @@ -103,6 +103,10 @@ export default storyBook(LineChartWidget, story => { shiftTimeserieToNow(durationTimeSeries1), shiftTimeserieToNow(durationTimeSeries2), ]} + aliases={{ + 'p50(span.duration)': '50th Percentile', + 'p99(span.duration)': '99th Percentile', + }} /> diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx index e7197ba47d9510..3696ffe98376e8 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.tsx @@ -60,6 +60,7 @@ export function LineChartWidget(props: LineChartWidgetProps) { diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx index 7240f1b7351c0c..1dc59664dbd8f2 100644 --- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidgetVisualization.tsx @@ -22,12 +22,13 @@ import {useWidgetSyncContext} from '../../contexts/widgetSyncContext'; import {formatTooltipValue} from '../common/formatTooltipValue'; import {formatYAxisValue} from '../common/formatYAxisValue'; import {ReleaseSeries} from '../common/releaseSeries'; -import type {Release, TimeseriesData} from '../common/types'; +import type {Aliases, Release, TimeseriesData} from '../common/types'; import {splitSeriesIntoCompleteAndIncomplete} from './splitSeriesIntoCompleteAndIncomplete'; export interface LineChartWidgetVisualizationProps { timeseries: TimeseriesData[]; + aliases?: Aliases; dataCompletenessDelay?: number; releases?: Release[]; } @@ -60,6 +61,10 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization releaseSeries = ReleaseSeries(theme, props.releases, onClick, utc ?? false); } + const formatSeriesName: (string) => string = name => { + return props.aliases?.[name] ?? name; + }; + const chartZoomProps = useChartZoom({ saveOnZoom: true, }); @@ -96,7 +101,7 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization const type = firstSeries?.meta?.fields?.[firstSeriesField] ?? 'number'; const unit = firstSeries?.meta?.units?.[firstSeriesField] ?? undefined; - const formatter: TooltipFormatterCallback = ( + const formatTooltip: TooltipFormatterCallback = ( params, asyncTicket ) => { @@ -136,6 +141,7 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization valueFormatter: value => { return formatTooltipValue(value, type, unit); }, + nameFormatter: formatSeriesName, truncate: true, utc: utc ?? false, })(deDupedParams, asyncTicket); @@ -202,6 +208,9 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization ? { top: 0, left: 0, + formatter(name: string) { + return formatSeriesName(name); + }, } : undefined } @@ -210,7 +219,7 @@ export function LineChartWidgetVisualization(props: LineChartWidgetVisualization axisPointer: { type: 'cross', }, - formatter, + formatter: formatTooltip, }} xAxis={{ animation: false, From 61f0f3ef082b9f148eceaf950daa1a59ff736a59 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:30:28 -0500 Subject: [PATCH 532/757] ref: add more modules to the mypy stronglist (#82664) these were found using an (imperfect) script in the `asottile-auto-stronglist` branch --- pyproject.toml | 116 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 35e10994f44637..0e08cd5e45ddd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -345,13 +345,22 @@ disable_error_code = [ # begin: stronger typing [[tool.mypy.overrides]] module = [ + "fixtures.safe_migrations_apps.*", + "sentry.analytics.*", + "sentry.api.endpoints.integrations.sentry_apps.installation.external_issue.*", "sentry.api.endpoints.project_backfill_similar_issues_embeddings_records", + "sentry.api.endpoints.release_thresholds.health_checks.*", + "sentry.api.endpoints.relocations.artifacts.*", "sentry.api.helpers.deprecation", "sentry.api.helpers.source_map_helper", + "sentry.api.serializers.models.organization_member.*", + "sentry.audit_log.services.*", "sentry.auth.services.*", "sentry.auth.view", "sentry.buffer.*", "sentry.build.*", + "sentry.data_secrecy.models.*", + "sentry.data_secrecy.service.*", "sentry.db.models.fields.citext", "sentry.db.models.fields.foreignkey", "sentry.db.models.fields.hybrid_cloud_foreign_key", @@ -360,21 +369,56 @@ module = [ "sentry.db.models.paranoia", "sentry.db.models.utils", "sentry.deletions.*", + "sentry.digests.*", "sentry.digests.notifications", + "sentry.dynamic_sampling.models.*", + "sentry.dynamic_sampling.rules.biases.*", + "sentry.dynamic_sampling.rules.combinators.*", + "sentry.dynamic_sampling.rules.helpers.*", + "sentry.dynamic_sampling.tasks.helpers.*", + "sentry.eventstore.reprocessing.*", "sentry.eventstore.reprocessing.redis", + "sentry.eventstream.*", "sentry.eventtypes.error", + "sentry.feedback.migrations.*", + "sentry.flags.migrations.*", "sentry.grouping.api", "sentry.grouping.component", "sentry.grouping.fingerprinting", + "sentry.grouping.fingerprinting.*", "sentry.grouping.grouping_info", "sentry.grouping.ingest.*", "sentry.grouping.parameterization", "sentry.grouping.utils", "sentry.grouping.variants", "sentry.hybridcloud.*", + "sentry.identity.discord.*", "sentry.identity.github_enterprise.*", + "sentry.identity.services.*", + "sentry.identity.vsts_extension.*", + "sentry.incidents.utils.*", "sentry.ingest.slicing", + "sentry.integrations.discord.actions.*", + "sentry.integrations.discord.message_builder.base.component.*", + "sentry.integrations.discord.message_builder.base.embed.*", + "sentry.integrations.discord.utils.*", + "sentry.integrations.discord.views.*", + "sentry.integrations.discord.webhooks.*", + "sentry.integrations.github.actions.*", + "sentry.integrations.github_enterprise.actions.*", + "sentry.integrations.jira.endpoints.*", + "sentry.integrations.jira.models.*", + "sentry.integrations.jira_server.actions.*", + "sentry.integrations.jira_server.utils.*", "sentry.integrations.models.integration_feature", + "sentry.integrations.project_management.*", + "sentry.integrations.repository.*", + "sentry.integrations.services.*", + "sentry.integrations.slack.threads.*", + "sentry.integrations.slack.views.*", + "sentry.integrations.vsts.actions.*", + "sentry.integrations.vsts.tasks.*", + "sentry.integrations.web.debug.*", "sentry.issues", "sentry.issues.analytics", "sentry.issues.apps", @@ -431,27 +475,55 @@ module = [ "sentry.models.event", "sentry.models.eventattachment", "sentry.models.groupsubscription", - "sentry.monkey", + "sentry.models.options.*", + "sentry.monkey.*", + "sentry.nodestore.*", "sentry.nodestore.base", "sentry.nodestore.bigtable.backend", "sentry.nodestore.django.backend", "sentry.nodestore.django.models", "sentry.nodestore.filesystem.backend", "sentry.nodestore.models", + "sentry.notifications.services.*", "sentry.organizations.*", "sentry.ownership.*", "sentry.plugins.base.response", "sentry.plugins.base.view", + "sentry.plugins.validators.*", + "sentry.post_process_forwarder.*", "sentry.profiles.*", - "sentry.projects.services.*", + "sentry.projects.*", + "sentry.queue.*", "sentry.ratelimits.leaky_bucket", "sentry.relay.config.metric_extraction", + "sentry.relay.types.*", + "sentry.release_health.release_monitor.*", + "sentry.relocation.services.relocation_export.*", + "sentry.remote_subscriptions.migrations.*", + "sentry.replays.consumers.*", + "sentry.replays.lib.new_query.*", + "sentry.replays.migrations.*", "sentry.reprocessing2", + "sentry.roles.*", + "sentry.rules.actions.sentry_apps.*", + "sentry.rules.conditions.*", + "sentry.rules.history.endpoints.*", "sentry.runner.*", "sentry.search.snuba.backend", - "sentry.seer.similarity.utils", - "sentry.sentry_metrics.consumers.indexer.slicing_router", + "sentry.security.*", + "sentry.seer.similarity.*", + "sentry.sentry_apps.external_issues.*", + "sentry.sentry_apps.services.*", + "sentry.sentry_apps.utils.*", + "sentry.sentry_apps.web.*", + "sentry.sentry_metrics.consumers.indexer.*", + "sentry.sentry_metrics.indexer.limiters.*", + "sentry.shared_integrations.exceptions.*", + "sentry.slug.*", "sentry.snuba.metrics.extraction", + "sentry.snuba.metrics.naming_layer.*", + "sentry.snuba.query_subscriptions.*", + "sentry.spans.grouping.*", "sentry.stacktraces.platform", "sentry.tasks.beacon", "sentry.tasks.commit_context", @@ -460,10 +532,15 @@ module = [ "sentry.tasks.reprocessing2", "sentry.tasks.store", "sentry.taskworker.*", + "sentry.tempest.endpoints.*", + "sentry.tempest.migrations.*", "sentry.testutils.helpers.task_runner", "sentry.testutils.skips", - "sentry.types.actor", - "sentry.types.region", + "sentry.toolbar.utils.*", + "sentry.trash.*", + "sentry.types.*", + "sentry.uptime.migrations.*", + "sentry.usage_accountant.*", "sentry.users.*", "sentry.utils.arroyo", "sentry.utils.assets", @@ -480,6 +557,7 @@ module = [ "sentry.utils.imports", "sentry.utils.iterators", "sentry.utils.javascript", + "sentry.utils.kvstore.*", "sentry.utils.lazy_service_wrapper", "sentry.utils.locking.*", "sentry.utils.migrations", @@ -490,6 +568,7 @@ module = [ "sentry.utils.pubsub", "sentry.utils.redis", "sentry.utils.redis_metrics", + "sentry.utils.sdk_crashes.*", "sentry.utils.sentry_apps.*", "sentry.utils.services", "sentry.utils.sms", @@ -500,12 +579,24 @@ module = [ "sentry.web.frontend.auth_provider_login", "sentry.web.frontend.cli", "sentry.web.frontend.csv", + "sentry.web.frontend.mixins.*", + "sentry.workflow_engine.handlers.action.*", + "sentry.workflow_engine.handlers.condition.*", + "sentry.workflow_engine.migrations.*", "sentry_plugins.base", + "social_auth.migrations.*", + "sudo.*", + "tests.sentry.audit_log.services.*", "tests.sentry.deletions.test_group", "tests.sentry.event_manager.test_event_manager", "tests.sentry.grouping.ingest.test_seer", "tests.sentry.grouping.test_fingerprinting", "tests.sentry.hybridcloud.*", + "tests.sentry.incidents.serializers.*", + "tests.sentry.integrations.msteams.webhook.*", + "tests.sentry.integrations.repository.base.*", + "tests.sentry.integrations.repository.issue_alert.*", + "tests.sentry.integrations.slack.threads.*", "tests.sentry.issues", "tests.sentry.issues.endpoints", "tests.sentry.issues.endpoints.test_actionable_items", @@ -549,12 +640,25 @@ module = [ "tests.sentry.issues.test_status_change", "tests.sentry.issues.test_status_change_consumer", "tests.sentry.issues.test_update_inbox", + "tests.sentry.organizations.*", "tests.sentry.ownership.*", + "tests.sentry.post_process_forwarder.*", + "tests.sentry.profiling.*", + "tests.sentry.queue.*", "tests.sentry.ratelimits.test_leaky_bucket", "tests.sentry.relay.config.test_metric_extraction", + "tests.sentry.replays.unit.lib.*", + "tests.sentry.rules.actions.base.*", + "tests.sentry.security.*", + "tests.sentry.snuba.metrics.test_metrics_query_layer.*", + "tests.sentry.tasks.integrations.*", "tests.sentry.tasks.test_on_demand_metrics", + "tests.sentry.types.*", "tests.sentry.types.test_actor", "tests.sentry.types.test_region", + "tests.sentry.usage_accountant.*", + "tests.sentry.users.services.*", + "tests.sentry.utils.mockdata.*", "tests.sentry.web.frontend.test_cli", "tools.*", ] From 654b960afefa795769381bb48c515c34f04ce706 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 10:37:57 -0800 Subject: [PATCH 533/757] chore(onboarding): Remove unused DocumentationWrapper (#82667) --- .../onboarding/documentationWrapper.tsx | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 static/app/components/onboarding/documentationWrapper.tsx diff --git a/static/app/components/onboarding/documentationWrapper.tsx b/static/app/components/onboarding/documentationWrapper.tsx deleted file mode 100644 index 978acc8b22e903..00000000000000 --- a/static/app/components/onboarding/documentationWrapper.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import styled from '@emotion/styled'; - -import type {AlertProps} from 'sentry/components/alert'; -import {alertStyles} from 'sentry/components/alert'; -import {space} from 'sentry/styles/space'; - -type AlertType = AlertProps['type']; - -const getAlertSelector = (type: AlertType) => - type === 'muted' ? null : `.alert[level="${type}"], .alert-${type}`; - -export const DocumentationWrapper = styled('div')` - /* Size of the new footer + 16px */ - padding-bottom: calc(72px + ${space(2)}); - - h2 { - font-size: 1.375rem; - } - - h3 { - font-size: 1.25rem; - } - - h1, - h2, - h3, - h4, - h5, - h6, - p, - ul, - ol, - li { - margin-top: 0.5em; - margin-bottom: 0.5em; - } - - blockquote, - hr, - pre, - pre[class*='language-'], - div[data-language] { - margin-top: 1em; - margin-bottom: 1em; - } - - blockquote { - padding: ${space(1.5)} ${space(2)}; - ${p => alertStyles({theme: p.theme, type: 'info'})} - } - - blockquote > * { - margin: 0; - } - - .gatsby-highlight:last-child { - margin-bottom: 0; - } - - hr { - border-color: ${p => p.theme.border}; - } - - code { - color: ${p => p.theme.pink400}; - } - - .alert { - border-radius: ${p => p.theme.borderRadius}; - } - - /** - * XXX(epurkhiser): This comes from the doc styles and avoids bottom margin issues in alerts - */ - .content-flush-bottom *:last-child { - margin-bottom: 0; - } - - ${p => - Object.keys(p.theme.alert).map( - type => ` - ${getAlertSelector(type as AlertType)} { - ${alertStyles({theme: p.theme, type: type as AlertType})}; - display: block; - } - ` - )} -`; From 6173d8ffc3e0d9443ec8bb7619e8243a05e2cfe1 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 10:38:13 -0800 Subject: [PATCH 534/757] chore(ui): Remove unused NotificationBar (#82666) --- .../app/components/alerts/notificationBar.tsx | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 static/app/components/alerts/notificationBar.tsx diff --git a/static/app/components/alerts/notificationBar.tsx b/static/app/components/alerts/notificationBar.tsx deleted file mode 100644 index ab7dc6666cd47b..00000000000000 --- a/static/app/components/alerts/notificationBar.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import styled from '@emotion/styled'; - -import {IconInfo} from 'sentry/icons'; -import {space} from 'sentry/styles/space'; - -const StyledNotificationBarIconInfo = styled(IconInfo)` - margin-right: ${space(1)}; - color: ${p => p.theme.alert.info.color}; -`; - -export const NotificationBar = styled('div')` - display: flex; - align-items: center; - color: ${p => p.theme.textColor}; - background-color: ${p => p.theme.alert.info.backgroundLight}; - border-bottom: 1px solid ${p => p.theme.alert.info.border}; - padding: ${space(1.5)}; - font-size: 14px; - line-height: normal; - ${StyledNotificationBarIconInfo} { - color: ${p => p.theme.alert.info.color}; - } -`; From fcb6d26ff58c967f59a1428b51b4579d9f761f1f Mon Sep 17 00:00:00 2001 From: Michael Sun <55160142+MichaelSun48@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:50:17 -0500 Subject: [PATCH 535/757] chore(api-docs): Update documentation for group_tagkey_values (#79801) Updates documentation for the tags/{tag}/values endpoint Before: image After: image --- .../api/endpoints/group_tagkey_values.py | 60 ++++++++++++++++--- src/sentry/apidocs/examples/tags_examples.py | 54 +++++++++++++++-- src/sentry/tagstore/types.py | 2 +- 3 files changed, 100 insertions(+), 16 deletions(-) diff --git a/src/sentry/api/endpoints/group_tagkey_values.py b/src/sentry/api/endpoints/group_tagkey_values.py index 42525aed6df77d..361d1e6b0a7cd0 100644 --- a/src/sentry/api/endpoints/group_tagkey_values.py +++ b/src/sentry/api/endpoints/group_tagkey_values.py @@ -1,7 +1,10 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework.request import Request from rest_framework.response import Response from sentry import analytics, tagstore +from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import EnvironmentMixin, region_silo_endpoint from sentry.api.bases.group import GroupEndpoint @@ -9,25 +12,64 @@ from sentry.api.helpers.environments import get_environments from sentry.api.serializers import serialize from sentry.api.serializers.models.tagvalue import UserTagValueSerializer +from sentry.apidocs.constants import ( + RESPONSE_BAD_REQUEST, + RESPONSE_FORBIDDEN, + RESPONSE_NOT_FOUND, + RESPONSE_UNAUTHORIZED, +) +from sentry.apidocs.examples.tags_examples import TagsExamples +from sentry.apidocs.parameters import GlobalParams, IssueParams +from sentry.apidocs.utils import inline_sentry_response_serializer +from sentry.tagstore.types import TagValueSerializerResponse +@extend_schema(tags=["Events"]) @region_silo_endpoint class GroupTagKeyValuesEndpoint(GroupEndpoint, EnvironmentMixin): publish_status = { - "GET": ApiPublishStatus.UNKNOWN, + "GET": ApiPublishStatus.PUBLIC, } + owner = ApiOwner.ISSUES + @extend_schema( + operation_id="List a Tag's Values for an Issue", + description="Returns a list of values associated with this key for an issue.\nReturns at most 1000 values when paginated.", + parameters=[ + IssueParams.ISSUE_ID, + IssueParams.ISSUES_OR_GROUPS, + GlobalParams.ORG_ID_OR_SLUG, + OpenApiParameter( + name="key", + location=OpenApiParameter.PATH, + type=OpenApiTypes.STR, + description="The tag key to look the values up for.", + required=True, + ), + OpenApiParameter( + name="sort", + location="query", + required=False, + type=str, + description="Sort order of the resulting tag values. Prefix with '-' for descending order. Default is '-id'.", + enum=["id", "date", "age", "count"], + ), + GlobalParams.ENVIRONMENT, + ], + responses={ + 200: inline_sentry_response_serializer( + "TagKeyValuesDict", list[TagValueSerializerResponse] + ), + 400: RESPONSE_BAD_REQUEST, + 401: RESPONSE_UNAUTHORIZED, + 403: RESPONSE_FORBIDDEN, + 404: RESPONSE_NOT_FOUND, + }, + examples=[TagsExamples.GROUP_TAGKEY_VALUES], + ) def get(self, request: Request, group, key) -> Response: """ List a Tag's Values - ``````````````````` - - Return a list of values associated with this key for an issue. - When paginated can return at most 1000 values. - - :pparam string issue_id: the ID of the issue to retrieve. - :pparam string key: the tag key to look the values up for. - :auth: required """ analytics.record( "eventuser_endpoint.request", diff --git a/src/sentry/apidocs/examples/tags_examples.py b/src/sentry/apidocs/examples/tags_examples.py index 08325a83443976..de4cf843ea0dc5 100644 --- a/src/sentry/apidocs/examples/tags_examples.py +++ b/src/sentry/apidocs/examples/tags_examples.py @@ -7,17 +7,17 @@ "totalValues": 3, "topValues": [ { - "key": "chunkymonkey", - "name": "Chunky Monkey", - "value": "chunkymonkey", + "key": "strawberry", + "name": "Strawberry", + "value": "strawberry", "count": 2, "lastSeen": "2024-01-01T00:00:00Z", "firstSeen": "2024-01-01T00:00:00Z", }, { - "key": "halfbaked", - "name": "Half Baked", - "value": "halfbaked", + "key": "vanilla", + "name": "Vanilla", + "value": "vanilla", "count": 1, "lastSeen": "2024-01-01T00:00:00Z", "firstSeen": "2024-01-01T00:00:00Z", @@ -25,6 +25,41 @@ ], } +SIMPLE_TAG_VALUES = [ + { + "key": "strawberry", + "name": "Strawberry", + "value": "strawberry", + "count": 2, + "lastSeen": "2024-01-01T00:00:00Z", + "firstSeen": "2024-01-01T00:00:00Z", + }, + { + "key": "vanilla", + "name": "Vanilla", + "value": "vanilla", + "count": 1, + "lastSeen": "2024-01-01T00:00:00Z", + "firstSeen": "2024-01-01T00:00:00Z", + }, + { + "key": "chocolate", + "name": "Chocolate", + "value": "chocolate", + "count": 1, + "lastSeen": "2024-01-01T00:00:00Z", + "firstSeen": "2024-01-01T00:00:00Z", + }, + { + "key": "Neopolitan", + "name": "Neopolitan", + "value": "neopolitan", + "count": 1, + "lastSeen": "2024-01-01T00:00:00Z", + "firstSeen": "2024-01-01T00:00:00Z", + }, +] + class TagsExamples: GROUP_TAGKEY_DETAILS = OpenApiExample( @@ -33,3 +68,10 @@ class TagsExamples: response_only=True, status_codes=["200"], ) + + GROUP_TAGKEY_VALUES = OpenApiExample( + "Return all tag values for a specific tag", + value=SIMPLE_TAG_VALUES, + response_only=True, + status_codes=["200"], + ) diff --git a/src/sentry/tagstore/types.py b/src/sentry/tagstore/types.py index 2ca77f628792ef..4c31ca71f5f0f0 100644 --- a/src/sentry/tagstore/types.py +++ b/src/sentry/tagstore/types.py @@ -137,7 +137,7 @@ class TagValueSerializerResponse(TagValueSerializerResponseOptional): @register(GroupTagValue) @register(TagValue) class TagValueSerializer(Serializer): - def serialize(self, obj, attrs, user, **kwargs): + def serialize(self, obj, attrs, user, **kwargs) -> TagValueSerializerResponse: from sentry import tagstore key = tagstore.get_standardized_key(obj.key) From 792f16b772cc9df72b7fdb964bff7b4cbf202832 Mon Sep 17 00:00:00 2001 From: Abdullah Khan <60121741+Abdkhan14@users.noreply.github.com> Date: Mon, 30 Dec 2024 13:50:29 -0500 Subject: [PATCH 536/757] feat(new-trace): Updating trace load meta data event (#82660) Added a `referrer` attr and a `has_exceeded_performance_usage_limit` attr. --------- Co-authored-by: Abdullah Khan --- .../performance/eventTraceView.spec.tsx | 10 ++++ .../app/utils/analytics/tracingEventMap.tsx | 2 + .../newTraceDetails/issuesTraceWaterfall.tsx | 18 +++++- .../newTraceDetails/trace.spec.tsx | 20 +++++++ .../newTraceDetails/traceAnalytics.tsx | 18 +++++- .../traceTypeWarnings/errorsOnlyWarnings.tsx | 50 +++-------------- .../usePerformanceSubscriptionDetails.tsx | 56 +++++++++++++++++++ .../newTraceDetails/traceWaterfall.tsx | 17 +++++- 8 files changed, 144 insertions(+), 47 deletions(-) create mode 100644 static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx diff --git a/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx b/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx index a14134f75d6d75..33fb521eb0b941 100644 --- a/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx +++ b/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx @@ -46,6 +46,11 @@ describe('EventTraceView', () => { }); it('renders a trace', async () => { + MockApiClient.addMockResponse({ + url: '/subscriptions/org-slug/', + method: 'GET', + body: {}, + }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/${organization.slug}/events-trace-meta/${traceId}/`, @@ -161,6 +166,11 @@ describe('EventTraceView', () => { }); it('does not render the trace preview if it has no transactions', async () => { + MockApiClient.addMockResponse({ + url: '/subscriptions/org-slug/', + method: 'GET', + body: {}, + }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/${organization.slug}/events-trace-meta/${traceId}/`, diff --git a/static/app/utils/analytics/tracingEventMap.tsx b/static/app/utils/analytics/tracingEventMap.tsx index 7a0f8df03818fc..c54f2c12058605 100644 --- a/static/app/utils/analytics/tracingEventMap.tsx +++ b/static/app/utils/analytics/tracingEventMap.tsx @@ -17,9 +17,11 @@ export type TracingEventParameters = { visualizes_count: number; }; 'trace.metadata': { + has_exceeded_performance_usage_limit: boolean | null; num_nodes: number; num_root_children: number; project_platforms: string[]; + referrer: string | null; shape: string; trace_duration_seconds: number; }; diff --git a/static/app/views/performance/newTraceDetails/issuesTraceWaterfall.tsx b/static/app/views/performance/newTraceDetails/issuesTraceWaterfall.tsx index ee33598a381b09..bb0db8e1686ee3 100644 --- a/static/app/views/performance/newTraceDetails/issuesTraceWaterfall.tsx +++ b/static/app/views/performance/newTraceDetails/issuesTraceWaterfall.tsx @@ -31,6 +31,7 @@ import {TraceScheduler} from './traceRenderers/traceScheduler'; import {TraceView as TraceViewModel} from './traceRenderers/traceView'; import {VirtualizedViewManager} from './traceRenderers/virtualizedViewManager'; import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvider'; +import {usePerformanceSubscriptionDetails} from './traceTypeWarnings/usePerformanceSubscriptionDetails'; import {Trace} from './trace'; import {traceAnalytics} from './traceAnalytics'; import type {TraceReducerState} from './traceState'; @@ -133,11 +134,24 @@ export function IssuesTraceWaterfall(props: IssuesTraceWaterfallProps) { [organization, projects, traceDispatch] ); + const { + data: {hasExceededPerformanceUsageLimit}, + isLoading: isLoadingSubscriptionDetails, + } = usePerformanceSubscriptionDetails(); + // Callback that is invoked when the trace loads and reaches its initialied state, // that is when the trace tree data and any data that the trace depends on is loaded, // but the trace is not yet rendered in the view. const onTraceLoad = useCallback(() => { - traceAnalytics.trackTraceShape(props.tree, projectsRef.current, props.organization); + if (!isLoadingSubscriptionDetails) { + traceAnalytics.trackTraceShape( + props.tree, + projectsRef.current, + props.organization, + hasExceededPerformanceUsageLimit + ); + } + // Construct the visual representation of the tree props.tree.build(); @@ -265,6 +279,8 @@ export function IssuesTraceWaterfall(props: IssuesTraceWaterfallProps) { props.tree, props.organization, props.event, + isLoadingSubscriptionDetails, + hasExceededPerformanceUsageLimit, ]); useTraceTimelineChangeSync({ diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 0ed38823ccf8bf..bd315400954ecf 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -92,6 +92,15 @@ function mockTraceResponse(resp?: Partial) { }); } +function mockPerformanceSubscriptionDetailsResponse(resp?: Partial) { + MockApiClient.addMockResponse({ + url: '/subscriptions/org-slug/', + method: 'GET', + asyncDelay: 1, + ...(resp ?? {body: {}}), + }); +} + function mockTraceMetaResponse(resp?: Partial) { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-trace-meta/trace-id/', @@ -234,6 +243,7 @@ function getVirtualizedRows(container: HTMLElement) { } async function keyboardNavigationTestSetup() { + mockPerformanceSubscriptionDetailsResponse(); const keyboard_navigation_transactions: TraceFullDetailed[] = []; for (let i = 0; i < 1e2; i++) { keyboard_navigation_transactions.push( @@ -285,6 +295,7 @@ async function keyboardNavigationTestSetup() { } async function pageloadTestSetup() { + mockPerformanceSubscriptionDetailsResponse(); const pageloadTransactions: TraceFullDetailed[] = []; for (let i = 0; i < 1e3; i++) { pageloadTransactions.push( @@ -339,6 +350,7 @@ async function pageloadTestSetup() { } async function nestedTransactionsTestSetup() { + mockPerformanceSubscriptionDetailsResponse(); const transactions: TraceFullDetailed[] = []; let txn = makeTransaction({ @@ -395,6 +407,7 @@ async function nestedTransactionsTestSetup() { } async function searchTestSetup() { + mockPerformanceSubscriptionDetailsResponse(); const transactions: TraceFullDetailed[] = []; for (let i = 0; i < 11; i++) { transactions.push( @@ -448,6 +461,7 @@ async function searchTestSetup() { } async function simpleTestSetup() { + mockPerformanceSubscriptionDetailsResponse(); const transactions: TraceFullDetailed[] = []; let parent: any; for (let i = 0; i < 1e3; i++) { @@ -505,6 +519,7 @@ async function simpleTestSetup() { } async function completeTestSetup() { + mockPerformanceSubscriptionDetailsResponse(); const start = Date.now() / 1e3; mockTraceResponse({ @@ -834,6 +849,7 @@ describe('trace view', () => { }); it('renders loading state', async () => { + mockPerformanceSubscriptionDetailsResponse(); mockTraceResponse(); mockTraceMetaResponse(); mockTraceTagsResponse(); @@ -843,6 +859,7 @@ describe('trace view', () => { }); it('renders error state if trace fails to load', async () => { + mockPerformanceSubscriptionDetailsResponse(); mockTraceResponse({statusCode: 404}); mockTraceMetaResponse({statusCode: 404}); mockTraceTagsResponse({statusCode: 404}); @@ -852,6 +869,7 @@ describe('trace view', () => { }); it('renders error state if meta fails to load', async () => { + mockPerformanceSubscriptionDetailsResponse(); mockTraceResponse({ statusCode: 200, body: { @@ -867,6 +885,7 @@ describe('trace view', () => { }); it('renders empty state', async () => { + mockPerformanceSubscriptionDetailsResponse(); mockTraceResponse({ body: { transactions: [], @@ -1573,6 +1592,7 @@ describe('trace view', () => { }); it('during search, expanding a row retriggers search', async () => { + mockPerformanceSubscriptionDetailsResponse(); mockTraceRootFacets(); mockTraceRootEvent('0'); mockTraceEventDetails(); diff --git a/static/app/views/performance/newTraceDetails/traceAnalytics.tsx b/static/app/views/performance/newTraceDetails/traceAnalytics.tsx index a64157282041b9..fc423cb5bf46d7 100644 --- a/static/app/views/performance/newTraceDetails/traceAnalytics.tsx +++ b/static/app/views/performance/newTraceDetails/traceAnalytics.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +import * as qs from 'query-string'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; @@ -9,7 +10,8 @@ import {TraceShape, type TraceTree} from './traceModels/traceTree'; const trackTraceMetadata = ( tree: TraceTree, projects: Project[], - organization: Organization + organization: Organization, + hasExceededPerformanceUsageLimit: boolean | null ) => { Sentry.metrics.increment(`trace.trace_shape.${tree.shape}`); @@ -25,10 +27,14 @@ const trackTraceMetadata = ( .filter(p => projectSlugs.includes(p.slug)) .map(project => project?.platform ?? ''); + const query = qs.parse(location.search); + trackAnalytics('trace.metadata', { shape: tree.shape, // round trace_duration_seconds to nearest two decimal places trace_duration_seconds: Math.round(trace_duration_seconds * 100) / 100, + has_exceeded_performance_usage_limit: hasExceededPerformanceUsageLimit, + referrer: query.source?.toString() || null, num_root_children: tree.root.children.length, num_nodes: tree.list.length, project_platforms: projectPlatforms, @@ -169,7 +175,8 @@ const trackMissingInstrumentationPreferenceChange = ( function trackTraceShape( tree: TraceTree, projects: Project[], - organization: Organization + organization: Organization, + hasExceededPerformanceUsageLimit: boolean | null ) { switch (tree.shape) { case TraceShape.BROKEN_SUBTRACES: @@ -179,7 +186,12 @@ function trackTraceShape( case TraceShape.NO_ROOT: case TraceShape.ONLY_ERRORS: case TraceShape.BROWSER_MULTIPLE_ROOTS: - traceAnalytics.trackTraceMetadata(tree, projects, organization); + traceAnalytics.trackTraceMetadata( + tree, + projects, + organization, + hasExceededPerformanceUsageLimit + ); break; default: { Sentry.captureMessage('Unknown trace type'); diff --git a/static/app/views/performance/newTraceDetails/traceTypeWarnings/errorsOnlyWarnings.tsx b/static/app/views/performance/newTraceDetails/traceTypeWarnings/errorsOnlyWarnings.tsx index 74f47314fd6ab2..5728bb81fe862d 100644 --- a/static/app/views/performance/newTraceDetails/traceTypeWarnings/errorsOnlyWarnings.tsx +++ b/static/app/views/performance/newTraceDetails/traceTypeWarnings/errorsOnlyWarnings.tsx @@ -11,7 +11,6 @@ import SidebarPanelStore from 'sentry/stores/sidebarPanelStore'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {browserHistory} from 'sentry/utils/browserHistory'; -import {useApiQuery} from 'sentry/utils/queryClient'; import {useLocation} from 'sentry/utils/useLocation'; import useProjects from 'sentry/utils/useProjects'; import {getDocsLinkForEventType} from 'sentry/views/settings/account/notifications/utils'; @@ -21,6 +20,7 @@ import type {TraceTree} from '../traceModels/traceTree'; import {TraceShape} from '../traceModels/traceTree'; import {TraceWarningComponents} from './styles'; +import {usePerformanceSubscriptionDetails} from './usePerformanceSubscriptionDetails'; import {usePerformanceUsageStats} from './usePerformanceUsageStats'; type ErrorOnlyWarningsProps = { @@ -126,59 +126,23 @@ function PerformanceSetupBanner({ ); } -type Subscription = { - categories: - | { - transactions: { - usageExceeded: boolean; - }; - } - | { - spans: { - usageExceeded: boolean; - }; - }; - planDetails: { - billingInterval: 'monthly' | 'annual'; - }; - planTier: string; - onDemandBudgets?: { - enabled: boolean; - }; -}; - function PerformanceQuotaExceededWarning(props: ErrorOnlyWarningsProps) { const {data: performanceUsageStats} = usePerformanceUsageStats({ organization: props.organization, tree: props.tree, }); - const {data: subscription} = useApiQuery( - [`/subscriptions/${props.organization.slug}/`], - { - staleTime: Infinity, - } - ); + const { + data: {hasExceededPerformanceUsageLimit, subscription}, + } = usePerformanceSubscriptionDetails(); // Check if events were dropped due to exceeding the transaction quota, around when the trace occurred. const droppedTransactionsCount = performanceUsageStats?.totals['sum(quantity)'] || 0; - // Check if the organization still has transaction quota maxed out. - const dataCategories = subscription?.categories; - let hasExceededTransactionLimit = false; - - if (dataCategories) { - if ('transactions' in dataCategories) { - hasExceededTransactionLimit = dataCategories.transactions.usageExceeded || false; - } else if ('spans' in dataCategories) { - hasExceededTransactionLimit = dataCategories.spans.usageExceeded || false; - } - } - const hideBanner = droppedTransactionsCount === 0 || !props.organization.features.includes('trace-view-quota-exceeded-banner') || - !hasExceededTransactionLimit; + !hasExceededPerformanceUsageLimit; useEffect(() => { if (hideBanner) { @@ -235,7 +199,9 @@ function PerformanceQuotaExceededWarning(props: ErrorOnlyWarningsProps) { }); }} docsRoute={getDocsLinkForEventType( - dataCategories && 'spans' in dataCategories ? 'span' : 'transaction' + subscription?.categories && 'spans' in subscription.categories + ? 'span' + : 'transaction' )} primaryButtonText={ctaText} /> diff --git a/static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx b/static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx new file mode 100644 index 00000000000000..d30b9b59a523d8 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails.tsx @@ -0,0 +1,56 @@ +import {useApiQuery} from 'sentry/utils/queryClient'; +import useOrganization from 'sentry/utils/useOrganization'; + +// Note: This does not fully represent the actual Subscription type. +// Contains only the subset of attributes that we used in the hook. +type Subscription = { + categories: + | { + transactions: { + usageExceeded: boolean; + }; + } + | { + spans: { + usageExceeded: boolean; + }; + }; + planDetails: { + billingInterval: 'monthly' | 'annual'; + }; + planTier: string; + onDemandBudgets?: { + enabled: boolean; + }; +}; + +export function usePerformanceSubscriptionDetails() { + const organization = useOrganization(); + + const {data: subscription, ...rest} = useApiQuery( + [`/subscriptions/${organization.slug}/`], + { + staleTime: Infinity, + } + ); + + let hasExceededPerformanceUsageLimit: boolean | null = null; + + const dataCategories = subscription?.categories; + if (dataCategories) { + if ('transactions' in dataCategories) { + hasExceededPerformanceUsageLimit = + dataCategories.transactions.usageExceeded || false; + } else if ('spans' in dataCategories) { + hasExceededPerformanceUsageLimit = dataCategories.spans.usageExceeded || false; + } + } + + return { + ...rest, + data: { + hasExceededPerformanceUsageLimit, + subscription, + }, + }; +} diff --git a/static/app/views/performance/newTraceDetails/traceWaterfall.tsx b/static/app/views/performance/newTraceDetails/traceWaterfall.tsx index 655cadc16e092f..c47b65bf4e9139 100644 --- a/static/app/views/performance/newTraceDetails/traceWaterfall.tsx +++ b/static/app/views/performance/newTraceDetails/traceWaterfall.tsx @@ -63,6 +63,7 @@ import { useTraceStateDispatch, useTraceStateEmitter, } from './traceState/traceStateProvider'; +import {usePerformanceSubscriptionDetails} from './traceTypeWarnings/usePerformanceSubscriptionDetails'; import {Trace} from './trace'; import TraceActionsMenu from './traceActionsMenu'; import {traceAnalytics} from './traceAnalytics'; @@ -514,11 +515,23 @@ export function TraceWaterfall(props: TraceWaterfallProps) { [onScrollToNode, setRowAsFocused] ); + const { + data: {hasExceededPerformanceUsageLimit}, + isLoading: isLoadingSubscriptionDetails, + } = usePerformanceSubscriptionDetails(); + // Callback that is invoked when the trace loads and reaches its initialied state, // that is when the trace tree data and any data that the trace depends on is loaded, // but the trace is not yet rendered in the view. const onTraceLoad = useCallback(() => { - traceAnalytics.trackTraceShape(props.tree, projectsRef.current, props.organization); + if (!isLoadingSubscriptionDetails) { + traceAnalytics.trackTraceShape( + props.tree, + projectsRef.current, + props.organization, + hasExceededPerformanceUsageLimit + ); + } // The tree has the data fetched, but does not yet respect the user preferences. // We will autogroup and inject missing instrumentation if the preferences are set. // and then we will perform a search to find the node the user is interested in. @@ -617,6 +630,8 @@ export function TraceWaterfall(props: TraceWaterfallProps) { scrollQueueRef, props.tree, props.organization, + isLoadingSubscriptionDetails, + hasExceededPerformanceUsageLimit, ]); // Setup the middleware for the trace reducer From 8ccd9fc3a281215eb952a069416c32ddc2bad96d Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Mon, 30 Dec 2024 11:00:20 -0800 Subject: [PATCH 537/757] :bug: fix(integration middleware): filter active integrations (#82570) --- .../middleware/hybrid_cloud/parser.py | 4 +- .../middleware/hybrid_cloud/test_base.py | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/sentry/integrations/middleware/hybrid_cloud/parser.py b/src/sentry/integrations/middleware/hybrid_cloud/parser.py index b1740f1e250b87..e1226f7c55e020 100644 --- a/src/sentry/integrations/middleware/hybrid_cloud/parser.py +++ b/src/sentry/integrations/middleware/hybrid_cloud/parser.py @@ -12,6 +12,7 @@ from rest_framework import status from sentry.api.base import ONE_DAY +from sentry.constants import ObjectStatus from sentry.hybridcloud.models.webhookpayload import WebhookPayload from sentry.hybridcloud.outbox.category import WebhookProviderIdentifier from sentry.hybridcloud.services.organization_mapping import organization_mapping_service @@ -366,7 +367,8 @@ def get_organizations_from_integration( logger.info("%s.no_integration", self.provider, extra={"path": self.request.path}) raise Integration.DoesNotExist() organization_integrations = OrganizationIntegration.objects.filter( - integration_id=integration.id + integration_id=integration.id, + status=ObjectStatus.ACTIVE, ) if organization_integrations.count() == 0: diff --git a/tests/sentry/integrations/middleware/hybrid_cloud/test_base.py b/tests/sentry/integrations/middleware/hybrid_cloud/test_base.py index 6c8cbf3579bed6..685c2a020b67c1 100644 --- a/tests/sentry/integrations/middleware/hybrid_cloud/test_base.py +++ b/tests/sentry/integrations/middleware/hybrid_cloud/test_base.py @@ -6,9 +6,11 @@ from pytest import raises from rest_framework import status +from sentry.constants import ObjectStatus from sentry.hybridcloud.models.webhookpayload import WebhookPayload from sentry.hybridcloud.outbox.category import WebhookProviderIdentifier from sentry.integrations.middleware.hybrid_cloud.parser import BaseRequestParser +from sentry.integrations.models.organization_integration import OrganizationIntegration from sentry.silo.base import SiloLimit, SiloMode from sentry.testutils.cases import TestCase from sentry.types.region import Region, RegionCategory @@ -106,3 +108,47 @@ class MockParser(BaseRequestParser): assert payload.mailbox_name == "slack:0" assert payload.request_path assert payload.request_method + + @override_settings(SILO_MODE=SiloMode.CONTROL) + def test_get_organizations_from_integration_success(self): + integration = self.create_integration( + organization=self.organization, + provider="test_provider", + external_id="test_external_id", + ) + # Create additional org integration to test multiple orgs + other_org = self.create_organization() + OrganizationIntegration.objects.create( + organization_id=other_org.id, + integration_id=integration.id, + status=ObjectStatus.ACTIVE, + ) + + parser = BaseRequestParser(self.request, self.response_handler) + organizations = parser.get_organizations_from_integration(integration) + + assert len(organizations) == 2 + org_ids = {org.id for org in organizations} + assert self.organization.id in org_ids + assert other_org.id in org_ids + + @override_settings(SILO_MODE=SiloMode.CONTROL) + @patch("sentry.integrations.middleware.hybrid_cloud.parser.logger.info") + def test_get_organizations_from_integration_inactive_org(self, mock_log): + integration = self.create_integration( + organization=self.organization, + provider="test_provider", + external_id="test_external_id", + ) + + other_org = self.create_organization() + OrganizationIntegration.objects.create( + organization_id=other_org.id, + integration_id=integration.id, + status=ObjectStatus.DISABLED, + ) + + parser = BaseRequestParser(self.request, self.response_handler) + organizations = parser.get_organizations_from_integration(integration) + assert len(organizations) == 1 + assert organizations[0].id == self.organization.id From 43a572a172e7ef8a0287086f95464844237c24fa Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:04:30 -0500 Subject: [PATCH 538/757] ref: fix types for sentry.integrations.jira.actions.form (#82669) --- pyproject.toml | 2 +- src/sentry/integrations/jira/actions/form.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0e08cd5e45ddd2..e92fa1a7406510 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -207,7 +207,6 @@ module = [ "sentry.integrations.gitlab.client", "sentry.integrations.gitlab.integration", "sentry.integrations.gitlab.issues", - "sentry.integrations.jira.actions.form", "sentry.integrations.jira.client", "sentry.integrations.jira.integration", "sentry.integrations.jira.views.base", @@ -406,6 +405,7 @@ module = [ "sentry.integrations.discord.webhooks.*", "sentry.integrations.github.actions.*", "sentry.integrations.github_enterprise.actions.*", + "sentry.integrations.jira.actions.*", "sentry.integrations.jira.endpoints.*", "sentry.integrations.jira.models.*", "sentry.integrations.jira_server.actions.*", diff --git a/src/sentry/integrations/jira/actions/form.py b/src/sentry/integrations/jira/actions/form.py index 7c5d1f7acfe2b9..22013a1f9872d0 100644 --- a/src/sentry/integrations/jira/actions/form.py +++ b/src/sentry/integrations/jira/actions/form.py @@ -14,6 +14,8 @@ class JiraNotifyServiceForm(IntegrationNotifyServiceForm): def clean(self) -> dict[str, Any] | None: cleaned_data = super().clean() + if cleaned_data is None: + return None integration_id = cleaned_data.get("integration") integration = integration_service.get_integration( From 925be7b37646345f79ec88565e434fe8de1bf07e Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 11:15:37 -0800 Subject: [PATCH 539/757] feat(react19): Remove findDomNode in ContextPickerModal (#82635) --- .../components/contextPickerModal.spec.tsx | 3 ++ static/app/components/contextPickerModal.tsx | 48 ++++--------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/static/app/components/contextPickerModal.spec.tsx b/static/app/components/contextPickerModal.spec.tsx index 06bc498defb7b0..88dce5a215711f 100644 --- a/static/app/components/contextPickerModal.spec.tsx +++ b/static/app/components/contextPickerModal.spec.tsx @@ -65,6 +65,7 @@ describe('ContextPickerModal', function () { render(getComponent()); expect(screen.getByText('Select an Organization')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toHaveFocus(); expect(screen.queryByText('Select a Project to continue')).not.toBeInTheDocument(); }); @@ -144,6 +145,7 @@ describe('ContextPickerModal', function () { // Should see 1 selected, and 1 as an option expect(screen.getAllByText('org-slug')).toHaveLength(2); + expect(screen.getByRole('textbox')).toHaveFocus(); expect(await screen.findByText('My Projects')).toBeInTheDocument(); expect(screen.getByText(project.slug)).toBeInTheDocument(); expect(screen.getByText(project2.slug)).toBeInTheDocument(); @@ -180,6 +182,7 @@ describe('ContextPickerModal', function () { // Should not have anything selected expect(screen.getByText('Select an Organization')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toHaveFocus(); // Select org2 await selectEvent.select(screen.getByText('Select an Organization'), org2.slug); diff --git a/static/app/components/contextPickerModal.tsx b/static/app/components/contextPickerModal.tsx index 4e5cf53c3f6b53..7955357540a144 100644 --- a/static/app/components/contextPickerModal.tsx +++ b/static/app/components/contextPickerModal.tsx @@ -1,5 +1,4 @@ import {Component, Fragment} from 'react'; -import {findDOMNode} from 'react-dom'; import {components} from 'react-select'; import styled from '@emotion/styled'; import type {Query} from 'history'; @@ -69,6 +68,10 @@ type Props = ModalRenderProps & { allowAllProjectsSelection?: boolean; }; +function autoFocusReactSelect(reactSelectRef: any) { + reactSelectRef?.select?.focus?.(); +} + const selectStyles: StylesConfig = { menu: provided => ({ ...provided, @@ -116,12 +119,6 @@ class ContextPickerModal extends Component { onFinishTimeout: number | undefined = undefined; - // TODO(ts) The various generics in react-select types make getting this - // right hard. - orgSelect: any | null = null; - projectSelect: any | null = null; - configSelect: any | null = null; - // Performs checks to see if we need to prompt user // i.e. When there is only 1 org and no project is needed or // there is only 1 org and only 1 project (which should be rare) @@ -178,21 +175,6 @@ class ContextPickerModal extends Component { ) ?? undefined; }; - doFocus = (ref: any | null) => { - if (!ref || this.props.loading) { - return; - } - - // eslint-disable-next-line react/no-find-dom-node - const el = findDOMNode(ref) as HTMLElement; - - if (el !== null) { - const input = el.querySelector('input'); - - input?.focus(); - } - }; - handleSelectOrganization = ({value}: {value: string}) => { // If we do not need to select a project, we can early return after selecting an org // No need to fetch org details @@ -316,10 +298,7 @@ class ContextPickerModal extends Component { return ( { - this.projectSelect = ref; - this.doFocus(this.projectSelect); - }} + ref={autoFocusReactSelect} placeholder={t('Select a Project to continue')} name="project" options={projectOptions} @@ -354,10 +333,7 @@ class ContextPickerModal extends Component { ]; return ( { - this.configSelect = ref; - this.doFocus(this.configSelect); - }} + ref={autoFocusReactSelect} placeholder={t('Select a configuration to continue')} name="configurations" options={options} @@ -398,18 +374,14 @@ class ContextPickerModal extends Component { return ( -
      {this.headerText}
      +
      +
      {this.headerText}
      +
      {loading && } {needOrg && ( { - this.orgSelect = ref; - if (shouldShowProjectSelector) { - return; - } - this.doFocus(this.orgSelect); - }} + ref={shouldShowProjectSelector ? undefined : autoFocusReactSelect} placeholder={t('Select an Organization')} name="organization" options={orgChoices} From 684d314294289eddcff146b55a9fc3c74840e643 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 11:21:33 -0800 Subject: [PATCH 540/757] chore(ui): Remove unused requestIdleCallback (#82673) --- static/app/utils/window/requestIdleCallback.tsx | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 static/app/utils/window/requestIdleCallback.tsx diff --git a/static/app/utils/window/requestIdleCallback.tsx b/static/app/utils/window/requestIdleCallback.tsx deleted file mode 100644 index 6a07ed082587aa..00000000000000 --- a/static/app/utils/window/requestIdleCallback.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const requestIdleCallback = - window.requestIdleCallback || - function requestIdleCallbackPolyfill(cb) { - const start = Date.now(); - return setTimeout(function () { - cb({ - didTimeout: false, - timeRemaining: function () { - return Math.max(0, 50 - (Date.now() - start)); - }, - }); - }, 1); - }; - -export default requestIdleCallback; From 1c21f08d4418e50e0c1598ace43485b0a2314144 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Mon, 30 Dec 2024 11:22:38 -0800 Subject: [PATCH 541/757] ref(js): Deprecate SmartSearchBar (#82672) --- .../actionButton.tsx | 0 .../index.spec.tsx | 143 +++++++++++------- .../index.tsx | 9 +- .../searchBarDatePicker.tsx | 0 .../searchDropdown.tsx | 0 .../searchHotkeysListener.tsx | 0 .../searchInvalidTag.tsx | 0 .../types.tsx | 0 .../utils.spec.tsx | 2 +- .../utils.tsx | 0 static/app/components/events/searchBar.tsx | 2 +- .../components/metrics/metricSamplesTable.tsx | 2 +- .../components/metrics/metricSearchBar.tsx | 2 +- .../components/metrics/queryFieldGroup.tsx | 2 +- .../app/components/performance/searchBar.tsx | 8 +- .../searchQueryBuilder/index.stories.tsx | 2 +- .../tokens/filter/valueCombobox.tsx | 10 +- static/app/types/group.tsx | 2 +- .../utils/analytics/searchAnalyticsEvents.tsx | 2 +- .../views/issueList/issueListSetAsDefault.tsx | 2 +- .../issueList/utils/useFetchIssueTags.tsx | 7 +- .../transactionProfiles/index.tsx | 2 +- static/app/views/profiling/content.tsx | 2 +- .../views/profiling/profileSummary/index.tsx | 2 +- .../views/replays/list/replaySearchBar.tsx | 2 +- 25 files changed, 121 insertions(+), 82 deletions(-) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/actionButton.tsx (100%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/index.spec.tsx (89%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/index.tsx (99%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/searchBarDatePicker.tsx (100%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/searchDropdown.tsx (100%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/searchHotkeysListener.tsx (100%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/searchInvalidTag.tsx (100%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/types.tsx (100%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/utils.spec.tsx (99%) rename static/app/components/{smartSearchBar => deprecatedSmartSearchBar}/utils.tsx (100%) diff --git a/static/app/components/smartSearchBar/actionButton.tsx b/static/app/components/deprecatedSmartSearchBar/actionButton.tsx similarity index 100% rename from static/app/components/smartSearchBar/actionButton.tsx rename to static/app/components/deprecatedSmartSearchBar/actionButton.tsx diff --git a/static/app/components/smartSearchBar/index.spec.tsx b/static/app/components/deprecatedSmartSearchBar/index.spec.tsx similarity index 89% rename from static/app/components/smartSearchBar/index.spec.tsx rename to static/app/components/deprecatedSmartSearchBar/index.spec.tsx index fa0862cd53552c..4141bbc0a33d35 100644 --- a/static/app/components/smartSearchBar/index.spec.tsx +++ b/static/app/components/deprecatedSmartSearchBar/index.spec.tsx @@ -11,7 +11,7 @@ import { waitFor, } from 'sentry-test/reactTestingLibrary'; -import {SmartSearchBar} from 'sentry/components/smartSearchBar'; +import {DeprecatedSmartSearchBar} from 'sentry/components/deprecatedSmartSearchBar'; import TagStore from 'sentry/stores/tagStore'; import {FieldKey} from 'sentry/utils/fields'; @@ -68,7 +68,9 @@ describe('SmartSearchBar', function () { .fn() .mockResolvedValue(['this is filled with spaces']); - render(); + render( + + ); const textbox = screen.getByRole('textbox'); await userEvent.click(textbox); @@ -86,7 +88,9 @@ describe('SmartSearchBar', function () { .fn() .mockResolvedValue(['this " is " filled " with " quotes']); - render(); + render( + + ); const textbox = screen.getByRole('textbox'); await userEvent.click(textbox); @@ -102,7 +106,7 @@ describe('SmartSearchBar', function () { it('does not search when pressing enter on a tag without a value', async function () { const onSearchMock = jest.fn(); - render(); + render(); const textbox = screen.getByRole('textbox'); await userEvent.type(textbox, 'browser:{enter}'); @@ -113,7 +117,7 @@ describe('SmartSearchBar', function () { it('autocompletes value with tab', async function () { const onSearchMock = jest.fn(); - render(); + render(); const textbox = screen.getByRole('textbox'); await userEvent.type(textbox, 'bro'); @@ -138,7 +142,7 @@ describe('SmartSearchBar', function () { it('autocompletes value with enter', async function () { const onSearchMock = jest.fn(); - render(); + render(); const textbox = screen.getByRole('textbox'); await userEvent.type(textbox, 'bro'); @@ -161,7 +165,7 @@ describe('SmartSearchBar', function () { }); it('searches and completes tags with negation operator', async function () { - render(); + render(); const textbox = screen.getByRole('textbox'); await userEvent.type(textbox, '!bro'); @@ -175,43 +179,51 @@ describe('SmartSearchBar', function () { describe('componentWillReceiveProps()', function () { it('should add a space when setting query', function () { - render(); + render(); expect(screen.getByRole('textbox')).toHaveValue('one '); }); it('updates query when prop changes', function () { - const {rerender} = render(); + const {rerender} = render( + + ); - rerender(); + rerender(); expect(screen.getByRole('textbox')).toHaveValue('two '); }); it('updates query when prop set to falsey value', function () { - const {rerender} = render(); + const {rerender} = render( + + ); - rerender(); + rerender(); expect(screen.getByRole('textbox')).toHaveValue(''); }); it('should not reset user textarea if a noop props change happens', async function () { - const {rerender} = render(); + const {rerender} = render( + + ); await userEvent.type(screen.getByRole('textbox'), 'two'); - rerender(); + rerender(); expect(screen.getByRole('textbox')).toHaveValue('one two'); }); it('should reset user textarea if a meaningful props change happens', async function () { - const {rerender} = render(); + const {rerender} = render( + + ); await userEvent.type(screen.getByRole('textbox'), 'two'); - rerender(); + rerender(); expect(screen.getByRole('textbox')).toHaveValue('blah '); }); @@ -222,7 +234,11 @@ describe('SmartSearchBar', function () { const mockOnSearch = jest.fn(); render( - + ); expect(screen.getByRole('textbox')).toHaveValue('is:unresolved '); @@ -238,7 +254,7 @@ describe('SmartSearchBar', function () { describe('dropdown open state', function () { it('opens the dropdown when the search box is clicked', async function () { - render(); + render(); const textbox = screen.getByRole('textbox'); @@ -248,7 +264,7 @@ describe('SmartSearchBar', function () { }); it('opens the dropdown when the search box gains focus', function () { - render(); + render(); const textbox = screen.getByRole('textbox'); @@ -260,7 +276,7 @@ describe('SmartSearchBar', function () { it('hides the drop down when clicking outside', async function () { render(
      - +
      ); @@ -275,7 +291,7 @@ describe('SmartSearchBar', function () { }); it('hides the drop down when pressing escape', async function () { - render(); + render(); const textbox = screen.getByRole('textbox'); @@ -291,7 +307,7 @@ describe('SmartSearchBar', function () { describe('pasting', function () { it('trims pasted content', async function () { const mockOnChange = jest.fn(); - render(); + render(); const textbox = screen.getByRole('textbox'); @@ -306,7 +322,9 @@ describe('SmartSearchBar', function () { it('invokes onSearch() on enter', async function () { const mockOnSearch = jest.fn(); - render(); + render( + + ); await userEvent.type(screen.getByRole('textbox'), '{Enter}'); @@ -314,7 +332,7 @@ describe('SmartSearchBar', function () { }); it('handles an empty query', function () { - render(); + render(); expect(screen.getByRole('textbox')).toHaveValue(''); }); @@ -323,7 +341,7 @@ describe('SmartSearchBar', function () { const getTagValuesMock = jest.fn().mockResolvedValue([]); render( - ); + render(); // Should have three invalid tokens (tag:, is:, and has:) expect(screen.getAllByTestId('filter-token-invalid')).toHaveLength(3); @@ -429,7 +447,7 @@ describe('SmartSearchBar', function () { it('renders nested keys correctly', async function () { render( - + ); const textbox = screen.getByRole('textbox'); @@ -528,7 +550,7 @@ describe('SmartSearchBar', function () { const getTagValuesMock = jest.fn().mockResolvedValue(['Chrome', 'Firefox']); render( - @@ -819,7 +841,7 @@ describe('SmartSearchBar', function () { it('can delete a middle token', async function () { render( - @@ -840,7 +862,7 @@ describe('SmartSearchBar', function () { it('can exclude a token', async function () { render( - @@ -861,7 +883,7 @@ describe('SmartSearchBar', function () { it('can include a token', async function () { render( - @@ -885,7 +907,7 @@ describe('SmartSearchBar', function () { }); it('displays invalid field message', async function () { - render(); + render(); const textbox = screen.getByRole('textbox'); @@ -897,7 +919,7 @@ describe('SmartSearchBar', function () { }); it('displays invalid field messages for when wildcard is disallowed', async function () { - render(); + render(); const textbox = screen.getByRole('textbox'); @@ -922,7 +944,7 @@ describe('SmartSearchBar', function () { }); it('displays date picker dropdown when appropriate', async () => { - render(); + render(); const textbox = screen.getByRole('textbox'); await userEvent.click(textbox); @@ -957,7 +979,7 @@ describe('SmartSearchBar', function () { }); it('can select a suggested relative time value', async () => { - render(); + render(); await userEvent.type(screen.getByRole('textbox'), 'lastSeen:'); @@ -967,7 +989,7 @@ describe('SmartSearchBar', function () { }); it('can select a specific date/time', async () => { - render(); + render(); await userEvent.type(screen.getByRole('textbox'), 'lastSeen:'); @@ -1007,7 +1029,7 @@ describe('SmartSearchBar', function () { }); it('can change an existing datetime', async () => { - render(); + render(); const textbox = screen.getByRole('textbox'); fireEvent.change(textbox, { @@ -1035,7 +1057,7 @@ describe('SmartSearchBar', function () { }); it('populates the date picker correctly for date without time', async () => { - render(); + render(); const textbox = screen.getByRole('textbox'); @@ -1055,7 +1077,12 @@ describe('SmartSearchBar', function () { }); it('populates the date picker correctly for date with time and no timezone', async () => { - render(); + render( + + ); const textbox = screen.getByRole('textbox'); @@ -1074,7 +1101,10 @@ describe('SmartSearchBar', function () { it('populates the date picker correctly for date with time and timezone', async () => { render( - + ); const textbox = screen.getByRole('textbox'); @@ -1113,7 +1143,7 @@ describe('SmartSearchBar', function () { it('displays a default group with custom wrapper', async function () { const mockOnChange = jest.fn(); render( - + ); const textbox = screen.getByRole('textbox'); @@ -1154,7 +1187,7 @@ describe('SmartSearchBar', function () { it('hides the default group after picking item with applyFilter', async function () { render( - { +/** + * @deprecated use SearchQueryBuilder instead + */ +class DeprecatedSmartSearchBar extends Component { static defaultProps = { id: 'smart-search-input', includeLabel: true, @@ -2199,14 +2202,14 @@ class SmartSearchBarContainer extends Component { render() { // SmartSearchBar doesn't use members, but we forward it to cause a re-render. - return ; + return ; } } export default withApi(withSentryRouter(withOrganization(SmartSearchBarContainer))); export type {Props as SmartSearchBarProps}; -export {SmartSearchBar}; +export {DeprecatedSmartSearchBar}; const Container = styled('div')<{inputHasFocus: boolean}>` min-height: ${p => p.theme.form.md.height}px; diff --git a/static/app/components/smartSearchBar/searchBarDatePicker.tsx b/static/app/components/deprecatedSmartSearchBar/searchBarDatePicker.tsx similarity index 100% rename from static/app/components/smartSearchBar/searchBarDatePicker.tsx rename to static/app/components/deprecatedSmartSearchBar/searchBarDatePicker.tsx diff --git a/static/app/components/smartSearchBar/searchDropdown.tsx b/static/app/components/deprecatedSmartSearchBar/searchDropdown.tsx similarity index 100% rename from static/app/components/smartSearchBar/searchDropdown.tsx rename to static/app/components/deprecatedSmartSearchBar/searchDropdown.tsx diff --git a/static/app/components/smartSearchBar/searchHotkeysListener.tsx b/static/app/components/deprecatedSmartSearchBar/searchHotkeysListener.tsx similarity index 100% rename from static/app/components/smartSearchBar/searchHotkeysListener.tsx rename to static/app/components/deprecatedSmartSearchBar/searchHotkeysListener.tsx diff --git a/static/app/components/smartSearchBar/searchInvalidTag.tsx b/static/app/components/deprecatedSmartSearchBar/searchInvalidTag.tsx similarity index 100% rename from static/app/components/smartSearchBar/searchInvalidTag.tsx rename to static/app/components/deprecatedSmartSearchBar/searchInvalidTag.tsx diff --git a/static/app/components/smartSearchBar/types.tsx b/static/app/components/deprecatedSmartSearchBar/types.tsx similarity index 100% rename from static/app/components/smartSearchBar/types.tsx rename to static/app/components/deprecatedSmartSearchBar/types.tsx diff --git a/static/app/components/smartSearchBar/utils.spec.tsx b/static/app/components/deprecatedSmartSearchBar/utils.spec.tsx similarity index 99% rename from static/app/components/smartSearchBar/utils.spec.tsx rename to static/app/components/deprecatedSmartSearchBar/utils.spec.tsx index 27f8c5a79b6a0e..b20adafa436a5a 100644 --- a/static/app/components/smartSearchBar/utils.spec.tsx +++ b/static/app/components/deprecatedSmartSearchBar/utils.spec.tsx @@ -4,7 +4,7 @@ import { filterKeysFromQuery, getTagItemsFromKeys, removeSpace, -} from 'sentry/components/smartSearchBar/utils'; +} from 'sentry/components/deprecatedSmartSearchBar/utils'; import {FieldKey, FieldKind, getFieldDefinition} from 'sentry/utils/fields'; describe('addSpace()', function () { diff --git a/static/app/components/smartSearchBar/utils.tsx b/static/app/components/deprecatedSmartSearchBar/utils.tsx similarity index 100% rename from static/app/components/smartSearchBar/utils.tsx rename to static/app/components/deprecatedSmartSearchBar/utils.tsx diff --git a/static/app/components/events/searchBar.tsx b/static/app/components/events/searchBar.tsx index 680b73931a801b..a990c611bbc047 100644 --- a/static/app/components/events/searchBar.tsx +++ b/static/app/components/events/searchBar.tsx @@ -3,9 +3,9 @@ import memoize from 'lodash/memoize'; import omit from 'lodash/omit'; import {fetchSpanFieldValues, fetchTagValues} from 'sentry/actionCreators/tags'; +import SmartSearchBar from 'sentry/components/deprecatedSmartSearchBar'; import type {SearchConfig} from 'sentry/components/searchSyntax/parser'; import {defaultConfig} from 'sentry/components/searchSyntax/parser'; -import SmartSearchBar from 'sentry/components/smartSearchBar'; import type {TagCollection} from 'sentry/types/group'; import {SavedSearchType} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; diff --git a/static/app/components/metrics/metricSamplesTable.tsx b/static/app/components/metrics/metricSamplesTable.tsx index 0394dbb1a07c28..2e4c24fb4784f6 100644 --- a/static/app/components/metrics/metricSamplesTable.tsx +++ b/static/app/components/metrics/metricSamplesTable.tsx @@ -6,6 +6,7 @@ import debounce from 'lodash/debounce'; import {Button, LinkButton} from 'sentry/components/button'; import {Flex} from 'sentry/components/container/flex'; import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton'; +import SmartSearchBar from 'sentry/components/deprecatedSmartSearchBar'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import GridEditable, { COL_WIDTH_UNDEFINED, @@ -17,7 +18,6 @@ import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import Link from 'sentry/components/links/link'; import type {SelectionRange} from 'sentry/components/metrics/chart/types'; import PerformanceDuration from 'sentry/components/performanceDuration'; -import SmartSearchBar from 'sentry/components/smartSearchBar'; import {Tooltip} from 'sentry/components/tooltip'; import {IconProfiling} from 'sentry/icons'; import {t} from 'sentry/locale'; diff --git a/static/app/components/metrics/metricSearchBar.tsx b/static/app/components/metrics/metricSearchBar.tsx index b73d67a407aa78..f336cc2cf5f4f4 100644 --- a/static/app/components/metrics/metricSearchBar.tsx +++ b/static/app/components/metrics/metricSearchBar.tsx @@ -1,12 +1,12 @@ import {useCallback, useMemo} from 'react'; import {css, type SerializedStyles} from '@emotion/react'; +import type {SmartSearchBarProps} from 'sentry/components/deprecatedSmartSearchBar'; import {QueryFieldGroup} from 'sentry/components/metrics/queryFieldGroup'; import { SearchQueryBuilder, type SearchQueryBuilderProps, } from 'sentry/components/searchQueryBuilder'; -import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import {t} from 'sentry/locale'; import {SavedSearchType, type TagCollection} from 'sentry/types/group'; import type {MRI} from 'sentry/types/metrics'; diff --git a/static/app/components/metrics/queryFieldGroup.tsx b/static/app/components/metrics/queryFieldGroup.tsx index 2f2cc87d58d740..e7b398753353f4 100644 --- a/static/app/components/metrics/queryFieldGroup.tsx +++ b/static/app/components/metrics/queryFieldGroup.tsx @@ -10,9 +10,9 @@ import { type SelectKey, type SingleSelectProps, } from 'sentry/components/compactSelect'; +import _SmartSearchBar from 'sentry/components/deprecatedSmartSearchBar'; import {DebouncedInput as _DebouncedInput} from 'sentry/components/modals/metricWidgetViewerModal/queries'; import {SearchQueryBuilder as _SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; -import _SmartSearchBar from 'sentry/components/smartSearchBar'; import {Tooltip} from 'sentry/components/tooltip'; import {SLOW_TOOLTIP_DELAY} from 'sentry/constants'; import {IconDelete} from 'sentry/icons'; diff --git a/static/app/components/performance/searchBar.tsx b/static/app/components/performance/searchBar.tsx index 70bec1c1122e12..f26ef55e9aa1af 100644 --- a/static/app/components/performance/searchBar.tsx +++ b/static/app/components/performance/searchBar.tsx @@ -2,8 +2,8 @@ import {useCallback, useRef, useState} from 'react'; import styled from '@emotion/styled'; import debounce from 'lodash/debounce'; +import {getSearchGroupWithItemMarkedActive} from 'sentry/components/deprecatedSmartSearchBar/utils'; import BaseSearchBar from 'sentry/components/searchBar'; -import {getSearchGroupWithItemMarkedActive} from 'sentry/components/smartSearchBar/utils'; import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants'; import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; @@ -18,9 +18,9 @@ import useApi from 'sentry/utils/useApi'; import useOnClickOutside from 'sentry/utils/useOnClickOutside'; import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils'; -import SearchDropdown from '../smartSearchBar/searchDropdown'; -import type {SearchGroup} from '../smartSearchBar/types'; -import {ItemType} from '../smartSearchBar/types'; +import SearchDropdown from '../deprecatedSmartSearchBar/searchDropdown'; +import type {SearchGroup} from '../deprecatedSmartSearchBar/types'; +import {ItemType} from '../deprecatedSmartSearchBar/types'; const TRANSACTION_SEARCH_PERIOD = '14d'; diff --git a/static/app/components/searchQueryBuilder/index.stories.tsx b/static/app/components/searchQueryBuilder/index.stories.tsx index 3a0b68dcc6454f..2a9b69235ea6f5 100644 --- a/static/app/components/searchQueryBuilder/index.stories.tsx +++ b/static/app/components/searchQueryBuilder/index.stories.tsx @@ -1,5 +1,6 @@ import {Fragment, useState} from 'react'; +import {ItemType} from 'sentry/components/deprecatedSmartSearchBar/types'; import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery'; @@ -8,7 +9,6 @@ import type { FilterKeySection, } from 'sentry/components/searchQueryBuilder/types'; import {InvalidReason} from 'sentry/components/searchSyntax/parser'; -import {ItemType} from 'sentry/components/smartSearchBar/types'; import JSXNode from 'sentry/components/stories/jsxNode'; import JSXProperty from 'sentry/components/stories/jsxProperty'; import storyBook from 'sentry/stories/storyBook'; diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx index 28745fbca05b84..6d0f941f59fccf 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueCombobox.tsx @@ -7,6 +7,11 @@ import type {KeyboardEvent} from '@react-types/shared'; import Checkbox from 'sentry/components/checkbox'; import type {SelectOptionWithKey} from 'sentry/components/compactSelect/types'; import {getItemsWithKeys} from 'sentry/components/compactSelect/utils'; +import { + ItemType, + type SearchGroup, + type SearchItem, +} from 'sentry/components/deprecatedSmartSearchBar/types'; import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context'; import { type CustomComboboxMenu, @@ -44,11 +49,6 @@ import { type TokenResult, } from 'sentry/components/searchSyntax/parser'; import {getKeyName} from 'sentry/components/searchSyntax/utils'; -import { - ItemType, - type SearchGroup, - type SearchItem, -} from 'sentry/components/smartSearchBar/types'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Tag, TagCollection} from 'sentry/types/group'; diff --git a/static/app/types/group.tsx b/static/app/types/group.tsx index a4eb85bb685297..9f48379828e1cd 100644 --- a/static/app/types/group.tsx +++ b/static/app/types/group.tsx @@ -1,7 +1,7 @@ import type {LocationDescriptor} from 'history'; +import type {SearchGroup} from 'sentry/components/deprecatedSmartSearchBar/types'; import type {TitledPlugin} from 'sentry/components/group/pluginActions'; -import type {SearchGroup} from 'sentry/components/smartSearchBar/types'; import type {FieldKind} from 'sentry/utils/fields'; import type {Actor, TimeseriesValue} from './core'; diff --git a/static/app/utils/analytics/searchAnalyticsEvents.tsx b/static/app/utils/analytics/searchAnalyticsEvents.tsx index 7dec41302ff6ef..309e8b0147ccaa 100644 --- a/static/app/utils/analytics/searchAnalyticsEvents.tsx +++ b/static/app/utils/analytics/searchAnalyticsEvents.tsx @@ -1,4 +1,4 @@ -import type {ShortcutType} from 'sentry/components/smartSearchBar/types'; +import type {ShortcutType} from 'sentry/components/deprecatedSmartSearchBar/types'; type SearchEventBase = { query: string; diff --git a/static/app/views/issueList/issueListSetAsDefault.tsx b/static/app/views/issueList/issueListSetAsDefault.tsx index 490072ab8e8591..51ecaf101ad0f1 100644 --- a/static/app/views/issueList/issueListSetAsDefault.tsx +++ b/static/app/views/issueList/issueListSetAsDefault.tsx @@ -1,5 +1,5 @@ import {Button} from 'sentry/components/button'; -import {removeSpace} from 'sentry/components/smartSearchBar/utils'; +import {removeSpace} from 'sentry/components/deprecatedSmartSearchBar/utils'; import {IconBookmark} from 'sentry/icons'; import {t} from 'sentry/locale'; import {SavedSearchType} from 'sentry/types/group'; diff --git a/static/app/views/issueList/utils/useFetchIssueTags.tsx b/static/app/views/issueList/utils/useFetchIssueTags.tsx index 1c9bc1d85ad868..dc9e1e7d63cae6 100644 --- a/static/app/views/issueList/utils/useFetchIssueTags.tsx +++ b/static/app/views/issueList/utils/useFetchIssueTags.tsx @@ -1,8 +1,11 @@ import {useMemo} from 'react'; import {useFetchOrganizationTags} from 'sentry/actionCreators/tags'; -import {ItemType, type SearchGroup} from 'sentry/components/smartSearchBar/types'; -import {escapeTagValue} from 'sentry/components/smartSearchBar/utils'; +import { + ItemType, + type SearchGroup, +} from 'sentry/components/deprecatedSmartSearchBar/types'; +import {escapeTagValue} from 'sentry/components/deprecatedSmartSearchBar/utils'; import {IconStar, IconUser} from 'sentry/icons'; import {t} from 'sentry/locale'; import MemberListStore from 'sentry/stores/memberListStore'; diff --git a/static/app/views/performance/transactionSummary/transactionProfiles/index.tsx b/static/app/views/performance/transactionSummary/transactionProfiles/index.tsx index 488eb9e3eeba49..0ee0f727c32c01 100644 --- a/static/app/views/performance/transactionSummary/transactionProfiles/index.tsx +++ b/static/app/views/performance/transactionSummary/transactionProfiles/index.tsx @@ -1,12 +1,12 @@ import {useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; +import type {SmartSearchBarProps} from 'sentry/components/deprecatedSmartSearchBar'; import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder'; -import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; diff --git a/static/app/views/profiling/content.tsx b/static/app/views/profiling/content.tsx index 93331450dc658c..29187163c0a67a 100644 --- a/static/app/views/profiling/content.tsx +++ b/static/app/views/profiling/content.tsx @@ -4,6 +4,7 @@ import type {Location} from 'history'; import {Alert} from 'sentry/components/alert'; import {Button, LinkButton} from 'sentry/components/button'; +import type {SmartSearchBarProps} from 'sentry/components/deprecatedSmartSearchBar'; import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton'; import * as Layout from 'sentry/components/layouts/thirds'; import {DatePageFilter} from 'sentry/components/organizations/datePageFilter'; @@ -22,7 +23,6 @@ import { import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {SidebarPanelKey} from 'sentry/components/sidebar/types'; -import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import {TabList, Tabs} from 'sentry/components/tabs'; import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters'; import {t} from 'sentry/locale'; diff --git a/static/app/views/profiling/profileSummary/index.tsx b/static/app/views/profiling/profileSummary/index.tsx index 7852f6448f2913..03c1e057879f47 100644 --- a/static/app/views/profiling/profileSummary/index.tsx +++ b/static/app/views/profiling/profileSummary/index.tsx @@ -7,6 +7,7 @@ import {CompactSelect} from 'sentry/components/compactSelect'; import type {SelectOption} from 'sentry/components/compactSelect/types'; import Count from 'sentry/components/count'; import {DateTime} from 'sentry/components/dateTime'; +import type {SmartSearchBarProps} from 'sentry/components/deprecatedSmartSearchBar'; import ErrorBoundary from 'sentry/components/errorBoundary'; import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton'; import IdBadge from 'sentry/components/idBadge'; @@ -26,7 +27,6 @@ import type {ProfilingBreadcrumbsProps} from 'sentry/components/profiling/profil import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs'; import {SegmentedControl} from 'sentry/components/segmentedControl'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; -import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar'; import {TabList, Tabs} from 'sentry/components/tabs'; import {IconPanel} from 'sentry/icons'; import {t} from 'sentry/locale'; diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index 75c5e8191a8265..400e79be1631d9 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -2,9 +2,9 @@ import {useCallback, useMemo} from 'react'; import orderBy from 'lodash/orderBy'; import {fetchTagValues, useFetchOrganizationTags} from 'sentry/actionCreators/tags'; +import type SmartSearchBar from 'sentry/components/deprecatedSmartSearchBar'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types'; -import type SmartSearchBar from 'sentry/components/smartSearchBar'; import {t} from 'sentry/locale'; import type {PageFilters} from 'sentry/types/core'; import type {Tag, TagCollection, TagValue} from 'sentry/types/group'; From d1d67359547fa5165244e62c2220738c33125069 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 11:23:01 -0800 Subject: [PATCH 542/757] chore(ui): Remove unused org headerItem (#82671) --- .../components/organizations/headerItem.tsx | 201 ------------------ 1 file changed, 201 deletions(-) delete mode 100644 static/app/components/organizations/headerItem.tsx diff --git a/static/app/components/organizations/headerItem.tsx b/static/app/components/organizations/headerItem.tsx deleted file mode 100644 index dd048a13d0f332..00000000000000 --- a/static/app/components/organizations/headerItem.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import {forwardRef} from 'react'; -import isPropValid from '@emotion/is-prop-valid'; -import type {Theme} from '@emotion/react'; -import styled from '@emotion/styled'; -import omit from 'lodash/omit'; - -import Link from 'sentry/components/links/link'; -import {Tooltip} from 'sentry/components/tooltip'; -import {IconChevron, IconClose, IconInfo, IconLock, IconSettings} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; - -type DefaultProps = { - allowClear: boolean; -}; - -type Props = { - icon: React.ReactNode; - forwardedRef?: React.Ref; - hasChanges?: boolean; - hasSelected?: boolean; - hint?: string; - isOpen?: boolean; - loading?: boolean; - locked?: boolean; - lockedMessage?: React.ReactNode; - onClear?: () => void; - settingsLink?: string; -} & Partial & - React.HTMLAttributes; - -function HeaderItem({ - children, - isOpen, - hasSelected, - icon, - locked, - lockedMessage, - settingsLink, - hint, - loading, - forwardedRef, - onClear, - allowClear = true, - ...props -}: Props) { - const handleClear = (e: React.MouseEvent) => { - e.stopPropagation(); - onClear?.(); - }; - - const textColorProps = { - locked, - isOpen, - hasSelected, - }; - - return ( - - {icon} - - {children} - - {settingsLink && ( - - - - )} - - {hint && ( - - - - - - )} - {hasSelected && !locked && allowClear && ( - - )} - {!locked && !loading && ( - - - - )} - {locked && ( - - - - )} - - ); -} - -// Infer props here because of styled/theme -const getColor = (p: ColorProps & {theme: Theme}) => { - if (p.locked) { - return p.theme.gray300; - } - return p.isOpen || p.hasSelected ? p.theme.textColor : p.theme.gray300; -}; - -type ColorProps = { - hasSelected?: boolean; - isOpen?: boolean; - locked?: boolean; -}; - -const StyledHeaderItem = styled('div', { - shouldForwardProp: p => typeof p === 'string' && isPropValid(p) && p !== 'loading', -})< - ColorProps & { - loading: boolean; - } ->` - display: flex; - padding: 0 ${space(4)}; - align-items: center; - cursor: ${p => (p.loading ? 'progress' : p.locked ? 'text' : 'pointer')}; - color: ${getColor}; - transition: 0.1s color; - user-select: none; -`; - -const Content = styled('div')` - display: flex; - flex: 1; - width: 0; - white-space: nowrap; - overflow: hidden; - margin-right: ${space(1.5)}; -`; - -const StyledContent = styled('div')` - overflow: hidden; - text-overflow: ellipsis; -`; - -const IconContainer = styled('span', {shouldForwardProp: isPropValid})` - color: ${getColor}; - margin-right: ${space(1.5)}; - display: flex; - font-size: ${p => p.theme.fontSizeMedium}; -`; - -const Hint = styled('div')` - position: relative; - top: ${space(0.25)}; - margin-right: ${space(1)}; -`; - -const StyledClose = styled(IconClose, {shouldForwardProp: isPropValid})` - color: ${getColor}; - height: ${space(1.5)}; - width: ${space(1.5)}; - stroke-width: 1.5; - padding: ${space(1)}; - box-sizing: content-box; - margin: -${space(1)} 0px -${space(1)} -${space(1)}; -`; - -const ChevronWrapper = styled('div')` - width: ${space(2)}; - height: ${space(2)}; - display: flex; - align-items: center; - justify-content: center; -`; - -const StyledChevron = styled(IconChevron, {shouldForwardProp: isPropValid})<{ - isOpen: boolean; -}>` - color: ${getColor}; -`; - -const SettingsIconLink = styled(Link)` - color: ${p => p.theme.gray300}; - align-items: center; - display: inline-flex; - justify-content: space-between; - margin-right: ${space(1.5)}; - margin-left: ${space(1.0)}; - transition: 0.5s opacity ease-out; - - &:hover { - color: ${p => p.theme.textColor}; - } -`; - -const StyledLock = styled(IconLock)` - margin-top: ${space(0.75)}; - stroke-width: 1.5; -`; - -export default forwardRef((props, ref) => ( - -)); From 9902041d3bb3b13bcedecee3f7c821d255d7c884 Mon Sep 17 00:00:00 2001 From: Matt Duncan <14761+mrduncan@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:30:18 -0500 Subject: [PATCH 543/757] chore(issues): Opt more modules into stronger typing (#82498) This includes a couple of other quick wins to get some other modules, which are not yet fully passing, incrementally closer. --- pyproject.toml | 2 ++ .../issues/endpoints/actionable_items.py | 12 ------------ src/sentry/issues/endpoints/group_details.py | 17 ++++++++--------- src/sentry/issues/endpoints/group_hashes.py | 5 +++-- .../issues/endpoints/group_similar_issues.py | 2 +- .../endpoints/organization_group_index.py | 18 ++++++++++++------ .../issues/endpoints/source_map_debug.py | 3 --- 7 files changed, 26 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e92fa1a7406510..7fa88c7e5ebccd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -424,6 +424,7 @@ module = [ "sentry.issues.apps", "sentry.issues.constants", "sentry.issues.endpoints", + "sentry.issues.endpoints.actionable_items", "sentry.issues.endpoints.group_activities", "sentry.issues.endpoints.group_event_details", "sentry.issues.endpoints.group_events", @@ -434,6 +435,7 @@ module = [ "sentry.issues.endpoints.group_tombstone", "sentry.issues.endpoints.group_tombstone_details", "sentry.issues.endpoints.organization_eventid", + "sentry.issues.endpoints.organization_group_index", "sentry.issues.endpoints.organization_group_index_stats", "sentry.issues.endpoints.organization_group_search_views", "sentry.issues.endpoints.organization_release_previous_commits", diff --git a/src/sentry/issues/endpoints/actionable_items.py b/src/sentry/issues/endpoints/actionable_items.py index 7e860fc7575b86..30087fb93faef8 100644 --- a/src/sentry/issues/endpoints/actionable_items.py +++ b/src/sentry/issues/endpoints/actionable_items.py @@ -1,5 +1,3 @@ -from typing import TypedDict - from rest_framework.exceptions import NotFound from rest_framework.request import Request from rest_framework.response import Response @@ -19,16 +17,6 @@ from sentry.models.project import Project -class ActionableItemResponse(TypedDict): - type: str - message: str - data: dict | None - - -class SourceMapProcessingResponse(TypedDict): - errors: list[ActionableItemResponse] - - @region_silo_endpoint class ActionableItemsEndpoint(ProjectEndpoint): """ diff --git a/src/sentry/issues/endpoints/group_details.py b/src/sentry/issues/endpoints/group_details.py index 99a87cb783dc51..a7675aa9323e23 100644 --- a/src/sentry/issues/endpoints/group_details.py +++ b/src/sentry/issues/endpoints/group_details.py @@ -83,14 +83,11 @@ class GroupDetailsEndpoint(GroupEndpoint, EnvironmentMixin): }, } - def _get_activity(self, request: Request, group, num): - return Activity.objects.get_activities_for_group(group, num) - - def _get_seen_by(self, request: Request, group): + def _get_seen_by(self, request: Request, group: Group): seen_by = list(GroupSeen.objects.filter(group=group).order_by("-last_seen")) return [seen for seen in serialize(seen_by, request.user) if seen is not None] - def _get_context_plugins(self, request: Request, group): + def _get_context_plugins(self, request: Request, group: Group): project = group.project return serialize( [ @@ -105,7 +102,9 @@ def _get_context_plugins(self, request: Request, group): ) @staticmethod - def __group_hourly_daily_stats(group: Group, environment_ids: Sequence[int]): + def __group_hourly_daily_stats( + group: Group, environment_ids: Sequence[int] + ) -> tuple[list[list[float]], list[list[float]]]: model = get_issue_tsdb_group_model(group.issue_category) now = timezone.now() hourly_stats = tsdb.backend.rollup( @@ -133,7 +132,7 @@ def __group_hourly_daily_stats(group: Group, environment_ids: Sequence[int]): return hourly_stats, daily_stats - def get(self, request: Request, group) -> Response: + def get(self, request: Request, group: Group) -> Response: """ Retrieve an Issue ````````````````` @@ -164,7 +163,7 @@ def get(self, request: Request, group) -> Response: ) # TODO: these probably should be another endpoint - activity = self._get_activity(request, group, num=100) + activity = Activity.objects.get_activities_for_group(group, 100) seen_by = self._get_seen_by(request, group) if "release" not in collapse: @@ -317,7 +316,7 @@ def get(self, request: Request, group) -> Response: ) raise - def put(self, request: Request, group) -> Response: + def put(self, request: Request, group: Group) -> Response: """ Update an Issue ``````````````` diff --git a/src/sentry/issues/endpoints/group_hashes.py b/src/sentry/issues/endpoints/group_hashes.py index 73c5104ec47d8d..69a095e4824e90 100644 --- a/src/sentry/issues/endpoints/group_hashes.py +++ b/src/sentry/issues/endpoints/group_hashes.py @@ -9,6 +9,7 @@ from sentry.api.bases import GroupEndpoint from sentry.api.paginator import GenericOffsetPaginator from sentry.api.serializers import EventSerializer, SimpleEventSerializer, serialize +from sentry.models.group import Group from sentry.models.grouphash import GroupHash from sentry.tasks.unmerge import unmerge from sentry.utils import metrics @@ -22,7 +23,7 @@ class GroupHashesEndpoint(GroupEndpoint): "GET": ApiPublishStatus.PRIVATE, } - def get(self, request: Request, group) -> Response: + def get(self, request: Request, group: Group) -> Response: """ List an Issue's Hashes `````````````````````` @@ -59,7 +60,7 @@ def get(self, request: Request, group) -> Response: paginator=GenericOffsetPaginator(data_fn=data_fn), ) - def put(self, request: Request, group) -> Response: + def put(self, request: Request, group: Group) -> Response: """ Perform an unmerge by reassigning events with hash values corresponding to the given grouphash ids from being part of the given group to being part of a new group. diff --git a/src/sentry/issues/endpoints/group_similar_issues.py b/src/sentry/issues/endpoints/group_similar_issues.py index cc7d8cedf49367..3e786b2f649185 100644 --- a/src/sentry/issues/endpoints/group_similar_issues.py +++ b/src/sentry/issues/endpoints/group_similar_issues.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -def _fix_label(label): +def _fix_label(label) -> str: if isinstance(label, tuple): return ":".join(label) return label diff --git a/src/sentry/issues/endpoints/organization_group_index.py b/src/sentry/issues/endpoints/organization_group_index.py index 7949ed879c9f27..08040c40b1c9ec 100644 --- a/src/sentry/issues/endpoints/organization_group_index.py +++ b/src/sentry/issues/endpoints/organization_group_index.py @@ -36,6 +36,7 @@ from sentry.models.group import QUERY_STATUS_LOOKUP, Group, GroupStatus from sentry.models.groupenvironment import GroupEnvironment from sentry.models.groupinbox import GroupInbox +from sentry.models.organization import Organization from sentry.models.project import Project from sentry.search.events.constants import EQUALITY_OPERATORS from sentry.search.snuba.backend import assigned_or_suggested_filter @@ -59,7 +60,7 @@ def inbox_search( date_to: datetime | None = None, max_hits: int | None = None, actor: Any | None = None, -) -> CursorResult: +) -> CursorResult[Group]: now: datetime = timezone.now() end: datetime | None = None end_params: list[datetime] = [ @@ -151,8 +152,13 @@ class OrganizationGroupIndexEndpoint(OrganizationEndpoint): enforce_rate_limit = True def _search( - self, request: Request, organization, projects, environments, extra_query_kwargs=None - ): + self, + request: Request, + organization: Organization, + projects: Sequence[Project], + environments: Sequence[Environment], + extra_query_kwargs: None | Mapping[str, Any] = None, + ) -> tuple[CursorResult[Group], Mapping[str, Any]]: with start_span(op="_search"): query_kwargs = build_query_params_from_request( request, organization, projects, environments @@ -201,7 +207,7 @@ def use_group_snuba_dataset() -> bool: return result, query_kwargs @track_slo_response("workflow") - def get(self, request: Request, organization) -> Response: + def get(self, request: Request, organization: Organization) -> Response: """ List an Organization's Issues ````````````````````````````` @@ -406,7 +412,7 @@ def get(self, request: Request, organization) -> Response: return response @track_slo_response("workflow") - def put(self, request: Request, organization) -> Response: + def put(self, request: Request, organization: Organization) -> Response: """ Bulk Mutate a List of Issues ```````````````````````````` @@ -493,7 +499,7 @@ def put(self, request: Request, organization) -> Response: return update_groups_with_search_fn(request, ids, projects, organization.id, search_fn) @track_slo_response("workflow") - def delete(self, request: Request, organization) -> Response: + def delete(self, request: Request, organization: Organization) -> Response: """ Bulk Remove a List of Issues ```````````````````````````` diff --git a/src/sentry/issues/endpoints/source_map_debug.py b/src/sentry/issues/endpoints/source_map_debug.py index fa07de1c795f40..a20d6008c55c02 100644 --- a/src/sentry/issues/endpoints/source_map_debug.py +++ b/src/sentry/issues/endpoints/source_map_debug.py @@ -78,9 +78,6 @@ def get(self, request: Request, project: Project, event_id: str) -> Response: debug_response = source_map_debug(project, event_id, exception_idx, frame_idx) issue, data = debug_response.issue, debug_response.data - return self._create_response(issue, data) - - def _create_response(self, issue=None, data=None) -> Response: errors_list = [] if issue: response = SourceMapProcessingIssue(issue, data=data).get_api_context() From 39a122da7ebaf131d44ba640a326562e115565b6 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Mon, 30 Dec 2024 11:39:47 -0800 Subject: [PATCH 544/757] chore(uptime): Bump sentry-kafka-schemas to 0.1.125 (#82550) --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com> --- requirements-base.txt | 2 +- requirements-dev-frozen.txt | 2 +- requirements-frozen.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-base.txt b/requirements-base.txt index 0be1fe411b04be..3e62fa94936174 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -66,7 +66,7 @@ rfc3339-validator>=0.1.2 rfc3986-validator>=0.1.1 # [end] jsonschema format validators sentry-arroyo>=2.19.7 -sentry-kafka-schemas>=0.1.124 +sentry-kafka-schemas>=0.1.125 sentry-ophio==1.0.0 sentry-protos>=0.1.37 sentry-redis-tools>=0.1.7 diff --git a/requirements-dev-frozen.txt b/requirements-dev-frozen.txt index 48299e32786d4c..8fa2ce5d82c5a8 100644 --- a/requirements-dev-frozen.txt +++ b/requirements-dev-frozen.txt @@ -187,7 +187,7 @@ sentry-covdefaults-disable-branch-coverage==1.0.2 sentry-devenv==1.14.2 sentry-forked-django-stubs==5.1.1.post1 sentry-forked-djangorestframework-stubs==3.15.2.post1 -sentry-kafka-schemas==0.1.124 +sentry-kafka-schemas==0.1.125 sentry-ophio==1.0.0 sentry-protos==0.1.39 sentry-redis-tools==0.1.7 diff --git a/requirements-frozen.txt b/requirements-frozen.txt index ee334b5ba5e7a2..4876d82461dd3b 100644 --- a/requirements-frozen.txt +++ b/requirements-frozen.txt @@ -126,7 +126,7 @@ rpds-py==0.20.0 rsa==4.8 s3transfer==0.10.0 sentry-arroyo==2.19.7 -sentry-kafka-schemas==0.1.124 +sentry-kafka-schemas==0.1.125 sentry-ophio==1.0.0 sentry-protos==0.1.39 sentry-redis-tools==0.1.7 From 022ce42c3c86341dcfd31887ecba094c3278df24 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 11:41:10 -0800 Subject: [PATCH 545/757] ref(replays): Remove unused iconWrapper (#82676) --- .../app/views/replays/detail/iconWrapper.tsx | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 static/app/views/replays/detail/iconWrapper.tsx diff --git a/static/app/views/replays/detail/iconWrapper.tsx b/static/app/views/replays/detail/iconWrapper.tsx deleted file mode 100644 index 9f909505758cdc..00000000000000 --- a/static/app/views/replays/detail/iconWrapper.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import styled from '@emotion/styled'; - -import type {SVGIconProps} from 'sentry/icons/svgIcon'; - -/** - * Taken `from events/interfaces/.../breadcrumbs/types` - */ -const IconWrapper = styled('div')< - {hasOccurred: boolean} & Required> ->` - display: flex; - align-items: center; - justify-content: center; - width: 24px; - min-width: 24px; - height: 24px; - border-radius: 50%; - color: ${p => p.theme.white}; - background: ${p => p.theme[p.color] ?? p.color}; - position: relative; - opacity: ${p => (p.hasOccurred ? 1 : 0.8)}; - - /* Make sure the icon is above the line through the back */ - z-index: ${p => p.theme.zIndex.initial}; -`; - -export default IconWrapper; From 708e60454752a922a878b656139ceb612ab0c329 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 11:41:35 -0800 Subject: [PATCH 546/757] ref(grouping): Remove unused group settings utils (#82675) --- .../settings/projectIssueGrouping/utils.tsx | 73 ------------------- 1 file changed, 73 deletions(-) delete mode 100644 static/app/views/settings/projectIssueGrouping/utils.tsx diff --git a/static/app/views/settings/projectIssueGrouping/utils.tsx b/static/app/views/settings/projectIssueGrouping/utils.tsx deleted file mode 100644 index 89b321ba35339b..00000000000000 --- a/static/app/views/settings/projectIssueGrouping/utils.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type {AlertProps} from 'sentry/components/alert'; -import {t} from 'sentry/locale'; -import type {EventGroupingConfig} from 'sentry/types/event'; -import type {Project} from 'sentry/types/project'; - -export function getGroupingChanges( - project: Project, - groupingConfigs: EventGroupingConfig[] -): { - latestGroupingConfig: EventGroupingConfig | null; - riskLevel: number; - updateNotes: string; -} { - const byId: Record = {}; - let updateNotes: string = ''; - let riskLevel: number = 0; - let latestGroupingConfig: EventGroupingConfig | null = null; - - groupingConfigs.forEach(cfg => { - byId[cfg.id] = cfg; - if (cfg.latest && project.groupingConfig !== cfg.id) { - updateNotes = cfg.changelog; - latestGroupingConfig = cfg; - riskLevel = cfg.risk; - } - }); - - if (latestGroupingConfig) { - let next = (latestGroupingConfig as EventGroupingConfig).base ?? ''; - while (next !== project.groupingConfig) { - const cfg = byId[next]; - if (!cfg) { - break; - } - riskLevel = Math.max(riskLevel, cfg.risk); - updateNotes = cfg.changelog + '\n' + updateNotes; - next = cfg.base ?? ''; - } - } - - return {updateNotes, riskLevel, latestGroupingConfig}; -} - -export function getGroupingRisk(riskLevel: number): { - alertType: AlertProps['type']; - riskNote: React.ReactNode; -} { - switch (riskLevel) { - case 0: - return { - riskNote: t('This upgrade has the chance to create some new issues.'), - alertType: 'info', - }; - case 1: - return { - riskNote: t('This upgrade will create some new issues.'), - alertType: 'warning', - }; - case 2: - return { - riskNote: ( - - {t( - 'The new grouping strategy is incompatible with the current and will create entirely new issues.' - )} - - ), - alertType: 'error', - }; - default: - return {riskNote: undefined, alertType: undefined}; - } -} From d77a76ac4badec5d1bb48add4a48fb97ff5475a5 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 11:42:12 -0800 Subject: [PATCH 547/757] chore(integrations): Remove unused repositoryEditForm (#82670) --- static/app/components/repositoryEditForm.tsx | 80 -------------------- 1 file changed, 80 deletions(-) delete mode 100644 static/app/components/repositoryEditForm.tsx diff --git a/static/app/components/repositoryEditForm.tsx b/static/app/components/repositoryEditForm.tsx deleted file mode 100644 index a7b869e704c820..00000000000000 --- a/static/app/components/repositoryEditForm.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import {Alert} from 'sentry/components/alert'; -import FieldFromConfig from 'sentry/components/forms/fieldFromConfig'; -import type {FormProps} from 'sentry/components/forms/form'; -import Form from 'sentry/components/forms/form'; -import type {Field} from 'sentry/components/forms/types'; -import ExternalLink from 'sentry/components/links/externalLink'; -import {t, tct} from 'sentry/locale'; -import type {Repository} from 'sentry/types/integrations'; - -type Props = Pick & { - closeModal: () => void; - onSubmitSuccess: (data: any) => void; - orgSlug: string; - repository: Repository; -}; - -const formFields: Field[] = [ - { - name: 'name', - type: 'string', - required: true, - label: t('Name of your repository.'), - }, - { - name: 'url', - type: 'string', - required: false, - label: t('Full URL to your repository.'), - placeholder: t('https://github.com/my-org/my-repo/'), - }, -]; - -function RepositoryEditForm({ - repository, - onCancel, - orgSlug, - onSubmitSuccess, - closeModal, -}: Props) { - const initialData = { - name: repository.name, - url: repository.url || '', - }; - - return ( -
      { - onSubmitSuccess(data); - closeModal(); - }} - apiEndpoint={`/organizations/${orgSlug}/repos/${repository.id}/`} - apiMethod="PUT" - onCancel={onCancel} - > - - {tct( - 'Changing the [name:repo name] may have consequences if it no longer matches the repo name used when [link:sending commits with releases].', - { - link: ( - - ), - name: repo name, - } - )} - - {formFields.map(field => ( - - ))} - - ); -} - -export default RepositoryEditForm; From 8c52628ac5101b69fe694e3b5fc12c5bf676ae70 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 11:42:49 -0800 Subject: [PATCH 548/757] ref(onboarding): Remove unused PlatformHeaderButtonBar (#82677) --- .../components/platformHeaderButtonBar.tsx | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 static/app/views/projectInstall/components/platformHeaderButtonBar.tsx diff --git a/static/app/views/projectInstall/components/platformHeaderButtonBar.tsx b/static/app/views/projectInstall/components/platformHeaderButtonBar.tsx deleted file mode 100644 index 5d4d0916e53558..00000000000000 --- a/static/app/views/projectInstall/components/platformHeaderButtonBar.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import {LinkButton} from 'sentry/components/button'; -import ButtonBar from 'sentry/components/buttonBar'; -import {IconChevron} from 'sentry/icons'; -import {t} from 'sentry/locale'; - -type Props = { - docsLink: string; - gettingStartedLink: string; -}; - -export default function PlatformHeaderButtonBar({gettingStartedLink, docsLink}: Props) { - return ( - - } - to={gettingStartedLink} - > - {t('Back')} - - - {t('Full Documentation')} - - - ); -} From 58d0b86059029630c7932ffe551ba86304bd575e Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 30 Dec 2024 11:49:44 -0800 Subject: [PATCH 549/757] chore(devservices): Bump devservices version to 1.0.7 (#82663) Bumps devservices to 1.0.7 Picks up https://github.com/getsentry/devservices/releases/tag/1.0.7 --- requirements-dev-frozen.txt | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev-frozen.txt b/requirements-dev-frozen.txt index 8fa2ce5d82c5a8..7a2295db034f5f 100644 --- a/requirements-dev-frozen.txt +++ b/requirements-dev-frozen.txt @@ -37,7 +37,7 @@ cryptography==43.0.1 cssselect==1.0.3 cssutils==2.9.0 datadog==0.49.1 -devservices==1.0.6 +devservices==1.0.7 distlib==0.3.8 distro==1.8.0 django==5.1.4 diff --git a/requirements-dev.txt b/requirements-dev.txt index b788a23c1352a5..a4a1f9a29360fc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ --index-url https://pypi.devinfra.sentry.io/simple sentry-devenv>=1.14.2 -devservices>=1.0.6 +devservices>=1.0.7 covdefaults>=2.3.0 sentry-covdefaults-disable-branch-coverage>=1.0.2 From cd23deb02f497db2f00af8d0972a1b88a05f33f0 Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:56:35 -0500 Subject: [PATCH 550/757] chore(insights): removes unused arg from web vitals timeseries helper function (#82650) We never use `useWeights = true` in `formatTimeSeriesResultsToChartData`, so just remove the arg and remove branching logic. --- .../performanceScoreBreakdownChart.spec.tsx | 1 - .../charts/performanceScoreBreakdownChart.tsx | 16 ++++++---------- .../widgets/performanceScoreListWidget.tsx | 1 - 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx index d34b0cabee99f8..0132c835d44b5a 100644 --- a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx @@ -96,7 +96,6 @@ describe('PerformanceScoreBreakdownChart', function () { total: [], }, ['#444674', '#895289', '#d6567f', '#f38150', '#f2b712'], - false, ['lcp', 'fcp', 'inp', 'cls', 'ttfb'] ); expect(result).toEqual([ diff --git a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx index 24f863ff4e62d6..b778bd1fa2ec68 100644 --- a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx +++ b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.tsx @@ -11,6 +11,7 @@ import { useProjectWebVitalsScoresTimeseriesQuery, type WebVitalsScoreBreakdown, } from 'sentry/views/insights/browser/webVitals/queries/storedScoreQueries/useProjectWebVitalsScoresTimeseriesQuery'; +import type {WebVitals} from 'sentry/views/insights/browser/webVitals/types'; import {applyStaticWeightsToTimeseries} from 'sentry/views/insights/browser/webVitals/utils/applyStaticWeightsToTimeseries'; import type {BrowserType} from 'sentry/views/insights/browser/webVitals/utils/queryParameterDecoders/browserType'; import {PERFORMANCE_SCORE_WEIGHTS} from 'sentry/views/insights/browser/webVitals/utils/scoreThresholds'; @@ -24,12 +25,11 @@ type Props = { transaction?: string; }; -export const formatTimeSeriesResultsToChartData = ( +export function formatTimeSeriesResultsToChartData( data: WebVitalsScoreBreakdown, segmentColors: string[], - useWeights = true, - order = ORDER -): Series[] => { + order: WebVitals[] = ORDER +): Series[] { return order.map((webVital, index) => { const series = data[webVital]; const color = segmentColors[index]; @@ -37,14 +37,12 @@ export const formatTimeSeriesResultsToChartData = ( seriesName: webVital.toUpperCase(), data: series.map(({name, value}) => ({ name, - value: Math.round( - value * (useWeights ? PERFORMANCE_SCORE_WEIGHTS[webVital] : 100) * 0.01 - ), + value: Math.round(value), })), color, }; }); -}; +} export function PerformanceScoreBreakdownChart({ transaction, @@ -68,7 +66,6 @@ export function PerformanceScoreBreakdownChart({ const weightedTimeseries = formatTimeSeriesResultsToChartData( weightedTimeseriesData, segmentColors, - false, chartSeriesOrder ); @@ -82,7 +79,6 @@ export function PerformanceScoreBreakdownChart({ total: timeseriesData.total, }, segmentColors, - false, chartSeriesOrder ); diff --git a/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx index 85a1d25625eb48..dbbce8d2070a2d 100644 --- a/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx +++ b/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx @@ -74,7 +74,6 @@ export function PerformanceScoreListWidget(props: PerformanceWidgetProps) { data={formatTimeSeriesResultsToChartData( weightedTimeseriesData, segmentColors, - false, order )} type={ChartType.AREA} From 6e5fa197bea6af8d674fb67294d61bcbd71a31da Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 12:05:56 -0800 Subject: [PATCH 551/757] chore(ui): Remove unused hasDuplicates (#82684) --- static/app/utils/array/hasDuplicates.ts | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 static/app/utils/array/hasDuplicates.ts diff --git a/static/app/utils/array/hasDuplicates.ts b/static/app/utils/array/hasDuplicates.ts deleted file mode 100644 index 33a40a2f82d7fe..00000000000000 --- a/static/app/utils/array/hasDuplicates.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Determines whether the provided array contains any duplicate elements. - * Returns `true` if there are duplicates, and `false` if all elements are unique. - * - * @param array - The array to be checked for duplicates. - * @returns `boolean` - `true` if duplicates are found; otherwise `false`. - */ -export function hasDuplicates(array: T[]): boolean { - const seen = new Set(); - for (const item of array) { - if (seen.has(item)) { - return true; // Duplicate found - } - seen.add(item); - } - return false; // No duplicates -} From 16dab378e333918aa65747f427e1ed6915baedea Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 12:07:50 -0800 Subject: [PATCH 552/757] ref(releases): Remove unused ReleaseDetailsTable (#82685) --- .../components/releaseDetailsSideTable.tsx | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 static/app/views/releases/components/releaseDetailsSideTable.tsx diff --git a/static/app/views/releases/components/releaseDetailsSideTable.tsx b/static/app/views/releases/components/releaseDetailsSideTable.tsx deleted file mode 100644 index b20d759f793aea..00000000000000 --- a/static/app/views/releases/components/releaseDetailsSideTable.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import styled from '@emotion/styled'; - -import {space} from 'sentry/styles/space'; - -type Props = { - children?: React.ReactNode; - type?: undefined | 'error' | 'warning'; -}; - -export const ReleaseDetailsTable = styled('div')<{noMargin?: boolean}>` - ${p => (p.noMargin ? 'margin-bottom: 0;' : null)} -`; - -export function ReleaseDetailsTableRow({type, children}: Props) { - return {children}; -} - -const Row = styled('div')<{type: Props['type']}>` - ${p => p.theme.overflowEllipsis}; - font-size: ${p => p.theme.fontSizeMedium}; - padding: ${space(0.5)} ${space(1)}; - font-weight: ${p => p.theme.fontWeightNormal}; - line-height: inherit; - - background-color: ${p => { - switch (p.type) { - case 'error': - return p.theme.red100 + ' !important'; - case 'warning': - return 'var(--background-warning-default, rgba(245, 176, 0, 0.09)) !important'; - default: - return 'inherit'; - } - }}; - &:nth-of-type(2n-1) { - background-color: ${p => p.theme.backgroundSecondary}; - } -`; From 04fcb5e88fc9018d4153546e8c84363042389d7f Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 12:29:38 -0800 Subject: [PATCH 553/757] chore(ui): Remove empty settings file (#82690) --- static/app/views/performance/settings.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 static/app/views/performance/settings.ts diff --git a/static/app/views/performance/settings.ts b/static/app/views/performance/settings.ts deleted file mode 100644 index e69de29bb2d1d6..00000000000000 From f602556946a9badc5009b5e2f24c6bb19a09ee8b Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 30 Dec 2024 12:31:51 -0800 Subject: [PATCH 554/757] ref(ui): Remove unused PageFilter components (#82689) --- .../pageFilters/pageFilterDropdownButton.tsx | 29 --------- .../pageFilters/pageFilterPinButton.tsx | 61 ------------------- .../pageFilters/pageFilterPinIndicator.tsx | 58 ------------------ .../organizations/pageFilters/utils.tsx | 9 +-- 4 files changed, 1 insertion(+), 156 deletions(-) delete mode 100644 static/app/components/organizations/pageFilters/pageFilterDropdownButton.tsx delete mode 100644 static/app/components/organizations/pageFilters/pageFilterPinButton.tsx delete mode 100644 static/app/components/organizations/pageFilters/pageFilterPinIndicator.tsx diff --git a/static/app/components/organizations/pageFilters/pageFilterDropdownButton.tsx b/static/app/components/organizations/pageFilters/pageFilterDropdownButton.tsx deleted file mode 100644 index 305fd4887ce8b7..00000000000000 --- a/static/app/components/organizations/pageFilters/pageFilterDropdownButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import styled from '@emotion/styled'; - -import DropdownButton from 'sentry/components/dropdownButton'; - -type Props = { - /** - * Highlights the button blue. For page filters this indicates the filter - * has been desynced from the URL. - */ - highlighted?: boolean; -}; - -const PageFilterDropdownButton = styled(DropdownButton)` - width: 100%; - text-overflow: ellipsis; - ${p => - p.highlighted && - ` - &, - &:active, - &:hover, - &:focus { - background-color: ${p.theme.purple100}; - border-color: ${p.theme.purple200}; - } - `} -`; - -export default PageFilterDropdownButton; diff --git a/static/app/components/organizations/pageFilters/pageFilterPinButton.tsx b/static/app/components/organizations/pageFilters/pageFilterPinButton.tsx deleted file mode 100644 index f9212df92bdc77..00000000000000 --- a/static/app/components/organizations/pageFilters/pageFilterPinButton.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import styled from '@emotion/styled'; - -import {pinFilter} from 'sentry/actionCreators/pageFilters'; -import type {ButtonProps} from 'sentry/components/button'; -import {Button} from 'sentry/components/button'; -import {IconLock} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import type {PinnedPageFilter} from 'sentry/types/core'; -import type {Organization} from 'sentry/types/organization'; -import {trackAnalytics} from 'sentry/utils/analytics'; -import usePageFilters from 'sentry/utils/usePageFilters'; - -type Props = { - filter: PinnedPageFilter; - organization: Organization; - size: Extract; - className?: string; -}; - -function PageFilterPinButton({organization, filter, size, className}: Props) { - const {pinnedFilters} = usePageFilters(); - const pinned = pinnedFilters.has(filter); - - const onPin = () => { - trackAnalytics('page_filters.pin_click', { - organization, - filter, - pin: !pinned, - }); - pinFilter(filter, !pinned); - }; - - return ( - } - title={t("Once locked, Sentry will remember this filter's value across pages.")} - tooltipProps={{delay: 500}} - > - {pinned ? t('Locked') : t('Lock')} - - ); -} - -const PinButton = styled(Button)<{pinned: boolean; size: 'xs' | 'zero'}>` - display: block; - color: ${p => p.theme.textColor}; - - :hover { - color: ${p => p.theme.headingColor}; - } - ${p => p.size === 'zero' && 'background: transparent'}; -`; - -export default PageFilterPinButton; diff --git a/static/app/components/organizations/pageFilters/pageFilterPinIndicator.tsx b/static/app/components/organizations/pageFilters/pageFilterPinIndicator.tsx deleted file mode 100644 index 87337e61998a61..00000000000000 --- a/static/app/components/organizations/pageFilters/pageFilterPinIndicator.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import styled from '@emotion/styled'; - -import {IconLock} from 'sentry/icons'; -import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; -import type {PinnedPageFilter} from 'sentry/types/core'; -import usePageFilters from 'sentry/utils/usePageFilters'; - -type Props = { - children: React.ReactNode; - filter: PinnedPageFilter; -}; - -function PageFilterPinIndicator({children, filter}: Props) { - const {pinnedFilters} = usePageFilters(); - const pinned = pinnedFilters.has(filter); - - return ( - - {children} - {pinned && ( - - - - )} - - ); -} - -export default PageFilterPinIndicator; - -const Wrap = styled('div')` - position: relative; - display: flex; - align-items: center; - transform: translateX(-${space(0.25)}); -`; - -const IndicatorWrap = styled('div')` - position: absolute; - bottom: 0; - right: 0; - transform: translate(50%, 35%); - border-radius: 50%; - background-color: ${p => p.theme.background}; - - padding: ${space(0.25)}; - - display: flex; - align-items: center; - justify-content: center; -`; - -const StyledIconLock = styled(IconLock)` - width: 0.5rem; - height: 0.5rem; - color: ${p => p.theme.textColor}; -`; diff --git a/static/app/components/organizations/pageFilters/utils.tsx b/static/app/components/organizations/pageFilters/utils.tsx index d4abbf37a0f1c5..98f17cd9138eec 100644 --- a/static/app/components/organizations/pageFilters/utils.tsx +++ b/static/app/components/organizations/pageFilters/utils.tsx @@ -5,7 +5,7 @@ import pick from 'lodash/pick'; import pickBy from 'lodash/pickBy'; import {DEFAULT_STATS_PERIOD} from 'sentry/constants'; -import {DATE_TIME_KEYS, URL_PARAM} from 'sentry/constants/pageFilters'; +import {URL_PARAM} from 'sentry/constants/pageFilters'; import type {PageFilters} from 'sentry/types/core'; /** @@ -35,13 +35,6 @@ export function extractSelectionParameters(query: Location['query']) { return pickBy(pick(query, Object.values(URL_PARAM)), identity); } -/** - * Extract the page filter datetime parameters from an object. - */ -export function extractDatetimeSelectionParameters(query: Location['query']) { - return pickBy(pick(query, Object.values(DATE_TIME_KEYS)), identity); -} - /** * Compare the non-utc values of two selections. * Useful when re-fetching data based on page filters changing. From b1f93d69c2b3f919a2f4745e9e4b5bea9682e76b Mon Sep 17 00:00:00 2001 From: Nikki Kapadia <72356613+nikkikapadia@users.noreply.github.com> Date: Mon, 30 Dec 2024 15:39:24 -0500 Subject: [PATCH 555/757] feat(widget-builder): Add functionality to Legend Alias fields for filters (#82621) Now the legend alias field is functional and can be added to the widget preview legend as shown below. Also fixed the issue where the filter field wasn't working. image Closes https://github.com/getsentry/sentry/issues/82369 --- .../components/queryFilterBuilder.spec.tsx | 74 +++++++++++++---- .../components/queryFilterBuilder.tsx | 82 ++++++++----------- .../hooks/useWidgetBuilderState.tsx | 12 +++ 3 files changed, 106 insertions(+), 62 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.spec.tsx b/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.spec.tsx index 8ae73ad727bc5d..97ee6b52706600 100644 --- a/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.spec.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.spec.tsx @@ -1,21 +1,24 @@ +import {LocationFixture} from 'sentry-fixture/locationFixture'; +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {RouterFixture} from 'sentry-fixture/routerFixture'; + import {render, screen} from 'sentry-test/reactTestingLibrary'; +import type {Organization} from 'sentry/types/organization'; import useCustomMeasurements from 'sentry/utils/useCustomMeasurements'; -import {WidgetType} from 'sentry/views/dashboards/types'; +import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import WidgetBuilderQueryFilterBuilder from 'sentry/views/dashboards/widgetBuilder/components/queryFilterBuilder'; import {WidgetBuilderProvider} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext'; -import useWidgetBuilderState from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState'; import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext'; -jest.mock('sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState'); jest.mock('sentry/utils/useCustomMeasurements'); jest.mock('sentry/views/explore/contexts/spanTagsContext'); describe('QueryFilterBuilder', () => { + let organization: Organization; beforeEach(() => { - jest.mocked(useWidgetBuilderState).mockReturnValue({ - dispatch: jest.fn(), - state: {dataset: WidgetType.ERRORS}, + organization = OrganizationFixture({ + features: ['dashboards-widget-builder-redesign'], }); jest.mocked(useCustomMeasurements).mockReturnValue({ customMeasurements: {}, @@ -28,27 +31,68 @@ describe('QueryFilterBuilder', () => { }); it('renders a dataset-specific query filter bar', async () => { - const {rerender} = render( + render( {}} /> - + , + { + organization, + router: RouterFixture({ + location: LocationFixture({ + query: { + query: [], + dataset: WidgetType.TRANSACTIONS, + displayType: DisplayType.TABLE, + }, + }), + }), + } ); expect( await screen.findByPlaceholderText('Search for events, users, tags, and more') ).toBeInTheDocument(); - jest.mocked(useWidgetBuilderState).mockReturnValue({ - dispatch: jest.fn(), - state: {dataset: WidgetType.SPANS}, - }); - - rerender( + render( {}} /> - + , + { + organization, + router: RouterFixture({ + location: LocationFixture({ + query: { + query: [], + dataset: WidgetType.SPANS, + displayType: DisplayType.TABLE, + }, + }), + }), + } ); expect( await screen.findByPlaceholderText('Search for spans, users, tags, and more') ).toBeInTheDocument(); }); + + it('renders a legend alias input for charts', async () => { + render( + + {}} /> + , + { + organization, + router: RouterFixture({ + location: LocationFixture({ + query: { + query: [], + dataset: WidgetType.TRANSACTIONS, + displayType: DisplayType.LINE, + }, + }), + }), + } + ); + + expect(await screen.findByPlaceholderText('Legend Alias')).toBeInTheDocument(); + }); }); diff --git a/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.tsx b/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.tsx index 65af2ee12dbad2..d4bbe2b9c64823 100644 --- a/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/queryFilterBuilder.tsx @@ -52,6 +52,11 @@ function WidgetBuilderQueryFilterBuilder({ type: BuilderStateAction.SET_QUERY, payload: state.query?.length ? [...state.query, ''] : ['', ''], }); + + dispatch({ + type: BuilderStateAction.SET_LEGEND_ALIAS, + payload: state.legendAlias?.length ? [...state.legendAlias, ''] : ['', ''], + }); }; const handleClose = useCallback( @@ -80,8 +85,18 @@ function WidgetBuilderQueryFilterBuilder({ type: BuilderStateAction.SET_QUERY, payload: state.query?.filter((_, i) => i !== queryIndex) ?? [], }); + dispatch({ + type: BuilderStateAction.SET_LEGEND_ALIAS, + payload: state.legendAlias?.filter((_, i) => i !== queryIndex) ?? [], + }); }, - [dispatch, queryConditionValidity, state.query, onQueryConditionChange] + [ + dispatch, + queryConditionValidity, + state.query, + onQueryConditionChange, + state.legendAlias, + ] ); const getOnDemandFilterWarning = createOnDemandFilterWarning( @@ -106,12 +121,12 @@ function WidgetBuilderQueryFilterBuilder({ } optional /> - {!state.query?.length ? ( - + {state.query?.map((_, index) => ( + { dispatch({ type: BuilderStateAction.SET_QUERY, - payload: [queryString], + payload: + state.query?.map((q, i) => (i === index ? queryString : q)) ?? [], }); }} - widgetQuery={widget.queries[0]} + widgetQuery={widget.queries[index]} dataset={getDiscoverDatasetFromWidgetType(widgetType)} /> {canAddSearchConditions && ( @@ -135,50 +151,22 @@ function WidgetBuilderQueryFilterBuilder({ type="text" name="name" placeholder={t('Legend Alias')} - onChange={() => {}} - /> - )} - - ) : ( - state.query?.map((_, index) => ( - - { + value={state.legendAlias?.[index] || ''} + onChange={e => { dispatch({ - type: BuilderStateAction.SET_QUERY, - payload: - state.query?.map((q, i) => (i === index ? queryString : q)) ?? [], + type: BuilderStateAction.SET_LEGEND_ALIAS, + payload: state.legendAlias?.length + ? state.legendAlias?.map((q, i) => (i === index ? e.target.value : q)) + : [e.target.value], }); }} - widgetQuery={widget.queries[index]} - dataset={getDiscoverDatasetFromWidgetType(widgetType)} /> - {canAddSearchConditions && ( - // TODO: Hook up alias to query hook when it's implemented - {}} - /> - )} - {state.query && state.query?.length > 1 && canAddSearchConditions && ( - - )} - - )) - )} + )} + {state.query && state.query?.length > 1 && canAddSearchConditions && ( + + )} + + ))} {canAddSearchConditions && (
    -); - export const NOTIFICATION_FEATURE_MAP: Partial< Record> > = { diff --git a/static/app/views/settings/account/notifications/notificationSettingsByType.tsx b/static/app/views/settings/account/notifications/notificationSettingsByType.tsx index 0d2c7ed0613eec..250dfca461a70b 100644 --- a/static/app/views/settings/account/notifications/notificationSettingsByType.tsx +++ b/static/app/views/settings/account/notifications/notificationSettingsByType.tsx @@ -474,7 +474,7 @@ class NotificationSettingsByTypeV2 extends DeprecatedAsyncComponent Date: Tue, 31 Dec 2024 11:02:41 -0800 Subject: [PATCH 589/757] ref(settings): Remove unused custom repositories Details (#82691) --- .../sources/customRepositories/details.tsx | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 static/app/views/settings/projectDebugFiles/sources/customRepositories/details.tsx diff --git a/static/app/views/settings/projectDebugFiles/sources/customRepositories/details.tsx b/static/app/views/settings/projectDebugFiles/sources/customRepositories/details.tsx deleted file mode 100644 index a5a10f482dab1f..00000000000000 --- a/static/app/views/settings/projectDebugFiles/sources/customRepositories/details.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import styled from '@emotion/styled'; - -import NotAvailable from 'sentry/components/notAvailable'; -import {t} from 'sentry/locale'; -import {space} from 'sentry/styles/space'; - -function Details() { - return ( - - {t('Last detected version')} - - - - - {t('Last detected build')} - {} - - {t('Detected last build on')} - - - - - ); -} - -export default Details; - -const Wrapper = styled('div')` - display: grid; - gap: ${space(1)}; - margin-top: ${space(0.5)}; - align-items: center; - font-size: ${p => p.theme.fontSizeSmall}; - font-weight: ${p => p.theme.fontWeightBold}; - - grid-column: 2/-1; - - @media (min-width: ${p => p.theme.breakpoints.small}) { - margin-top: ${space(1)}; - grid-template-columns: max-content 1fr; - gap: ${space(1)}; - grid-row: 3/3; - } -`; - -const Value = styled('div')` - font-weight: ${p => p.theme.fontWeightNormal}; - white-space: pre-wrap; - word-break: break-all; - padding: ${space(1)} ${space(1.5)}; - font-family: ${p => p.theme.text.familyMono}; - background-color: ${p => p.theme.backgroundSecondary}; - - @media (max-width: ${p => p.theme.breakpoints.small}) { - :not(:last-child) { - margin-bottom: ${space(1)}; - } - } -`; From 392006bd62cb718031f76919de4e4ffa352dc4b0 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 31 Dec 2024 11:02:48 -0800 Subject: [PATCH 590/757] meta: Set @getsentry/app-frontend as codeowners for some unowned things (#82755) The forms/ and events/interfaces folders are unowned by anyone; so when PRs come up there are no reviewers assigned. So I'm adding rules where `@getsentry/app-frontend` owns these things... these are the only things owned by app-frontend at the moment. --- .github/CODEOWNERS | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 89c2a53cc3f41b..fbd65b9e7b7867 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -359,10 +359,12 @@ tests/sentry/api/endpoints/test_organization_dashboard_widget_details.py @ge ## End of DevToolbar -## Misc Replay -/static/app/components/analyticsArea.tsx @getsentry/replay-frontend -/static/app/components/analyticsArea.spec.tsx @getsentry/replay-frontend -## End of Misc Replay +## Frontend +/static/app/components/analyticsArea.spec.tsx @getsentry/app-frontend +/static/app/components/analyticsArea.tsx @getsentry/app-frontend +/static/app/components/events/interfaces/ @getsentry/app-frontend +/static/app/components/forms/ @getsentry/app-frontend +## End of Frontend ## Integrations From a022bc928683435924c3c5b03306f5e443a63c75 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 31 Dec 2024 11:07:13 -0800 Subject: [PATCH 591/757] ref: Consolidate @emotion eslint rules (#82745) --- eslint.config.mjs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 496dfd03dab575..ee33f111fb01e8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -481,16 +481,6 @@ const reactRules = { }; const appRules = { - /** - * emotion rules for v10 - * - * This probably aren't as necessary anymore, but let's remove when we move to v11 - */ - '@emotion/jsx-import': 'off', - '@emotion/no-vanilla': 'error', - '@emotion/import-from-emotion': 'error', - '@emotion/styled-import': 'error', - // no-undef is redundant with typescript as tsc will complain // A downside is that we won't get eslint errors about it, but your editors should // support tsc errors so.... @@ -790,7 +780,6 @@ export default typescript.config([ plugins: { ...react.configs.flat.plugins, ...react.configs.flat['jsx-runtime'].plugins, - '@emotion': emotion, '@typescript-eslint': typescript.plugin, 'react-hooks': fixupPluginRules(reactHooks), 'simple-import-sort': simpleImportSort, @@ -895,6 +884,20 @@ export default typescript.config([ ...strictRules, }, }, + { + name: '@emotion', + plugins: { + '@emotion': emotion, + }, + rules: { + '@emotion/import-from-emotion': 'off', // Not needed, in v11 we import from @emotion/react + '@emotion/jsx-import': 'off', // Not needed, handled by babel + '@emotion/no-vanilla': 'error', + '@emotion/pkg-renaming': 'off', // Not needed, we have migrated to v11 and the old package names cannot be used anymore + '@emotion/styled-import': 'error', + '@emotion/syntax-preference': ['off', 'string'], // TODO(ryan953): Enable this so `css={css``}` is required + }, + }, { name: 'devtoolbar', files: ['static/app/components/devtoolbar/**/*.{ts,tsx}'], From 5c39e540eb7b92b76293e6444ce95ca65eb1e428 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 31 Dec 2024 11:08:14 -0800 Subject: [PATCH 592/757] ci: Update codeowners for eslint.config.mjs and make sure to lint that file (#82749) --- .github/CODEOWNERS | 3 +-- .pre-commit-config.yaml | 2 +- eslint.config.mjs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fbd65b9e7b7867..098974ecd3aa4a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -122,8 +122,7 @@ pyproject.toml @getsentry/owners-pytho babel.config.* @getsentry/owners-js-build biome.json @getsentry/owners-js-build build-utils/ @getsentry/owners-js-build -eslint.config.js @getsentry/owners-js-build -eslintrc.js @getsentry/owners-js-build +eslint.config.mjs @getsentry/owners-js-build jest.config.ts @getsentry/owners-js-build tsconfig.* @getsentry/owners-js-build webpack.config.* @getsentry/owners-js-build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd07e25b9a6872..4a6418b9c0f0f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -117,7 +117,7 @@ repos: - id: eslint name: eslint language: system - files: \.[jt]sx?$ + files: \.(ts|js|tsx|jsx|mjs)$ entry: ./node_modules/.bin/eslint --quiet --fix - id: stylelint diff --git a/eslint.config.mjs b/eslint.config.mjs index ee33f111fb01e8..0412f79d0b1e7c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -803,7 +803,7 @@ export default typescript.config([ { // Default file selection // https://eslint.org/docs/latest/use/configure/configuration-files#specifying-files-and-ignores - files: ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx'], + files: ['**/*.js', '**/*.mjs', '**/*.ts', '**/*.jsx', '**/*.tsx'], }, { // Global ignores From e16c0b3ee81a5a90e166f01fb401d7e0947628ba Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 31 Dec 2024 11:10:06 -0800 Subject: [PATCH 593/757] chore(ui): Remove unused semverCompare export (#82687) --- static/app/utils/versions.tsx | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 static/app/utils/versions.tsx diff --git a/static/app/utils/versions.tsx b/static/app/utils/versions.tsx deleted file mode 100644 index a889526e798bfa..00000000000000 --- a/static/app/utils/versions.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import {semverCompare} from 'sentry/utils/versions/semverCompare'; - -/** - * This is here for getsentry - * - * @deprecated Import directly from sentry/utils/versions/semverCompare instead. - */ -export {semverCompare}; From a0bcc41f8379794482ddcc453f86817cd79f12fe Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 31 Dec 2024 11:11:09 -0800 Subject: [PATCH 594/757] chore(integrations): Remove unused actions (#82668) --- static/app/actionCreators/integrations.tsx | 76 +--------------------- 1 file changed, 1 insertion(+), 75 deletions(-) diff --git a/static/app/actionCreators/integrations.tsx b/static/app/actionCreators/integrations.tsx index 3cc29123dcd076..ca6da8c9c2b959 100644 --- a/static/app/actionCreators/integrations.tsx +++ b/static/app/actionCreators/integrations.tsx @@ -4,84 +4,10 @@ import { addSuccessMessage, clearIndicators, } from 'sentry/actionCreators/indicator'; -import {Client} from 'sentry/api'; +import type {Client} from 'sentry/api'; import {t, tct} from 'sentry/locale'; import type {Integration, Repository} from 'sentry/types/integrations'; -const api = new Client(); - -/** - * Removes an integration from a project. - * - * @param orgSlug Organization Slug - * @param projectId Project Slug - * @param integration The organization integration to remove - */ -export function removeIntegrationFromProject( - orgSlug: string, - projectId: string, - integration: Integration -) { - const endpoint = `/projects/${orgSlug}/${projectId}/integrations/${integration.id}/`; - addLoadingMessage(); - - return api.requestPromise(endpoint, {method: 'DELETE'}).then( - () => { - addSuccessMessage(t('Disabled %s for %s', integration.name, projectId)); - }, - () => { - addErrorMessage(t('Failed to disable %s for %s', integration.name, projectId)); - } - ); -} - -/** - * Add an integration to a project - * - * @param orgSlug Organization Slug - * @param projectId Project Slug - * @param integration The organization integration to add - */ -export function addIntegrationToProject( - orgSlug: string, - projectId: string, - integration: Integration -) { - const endpoint = `/projects/${orgSlug}/${projectId}/integrations/${integration.id}/`; - addLoadingMessage(); - - return api.requestPromise(endpoint, {method: 'PUT'}).then( - () => { - addSuccessMessage(t('Enabled %s for %s', integration.name, projectId)); - }, - () => { - addErrorMessage(t('Failed to enabled %s for %s', integration.name, projectId)); - } - ); -} - -/** - * Delete a respository - * - * @param client ApiClient - * @param orgSlug Organization Slug - * @param repositoryId Repository ID - */ -export function deleteRepository(client: Client, orgSlug: string, repositoryId: string) { - addLoadingMessage(); - const promise = client.requestPromise( - `/organizations/${orgSlug}/repos/${repositoryId}/`, - { - method: 'DELETE', - } - ); - promise.then( - () => clearIndicators(), - () => addErrorMessage(t('Unable to delete repository.')) - ); - return promise; -} - /** * Cancel the deletion of a respository * From 0d76cdc68d85797c6c9e4e04c57b1c2a5fd1cfa3 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 31 Dec 2024 11:11:38 -0800 Subject: [PATCH 595/757] test: Fix testing-library/prefer-query-by-disappearance violations (#82775) --- eslint.config.mjs | 1 - .../performanceScoreBreakdownChart.spec.tsx | 2 +- .../components/webVitalsDetailPanel.spec.tsx | 2 +- .../components/fullSpanDescription.spec.tsx | 6 +++--- .../components/spanDescription.spec.tsx | 8 ++++---- .../spans/selectors/domainSelector.spec.tsx | 4 ++-- .../tagDetailsDrawerContent.spec.tsx | 4 ++-- static/app/views/issueList/overview.spec.tsx | 20 +++++++++---------- .../timeline/timelineCursor.spec.tsx | 2 +- .../views/performance/landing/index.spec.tsx | 4 ++-- .../auditLogView.spec.tsx | 2 +- 11 files changed, 27 insertions(+), 28 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 0412f79d0b1e7c..c0445fbf0022c4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -964,7 +964,6 @@ export default typescript.config([ rules: { 'testing-library/no-container': 'warn', // TODO(ryan953): Fix the violations, then delete this line 'testing-library/no-node-access': 'warn', // TODO(ryan953): Fix the violations, then delete this line - 'testing-library/prefer-query-by-disappearance': 'warn', // TODO(ryan953): Fix the violations, then delete this line 'testing-library/prefer-screen-queries': 'warn', // TODO(ryan953): Fix the violations, then delete this line }, }, diff --git a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx index 0132c835d44b5a..61931dc8493b21 100644 --- a/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/charts/performanceScoreBreakdownChart.spec.tsx @@ -64,7 +64,7 @@ describe('PerformanceScoreBreakdownChart', function () { key: '', }); render(, {organization}); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(eventsStatsMock).toHaveBeenCalledWith( '/organizations/org-slug/events-stats/', diff --git a/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx b/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx index 0de0bf2f021368..5129c77ae406c2 100644 --- a/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx +++ b/static/app/views/insights/browser/webVitals/components/webVitalsDetailPanel.spec.tsx @@ -61,7 +61,7 @@ describe('WebVitalsDetailPanel', function () { render( undefined} webVital="lcp" />, { organization, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); // Raw web vital metric tile queries expect(eventsMock).toHaveBeenNthCalledWith( 1, diff --git a/static/app/views/insights/common/components/fullSpanDescription.spec.tsx b/static/app/views/insights/common/components/fullSpanDescription.spec.tsx index a97a94039c757a..75df64db45e015 100644 --- a/static/app/views/insights/common/components/fullSpanDescription.spec.tsx +++ b/static/app/views/insights/common/components/fullSpanDescription.spec.tsx @@ -81,7 +81,7 @@ describe('FullSpanDescription', function () { {organization} ); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); const queryCodeSnippet = await screen.findByText( /select users from my_table limit 1;/i @@ -127,7 +127,7 @@ describe('FullSpanDescription', function () { organization, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); const queryCodeSnippet = screen.getByText( /\{ "insert": "my_cool_collection😎", "a": \{\} \}/i @@ -173,7 +173,7 @@ describe('FullSpanDescription', function () { organization, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); // The last truncated entry will have a null value assigned and the JSON document is properly closed const queryCodeSnippet = screen.getByText( diff --git a/static/app/views/insights/common/components/spanDescription.spec.tsx b/static/app/views/insights/common/components/spanDescription.spec.tsx index b59afb98f1e272..115af8f1389d8a 100644 --- a/static/app/views/insights/common/components/spanDescription.spec.tsx +++ b/static/app/views/insights/common/components/spanDescription.spec.tsx @@ -54,7 +54,7 @@ describe('DatabaseSpanDescription', function () { {organization} ); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(screen.getByText('SELECT USERS FRO*')).toBeInTheDocument(); }); @@ -99,7 +99,7 @@ describe('DatabaseSpanDescription', function () { {organization} ); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect( await screen.findByText('SELECT users FROM my_table LIMIT 1;') @@ -150,7 +150,7 @@ describe('DatabaseSpanDescription', function () { {organization} ); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect( await screen.findByText('SELECT users FROM my_table LIMIT 1;') @@ -205,7 +205,7 @@ describe('DatabaseSpanDescription', function () { {organization} ); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); // expect(await screen.findBy).toBeInTheDocument(); const mongoQuerySnippet = await screen.findByText( diff --git a/static/app/views/insights/common/views/spans/selectors/domainSelector.spec.tsx b/static/app/views/insights/common/views/spans/selectors/domainSelector.spec.tsx index 30a5d19530e453..701a064293d60c 100644 --- a/static/app/views/insights/common/views/spans/selectors/domainSelector.spec.tsx +++ b/static/app/views/insights/common/views/spans/selectors/domainSelector.spec.tsx @@ -64,7 +64,7 @@ describe('DomainSelector', function () { it('allows selecting a domain', async function () { render(); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); await selectEvent.openMenu(screen.getByText('All')); @@ -93,7 +93,7 @@ describe('DomainSelector', function () { render(); expect(fetchMoreResponse).not.toHaveBeenCalled(); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); await selectEvent.openMenu(screen.getByText('All')); diff --git a/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.spec.tsx b/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.spec.tsx index 48d43d7f4adc23..548377b2b422e5 100644 --- a/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.spec.tsx +++ b/static/app/views/issueDetails/groupTags/tagDetailsDrawerContent.spec.tsx @@ -63,7 +63,7 @@ describe('TagDetailsDrawerContent', () => { }); render(, {router}); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(screen.getByText('Value')).toBeInTheDocument(); expect(screen.getByText('Last Seen')).toBeInTheDocument(); @@ -101,7 +101,7 @@ describe('TagDetailsDrawerContent', () => { }); render(, {router}); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(screen.getByRole('button', {name: 'Previous'})).toBeDisabled(); expect(screen.getByRole('button', {name: 'Next'})).toBeEnabled(); diff --git a/static/app/views/issueList/overview.spec.tsx b/static/app/views/issueList/overview.spec.tsx index 374c4b757a43d1..117f728f0abf12 100644 --- a/static/app/views/issueList/overview.spec.tsx +++ b/static/app/views/issueList/overview.spec.tsx @@ -214,7 +214,7 @@ describe('IssueList', function () { render(, {router}); // Loading saved searches - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(savedSearchesRequest).toHaveBeenCalledTimes(1); await screen.findByRole('grid', {name: 'Create a search query'}); @@ -481,7 +481,7 @@ describe('IssueList', function () { router, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); await userEvent.click(screen.getByRole('button', {name: /custom search/i})); await userEvent.click(screen.getByRole('button', {name: localSavedSearch.name})); @@ -512,7 +512,7 @@ describe('IssueList', function () { router, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); await screen.findByRole('grid', {name: 'Create a search query'}); await userEvent.click(screen.getByRole('button', {name: 'Clear search query'})); @@ -552,7 +552,7 @@ describe('IssueList', function () { router, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); await screen.findByRole('grid', {name: 'Create a search query'}); await userEvent.click(screen.getByRole('button', {name: 'Clear search query'})); @@ -628,7 +628,7 @@ describe('IssueList', function () { router: routerWithSavedSearch, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(screen.getByRole('button', {name: 'My Default Search'})).toBeInTheDocument(); @@ -677,7 +677,7 @@ describe('IssueList', function () { router: routerWithSavedSearch, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(screen.getByRole('button', {name: savedSearch.name})).toBeInTheDocument(); @@ -728,7 +728,7 @@ describe('IssueList', function () { {router: newRouter} ); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); const createPin = MockApiClient.addMockResponse({ url: '/organizations/org-slug/pinned-searches/', @@ -810,7 +810,7 @@ describe('IssueList', function () { {router: newRouter} ); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); await userEvent.click(screen.getByLabelText(/Remove Default/i)); @@ -835,7 +835,7 @@ describe('IssueList', function () { router, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(screen.getByRole('button', {name: 'Previous'})).toBeDisabled(); @@ -1159,7 +1159,7 @@ describe('IssueList', function () { }; render(, {router}); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); }; it('displays when no projects selected and all projects user is member of, async does not have first event', async function () { diff --git a/static/app/views/monitors/components/timeline/timelineCursor.spec.tsx b/static/app/views/monitors/components/timeline/timelineCursor.spec.tsx index 5e379b502c2e1a..ba9605c3006f70 100644 --- a/static/app/views/monitors/components/timeline/timelineCursor.spec.tsx +++ b/static/app/views/monitors/components/timeline/timelineCursor.spec.tsx @@ -59,6 +59,6 @@ describe('TimelineCursor', function () { // move cursor outside it is not visible fireEvent.mouseMove(body, {clientX: 120, clientY: 20}); - await waitForElementToBeRemoved(() => screen.getByRole('presentation')); + await waitForElementToBeRemoved(() => screen.queryByRole('presentation')); }); }); diff --git a/static/app/views/performance/landing/index.spec.tsx b/static/app/views/performance/landing/index.spec.tsx index 494fc6d756914b..be1e5200c34cb1 100644 --- a/static/app/views/performance/landing/index.spec.tsx +++ b/static/app/views/performance/landing/index.spec.tsx @@ -296,7 +296,7 @@ describe('Performance > Landing > Index', function () { render(); - await waitForElementToBeRemoved(() => screen.getAllByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator')); await userEvent.type(screen.getByPlaceholderText('Search Transactions'), '{enter}'); expect(searchHandlerMock).toHaveBeenCalledWith('', 'transactionsOnly'); }); @@ -320,7 +320,7 @@ describe('Performance > Landing > Index', function () { wrapper = render(); - await waitForElementToBeRemoved(() => screen.getAllByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-indicator')); expect(await screen.findByPlaceholderText('Search Transactions')).toHaveValue(''); }); diff --git a/static/app/views/settings/organizationAuditLog/auditLogView.spec.tsx b/static/app/views/settings/organizationAuditLog/auditLogView.spec.tsx index 991815afbb7eed..fb6f41185fdc11 100644 --- a/static/app/views/settings/organizationAuditLog/auditLogView.spec.tsx +++ b/static/app/views/settings/organizationAuditLog/auditLogView.spec.tsx @@ -126,7 +126,7 @@ describe('OrganizationAuditLog', function () { router, }); - await waitForElementToBeRemoved(() => screen.getByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); // rule.edit -> issue-alert.edit expect(screen.getByText('issue-alert.edit')).toBeInTheDocument(); From ce39e5cceef3aa7286ff495cd0f81f1d60d91984 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Tue, 31 Dec 2024 14:23:07 -0500 Subject: [PATCH 596/757] chore(typing): Add typing to datasets/function_aliases.py (#82778) --- pyproject.toml | 1 - .../events/datasets/function_aliases.py | 23 +++++++++++-------- src/sentry/search/events/datasets/metrics.py | 8 ++----- src/sentry/snuba/metrics/fields/snql.py | 10 +++----- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 70578478f99d27..5b3690d2f261a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -277,7 +277,6 @@ module = [ "sentry.search.events.builder.errors", "sentry.search.events.builder.metrics", "sentry.search.events.datasets.filter_aliases", - "sentry.search.events.datasets.function_aliases", "sentry.search.events.datasets.metrics", "sentry.search.events.datasets.metrics_layer", "sentry.search.events.fields", diff --git a/src/sentry/search/events/datasets/function_aliases.py b/src/sentry/search/events/datasets/function_aliases.py index 071cccd476b759..70480101f3b9f6 100644 --- a/src/sentry/search/events/datasets/function_aliases.py +++ b/src/sentry/search/events/datasets/function_aliases.py @@ -12,19 +12,16 @@ from sentry.search.events import constants from sentry.search.events.builder.base import BaseQueryBuilder from sentry.search.events.types import SelectType -from sentry.sentry_metrics.configuration import UseCaseKey -from sentry.sentry_metrics.use_case_id_registry import UseCaseID from sentry.utils.hashlib import fnv1a_32 def resolve_project_threshold_config( # See resolve_tag_value signature - tag_value_resolver: Callable[[UseCaseID | UseCaseKey, int, str], int | str | None], + tag_value_resolver: Callable[[int, str], int | str | None], # See resolve_tag_key signature - column_name_resolver: Callable[[UseCaseID | UseCaseKey, int, str], str], + column_name_resolver: Callable[[int, str], str], project_ids: Sequence[int], org_id: int, - use_case_id: UseCaseID | None = None, ) -> SelectType: """ Shared function that resolves the project threshold configuration used by both snuba/metrics @@ -89,7 +86,7 @@ def resolve_project_threshold_config( # and no project configs were set, we can skip it in the final query continue - transaction_id = tag_value_resolver(use_case_id, org_id, transaction) + transaction_id = tag_value_resolver(org_id, transaction) # Don't add to the config if we can't resolve it if transaction_id is None: continue @@ -116,7 +113,7 @@ def resolve_project_threshold_config( project_threshold_override_config_keys, ( Column(name="project_id"), - Column(name=column_name_resolver(use_case_id, org_id, "transaction")), + Column(name=column_name_resolver(org_id, "transaction")), ), ], constants.PROJECT_THRESHOLD_OVERRIDE_CONFIG_INDEX_ALIAS, @@ -183,7 +180,7 @@ def resolve_metrics_percentile( fixed_percentile: float | None = None, extra_conditions: list[Function] | None = None, ) -> SelectType: - if fixed_percentile is None: + if fixed_percentile is None and isinstance(args["percentile"], float): fixed_percentile = args["percentile"] if fixed_percentile not in constants.METRIC_PERCENTILES: raise IncompatibleMetricsQuery("Custom quantile incompatible with metrics") @@ -242,6 +239,10 @@ def resolve_avg_compare_if( alias: str | None, ) -> SelectType: """Helper function for avg compare""" + if not isinstance(args["comparison_column"], str): + raise InvalidSearchQuery( + f"Invalid column type: expected got {args['comparison_column']}" + ) return Function( "avgIf", [ @@ -280,10 +281,12 @@ def resolve_metrics_layer_percentile( fixed_percentile: float | None = None, ) -> SelectType: # TODO: rename to just resolve_metrics_percentile once the non layer code can be retired - if fixed_percentile is None: + if fixed_percentile is None and isinstance(args["percentile"], float): fixed_percentile = args["percentile"] if fixed_percentile not in constants.METRIC_PERCENTILES: raise IncompatibleMetricsQuery("Custom quantile incompatible with metrics") + if not isinstance(args["column"], str): + raise InvalidSearchQuery(f"Invalid column type: expected got {args['column']}") column = resolve_mri(args["column"]) return ( Function( @@ -305,7 +308,7 @@ def resolve_metrics_layer_percentile( def resolve_division( - dividend: SelectType, divisor: SelectType, alias: str, fallback: Any | None = None + dividend: SelectType, divisor: SelectType, alias: str | None, fallback: Any | None = None ) -> SelectType: return Function( "if", diff --git a/src/sentry/search/events/datasets/metrics.py b/src/sentry/search/events/datasets/metrics.py index ee851f009946de..2244eef99af70a 100644 --- a/src/sentry/search/events/datasets/metrics.py +++ b/src/sentry/search/events/datasets/metrics.py @@ -981,12 +981,8 @@ def _resolve_transaction_alias_on_demand(self, _: str) -> SelectType: @cached_property def _resolve_project_threshold_config(self) -> SelectType: return function_aliases.resolve_project_threshold_config( - tag_value_resolver=lambda _use_case_id, _org_id, value: self.builder.resolve_tag_value( - value - ), - column_name_resolver=lambda _use_case_id, _org_id, value: self.builder.resolve_column_name( - value - ), + tag_value_resolver=lambda _org_id, value: self.builder.resolve_tag_value(value), + column_name_resolver=lambda _org_id, value: self.builder.resolve_column_name(value), org_id=( self.builder.params.organization.id if self.builder.params.organization else None ), diff --git a/src/sentry/snuba/metrics/fields/snql.py b/src/sentry/snuba/metrics/fields/snql.py index e4f25719760ee7..85925024144292 100644 --- a/src/sentry/snuba/metrics/fields/snql.py +++ b/src/sentry/snuba/metrics/fields/snql.py @@ -775,16 +775,12 @@ def team_key_transaction_snql( def _resolve_project_threshold_config(project_ids: Sequence[int], org_id: int) -> SelectType: + use_case_id = UseCaseID.TRANSACTIONS return resolve_project_threshold_config( - tag_value_resolver=lambda use_case_id, org_id, value: resolve_tag_value( - use_case_id, org_id, value - ), - column_name_resolver=lambda use_case_id, org_id, value: resolve_tag_key( - use_case_id, org_id, value - ), + tag_value_resolver=lambda org_id, value: resolve_tag_value(use_case_id, org_id, value), + column_name_resolver=lambda org_id, value: resolve_tag_key(use_case_id, org_id, value), project_ids=project_ids, org_id=org_id, - use_case_id=UseCaseID.TRANSACTIONS, ) From 0d731f4e4aba332d51a922203018f2412fe3e6ad Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:26:36 -0500 Subject: [PATCH 597/757] feat(dashboards): Improve `WidgetFrame` error handling (#82759) - Don't allow full screen in error state - Show actions in error mode if there is no retry - Add error boundary --- .../dashboards/widgets/common/settings.tsx | 3 + .../widgets/common/widgetFrame.spec.tsx | 73 +++++++++++ .../dashboards/widgets/common/widgetFrame.tsx | 114 ++++++++++-------- 3 files changed, 143 insertions(+), 47 deletions(-) diff --git a/static/app/views/dashboards/widgets/common/settings.tsx b/static/app/views/dashboards/widgets/common/settings.tsx index 997cf344ab11d1..9e5cb22fcc306d 100644 --- a/static/app/views/dashboards/widgets/common/settings.tsx +++ b/static/app/views/dashboards/widgets/common/settings.tsx @@ -11,3 +11,6 @@ export const DEFAULT_FIELD = 'unknown'; // Numeric data might, in theory, have a export const MISSING_DATA_MESSAGE = t('No Data'); export const NON_FINITE_NUMBER_MESSAGE = t('Value is not a finite number.'); +export const WIDGET_RENDER_ERROR_MESSAGE = t( + 'Sorry, something went wrong when rendering this widget.' +); diff --git a/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx b/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx index 32a3fa17293b16..82931c954222ba 100644 --- a/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx +++ b/static/app/views/dashboards/widgets/common/widgetFrame.spec.tsx @@ -12,6 +12,24 @@ describe('WidgetFrame', () => { await userEvent.hover(screen.getByRole('button', {name: 'Widget description'})); expect(await screen.findByText('Number of events per second')).toBeInTheDocument(); }); + + it('Catches errors in the visualization', async () => { + jest.spyOn(console, 'error').mockImplementation(); + + render( + + + + ); + + expect(screen.getByText('Uh Oh')).toBeInTheDocument(); + + expect( + await screen.findByText('Sorry, something went wrong when rendering this widget.') + ).toBeInTheDocument(); + + jest.resetAllMocks(); + }); }); describe('Warnings', () => { @@ -184,6 +202,41 @@ describe('WidgetFrame', () => { await userEvent.hover($trigger); expect(await screen.findByText('Actions are not supported')).toBeInTheDocument(); }); + + it('Shows actions even in error state', async () => { + const onAction = jest.fn(); + const error = new Error('Something is wrong'); + + render( + + ); + + const $button = screen.getByRole('button', {name: 'Make Go'}); + expect($button).toBeInTheDocument(); + await userEvent.click($button); + + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('Shows a "Retry" action if a retry callback is provided', () => { + const onRetry = jest.fn(); + const error = new Error('Something is wrong'); + + render(); + + expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument(); + }); }); describe('Full Screen View Button', () => { @@ -205,5 +258,25 @@ describe('WidgetFrame', () => { expect(onFullScreenViewClick).toHaveBeenCalledTimes(1); }); + + it('Hides full screen button if the widget has an error', () => { + const onFullScreenViewClick = jest.fn(); + + render( + + ); + + const $button = screen.queryByRole('button', {name: 'Open Full-Screen View'}); + expect($button).not.toBeInTheDocument(); + }); }); }); + +function UhOh() { + const items: string[] = []; + return
    {items[0].toUpperCase()}
    ; +} diff --git a/static/app/views/dashboards/widgets/common/widgetFrame.tsx b/static/app/views/dashboards/widgets/common/widgetFrame.tsx index 8528412b4dbb00..f8866ad1953c95 100644 --- a/static/app/views/dashboards/widgets/common/widgetFrame.tsx +++ b/static/app/views/dashboards/widgets/common/widgetFrame.tsx @@ -4,13 +4,20 @@ import Badge, {type BadgeProps} from 'sentry/components/badge/badge'; import {Button, LinkButton} from 'sentry/components/button'; import {HeaderTitle} from 'sentry/components/charts/styles'; import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu'; +import ErrorBoundary from 'sentry/components/errorBoundary'; import {Tooltip} from 'sentry/components/tooltip'; import {IconEllipsis, IconExpand, IconInfo, IconWarning} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {ErrorPanel} from './errorPanel'; -import {MIN_HEIGHT, MIN_WIDTH, X_GUTTER, Y_GUTTER} from './settings'; +import { + MIN_HEIGHT, + MIN_WIDTH, + WIDGET_RENDER_ERROR_MESSAGE, + X_GUTTER, + Y_GUTTER, +} from './settings'; import {TooltipIconTrigger} from './tooltipIconTrigger'; import type {StateProps} from './types'; import {WarningsList} from './warningsList'; @@ -42,9 +49,14 @@ export function WidgetFrame(props: WidgetFrameProps) { onAction: props.onRetry, }, ] - : [] + : props.actions : props.actions) ?? []; + const shouldShowFullScreenViewButton = + Boolean(props.onFullScreenViewClick) && !props.error; + + const shouldShowActions = actions && actions.length > 0; + return (
    @@ -65,9 +77,7 @@ export function WidgetFrame(props: WidgetFrameProps) { (currentBadgeProps, i) => )} - {(props.description || - props.onFullScreenViewClick || - (actions && actions.length > 0)) && ( + {(props.description || shouldShowFullScreenViewButton || shouldShowActions) && ( {props.description && ( // Ideally we'd use `QuestionTooltip` but we need to firstly paint the icon dark, give it 100% opacity, and remove hover behaviour. @@ -96,48 +106,50 @@ export function WidgetFrame(props: WidgetFrameProps) { )} - - {actions.length === 1 ? ( - actions[0].to ? ( - - {actions[0].label} - - ) : ( - - ) - ) : null} - - {actions.length > 1 ? ( - , - }} - position="bottom-end" - /> - ) : null} - + {shouldShowActions && ( + + {actions.length === 1 ? ( + actions[0].to ? ( + + {actions[0].label} + + ) : ( + + ) + ) : null} + + {actions.length > 1 ? ( + , + }} + position="bottom-end" + /> + ) : null} + + )} - {props.onFullScreenViewClick && ( + {shouldShowFullScreenViewButton && (
    diff --git a/static/app/components/replays/diff/learnMoreButton.tsx b/static/app/components/replays/diff/learnMoreButton.tsx index 1de593ac9314ec..82aeaf0060b6bd 100644 --- a/static/app/components/replays/diff/learnMoreButton.tsx +++ b/static/app/components/replays/diff/learnMoreButton.tsx @@ -1,4 +1,4 @@ -import type {ReactNode} from 'react'; +import type {ComponentProps, ReactNode} from 'react'; import {ClassNames} from '@emotion/react'; import styled from '@emotion/styled'; @@ -58,12 +58,15 @@ function Buttons() { ); } -export default function LearnMoreButton() { +export default function LearnMoreButton( + hoverCardProps: Partial> +) { return ( {({css}) => ( } bodyClassName={css` padding: ${space(1)}; diff --git a/static/app/utils/useHoverOverlay.tsx b/static/app/utils/useHoverOverlay.tsx index 2032436cfb229c..ea5c8d3191357d 100644 --- a/static/app/utils/useHoverOverlay.tsx +++ b/static/app/utils/useHoverOverlay.tsx @@ -98,6 +98,19 @@ interface UseHoverOverlayProps { * Offset along the main axis. */ offset?: number; + + /** + * Callback whenever the hovercard is blurred + * See also `onHover` + */ + onBlur?: () => void; + + /** + * Callback whenever the hovercard is hovered + * See also `onBlur` + */ + onHover?: () => void; + /** * Position for the overlay. */ @@ -115,6 +128,7 @@ interface UseHoverOverlayProps { * If child node supports ref forwarding, you can skip apply a wrapper */ skipWrapper?: boolean; + /** * Color of the dotted underline, if available. See also: showUnderline. */ @@ -159,6 +173,8 @@ function useHoverOverlay( offset = 8, position = 'top', containerDisplayMode = 'inline-block', + onHover, + onBlur, }: UseHoverOverlayProps ) { const theme = useTheme(); @@ -167,6 +183,14 @@ function useHoverOverlay( const [isVisible, setIsVisible] = useState(forceVisible ?? false); const isOpen = forceVisible ?? isVisible; + useEffect(() => { + if (isOpen) { + onHover?.(); + } else { + onBlur?.(); + } + }, [isOpen, onBlur, onHover]); + const [triggerElement, setTriggerElement] = useState(null); const [overlayElement, setOverlayElement] = useState(null); const [arrowElement, setArrowElement] = useState(null); From d6b64d5cc4b83504f2fc125bf36e23721e330254 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Tue, 31 Dec 2024 15:09:52 -0500 Subject: [PATCH 600/757] chore(typing): Add typing to datasets/metrics.py (#82781) --- pyproject.toml | 1 - src/sentry/search/events/datasets/metrics.py | 49 ++++++++++++-------- src/sentry/snuba/metrics/utils.py | 3 +- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5b3690d2f261a2..79b9ee76d5a6bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -277,7 +277,6 @@ module = [ "sentry.search.events.builder.errors", "sentry.search.events.builder.metrics", "sentry.search.events.datasets.filter_aliases", - "sentry.search.events.datasets.metrics", "sentry.search.events.datasets.metrics_layer", "sentry.search.events.fields", "sentry.search.events.filter", diff --git a/src/sentry/search/events/datasets/metrics.py b/src/sentry/search/events/datasets/metrics.py index 2244eef99af70a..300db80c4034b9 100644 --- a/src/sentry/search/events/datasets/metrics.py +++ b/src/sentry/search/events/datasets/metrics.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Callable, Mapping, MutableMapping from django.utils.functional import cached_property from snuba_sdk import Column, Condition, Function, Op, OrderBy @@ -980,12 +980,13 @@ def _resolve_transaction_alias_on_demand(self, _: str) -> SelectType: @cached_property def _resolve_project_threshold_config(self) -> SelectType: + org_id = self.builder.params.organization_id + if org_id is None: + raise InvalidSearchQuery("Missing organization") return function_aliases.resolve_project_threshold_config( tag_value_resolver=lambda _org_id, value: self.builder.resolve_tag_value(value), column_name_resolver=lambda _org_id, value: self.builder.resolve_column_name(value), - org_id=( - self.builder.params.organization.id if self.builder.params.organization else None - ), + org_id=org_id, project_ids=self.builder.params.project_ids, ) @@ -1044,17 +1045,18 @@ def _transaction_filter_converter(self, search_filter: SearchFilter) -> WhereTyp return None if isinstance(value, list): - resolved_value = [] + resolved_values = [] for item in value: resolved_item = self.builder.resolve_tag_value(item) if resolved_item is None: raise IncompatibleMetricsQuery(f"Transaction value {item} in filter not found") - resolved_value.append(resolved_item) + resolved_values.append(resolved_item) + value = resolved_values else: resolved_value = self.builder.resolve_tag_value(value) if resolved_value is None: raise IncompatibleMetricsQuery(f"Transaction value {value} in filter not found") - value = resolved_value + value = resolved_value if search_filter.value.is_wildcard(): return Condition( @@ -1231,8 +1233,9 @@ def _resolve_histogram_function( buckets""" zoom_params = getattr(self.builder, "zoom_params", None) num_buckets = getattr(self.builder, "num_buckets", 250) + histogram_aliases = getattr(self.builder, "histogram_aliases", []) + histogram_aliases.append(alias) metric_condition = Function("equals", [Column("metric_id"), args["metric_id"]]) - self.builder.histogram_aliases.append(alias) return Function( f"histogramIf({num_buckets})", [ @@ -1297,6 +1300,8 @@ def _resolve_user_misery_function( args: Mapping[str, str | Column | SelectType | int | float], alias: str | None = None, ) -> SelectType: + if not isinstance(args["alpha"], float) or not isinstance(args["beta"], float): + raise InvalidSearchQuery("Cannot query user_misery with non floating point alpha/beta") if args["satisfaction"] is not None: raise IncompatibleMetricsQuery( "Cannot query user_misery with a threshold parameter on the metrics dataset" @@ -1464,9 +1469,13 @@ def _resolve_web_vital_function( ) -> SelectType: column = args["column"] metric_id = args["metric_id"] - quality = args["quality"].lower() + quality = args["quality"] - if column not in [ + if not isinstance(quality, str): + raise InvalidSearchQuery(f"Invalid argument quanlity: {quality}") + quality = quality.lower() + + if not isinstance(column, str) or column not in [ "measurements.lcp", "measurements.fcp", "measurements.fp", @@ -1520,12 +1529,12 @@ def _resolve_web_vital_function( def _resolve_web_vital_score_function( self, args: Mapping[str, str | Column | SelectType | int | float], - alias: str, + alias: str | None, ) -> SelectType: column = args["column"] metric_id = args["metric_id"] - if column not in [ + if not isinstance(column, str) or column not in [ "measurements.score.lcp", "measurements.score.fcp", "measurements.score.fid", @@ -1609,7 +1618,7 @@ def _resolve_web_vital_opportunity_score_function( column = args["column"] metric_id = args["metric_id"] - if column not in [ + if not isinstance(column, str) or column not in [ "measurements.score.lcp", "measurements.score.fcp", "measurements.score.fid", @@ -1765,7 +1774,7 @@ def _resolve_total_web_vital_opportunity_score_with_fixed_weights_function( alias, ) - def _resolve_total_score_weights_function(self, column: str, alias: str) -> SelectType: + def _resolve_total_score_weights_function(self, column: str, alias: str | None) -> SelectType: """Calculates the total sum score weights for a given web vital. This must be cached since it runs another query.""" @@ -1824,7 +1833,7 @@ def _resolve_count_scores_function( def _resolve_total_performance_score_function( self, _: Mapping[str, str | Column | SelectType | int | float], - alias: str, + alias: str | None, ) -> SelectType: vitals = ["lcp", "fcp", "cls", "ttfb", "inp"] scores = { @@ -1906,6 +1915,8 @@ def _resolve_total_transaction_duration(self, alias: str, scope: str) -> SelectT def _resolve_time_spent_percentage( self, args: Mapping[str, str | Column | SelectType | int | float], alias: str ) -> SelectType: + if not isinstance(args["scope"], str): + raise InvalidSearchQuery(f"Invalid scope: {args['scope']}") total_time = self._resolve_total_transaction_duration( constants.TOTAL_TRANSACTION_DURATION_ALIAS, args["scope"] ) @@ -1928,7 +1939,7 @@ def _resolve_time_spent_percentage( def _resolve_epm( self, - args: Mapping[str, str | Column | SelectType | int | float], + args: MutableMapping[str, str | Column | SelectType | int | float], alias: str | None = None, extra_condition: Function | None = None, ) -> SelectType: @@ -1938,7 +1949,7 @@ def _resolve_epm( def _resolve_spm( self, - args: Mapping[str, str | Column | SelectType | int | float], + args: MutableMapping[str, str | Column | SelectType | int | float], alias: str | None = None, extra_condition: Function | None = None, ) -> SelectType: @@ -1948,7 +1959,7 @@ def _resolve_spm( def _resolve_eps( self, - args: Mapping[str, str | Column | SelectType | int | float], + args: MutableMapping[str, str | Column | SelectType | int | float], alias: str | None = None, extra_condition: Function | None = None, ) -> SelectType: @@ -1962,7 +1973,7 @@ def _resolve_rate( args: Mapping[str, str | Column | SelectType | int | float], alias: str | None = None, extra_condition: Function | None = None, - metric: str | None = "transaction.duration", + metric: str = "transaction.duration", ) -> SelectType: base_condition = Function( "equals", diff --git a/src/sentry/snuba/metrics/utils.py b/src/sentry/snuba/metrics/utils.py index 38af21944437ee..df12ad96eaf7cb 100644 --- a/src/sentry/snuba/metrics/utils.py +++ b/src/sentry/snuba/metrics/utils.py @@ -4,7 +4,7 @@ from abc import ABC from collections.abc import Collection, Generator, Mapping, Sequence from datetime import datetime, timedelta, timezone -from typing import Literal, TypedDict, overload +from typing import Literal, NotRequired, TypedDict, overload from sentry.sentry_metrics.use_case_id_registry import UseCaseID from sentry.snuba.dataset import EntityKey @@ -339,6 +339,7 @@ class MetricMeta(TypedDict): type: MetricType operations: Collection[MetricOperationType] unit: MetricUnit | None + metric_id: NotRequired[int] mri: str projectIds: Sequence[int] blockingStatus: Sequence[BlockedMetric] | None From ddc5d25208e95187e96342fe8d482f8beac43442 Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:04:27 -0500 Subject: [PATCH 601/757] feat(insights): adds flag for handling missing vitals (#82762) Currently, if a specific web vital is never reported in a query, we treat the score as 0. This is actually misleading, instead we want to exclude missing vitals from any score calculation. This flag will control the logic in the frontend and backend to handle that. --- src/sentry/features/temporary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index b426617ccd3368..d91e8323bd2a7b 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -321,6 +321,8 @@ def register_temporary_features(manager: FeatureManager): manager.add("organizations:performance-use-metrics", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True) # Enable showing INP web vital in default views manager.add("organizations:performance-vitals-inp", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) + # Enable handling missing webvitals in performance score + manager.add("organizations:performance-vitals-handle-missing-webvitals", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False) # Enable profiling manager.add("organizations:profiling", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True) # Enabled for those orgs who participated in the profiling Beta program From 47ef6341e0d2a43e40222b7d2b742719af509b91 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:06:09 -0500 Subject: [PATCH 602/757] ref: fix missing coverage due to typo in test (#82788) --- .../api/endpoints/test_organization_event_details.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/snuba/api/endpoints/test_organization_event_details.py b/tests/snuba/api/endpoints/test_organization_event_details.py index 01c83b6b45da38..09d6a7bfa07e4e 100644 --- a/tests/snuba/api/endpoints/test_organization_event_details.py +++ b/tests/snuba/api/endpoints/test_organization_event_details.py @@ -14,10 +14,6 @@ pytestmark = pytest.mark.sentry_metrics -def format_project_event(project_id_or_slug, event_id): - return f"{project_id_or_slug}:{event_id}" - - class OrganizationEventDetailsEndpointTest(APITestCase, SnubaTestCase, OccurrenceTestMixin): def setUp(self): super().setUp() @@ -381,7 +377,7 @@ def test_get_multiple_columns(self): "avg(span.self_time)": 1.0, "avg(span.duration)": 2.0, } - if span["op"] == "django.middlewares": + if span["op"] == "django.middleware": assert self.RESULT_COLUMN not in span def test_nan_column(self): @@ -397,7 +393,7 @@ def test_nan_column(self): for span in entry["data"]: if span["op"] == "db": assert span[self.RESULT_COLUMN] == {"avg(span.self_time)": 1.0} - if span["op"] == "django.middlewares": + if span["op"] == "django.middleware": assert self.RESULT_COLUMN not in span def test_invalid_column(self): From 777bb39118867b3096227a510e53741f839b710f Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:13:20 -0500 Subject: [PATCH 603/757] ref: first pass at removing some dead code in tests (#82792) I only got through about ~30% of the test files -- will follow up with other ones some explanation for some patterns: - deleted function => nothing called it - `raise SomeType` -> `raise AssertionError` -- `AssertionError` is excluded from coverage - `pass` -> `raise NotImplementedError` -- these functions are never called -- only used for their shape / name and `NotImplementedError` is excluded from coverage - `try: some_test_code except: fail(...)` -> no need for the `try`, an uncaught exception already fails the test --- .../consumers/test_slicing_router.py | 5 +- .../sentry_metrics/test_postgres_indexer.py | 10 +- tests/sentry/sentry_metrics/test_snuba.py | 4 - tests/sentry/sentry_metrics/test_strings.py | 2 +- .../test_metrics_enhanced_performance.py | 8 +- tests/sentry/snuba/metrics/test_utils.py | 6 +- tests/sentry/tasks/test_relay.py | 10 -- tests/sentry/tasks/test_relocation.py | 2 +- tests/sentry/tasks/test_reprocessing2.py | 4 +- tests/sentry/tasks/test_store.py | 10 +- tests/sentry/tasks/test_symbolication.py | 8 +- tests/sentry/taskworker/test_registry.py | 21 ++-- tests/sentry/taskworker/test_task.py | 7 +- .../pytest/mocking/animals/__init__.py | 2 +- .../sentry/uptime/subscriptions/test_tasks.py | 5 +- tests/sentry/utils/kvstore/test_common.py | 4 +- .../utils/locking/backends/test_migration.py | 8 +- .../utils/locking/backends/test_redis.py | 2 +- tests/sentry/utils/test_auth.py | 4 +- tests/sentry/utils/test_circuit_breaker2.py | 106 ------------------ tests/sentry/utils/test_committers.py | 15 --- tests/sentry/utils/test_concurrent.py | 16 +-- tests/sentry/utils/test_glob.py | 57 +++++----- tests/sentry/utils/test_math.py | 9 -- tests/sentry/utils/test_registry.py | 6 +- tests/sentry/utils/test_retries.py | 7 +- tests/sentry/web/frontend/test_auth_oauth2.py | 2 +- .../test_organization_auth_settings.py | 6 - .../endpoints/test_validators.py | 16 +-- .../workflow_engine/test_integration.py | 14 +-- .../sentry_plugins/phabricator/test_plugin.py | 5 - tests/sentry_plugins/trello/test_plugin.py | 5 - ..._organization_events_facets_performance.py | 4 +- ...ion_events_facets_performance_histogram.py | 5 +- ...t_organization_events_spans_performance.py | 8 +- .../test_organization_events_stats_mep.py | 12 -- .../test_organization_group_index_stats.py | 16 +-- .../test_organization_spans_trace.py | 1 - .../test_organization_tagkey_values.py | 4 - tests/snuba/incidents/test_tasks.py | 5 - .../rules/conditions/test_event_frequency.py | 7 +- tests/snuba/search/test_backend.py | 28 ++--- tests/snuba/sessions/test_sessions.py | 14 +-- tests/snuba/tasks/test_unmerge.py | 3 +- tests/snuba/tsdb/test_tsdb_backend.py | 10 +- 45 files changed, 108 insertions(+), 395 deletions(-) diff --git a/tests/sentry/sentry_metrics/consumers/test_slicing_router.py b/tests/sentry/sentry_metrics/consumers/test_slicing_router.py index 380dcda84b6c68..3d8c7a59b2c6f6 100644 --- a/tests/sentry/sentry_metrics/consumers/test_slicing_router.py +++ b/tests/sentry/sentry_metrics/consumers/test_slicing_router.py @@ -169,10 +169,7 @@ def test_validate_slicing_consumer_config(monkeypatch) -> None: {"bootstrap.servers": "127.0.0.1:9092"}, ) - try: - _validate_slicing_consumer_config("generic_metrics") - except SlicingConfigurationException as e: - assert False, f"Should not raise exception: {e}" + _validate_slicing_consumer_config("generic_metrics") # should not raise def test_validate_slicing_config(monkeypatch) -> None: diff --git a/tests/sentry/sentry_metrics/test_postgres_indexer.py b/tests/sentry/sentry_metrics/test_postgres_indexer.py index 91b2f29df9d4a2..6bd5807b73aa88 100644 --- a/tests/sentry/sentry_metrics/test_postgres_indexer.py +++ b/tests/sentry/sentry_metrics/test_postgres_indexer.py @@ -1,7 +1,5 @@ -from collections.abc import Mapping - from sentry.sentry_metrics.configuration import UseCaseKey -from sentry.sentry_metrics.indexer.base import FetchType, Metadata, UseCaseKeyCollection +from sentry.sentry_metrics.indexer.base import UseCaseKeyCollection from sentry.sentry_metrics.indexer.cache import CachingIndexer from sentry.sentry_metrics.indexer.postgres.postgres_v2 import PGStringIndexerV2, indexer_cache from sentry.sentry_metrics.use_case_id_registry import UseCaseID @@ -9,12 +7,6 @@ from sentry.utils.cache import cache -def assert_fetch_type_for_tag_string_set( - meta: Mapping[str, Metadata], fetch_type: FetchType, str_set: set[str] -): - assert all([meta[string].fetch_type == fetch_type for string in str_set]) - - class PostgresIndexerV2Test(TestCase): def setUp(self) -> None: self.strings = {"hello", "hey", "hi"} diff --git a/tests/sentry/sentry_metrics/test_snuba.py b/tests/sentry/sentry_metrics/test_snuba.py index 6a1a8b76a3beba..3bbd6d87b89b10 100644 --- a/tests/sentry/sentry_metrics/test_snuba.py +++ b/tests/sentry/sentry_metrics/test_snuba.py @@ -23,10 +23,6 @@ class SnubaMetricsInterfaceTest(MetricsInterfaceTestCase): This test is also very similar to those in the Metrics Layer. """ - @property - def now(self): - return BaseMetricsLayerTestCase.MOCK_DATETIME - def test_count_query(self): generic_metrics_backend.distribution( self.use_case_id, diff --git a/tests/sentry/sentry_metrics/test_strings.py b/tests/sentry/sentry_metrics/test_strings.py index ed8e81a9323e81..ff4d94523fc350 100644 --- a/tests/sentry/sentry_metrics/test_strings.py +++ b/tests/sentry/sentry_metrics/test_strings.py @@ -118,7 +118,7 @@ def test_shared_mri_string_range(mri, id): "metric_stats": (800, 899), }[parsed_mri.namespace] except KeyError: - raise Exception(f"Unknown namespace: {parsed_mri.namespace}") + raise AssertionError(f"Unknown namespace: {parsed_mri.namespace}") start += PREFIX end += PREFIX diff --git a/tests/sentry/snuba/metrics/test_metrics_layer/test_metrics_enhanced_performance.py b/tests/sentry/snuba/metrics/test_metrics_layer/test_metrics_enhanced_performance.py index 16bf4c81ec37a8..5a597b78fb3c0b 100644 --- a/tests/sentry/snuba/metrics/test_metrics_layer/test_metrics_enhanced_performance.py +++ b/tests/sentry/snuba/metrics/test_metrics_layer/test_metrics_enhanced_performance.py @@ -530,7 +530,7 @@ def test_query_with_order_by_str_field_not_in_group_by(self): ) with pytest.raises(InvalidParams): - metrics_query = self.build_metrics_query( + self.build_metrics_query( before_now="1h", granularity="1h", select=[ @@ -550,12 +550,6 @@ def test_query_with_order_by_str_field_not_in_group_by(self): offset=Offset(offset=0), include_series=False, ) - get_series( - [self.project], - metrics_query=metrics_query, - include_meta=True, - use_case_id=UseCaseID.TRANSACTIONS, - ) def test_query_with_sum_if_column(self): for value, transaction in ((10, "/foo"), (20, "/bar"), (30, "/lorem")): diff --git a/tests/sentry/snuba/metrics/test_utils.py b/tests/sentry/snuba/metrics/test_utils.py index da08e55e25f176..c5db48a7f9cccd 100644 --- a/tests/sentry/snuba/metrics/test_utils.py +++ b/tests/sentry/snuba/metrics/test_utils.py @@ -162,11 +162,7 @@ ids=[x[5] for x in test_get_num_intervals_cases], ) def test_get_num_intervals(start, end, granularity, interval, expected, test_message): - if start is not None: - start_date = datetime.fromisoformat(start) - else: - start_date = None - + start_date = datetime.fromisoformat(start) end_date = datetime.fromisoformat(end) actual = get_num_intervals( diff --git a/tests/sentry/tasks/test_relay.py b/tests/sentry/tasks/test_relay.py index 15534c5f74308b..6948b794235633 100644 --- a/tests/sentry/tasks/test_relay.py +++ b/tests/sentry/tasks/test_relay.py @@ -7,7 +7,6 @@ from sentry.db.postgres.transactions import in_test_hide_transaction_boundary from sentry.models.options.project_option import ProjectOption -from sentry.models.project import Project from sentry.models.projectkey import ProjectKey, ProjectKeyStatus from sentry.relay.projectconfig_cache.redis import RedisProjectConfigCache from sentry.relay.projectconfig_debounce_cache.redis import RedisProjectConfigDebounceCache @@ -28,15 +27,6 @@ def _cache_keys_for_project(project): yield key.public_key -def _cache_keys_for_org(org): - # The `ProjectKey` model doesn't have any attribute we can use to filter by - # org, and the `Project` model doesn't have a project key exposed. So using - # the org we fetch the project, and then the project key. - for proj in Project.objects.filter(organization_id=org.id): - for key in ProjectKey.objects.filter(project_id=proj.id): - yield key.public_key - - @pytest.fixture(autouse=True) def disable_auto_on_commit(): simulated_transaction_watermarks.state["default"] = -1 diff --git a/tests/sentry/tasks/test_relocation.py b/tests/sentry/tasks/test_relocation.py index 1304e4c71238a1..f2b47af09bb1f8 100644 --- a/tests/sentry/tasks/test_relocation.py +++ b/tests/sentry/tasks/test_relocation.py @@ -2192,7 +2192,7 @@ def setUp(self): @staticmethod def noop_relocated_signal_receiver(sender, **kwargs) -> None: - pass + raise NotImplementedError def test_success( self, diff --git a/tests/sentry/tasks/test_reprocessing2.py b/tests/sentry/tasks/test_reprocessing2.py index 3b59699a829bc9..fab5254166873c 100644 --- a/tests/sentry/tasks/test_reprocessing2.py +++ b/tests/sentry/tasks/test_reprocessing2.py @@ -351,7 +351,7 @@ def event_preprocessor(data): != group_id ) else: - raise ValueError(remaining_events) + raise AssertionError(remaining_events) else: assert event is not None assert event.group_id != group_id @@ -367,7 +367,7 @@ def event_preprocessor(data): assert event.group is not None assert event.group.times_seen == 5 else: - raise ValueError(remaining_events) + raise AssertionError(remaining_events) assert is_group_finished(group_id) diff --git a/tests/sentry/tasks/test_store.py b/tests/sentry/tasks/test_store.py index 48544658436c94..4d434c1ba947e2 100644 --- a/tests/sentry/tasks/test_store.py +++ b/tests/sentry/tasks/test_store.py @@ -87,12 +87,6 @@ def mock_refund(): yield m -@pytest.fixture -def mock_metrics_timing(): - with mock.patch("sentry.tasks.store.metrics.timing") as m: - yield m - - @django_db_all def test_move_to_process_event( default_project, mock_process_event, mock_save_event, mock_symbolicate_event, register_plugin @@ -242,7 +236,7 @@ def options_model(request, default_organization, default_project): elif request.param == "project": return default_project else: - raise ValueError(request.param) + raise AssertionError(request.param) @django_db_all @@ -278,7 +272,7 @@ def is_enabled(self, project=None): "sentry:relay_pii_config", '{"applications": {"extra.ooo": ["@anything:replace"]}}' ) else: - raise ValueError(setting_method) + raise AssertionError(setting_method) data = { "project": default_project.id, diff --git a/tests/sentry/tasks/test_symbolication.py b/tests/sentry/tasks/test_symbolication.py index db2f395858b7c8..96cc42805c0a96 100644 --- a/tests/sentry/tasks/test_symbolication.py +++ b/tests/sentry/tasks/test_symbolication.py @@ -3,7 +3,7 @@ import pytest from sentry.tasks.store import preprocess_event -from sentry.tasks.symbolication import submit_symbolicate, symbolicate_event +from sentry.tasks.symbolication import symbolicate_event from sentry.testutils.pytest.fixtures import django_db_all EVENT_ID = "cc3e6c2bb6b6498097f336d1e6979f4b" @@ -39,12 +39,6 @@ def mock_event_processing_store(): yield m -@pytest.fixture -def mock_submit_symbolicate(): - with mock.patch("sentry.tasks.symbolication.submit_symbolicate", wraps=submit_symbolicate) as m: - yield m - - @django_db_all def test_move_to_symbolicate_event( default_project, mock_process_event, mock_save_event, mock_symbolicate_event diff --git a/tests/sentry/taskworker/test_registry.py b/tests/sentry/taskworker/test_registry.py index 6a0e8bbd3dc061..d100441492b87e 100644 --- a/tests/sentry/taskworker/test_registry.py +++ b/tests/sentry/taskworker/test_registry.py @@ -1,4 +1,3 @@ -import logging from unittest.mock import patch import pytest @@ -19,7 +18,7 @@ def test_namespace_register_task() -> None: @namespace.register(name="tests.simple_task") def simple_task(): - logging.info("simple_task") + raise NotImplementedError assert namespace.default_retry is None assert namespace.contains("tests.simple_task") @@ -39,20 +38,20 @@ def test_namespace_register_inherits_default_retry() -> None: @namespace.register(name="test.no_retry_param") def no_retry_param() -> None: - pass + raise NotImplementedError retry = Retry(times=2, times_exceeded=LastAction.Deadletter) @namespace.register(name="test.with_retry_param", retry=retry) def with_retry_param() -> None: - pass + raise NotImplementedError with_retry = namespace.get("test.with_retry_param") assert with_retry.retry == retry @namespace.register(name="test.retry_none", retry=None) def retry_none_param() -> None: - pass + raise NotImplementedError with_retry = namespace.get("test.retry_none") assert with_retry.retry == namespace.default_retry @@ -69,11 +68,11 @@ def test_register_inherits_default_expires_processing_deadline() -> None: @namespace.register(name="test.no_expires") def no_expires() -> None: - pass + raise NotImplementedError @namespace.register(name="test.with_expires", expires=30 * 60, processing_deadline_duration=10) def with_expires() -> None: - pass + raise NotImplementedError no_expires_task = namespace.get("test.no_expires") activation = no_expires_task.create_activation() @@ -107,7 +106,7 @@ def test_namespace_send_task_no_retry() -> None: @namespace.register(name="test.simpletask") def simple_task() -> None: - pass + raise NotImplementedError activation = simple_task.create_activation() assert activation.retry_state.attempts == 0 @@ -136,7 +135,7 @@ def test_namespace_send_task_with_retry() -> None: name="test.simpletask", retry=Retry(times=3, times_exceeded=LastAction.Deadletter) ) def simple_task() -> None: - pass + raise NotImplementedError activation = simple_task.create_activation() assert activation.retry_state.attempts == 0 @@ -161,7 +160,7 @@ def test_namespace_with_retry_send_task() -> None: @namespace.register(name="test.simpletask") def simple_task() -> None: - pass + raise NotImplementedError activation = simple_task.create_activation() assert activation.retry_state.attempts == 0 @@ -201,7 +200,7 @@ def test_registry_get_task() -> None: @ns.register(name="test.simpletask") def simple_task() -> None: - pass + raise NotImplementedError task = registry.get_task(ns.name, "test.simpletask") assert isinstance(task, Task) diff --git a/tests/sentry/taskworker/test_task.py b/tests/sentry/taskworker/test_task.py index 19f121491179bc..1d4d9aeac8be86 100644 --- a/tests/sentry/taskworker/test_task.py +++ b/tests/sentry/taskworker/test_task.py @@ -1,5 +1,4 @@ import datetime -import logging import pytest import sentry_sdk @@ -13,7 +12,7 @@ def do_things() -> None: - logging.info("Ran do_things") + raise NotImplementedError @pytest.fixture @@ -168,7 +167,7 @@ def test_create_activation(task_namespace: TaskNamespace) -> None: def test_create_activation_parameters(task_namespace: TaskNamespace) -> None: @task_namespace.register(name="test.parameters") def with_parameters(one: str, two: int, org_id: int) -> None: - pass + raise NotImplementedError activation = with_parameters.create_activation("one", 22, org_id=99) params = json.loads(activation.parameters) @@ -180,7 +179,7 @@ def with_parameters(one: str, two: int, org_id: int) -> None: def test_create_activation_tracing(task_namespace: TaskNamespace) -> None: @task_namespace.register(name="test.parameters") def with_parameters(one: str, two: int, org_id: int) -> None: - pass + raise NotImplementedError with sentry_sdk.start_transaction(op="test.task"): activation = with_parameters.create_activation("one", 22, org_id=99) diff --git a/tests/sentry/testutils/pytest/mocking/animals/__init__.py b/tests/sentry/testutils/pytest/mocking/animals/__init__.py index 447e3a91618d2d..c5ed42be2b0aba 100644 --- a/tests/sentry/testutils/pytest/mocking/animals/__init__.py +++ b/tests/sentry/testutils/pytest/mocking/animals/__init__.py @@ -24,4 +24,4 @@ def a_function_that_calls_erroring_get_dog(): except TypeError: return "Well, we tried." - return "We shouldn't ever get here" + raise AssertionError("We shouldn't ever get here") diff --git a/tests/sentry/uptime/subscriptions/test_tasks.py b/tests/sentry/uptime/subscriptions/test_tasks.py index 6c75213479d367..6a8e4ae028f7a6 100644 --- a/tests/sentry/uptime/subscriptions/test_tasks.py +++ b/tests/sentry/uptime/subscriptions/test_tasks.py @@ -90,11 +90,8 @@ def task(self, uptime_subscription_id: int) -> None: pass def create_subscription( - self, status: UptimeSubscription.Status | None = None, subscription_id: str | None = None + self, status: UptimeSubscription.Status, subscription_id: str | None = None ): - if status is None: - status = self.expected_status - return UptimeSubscription.objects.create( status=status.value, type="something", diff --git a/tests/sentry/utils/kvstore/test_common.py b/tests/sentry/utils/kvstore/test_common.py index 99e31b394380c6..553b0f85f33ffd 100644 --- a/tests/sentry/utils/kvstore/test_common.py +++ b/tests/sentry/utils/kvstore/test_common.py @@ -42,7 +42,7 @@ def properties(request) -> Properties: if backend_label == "default": from sentry.cache import default_cache as cache else: - raise ValueError("unknown cache backend label") + raise AssertionError("unknown cache backend label") return Properties( CacheKVStorage(cache), @@ -77,7 +77,7 @@ def properties(request) -> Properties: values=itertools.count(), ) else: - raise ValueError("unknown kvstore label") + raise AssertionError("unknown kvstore label") def test_single_key_operations(properties: Properties) -> None: diff --git a/tests/sentry/utils/locking/backends/test_migration.py b/tests/sentry/utils/locking/backends/test_migration.py index b60d01104b9ce0..536de7f72ffd78 100644 --- a/tests/sentry/utils/locking/backends/test_migration.py +++ b/tests/sentry/utils/locking/backends/test_migration.py @@ -15,7 +15,7 @@ def __init__(self): def acquire(self, key: str, duration: int, routing_key: str | None = None) -> None: if self.locked(key=key, routing_key=routing_key): - raise Exception(f"Could not acquire ({key}, {routing_key})") + raise AssertionError(f"Could not acquire ({key}, {routing_key})") self._locks[(key, routing_key)] = duration def release(self, key, routing_key=None): @@ -61,13 +61,13 @@ def test_lock_check_both_backends(self): backend.backend_old.acquire(lk, 10) assert backend.locked(lk) - def selector_always_old(key, routing_key, backend_new, backend_old): - return backend_old + def selector_plzno_call(key, routing_key, backend_new, backend_old): + raise AssertionError("should not be called!") backend = MigrationLockBackend( backend_new_config={"path": DummyLockBackend.path}, backend_old_config={"path": DummyLockBackend.path}, - selector_func_path=selector_always_old, + selector_func_path=selector_plzno_call, ) backend.backend_new.acquire(lk, 10) assert backend.locked(lk) diff --git a/tests/sentry/utils/locking/backends/test_redis.py b/tests/sentry/utils/locking/backends/test_redis.py index 2be02ad6627012..dfe1a74df4f7e3 100644 --- a/tests/sentry/utils/locking/backends/test_redis.py +++ b/tests/sentry/utils/locking/backends/test_redis.py @@ -18,7 +18,7 @@ class RedisBackendTestCaseBase: @property def cluster(self): - return clusters.get("default") + raise NotImplementedError @property def backend(self): diff --git a/tests/sentry/utils/test_auth.py b/tests/sentry/utils/test_auth.py index 2115b70bd24b68..e121227cceb0aa 100644 --- a/tests/sentry/utils/test_auth.py +++ b/tests/sentry/utils/test_auth.py @@ -135,13 +135,11 @@ def test_no_value_uses_default(self): @control_silo_test class LoginTest(TestCase): - def _make_request(self, next=None): + def _make_request(self): request = HttpRequest() request.META["REMOTE_ADDR"] = "127.0.0.1" request.session = self.session request.user = AnonymousUser() - if next: - request.session["_next"] = next return request def test_simple(self): diff --git a/tests/sentry/utils/test_circuit_breaker2.py b/tests/sentry/utils/test_circuit_breaker2.py index d0e16a0c50ada2..1b351605dfe87c 100644 --- a/tests/sentry/utils/test_circuit_breaker2.py +++ b/tests/sentry/utils/test_circuit_breaker2.py @@ -106,112 +106,6 @@ def _add_quota_usage( window_end_time, ) - def _clear_quota(self, quota: Quota, window_end: int | None = None) -> list[int]: - """ - Clear usage of the given quota up until the end of the given time window. If no window end - is given, clear the quota up to the present. - - Returns the list of granule values which were cleared. - """ - now = int(time.time()) - window_end_time = window_end or now - granule_end_times = self._get_granule_end_times(quota, window_end_time) - num_granules = len(granule_end_times) - previous_granule_values = [0] * num_granules - - current_total_quota_used = quota.limit - self._get_remaining_error_quota( - quota, window_end_time - ) - if current_total_quota_used != 0: - # Empty the granules one by one, starting with the oldest. - # - # To empty each granule, we need to add negative quota usage, which means we need to - # know how much usage is currently in each granule. Unfortunately, the limiter will only - # report quota usage at the window level, not the granule level. To get around this, we - # start with a window ending with the oldest granule. Any granules before it will have - # expired, so the window usage will equal the granule usage.ending in that granule will - # have a total usage equal to that of the granule. - # - # Once we zero-out the granule, we can move the window one granule forward. It will now - # consist of expired granules, the granule we just set to 0, and the granule we care - # about. Thus the window usage will again match the granule usage, which we can use to - # empty the granule. We then just repeat the pattern until we've reached the end of the - # window we want to clear. - for i, granule_end_time in enumerate(granule_end_times): - granule_quota_used = quota.limit - self._get_remaining_error_quota( - quota, granule_end_time - ) - previous_granule_values[i] = granule_quota_used - self._add_quota_usage(quota, -granule_quota_used, granule_end_time) - - new_total_quota_used = quota.limit - self._get_remaining_error_quota( - quota, window_end_time - ) - assert new_total_quota_used == 0 - - return previous_granule_values - - def _get_granule_end_times( - self, quota: Quota, window_end: int, newest_first: bool = False - ) -> list[int]: - """ - Given a quota and the end of the time window it's covering, return the timestamps - corresponding to the end of each granule. - """ - window_duration = quota.window_seconds - granule_duration = quota.granularity_seconds - num_granules = window_duration // granule_duration - - # Walk backwards through the granules - end_times_newest_first = [ - window_end - num_granules_ago * granule_duration - for num_granules_ago in range(num_granules) - ] - - return end_times_newest_first if newest_first else list(reversed(end_times_newest_first)) - - def _set_granule_values( - self, - quota: Quota, - values: list[int | None], - window_end: int | None = None, - ) -> None: - """ - Set the usage in each granule of the given quota, for the time window ending at the given - time. - - If no ending time is given, the current time is used. - - The list of values should be ordered from oldest to newest and must contain the same number - of elements as the window has granules. To only change some of the values, pass `None` in - the spot of any value which should remain unchanged. (For example, in a two-granule window, - to only change the older granule, pass `[3, None]`.) - """ - window_duration = quota.window_seconds - granule_duration = quota.granularity_seconds - num_granules = window_duration // granule_duration - - if len(values) != num_granules: - raise Exception( - f"Exactly {num_granules} granule values must be provided. " - + "To leave an existing value as is, include `None` in its spot." - ) - - now = int(time.time()) - window_end_time = window_end or now - - previous_values = self._clear_quota(quota, window_end_time) - - for i, granule_end_time, value in zip( - range(num_granules), self._get_granule_end_times(quota, window_end_time), values - ): - # When we cleared the quota above, we set each granule's value to 0, so here "adding" - # usage is actually setting usage - if value is not None: - self._add_quota_usage(quota, value, granule_end_time) - else: - self._add_quota_usage(quota, previous_values[i], granule_end_time) - def _delete_from_redis(self, keys: list[str]) -> Any: for key in keys: self.redis_pipeline.delete(key) diff --git a/tests/sentry/utils/test_committers.py b/tests/sentry/utils/test_committers.py index 3d6969c5976cd8..f90ca1f9fac1bf 100644 --- a/tests/sentry/utils/test_committers.py +++ b/tests/sentry/utils/test_committers.py @@ -10,7 +10,6 @@ from sentry.integrations.github.integration import GitHubIntegration from sentry.integrations.models.integration import Integration from sentry.models.commit import Commit -from sentry.models.commitauthor import CommitAuthor from sentry.models.commitfilechange import CommitFileChange from sentry.models.groupowner import GroupOwner, GroupOwnerType from sentry.models.grouprelease import GroupRelease @@ -49,20 +48,6 @@ def create_commit(self, author=None): author=author, ) - def create_commit_with_author(self, user=None, commit=None): - if not user: - user = self.create_user(name="Sentry", email="sentry@sentry.io") - - author = CommitAuthor.objects.create( - organization_id=self.organization.id, - name=user.name, - email=user.email, - external_id=user.id, - ) - if not commit: - commit = self.create_commit(author) - return commit - def create_commitfilechange(self, commit=None, filename=None, type=None): return CommitFileChange.objects.create( organization_id=self.organization.id, diff --git a/tests/sentry/utils/test_concurrent.py b/tests/sentry/utils/test_concurrent.py index fd69c28c1a8326..c4017df107f4ff 100644 --- a/tests/sentry/utils/test_concurrent.py +++ b/tests/sentry/utils/test_concurrent.py @@ -78,10 +78,7 @@ def test_future_broken_callback(): callback = mock.Mock(side_effect=Exception("Boom!")) - try: - future_set.add_done_callback(callback) - except Exception: - assert False, "should not raise" + future_set.add_done_callback(callback) # should not raise assert callback.call_count == 1 assert callback.call_args == mock.call(future_set) @@ -191,16 +188,15 @@ def test_synchronous_executor(): assert executor.submit(lambda: mock.sentinel.RESULT).result() is mock.sentinel.RESULT + class SentinelException(ValueError): + pass + def callable(): - raise Exception(mock.sentinel.EXCEPTION) + raise SentinelException future = executor.submit(callable) - try: + with pytest.raises(SentinelException): future.result() - except Exception as e: - assert e.args[0] == mock.sentinel.EXCEPTION - else: - assert False, "expected future to raise" def test_threaded_same_priority_Tasks(): diff --git a/tests/sentry/utils/test_glob.py b/tests/sentry/utils/test_glob.py index a00b9516c0ec11..060decd54c3deb 100644 --- a/tests/sentry/utils/test_glob.py +++ b/tests/sentry/utils/test_glob.py @@ -1,43 +1,50 @@ +from typing import NamedTuple, Self + import pytest from sentry.utils.glob import glob_match -class GlobInput: - def __init__(self, value, pat, **kwargs): - self.value = value - self.pat = pat - self.kwargs = kwargs +class GlobInput(NamedTuple): + value: str | None + pat: str + kwargs: dict[str, bool] + + @classmethod + def make(cls, value: str | None, pat: str, **kwargs: bool) -> Self: + return cls(value=value, pat=pat, kwargs=kwargs) def __call__(self): return glob_match(self.value, self.pat, **self.kwargs) - def __repr__(self): - return f"" - @pytest.mark.parametrize( "glob_input,expect", [ - [GlobInput("hello.py", "*.py"), True], - [GlobInput("hello.py", "*.js"), False], - [GlobInput(None, "*.js"), False], - [GlobInput(None, "*"), True], - [GlobInput("foo/hello.py", "*.py"), True], - [GlobInput("foo/hello.py", "*.py", doublestar=True), False], - [GlobInput("foo/hello.py", "**/*.py", doublestar=True), True], - [GlobInput("foo/hello.PY", "**/*.py"), False], - [GlobInput("foo/hello.PY", "**/*.py", doublestar=True), False], - [GlobInput("foo/hello.PY", "**/*.py", ignorecase=True), True], - [GlobInput("foo/hello.PY", "**/*.py", doublestar=True, ignorecase=True), True], - [GlobInput("root\\foo\\hello.PY", "root/**/*.py", ignorecase=True), False], - [GlobInput("root\\foo\\hello.PY", "root/**/*.py", doublestar=True, ignorecase=True), False], + [GlobInput.make("hello.py", "*.py"), True], + [GlobInput.make("hello.py", "*.js"), False], + [GlobInput.make(None, "*.js"), False], + [GlobInput.make(None, "*"), True], + [GlobInput.make("foo/hello.py", "*.py"), True], + [GlobInput.make("foo/hello.py", "*.py", doublestar=True), False], + [GlobInput.make("foo/hello.py", "**/*.py", doublestar=True), True], + [GlobInput.make("foo/hello.PY", "**/*.py"), False], + [GlobInput.make("foo/hello.PY", "**/*.py", doublestar=True), False], + [GlobInput.make("foo/hello.PY", "**/*.py", ignorecase=True), True], + [GlobInput.make("foo/hello.PY", "**/*.py", doublestar=True, ignorecase=True), True], + [GlobInput.make("root\\foo\\hello.PY", "root/**/*.py", ignorecase=True), False], [ - GlobInput("root\\foo\\hello.PY", "root/**/*.py", ignorecase=True, path_normalize=True), + GlobInput.make("root\\foo\\hello.PY", "root/**/*.py", doublestar=True, ignorecase=True), + False, + ], + [ + GlobInput.make( + "root\\foo\\hello.PY", "root/**/*.py", ignorecase=True, path_normalize=True + ), True, ], [ - GlobInput( + GlobInput.make( "root\\foo\\hello.PY", "root/**/*.py", doublestar=True, @@ -46,8 +53,8 @@ def __repr__(self): ), True, ], - [GlobInput("foo:\nbar", "foo:*"), True], - [GlobInput("foo:\nbar", "foo:*", allow_newline=False), False], + [GlobInput.make("foo:\nbar", "foo:*"), True], + [GlobInput.make("foo:\nbar", "foo:*", allow_newline=False), False], ], ) def test_glob_match(glob_input, expect): diff --git a/tests/sentry/utils/test_math.py b/tests/sentry/utils/test_math.py index b6f409350f37fb..1a1f04ec17cb9c 100644 --- a/tests/sentry/utils/test_math.py +++ b/tests/sentry/utils/test_math.py @@ -3,15 +3,6 @@ from sentry.utils.math import ExponentialMovingAverage, nice_int -def linspace(start, stop, n): - if n == 1: - yield stop - else: - h = (stop - start) / (n - 1) - for i in range(n): - yield start + h * i - - @pytest.mark.parametrize( "start,stop,expected", [ diff --git a/tests/sentry/utils/test_registry.py b/tests/sentry/utils/test_registry.py index cbb886a7884ca8..a85a075901051a 100644 --- a/tests/sentry/utils/test_registry.py +++ b/tests/sentry/utils/test_registry.py @@ -12,10 +12,10 @@ def test(self): @test_registry.register("something") def registered_func(): - pass + raise NotImplementedError def unregistered_func(): - pass + raise NotImplementedError assert test_registry.get("something") == registered_func with pytest.raises(NoRegistrationExistsError): @@ -40,7 +40,7 @@ def test_allow_duplicate_values(self): @test_registry.register("something") @test_registry.register("something 2") def registered_func(): - pass + raise NotImplementedError assert test_registry.get("something") == registered_func assert test_registry.get("something 2") == registered_func diff --git a/tests/sentry/utils/test_retries.py b/tests/sentry/utils/test_retries.py index 503774df060a82..7240ba6c62f335 100644 --- a/tests/sentry/utils/test_retries.py +++ b/tests/sentry/utils/test_retries.py @@ -69,12 +69,9 @@ def test_policy_failure(self): retry.clock.sleep = mock.MagicMock() retry.clock.time = mock.MagicMock(side_effect=[0, 0.15, 0.25]) - try: + with pytest.raises(RetryException) as excinfo: retry(callable) - except RetryException as exception: - assert exception.exception is bomb - else: - self.fail(f"Expected {RetryException!r}!") + assert excinfo.value.exception is bomb assert callable.call_count == 2 diff --git a/tests/sentry/web/frontend/test_auth_oauth2.py b/tests/sentry/web/frontend/test_auth_oauth2.py index 2b3b80070f7f1f..b9df4100211143 100644 --- a/tests/sentry/web/frontend/test_auth_oauth2.py +++ b/tests/sentry/web/frontend/test_auth_oauth2.py @@ -40,7 +40,7 @@ def get_refresh_token_url(self) -> str: raise NotImplementedError def build_config(self, state): - pass + raise NotImplementedError def get_auth_pipeline(self): return [DummyOAuth2Login(), DummyOAuth2Callback()] diff --git a/tests/sentry/web/frontend/test_organization_auth_settings.py b/tests/sentry/web/frontend/test_organization_auth_settings.py index 0d590d362d81e6..1e42537a0cbc21 100644 --- a/tests/sentry/web/frontend/test_organization_auth_settings.py +++ b/tests/sentry/web/frontend/test_organization_auth_settings.py @@ -726,12 +726,6 @@ def test_edit_sso_settings(self): class DummyGenericSAML2Provider(GenericSAML2Provider): name = "saml2_generic_dummy" - def get_saml_setup_pipeline(self): - return [] - - def build_config(self, state): - return dummy_provider_config - @control_silo_test class OrganizationAuthSettingsGenericSAML2Test(AuthProviderTestCase): diff --git a/tests/sentry/workflow_engine/endpoints/test_validators.py b/tests/sentry/workflow_engine/endpoints/test_validators.py index 5e8ac74cce059c..1770dd01fd34da 100644 --- a/tests/sentry/workflow_engine/endpoints/test_validators.py +++ b/tests/sentry/workflow_engine/endpoints/test_validators.py @@ -82,17 +82,7 @@ def setUp(self): super().setUp() self.project = self.create_project() - # Create a concrete implementation for testing - class ConcreteGroupTypeValidator(BaseGroupTypeDetectorValidator): - @property - def data_source(self): - return mock.Mock() - - @property - def data_conditions(self): - return mock.Mock() - - self.validator_class = ConcreteGroupTypeValidator + self.validator_class = BaseGroupTypeDetectorValidator def test_validate_group_type_valid(self): with mock.patch.object(grouptype.registry, "get_by_slug") as mock_get_by_slug: @@ -421,10 +411,6 @@ class MockDataSourceValidator(BaseDataSourceValidator[MockModel]): field2 = serializers.IntegerField() data_source_type_handler = QuerySubscriptionDataSourceHandler - @property - def model_class(self) -> type[MockModel]: - return MockModel - def create_source(self, validated_data) -> MockModel: return MockModel.objects.create() diff --git a/tests/sentry/workflow_engine/test_integration.py b/tests/sentry/workflow_engine/test_integration.py index f9a022001b496c..8f85963cc74d0c 100644 --- a/tests/sentry/workflow_engine/test_integration.py +++ b/tests/sentry/workflow_engine/test_integration.py @@ -51,8 +51,7 @@ def setUp(self): self.occurrence, group_info = save_issue_occurrence(occurrence_data, self.event) assert group_info is not None - self.group = Group.objects.filter(grouphash__hash=self.occurrence.fingerprint[0]).first() - assert self.group is not None + self.group = Group.objects.get(grouphash__hash=self.occurrence.fingerprint[0]) assert self.group.type == MetricAlertFire.type_id def call_post_process_group( @@ -135,9 +134,6 @@ def test_workflow_engine__workflows(self): """ self.create_event(self.project.id, datetime.utcnow(), str(self.detector.id)) - if not self.group: - assert False, "Group not created" - with mock.patch( "sentry.workflow_engine.processors.workflow.process_workflows" ) as mock_process_workflow: @@ -160,10 +156,7 @@ def test_workflow_engine__workflows__other_events(self): ) self.occurrence, group_info = save_issue_occurrence(occurrence_data, error_event) - self.group = Group.objects.filter(grouphash__hash=self.occurrence.fingerprint[0]).first() - - if not self.group: - assert False, "Group not created" + self.group = Group.objects.get(grouphash__hash=self.occurrence.fingerprint[0]) with mock.patch( "sentry.workflow_engine.processors.workflow.process_workflows" @@ -176,8 +169,7 @@ def test_workflow_engine__workflows__other_events(self): def test_workflow_engine__workflows__no_flag(self): self.create_event(self.project.id, datetime.utcnow(), str(self.detector.id)) - if not self.group: - assert False, "Group not created" + assert self.group with mock.patch( "sentry.workflow_engine.processors.workflow.process_workflows" diff --git a/tests/sentry_plugins/phabricator/test_plugin.py b/tests/sentry_plugins/phabricator/test_plugin.py index 5f44e9cbc5401a..4d89ba5632bbc9 100644 --- a/tests/sentry_plugins/phabricator/test_plugin.py +++ b/tests/sentry_plugins/phabricator/test_plugin.py @@ -1,7 +1,6 @@ from functools import cached_property import responses -from django.test import RequestFactory from pytest import raises from sentry.exceptions import PluginError @@ -15,10 +14,6 @@ class PhabricatorPluginTest(PluginTestCase): def plugin(self): return PhabricatorPlugin() - @cached_property - def request(self): - return RequestFactory() - def test_conf_key(self): assert self.plugin.conf_key == "phabricator" diff --git a/tests/sentry_plugins/trello/test_plugin.py b/tests/sentry_plugins/trello/test_plugin.py index f35a6f62bde25b..b3fa257253ab1f 100644 --- a/tests/sentry_plugins/trello/test_plugin.py +++ b/tests/sentry_plugins/trello/test_plugin.py @@ -3,7 +3,6 @@ import orjson import responses -from django.test import RequestFactory from sentry.testutils.cases import PluginTestCase from sentry_plugins.trello.plugin import TrelloPlugin @@ -14,10 +13,6 @@ class TrelloPluginTestBase(PluginTestCase): def plugin(self): return TrelloPlugin() - @cached_property - def request(self): - return RequestFactory() - class TrelloPluginTest(TrelloPluginTestBase): def test_conf_key(self): diff --git a/tests/snuba/api/endpoints/test_organization_events_facets_performance.py b/tests/snuba/api/endpoints/test_organization_events_facets_performance.py index fdae7b997e4be3..55431088313d6c 100644 --- a/tests/snuba/api/endpoints/test_organization_events_facets_performance.py +++ b/tests/snuba/api/endpoints/test_organization_events_facets_performance.py @@ -60,10 +60,8 @@ def setUp(self): ) def store_transaction( - self, name="exampleTransaction", duration=100, tags=None, project_id=None, lcp=None + self, name="exampleTransaction", duration=100, project_id=None, lcp=None, *, tags ): - if tags is None: - tags = [] if project_id is None: project_id = self.project.id event = load_data("transaction") diff --git a/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py b/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py index 8647fda3db5f78..a827ea458d4f31 100644 --- a/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py +++ b/tests/snuba/api/endpoints/test_organization_events_facets_performance_histogram.py @@ -51,13 +51,12 @@ def store_transaction( self, name="exampleTransaction", duration=100, - tags=None, project_id=None, lcp=None, user_id=None, + *, + tags, ): - if tags is None: - tags = [] if project_id is None: project_id = self.project.id event = load_data("transaction") diff --git a/tests/snuba/api/endpoints/test_organization_events_spans_performance.py b/tests/snuba/api/endpoints/test_organization_events_spans_performance.py index c3736bd28a8bb0..ddb6077738b34f 100644 --- a/tests/snuba/api/endpoints/test_organization_events_spans_performance.py +++ b/tests/snuba/api/endpoints/test_organization_events_spans_performance.py @@ -118,7 +118,7 @@ def suspect_span_examples_snuba_results(self, op, event): } ) else: - assert False, f"Unexpected Op: {op}" + raise AssertionError(f"Unexpected Op: {op}") return results @@ -231,7 +231,7 @@ def suspect_span_group_snuba_results(self, op, event): } ) else: - assert False, f"Unexpected Op: {op}" + raise AssertionError(f"Unexpected Op: {op}") return results @@ -325,7 +325,7 @@ def span_example_results(self, op, event): } else: - assert False, f"Unexpected Op: {op}" + raise AssertionError(f"Unexpected Op: {op}") def suspect_span_results(self, op, event): results = self.span_example_results(op, event) @@ -381,7 +381,7 @@ def suspect_span_results(self, op, event): ) else: - assert False, f"Unexpected Op: {op}" + raise AssertionError(f"Unexpected Op: {op}") return results diff --git a/tests/snuba/api/endpoints/test_organization_events_stats_mep.py b/tests/snuba/api/endpoints/test_organization_events_stats_mep.py index f0bc7d9d31b0ae..128bf673654bdd 100644 --- a/tests/snuba/api/endpoints/test_organization_events_stats_mep.py +++ b/tests/snuba/api/endpoints/test_organization_events_stats_mep.py @@ -6,7 +6,6 @@ import pytest from django.urls import reverse -from rest_framework.response import Response from sentry.discover.models import DatasetSourcesTypes from sentry.models.dashboard_widget import DashboardWidget, DashboardWidgetTypes @@ -1283,17 +1282,6 @@ def setUp(self): "organizations:on-demand-metrics-extraction": True, } - def _make_on_demand_request( - self, params: dict[str, Any], extra_features: dict[str, bool] | None = None - ) -> Response: - """Ensures that the required parameters for an on-demand request are included.""" - # Expected parameters for this helper function - params["dataset"] = "metricsEnhanced" - params["useOnDemandMetrics"] = "true" - params["onDemandType"] = "dynamic_query" - _features = {**self.features, **(extra_features or {})} - return self.do_request(params, features=_features) - def test_top_events_wrong_on_demand_type(self): query = "transaction.duration:>=100" yAxis = ["count()", "count_web_vitals(measurements.lcp, good)"] diff --git a/tests/snuba/api/endpoints/test_organization_group_index_stats.py b/tests/snuba/api/endpoints/test_organization_group_index_stats.py index a792a7dc24dcc1..ebf82ecdfaf4d6 100644 --- a/tests/snuba/api/endpoints/test_organization_group_index_stats.py +++ b/tests/snuba/api/endpoints/test_organization_group_index_stats.py @@ -2,7 +2,7 @@ from sentry.issues.grouptype import ProfileFileIOGroupType from sentry.testutils.cases import APITestCase, SnubaTestCase -from sentry.testutils.helpers import parse_link_header, with_feature +from sentry.testutils.helpers import with_feature from sentry.testutils.helpers.datetime import before_now from tests.sentry.issues.test_utils import OccurrenceTestMixin @@ -14,20 +14,8 @@ def setUp(self): super().setUp() self.min_ago = before_now(minutes=1) - def _parse_links(self, header): - # links come in {url: {...attrs}}, but we need {rel: {...attrs}} - links = {} - for url, attrs in parse_link_header(header).items(): - links[attrs["rel"]] = attrs - attrs["href"] = url - return links - def get_response(self, *args, **kwargs): - if not args: - org = self.project.organization.slug - else: - org = args[0] - return super().get_response(org, **kwargs) + return super().get_response(self.project.organization.slug, **kwargs) def test_simple(self): self.store_event( diff --git a/tests/snuba/api/endpoints/test_organization_spans_trace.py b/tests/snuba/api/endpoints/test_organization_spans_trace.py index 9b7deef95f23fb..125f33c70f8c7b 100644 --- a/tests/snuba/api/endpoints/test_organization_spans_trace.py +++ b/tests/snuba/api/endpoints/test_organization_spans_trace.py @@ -66,7 +66,6 @@ def assert_trace_data(self, root, gen2_no_children=True): def assert_performance_issues(self, root): """Broken in the non-spans endpoint, but we're not maintaining that anymore""" - pass def client_get(self, data, url=None): if url is None: diff --git a/tests/snuba/api/endpoints/test_organization_tagkey_values.py b/tests/snuba/api/endpoints/test_organization_tagkey_values.py index 760f7ea6837b65..d1573fc11c8ac0 100644 --- a/tests/snuba/api/endpoints/test_organization_tagkey_values.py +++ b/tests/snuba/api/endpoints/test_organization_tagkey_values.py @@ -38,10 +38,6 @@ def run_test(self, key, expected, **kwargs): def project(self): return self.create_project(organization=self.org, teams=[self.team]) - @cached_property - def group(self): - return self.create_group(project=self.project) - class OrganizationTagKeyValuesTest(OrganizationTagKeyTestCase): def test_simple(self): diff --git a/tests/snuba/incidents/test_tasks.py b/tests/snuba/incidents/test_tasks.py index ab27ca4a0e1489..79ef90deea3129 100644 --- a/tests/snuba/incidents/test_tasks.py +++ b/tests/snuba/incidents/test_tasks.py @@ -1,6 +1,5 @@ from copy import deepcopy from functools import cached_property -from uuid import uuid4 from arroyo.utils import metrics from confluent_kafka import Producer @@ -105,10 +104,6 @@ def producer(self): } return Producer(conf) - @cached_property - def topic(self): - return uuid4().hex - def run_test(self, consumer): # Full integration test to ensure that when a subscription receives an update # the `QuerySubscriptionConsumer` successfully retries the subscription and diff --git a/tests/snuba/rules/conditions/test_event_frequency.py b/tests/snuba/rules/conditions/test_event_frequency.py index 1a38a0962d58d5..5924802241fbcc 100644 --- a/tests/snuba/rules/conditions/test_event_frequency.py +++ b/tests/snuba/rules/conditions/test_event_frequency.py @@ -278,7 +278,7 @@ def add_event(self, data, project_id, timestamp): tag[1] = data.get("environment") break else: - event_data["tags"].append(data.get("environment")) + raise AssertionError("expected `environment` tag") # Store a performance event event = self.create_performance_issue( @@ -300,9 +300,6 @@ def increment(self, event, count, environment=None, timestamp=None): raise NotImplementedError def _run_test(self, minutes, data, passes, add_events=False): - if not self.environment: - self.environment = self.create_environment(name="prod") - rule = self.get_rule(data=data, rule=Rule(environment_id=None)) environment_rule = self.get_rule(data=data, rule=Rule(environment_id=self.environment.id)) @@ -661,8 +658,6 @@ def test_comparison_empty_comparison_period(self): self.assertDoesNotPass(rule, event, is_new=False) def _run_test(self, minutes, data, passes, add_events=False): - if not self.environment: - self.environment = self.create_environment(name="prod") data["filter_match"] = "all" data["conditions"] = data.get("conditions", []) rule = self.get_rule( diff --git a/tests/snuba/search/test_backend.py b/tests/snuba/search/test_backend.py index 2d1af8070f77ea..a61df6090b611f 100644 --- a/tests/snuba/search/test_backend.py +++ b/tests/snuba/search/test_backend.py @@ -5,7 +5,6 @@ from unittest import mock import pytest -import urllib3 from django.utils import timezone from sentry_kafka_schemas.schema_types.group_attributes_v1 import GroupAttributesSnapshot @@ -2567,25 +2566,18 @@ class EventsSnubaSearchTest(TestCase, EventsSnubaSearchTestCases): @apply_feature_flag_on_cls("organizations:issue-search-group-attributes-side-query") class EventsJoinedGroupAttributesSnubaSearchTest(TransactionTestCase, EventsSnubaSearchTestCases): def setUp(self): - def post_insert(snapshot: GroupAttributesSnapshot): + def post_insert(snapshot: GroupAttributesSnapshot) -> None: from sentry.utils import snuba - try: - resp = snuba._snuba_pool.urlopen( - "POST", - "/tests/entities/group_attributes/insert", - body=json.dumps([snapshot]), - headers={}, - ) - if resp.status != 200: - raise snuba.SnubaError( - f"HTTP {resp.status} response from Snuba! {json.loads(resp.data)}" - ) - return None - except urllib3.exceptions.HTTPError as err: - raise snuba.SnubaError(err) - - with (mock.patch("sentry.issues.attributes.produce_snapshot_to_kafka", post_insert),): + resp = snuba._snuba_pool.urlopen( + "POST", + "/tests/entities/group_attributes/insert", + body=json.dumps([snapshot]), + headers={}, + ) + assert resp.status == 200 + + with mock.patch("sentry.issues.attributes.produce_snapshot_to_kafka", post_insert): super().setUp() @mock.patch("sentry.utils.metrics.timer") diff --git a/tests/snuba/sessions/test_sessions.py b/tests/snuba/sessions/test_sessions.py index 3e32446872f4e2..4f155aa3be4101 100644 --- a/tests/snuba/sessions/test_sessions.py +++ b/tests/snuba/sessions/test_sessions.py @@ -1,7 +1,7 @@ from __future__ import annotations import time -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta from unittest import mock import pytest @@ -9,7 +9,6 @@ from sentry.release_health.base import OverviewStat from sentry.release_health.metrics import MetricsReleaseHealthBackend -from sentry.snuba.sessions import _make_stats from sentry.testutils.cases import BaseMetricsTestCase, TestCase pytestmark = pytest.mark.sentry_metrics @@ -21,17 +20,6 @@ def format_timestamp(dt): return dt.strftime("%Y-%m-%dT%H:%M:%S+00:00") -def make_24h_stats(ts, adjust_start=False): - ret_val = _make_stats(datetime.fromtimestamp(ts, UTC), 3600, 24) - - if adjust_start: - # HACK this adds another interval at the beginning in accordance with the new way of calculating intervals - # https://www.notion.so/sentry/Metrics-Layer-get_intervals-bug-dce140607d054201a5e6629b070cb969 - ret_val.insert(0, [ret_val[0][0] - 3600, 0]) - - return ret_val - - class SnubaSessionsTest(TestCase, BaseMetricsTestCase): backend = MetricsReleaseHealthBackend() diff --git a/tests/snuba/tasks/test_unmerge.py b/tests/snuba/tasks/test_unmerge.py index 4b36548c1aca10..a32531d6f7c10e 100644 --- a/tests/snuba/tasks/test_unmerge.py +++ b/tests/snuba/tasks/test_unmerge.py @@ -511,8 +511,7 @@ def strip_zeroes(data): def collect_by_release(group, aggregate, event): aggregate = aggregate if aggregate is not None else {} release = event.get_tag("sentry:release") - if not release: - return aggregate + assert release release = GroupRelease.objects.get( group_id=group.id, environment=event.data["environment"], diff --git a/tests/snuba/tsdb/test_tsdb_backend.py b/tests/snuba/tsdb/test_tsdb_backend.py index 62b1b63bf32d90..acecfd19e199aa 100644 --- a/tests/snuba/tsdb/test_tsdb_backend.py +++ b/tests/snuba/tsdb/test_tsdb_backend.py @@ -21,7 +21,7 @@ def timestamp(d): return t - (t % 3600) -def has_shape(data, shape, allow_empty=False): +def has_shape(data, shape): """ Determine if a data object has the provided shape @@ -33,18 +33,16 @@ def has_shape(data, shape, allow_empty=False): A tuple is the same shape if it has the same length as `shape` and all the values have the same shape as the corresponding value in `shape` Any other object simply has to have the same type. - If `allow_empty` is set, lists and dicts in `data` will pass even if they are empty. """ - if not isinstance(data, type(shape)): - return False + assert isinstance(data, type(shape)) if isinstance(data, dict): return ( - (allow_empty or len(data) > 0) + len(data) > 0 and all(has_shape(k, list(shape.keys())[0]) for k in data.keys()) and all(has_shape(v, list(shape.values())[0]) for v in data.values()) ) elif isinstance(data, list): - return (allow_empty or len(data) > 0) and all(has_shape(v, shape[0]) for v in data) + return len(data) > 0 and all(has_shape(v, shape[0]) for v in data) elif isinstance(data, tuple): return len(data) == len(shape) and all( has_shape(data[i], shape[i]) for i in range(len(data)) From 0e4f90ce7ae0a88abdb8f54c10673469100e57cc Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 31 Dec 2024 13:35:09 -0800 Subject: [PATCH 604/757] test: Fix testing-library/prefer-screen-queries violations (#82786) https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md --- eslint.config.mjs | 1 - .../breadcrumbs/breadcrumbsDrawer.spec.tsx | 110 ++++++++------- .../featureFlags/featureFlagDrawer.spec.tsx | 127 ++++++++++-------- static/app/components/nav/index.spec.tsx | 15 +-- .../profiling/flamegraph/flamegraph.spec.tsx | 10 +- .../sampleTable/sampleTable.spec.tsx | 37 +++-- .../newTraceDetails/trace.spec.tsx | 48 ++++--- .../transactionOverview/index.spec.tsx | 8 +- .../landing/slowestFunctionsWidget.spec.tsx | 4 +- .../settings/projectPlugins/index.spec.tsx | 6 +- tests/js/sentry-test/selectEvent.tsx | 6 +- 11 files changed, 193 insertions(+), 179 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index c0445fbf0022c4..cb696a686f424b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -964,7 +964,6 @@ export default typescript.config([ rules: { 'testing-library/no-container': 'warn', // TODO(ryan953): Fix the violations, then delete this line 'testing-library/no-node-access': 'warn', // TODO(ryan953): Fix the violations, then delete this line - 'testing-library/prefer-screen-queries': 'warn', // TODO(ryan953): Fix the violations, then delete this line }, }, { diff --git a/static/app/components/events/breadcrumbs/breadcrumbsDrawer.spec.tsx b/static/app/components/events/breadcrumbs/breadcrumbsDrawer.spec.tsx index c152ff6c839fd8..25eb4c9d4eb50c 100644 --- a/static/app/components/events/breadcrumbs/breadcrumbsDrawer.spec.tsx +++ b/static/app/components/events/breadcrumbs/breadcrumbsDrawer.spec.tsx @@ -23,42 +23,50 @@ async function renderBreadcrumbDrawer() { })); render(); await userEvent.click(screen.getByRole('button', {name: 'View All Breadcrumbs'})); - return within(screen.getByRole('complementary', {name: 'breadcrumb drawer'})); + return screen.getByRole('complementary', {name: 'breadcrumb drawer'}); } describe('BreadcrumbsDrawer', function () { it('renders the drawer as expected', async function () { const drawerScreen = await renderBreadcrumbDrawer(); - expect(drawerScreen.getByRole('button', {name: 'Close Drawer'})).toBeInTheDocument(); + expect( + within(drawerScreen).getByRole('button', {name: 'Close Drawer'}) + ).toBeInTheDocument(); // Inner drawer breadcrumbs const {event, group} = MOCK_DATA_SECTION_PROPS; - expect(drawerScreen.getByText(group.shortId)).toBeInTheDocument(); - expect(drawerScreen.getByText(event.id.slice(0, 8))).toBeInTheDocument(); - expect(drawerScreen.getByText('Breadcrumbs', {selector: 'span'})).toBeInTheDocument(); + expect(within(drawerScreen).getByText(group.shortId)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(event.id.slice(0, 8))).toBeInTheDocument(); + expect( + within(drawerScreen).getByText('Breadcrumbs', {selector: 'span'}) + ).toBeInTheDocument(); // Header & Controls - expect(drawerScreen.getByText('Breadcrumbs', {selector: 'h3'})).toBeInTheDocument(); expect( - drawerScreen.getByRole('textbox', {name: 'Search All Breadcrumbs'}) + within(drawerScreen).getByText('Breadcrumbs', {selector: 'h3'}) ).toBeInTheDocument(); expect( - drawerScreen.getByRole('button', {name: 'Sort All Breadcrumbs'}) + within(drawerScreen).getByRole('textbox', {name: 'Search All Breadcrumbs'}) ).toBeInTheDocument(); expect( - drawerScreen.getByRole('button', {name: 'Filter All Breadcrumbs'}) + within(drawerScreen).getByRole('button', {name: 'Sort All Breadcrumbs'}) ).toBeInTheDocument(); expect( - drawerScreen.getByRole('button', {name: 'Change Time Format for All Breadcrumbs'}) + within(drawerScreen).getByRole('button', {name: 'Filter All Breadcrumbs'}) + ).toBeInTheDocument(); + expect( + within(drawerScreen).getByRole('button', { + name: 'Change Time Format for All Breadcrumbs', + }) ).toBeInTheDocument(); // Contents for (const {category, level, message} of MOCK_BREADCRUMBS) { - expect(drawerScreen.getByText(category)).toBeInTheDocument(); - expect(drawerScreen.getByText(level)).toBeInTheDocument(); - expect(drawerScreen.getByText(message)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(category)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(level)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(message)).toBeInTheDocument(); } - expect(drawerScreen.getAllByText('06:00:48.760 PM')).toHaveLength( + expect(within(drawerScreen).getAllByText('06:00:48.760 PM')).toHaveLength( MOCK_BREADCRUMBS.length ); }); @@ -67,16 +75,16 @@ describe('BreadcrumbsDrawer', function () { const drawerScreen = await renderBreadcrumbDrawer(); const [warningCrumb, logCrumb] = MOCK_BREADCRUMBS; - expect(drawerScreen.getByText(warningCrumb.category)).toBeInTheDocument(); - expect(drawerScreen.getByText(logCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(warningCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(logCrumb.category)).toBeInTheDocument(); - const searchInput = drawerScreen.getByRole('textbox', { + const searchInput = within(drawerScreen).getByRole('textbox', { name: 'Search All Breadcrumbs', }); await userEvent.type(searchInput, warningCrumb.message); - expect(drawerScreen.getByText(warningCrumb.category)).toBeInTheDocument(); - expect(drawerScreen.queryByText(logCrumb.category)).not.toBeInTheDocument(); + expect(within(drawerScreen).getByText(warningCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).queryByText(logCrumb.category)).not.toBeInTheDocument(); }); it('allows type filter to affect displayed crumbs', async function () { @@ -84,16 +92,18 @@ describe('BreadcrumbsDrawer', function () { const queryCrumb = MOCK_BREADCRUMBS[3]; const requestCrumb = MOCK_BREADCRUMBS[2]; - expect(drawerScreen.getByText(queryCrumb.category)).toBeInTheDocument(); - expect(drawerScreen.getByText(requestCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(queryCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(requestCrumb.category)).toBeInTheDocument(); await userEvent.click( - drawerScreen.getByRole('button', {name: 'Filter All Breadcrumbs'}) + within(drawerScreen).getByRole('button', {name: 'Filter All Breadcrumbs'}) ); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Query'})); + await userEvent.click(within(drawerScreen).getByRole('option', {name: 'Query'})); - expect(drawerScreen.getByText(queryCrumb.category)).toBeInTheDocument(); - expect(drawerScreen.queryByText(requestCrumb.category)).not.toBeInTheDocument(); + expect(within(drawerScreen).getByText(queryCrumb.category)).toBeInTheDocument(); + expect( + within(drawerScreen).queryByText(requestCrumb.category) + ).not.toBeInTheDocument(); }); it('allows level spofilter to affect displayed crumbs', async function () { @@ -101,16 +111,16 @@ describe('BreadcrumbsDrawer', function () { const [warningCrumb, logCrumb] = MOCK_BREADCRUMBS; - expect(drawerScreen.getByText(warningCrumb.category)).toBeInTheDocument(); - expect(drawerScreen.getByText(logCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(warningCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(logCrumb.category)).toBeInTheDocument(); await userEvent.click( - drawerScreen.getByRole('button', {name: 'Filter All Breadcrumbs'}) + within(drawerScreen).getByRole('button', {name: 'Filter All Breadcrumbs'}) ); - await userEvent.click(drawerScreen.getByRole('option', {name: 'warning'})); + await userEvent.click(within(drawerScreen).getByRole('option', {name: 'warning'})); - expect(drawerScreen.getByText(warningCrumb.category)).toBeInTheDocument(); - expect(drawerScreen.queryByText(logCrumb.category)).not.toBeInTheDocument(); + expect(within(drawerScreen).getByText(warningCrumb.category)).toBeInTheDocument(); + expect(within(drawerScreen).queryByText(logCrumb.category)).not.toBeInTheDocument(); }); it('allows sort dropdown to affect displayed crumbs', async function () { @@ -119,54 +129,56 @@ describe('BreadcrumbsDrawer', function () { const [warningCrumb, logCrumb] = MOCK_BREADCRUMBS; expect( - drawerScreen + within(drawerScreen) .getByText(warningCrumb.category) - .compareDocumentPosition(drawerScreen.getByText(logCrumb.category)) + .compareDocumentPosition(within(drawerScreen).getByText(logCrumb.category)) ).toBe(document.DOCUMENT_POSITION_PRECEDING); - const sortControl = drawerScreen.getByRole('button', { + const sortControl = within(drawerScreen).getByRole('button', { name: 'Sort All Breadcrumbs', }); await userEvent.click(sortControl); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Oldest'})); + await userEvent.click(within(drawerScreen).getByRole('option', {name: 'Oldest'})); expect( - drawerScreen + within(drawerScreen) .getByText(warningCrumb.category) - .compareDocumentPosition(drawerScreen.getByText(logCrumb.category)) + .compareDocumentPosition(within(drawerScreen).getByText(logCrumb.category)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); await userEvent.click(sortControl); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Newest'})); + await userEvent.click(within(drawerScreen).getByRole('option', {name: 'Newest'})); expect( - drawerScreen + within(drawerScreen) .getByText(warningCrumb.category) - .compareDocumentPosition(drawerScreen.getByText(logCrumb.category)) + .compareDocumentPosition(within(drawerScreen).getByText(logCrumb.category)) ).toBe(document.DOCUMENT_POSITION_PRECEDING); }); it('allows time display dropdown to change all displayed crumbs', async function () { const drawerScreen = await renderBreadcrumbDrawer(); - expect(drawerScreen.getAllByText('06:00:48.760 PM')).toHaveLength( + expect(within(drawerScreen).getAllByText('06:00:48.760 PM')).toHaveLength( MOCK_BREADCRUMBS.length ); - expect(drawerScreen.queryByText('-1min 2ms')).not.toBeInTheDocument(); - const timeControl = drawerScreen.getByRole('button', { + expect(within(drawerScreen).queryByText('-1min 2ms')).not.toBeInTheDocument(); + const timeControl = within(drawerScreen).getByRole('button', { name: 'Change Time Format for All Breadcrumbs', }); await userEvent.click(timeControl); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Relative'})); + await userEvent.click(within(drawerScreen).getByRole('option', {name: 'Relative'})); - expect(drawerScreen.queryByText('06:00:48.760 PM')).not.toBeInTheDocument(); - expect(drawerScreen.getAllByText('-1min 2ms')).toHaveLength(MOCK_BREADCRUMBS.length); + expect(within(drawerScreen).queryByText('06:00:48.760 PM')).not.toBeInTheDocument(); + expect(within(drawerScreen).getAllByText('-1min 2ms')).toHaveLength( + MOCK_BREADCRUMBS.length + ); await userEvent.click(timeControl); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Absolute'})); + await userEvent.click(within(drawerScreen).getByRole('option', {name: 'Absolute'})); - expect(drawerScreen.getAllByText('06:00:48.760 PM')).toHaveLength( + expect(within(drawerScreen).getAllByText('06:00:48.760 PM')).toHaveLength( MOCK_BREADCRUMBS.length ); - expect(drawerScreen.queryByText('-1min 2ms')).not.toBeInTheDocument(); + expect(within(drawerScreen).queryByText('-1min 2ms')).not.toBeInTheDocument(); }); }); diff --git a/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx b/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx index d7f38d6317839c..fe7d03a9646203 100644 --- a/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx +++ b/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx @@ -25,7 +25,7 @@ async function renderFlagDrawer() { })); render(); await userEvent.click(screen.getByRole('button', {name: 'View All'})); - return within(screen.getByRole('complementary', {name: 'Feature flags drawer'})); + return screen.getByRole('complementary', {name: 'Feature flags drawer'}); } describe('FeatureFlagDrawer', function () { @@ -45,25 +45,33 @@ describe('FeatureFlagDrawer', function () { }); it('renders the drawer as expected', async function () { const drawerScreen = await renderFlagDrawer(); - expect(drawerScreen.getByRole('button', {name: 'Close Drawer'})).toBeInTheDocument(); + expect( + within(drawerScreen).getByRole('button', {name: 'Close Drawer'}) + ).toBeInTheDocument(); // Inner drawer flags const {event, group} = MOCK_DATA_SECTION_PROPS; - expect(drawerScreen.getByText(group.shortId)).toBeInTheDocument(); - expect(drawerScreen.getByText(event.id.slice(0, 8))).toBeInTheDocument(); + expect(within(drawerScreen).getByText(group.shortId)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(event.id.slice(0, 8))).toBeInTheDocument(); expect( - drawerScreen.getByText('Feature Flags', {selector: 'span'}) + within(drawerScreen).getByText('Feature Flags', {selector: 'span'}) ).toBeInTheDocument(); // Header & Controls - expect(drawerScreen.getByText('Feature Flags', {selector: 'h3'})).toBeInTheDocument(); - expect(drawerScreen.getByRole('textbox', {name: 'Search Flags'})).toBeInTheDocument(); - expect(drawerScreen.getByRole('button', {name: 'Sort Flags'})).toBeInTheDocument(); + expect( + within(drawerScreen).getByText('Feature Flags', {selector: 'h3'}) + ).toBeInTheDocument(); + expect( + within(drawerScreen).getByRole('textbox', {name: 'Search Flags'}) + ).toBeInTheDocument(); + expect( + within(drawerScreen).getByRole('button', {name: 'Sort Flags'}) + ).toBeInTheDocument(); // Contents for (const {flag, result} of MOCK_FLAGS) { - expect(drawerScreen.getByText(flag)).toBeInTheDocument(); - expect(drawerScreen.getAllByText(result.toString())[0]).toBeInTheDocument(); + expect(within(drawerScreen).getByText(flag)).toBeInTheDocument(); + expect(within(drawerScreen).getAllByText(result.toString())[0]).toBeInTheDocument(); } }); @@ -71,16 +79,16 @@ describe('FeatureFlagDrawer', function () { const drawerScreen = await renderFlagDrawer(); const [webVitalsFlag, enableReplay] = MOCK_FLAGS.filter(f => f.result === true); - expect(drawerScreen.getByText(webVitalsFlag.flag)).toBeInTheDocument(); - expect(drawerScreen.getByText(enableReplay.flag)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(webVitalsFlag.flag)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(enableReplay.flag)).toBeInTheDocument(); - const searchInput = drawerScreen.getByRole('textbox', { + const searchInput = within(drawerScreen).getByRole('textbox', { name: 'Search Flags', }); await userEvent.type(searchInput, webVitalsFlag.flag); - expect(drawerScreen.getByText(webVitalsFlag.flag)).toBeInTheDocument(); - expect(drawerScreen.queryByText(enableReplay.flag)).not.toBeInTheDocument(); + expect(within(drawerScreen).getByText(webVitalsFlag.flag)).toBeInTheDocument(); + expect(within(drawerScreen).queryByText(enableReplay.flag)).not.toBeInTheDocument(); }); it('allows sort dropdown to affect displayed flags', async function () { @@ -90,62 +98,69 @@ describe('FeatureFlagDrawer', function () { // the flags are reversed by default, so webVitalsFlag should be following enableReplay expect( - drawerScreen + within(drawerScreen) .getByText(enableReplay.flag) - .compareDocumentPosition(drawerScreen.getByText(webVitalsFlag.flag)) + .compareDocumentPosition(within(drawerScreen).getByText(webVitalsFlag.flag)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); - const sortControl = drawerScreen.getByRole('button', { + const sortControl = within(drawerScreen).getByRole('button', { name: 'Sort Flags', }); await userEvent.click(sortControl); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Oldest First'})); + await userEvent.click( + within(drawerScreen).getByRole('option', {name: 'Oldest First'}) + ); // expect webVitalsFlag to be preceding enableReplay expect( - drawerScreen + within(drawerScreen) .getByText(enableReplay.flag) - .compareDocumentPosition(drawerScreen.getByText(webVitalsFlag.flag)) + .compareDocumentPosition(within(drawerScreen).getByText(webVitalsFlag.flag)) ).toBe(document.DOCUMENT_POSITION_PRECEDING); await userEvent.click(sortControl); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Alphabetical'})); + await userEvent.click( + within(drawerScreen).getByRole('option', {name: 'Alphabetical'}) + ); await userEvent.click(sortControl); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Z-A'})); + await userEvent.click(within(drawerScreen).getByRole('option', {name: 'Z-A'})); // enableReplay follows webVitalsFlag in Z-A sort expect( - drawerScreen + within(drawerScreen) .getByText(webVitalsFlag.flag) - .compareDocumentPosition(drawerScreen.getByText(enableReplay.flag)) + .compareDocumentPosition(within(drawerScreen).getByText(enableReplay.flag)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); }); it('renders a sort dropdown with Evaluation Order as the default', async function () { const drawerScreen = await renderFlagDrawer(); - const control = drawerScreen.getByRole('button', {name: 'Sort Flags'}); + const control = within(drawerScreen).getByRole('button', {name: 'Sort Flags'}); expect(control).toBeInTheDocument(); await userEvent.click(control); expect( - drawerScreen.getByRole('option', {name: 'Evaluation Order'}) + within(drawerScreen).getByRole('option', {name: 'Evaluation Order'}) + ).toBeInTheDocument(); + expect( + within(drawerScreen).getByRole('option', {name: 'Alphabetical'}) ).toBeInTheDocument(); - expect(drawerScreen.getByRole('option', {name: 'Alphabetical'})).toBeInTheDocument(); }); it('renders a sort dropdown which affects the granular sort dropdown', async function () { const drawerScreen = await renderFlagDrawer(); - const control = drawerScreen.getByRole('button', {name: 'Sort Flags'}); + const control = within(drawerScreen).getByRole('button', {name: 'Sort Flags'}); expect(control).toBeInTheDocument(); await userEvent.click(control); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Alphabetical'})); - await userEvent.click(control); - expect(drawerScreen.getByRole('option', {name: 'Alphabetical'})).toHaveAttribute( - 'aria-selected', - 'true' + await userEvent.click( + within(drawerScreen).getByRole('option', {name: 'Alphabetical'}) ); - expect(drawerScreen.getByRole('option', {name: 'A-Z'})).toHaveAttribute( + await userEvent.click(control); + expect( + within(drawerScreen).getByRole('option', {name: 'Alphabetical'}) + ).toHaveAttribute('aria-selected', 'true'); + expect(within(drawerScreen).getByRole('option', {name: 'A-Z'})).toHaveAttribute( 'aria-selected', 'true' ); @@ -154,35 +169,35 @@ describe('FeatureFlagDrawer', function () { it('renders a sort dropdown which disables the appropriate options', async function () { const drawerScreen = await renderFlagDrawer(); - const control = drawerScreen.getByRole('button', {name: 'Sort Flags'}); + const control = within(drawerScreen).getByRole('button', {name: 'Sort Flags'}); expect(control).toBeInTheDocument(); await userEvent.click(control); - await userEvent.click(drawerScreen.getByRole('option', {name: 'Alphabetical'})); - await userEvent.click(control); - expect(drawerScreen.getByRole('option', {name: 'Alphabetical'})).toHaveAttribute( - 'aria-selected', - 'true' - ); - expect(drawerScreen.getByRole('option', {name: 'Newest First'})).toHaveAttribute( - 'aria-disabled', - 'true' + await userEvent.click( + within(drawerScreen).getByRole('option', {name: 'Alphabetical'}) ); - expect(drawerScreen.getByRole('option', {name: 'Oldest First'})).toHaveAttribute( - 'aria-disabled', - 'true' - ); - - await userEvent.click(drawerScreen.getByRole('option', {name: 'Evaluation Order'})); await userEvent.click(control); - expect(drawerScreen.getByRole('option', {name: 'Evaluation Order'})).toHaveAttribute( - 'aria-selected', - 'true' + expect( + within(drawerScreen).getByRole('option', {name: 'Alphabetical'}) + ).toHaveAttribute('aria-selected', 'true'); + expect( + within(drawerScreen).getByRole('option', {name: 'Newest First'}) + ).toHaveAttribute('aria-disabled', 'true'); + expect( + within(drawerScreen).getByRole('option', {name: 'Oldest First'}) + ).toHaveAttribute('aria-disabled', 'true'); + + await userEvent.click( + within(drawerScreen).getByRole('option', {name: 'Evaluation Order'}) ); - expect(drawerScreen.getByRole('option', {name: 'Z-A'})).toHaveAttribute( + await userEvent.click(control); + expect( + within(drawerScreen).getByRole('option', {name: 'Evaluation Order'}) + ).toHaveAttribute('aria-selected', 'true'); + expect(within(drawerScreen).getByRole('option', {name: 'Z-A'})).toHaveAttribute( 'aria-disabled', 'true' ); - expect(drawerScreen.getByRole('option', {name: 'A-Z'})).toHaveAttribute( + expect(within(drawerScreen).getByRole('option', {name: 'A-Z'})).toHaveAttribute( 'aria-disabled', 'true' ); diff --git a/static/app/components/nav/index.spec.tsx b/static/app/components/nav/index.spec.tsx index d58260fd23faf2..3ad846511a10e5 100644 --- a/static/app/components/nav/index.spec.tsx +++ b/static/app/components/nav/index.spec.tsx @@ -8,7 +8,7 @@ jest.mock('sentry/utils/analytics', () => ({ trackAnalytics: jest.fn(), })); -import {getAllByRole, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; import Nav from 'sentry/components/nav'; @@ -54,10 +54,9 @@ describe('Nav', function () { it('renders expected primary nav items', function () { renderNav(); - const links = getAllByRole( - screen.getByRole('navigation', {name: 'Primary Navigation'}), - 'link' - ); + const links = within( + screen.getByRole('navigation', {name: 'Primary Navigation'}) + ).getAllByRole('link'); expect(links).toHaveLength(8); [ @@ -98,7 +97,7 @@ describe('Nav', function () { it('includes expected submenu items', function () { renderNav(); const container = screen.getByRole('navigation', {name: 'Secondary Navigation'}); - const links = getAllByRole(container, 'link'); + const links = within(container).getAllByRole('link'); expect(links).toHaveLength(6); ['All', 'Error & Outage', 'Trend', 'Craftsmanship', 'Security', 'Feedback'].forEach( @@ -131,7 +130,7 @@ describe('Nav', function () { it('includes expected submenu items', function () { renderNav(); const container = screen.getByRole('navigation', {name: 'Secondary Navigation'}); - const links = getAllByRole(container, 'link'); + const links = within(container).getAllByRole('link'); expect(links).toHaveLength(4); ['Frontend', 'Backend', 'Mobile', 'AI'].forEach((title, index) => { expect(links[index]).toHaveAccessibleName(title); @@ -159,7 +158,7 @@ describe('Nav', function () { it('includes expected submenu items', function () { renderNav(); const container = screen.getByRole('navigation', {name: 'Secondary Navigation'}); - const links = getAllByRole(container, 'link'); + const links = within(container).getAllByRole('link'); expect(links).toHaveLength(7); [ 'Traces', diff --git a/static/app/components/profiling/flamegraph/flamegraph.spec.tsx b/static/app/components/profiling/flamegraph/flamegraph.spec.tsx index e47d2700716ef6..b91ce99e7da05d 100644 --- a/static/app/components/profiling/flamegraph/flamegraph.spec.tsx +++ b/static/app/components/profiling/flamegraph/flamegraph.spec.tsx @@ -1,13 +1,7 @@ import {ProjectFixture} from 'sentry-fixture/project'; import {initializeOrg} from 'sentry-test/initializeOrg'; -import { - act, - findAllByTestId, - render, - screen, - userEvent, -} from 'sentry-test/reactTestingLibrary'; +import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; import {useParams} from 'sentry/utils/useParams'; @@ -165,7 +159,7 @@ describe('Flamegraph', function () { {organization: initializeOrg().organization} ); - const frames = await findAllByTestId(document.body, 'flamegraph-frame', undefined, { + const frames = await screen.findAllByTestId('flamegraph-frame', undefined, { timeout: 5000, }); diff --git a/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx b/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx index c87136ad9c3e8d..803f2a21584036 100644 --- a/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx +++ b/static/app/views/insights/common/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx @@ -1,5 +1,6 @@ import { render, + screen, waitFor, waitForElementToBeRemoved, } from 'sentry-test/reactTestingLibrary'; @@ -39,7 +40,7 @@ describe('SampleTable', function () { describe('When all data is available', () => { it('should finsh loading', async () => { - const container = render( + render( ); - await waitForElementToBeRemoved(() => container.queryByTestId('loading-indicator')); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); }); it('should never show no results', async () => { - const container = render( + render( ); - await expectNever(() => container.getByText('No results found for your query')); - expect(container.queryByTestId('loading-indicator')).not.toBeInTheDocument(); + await expectNever(() => screen.getByText('No results found for your query')); + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); }); it('should show span IDs by default', async () => { - const container = render( + render( - expect(container.queryByTestId('loading-indicator')).not.toBeInTheDocument() + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument() ); - expect(container.queryAllByTestId('grid-head-cell')[0]).toHaveTextContent( - 'Span ID' - ); - expect(container.queryAllByTestId('grid-body-cell')[0]).toHaveTextContent( + expect(screen.queryAllByTestId('grid-head-cell')[0]).toHaveTextContent('Span ID'); + expect(screen.queryAllByTestId('grid-body-cell')[0]).toHaveTextContent( 'span-id123' ); }); it('should show transaction IDs instead of span IDs when in columnOrder', async () => { - const container = render( + render( - expect(container.queryByTestId('loading-indicator')).not.toBeInTheDocument() + expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument() ); - expect(container.queryAllByTestId('grid-head-cell')[0]).toHaveTextContent( - 'Event ID' - ); - expect(container.queryAllByTestId('grid-body-cell')[0]).toHaveTextContent( + expect(screen.queryAllByTestId('grid-head-cell')[0]).toHaveTextContent('Event ID'); + expect(screen.queryAllByTestId('grid-body-cell')[0]).toHaveTextContent( 'transaction-id123'.slice(0, 8) ); }); @@ -137,7 +134,7 @@ describe('SampleTable', function () { }, }); - const container = render( + render( ); - await waitForElementToBeRemoved(() => container.queryByTestId('loading-indicator')); - expect(container.getByText('No results found for your query')).toBeInTheDocument(); + await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); + expect(screen.getByText('No results found for your query')).toBeInTheDocument(); }); }); }); diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 339a6cb4595a84..5152525dbc5fbf 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -4,8 +4,6 @@ import {TransactionEventFixture} from 'sentry-fixture/event'; import {initializeOrg} from 'sentry-test/initializeOrg'; import { - findAllByText, - findByText, render, screen, userEvent, @@ -286,7 +284,7 @@ async function keyboardNavigationTestSetup() { // Awaits for the placeholder rendering rows to be removed try { - await findAllByText(virtualizedContainer, /transaction-op-/i, undefined, { + await within(virtualizedContainer).findAllByText(/transaction-op-/i, undefined, { timeout: 5000, }); } catch (e) { @@ -343,7 +341,7 @@ async function pageloadTestSetup() { // Awaits for the placeholder rendering rows to be removed try { - await findAllByText(virtualizedContainer, /transaction-op-/i, undefined, { + await within(virtualizedContainer).findAllByText(/transaction-op-/i, undefined, { timeout: 5000, }); } catch (e) { @@ -401,7 +399,7 @@ async function nestedTransactionsTestSetup() { // Awaits for the placeholder rendering rows to be removed try { - await findAllByText(virtualizedContainer, /transaction-op-/i, undefined, { + await within(virtualizedContainer).findAllByText(/transaction-op-/i, undefined, { timeout: 5000, }); } catch (e) { @@ -457,7 +455,7 @@ async function searchTestSetup() { // Awaits for the placeholder rendering rows to be removed try { - await findAllByText(virtualizedContainer, /transaction-op-/i, undefined, { + await within(virtualizedContainer).findAllByText(/transaction-op-/i, undefined, { timeout: 5000, }); } catch (e) { @@ -517,7 +515,7 @@ async function simpleTestSetup() { // Awaits for the placeholder rendering rows to be removed try { - await findAllByText(virtualizedContainer, /transaction-op-/i, undefined, { + await within(virtualizedContainer).findAllByText(/transaction-op-/i, undefined, { timeout: 5000, }); } catch (e) { @@ -729,7 +727,7 @@ async function completeTestSetup() { // Awaits for the placeholder rendering rows to be removed try { - await findAllByText(virtualizedContainer, /transaction-op-/i, undefined, { + await within(virtualizedContainer).findAllByText(/transaction-op-/i, undefined, { timeout: 5000, }); } catch (e) { @@ -937,7 +935,7 @@ describe('trace view', () => { mockQueryString('?node=span-span0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); // We need to await a tick because the row is not focused until the next tick const rows = getVirtualizedRows(virtualizedContainer); @@ -951,7 +949,7 @@ describe('trace view', () => { mockQueryString('?node=ag-redis0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); // We need to await a tick because the row is not focused until the next tick const rows = getVirtualizedRows(virtualizedContainer); @@ -964,7 +962,7 @@ describe('trace view', () => { mockQueryString('?node=span-redis0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); // We need to await a tick because the row is not focused until the next tick const rows = getVirtualizedRows(virtualizedContainer); @@ -978,7 +976,7 @@ describe('trace view', () => { mockQueryString('?node=ag-http0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); // We need to await a tick because the row is not focused until the next tick const rows = getVirtualizedRows(virtualizedContainer); @@ -992,7 +990,7 @@ describe('trace view', () => { mockQueryString('?node=span-http0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); // We need to await a tick because the row is not focused until the next tick const rows = getVirtualizedRows(virtualizedContainer); @@ -1006,7 +1004,7 @@ describe('trace view', () => { mockQueryString('?node=ms-queueprocess0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); // We need to await a tick because the row is not focused until the next ticks const rows = getVirtualizedRows(virtualizedContainer); @@ -1020,7 +1018,7 @@ describe('trace view', () => { mockQueryString('?node=error-error0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); // We need to await a tick because the row is not focused until the next ticks const rows = getVirtualizedRows(virtualizedContainer); @@ -1043,7 +1041,7 @@ describe('trace view', () => { it('supports expanded node path', async () => { mockQueryString('?node=span-span0&node=txn-1&span-0&node=txn-0'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); const rows = getVirtualizedRows(virtualizedContainer); await waitFor(() => { @@ -1081,7 +1079,7 @@ describe('trace view', () => { const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /process/i); + await within(virtualizedContainer).findAllByText(/process/i); expect(screen.queryByText(/Autogrouped/i)).not.toBeInTheDocument(); }); @@ -1091,7 +1089,7 @@ describe('trace view', () => { const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /process/i); + await within(virtualizedContainer).findAllByText(/process/i); expect(screen.queryByText(/Missing instrumentation/i)).not.toBeInTheDocument(); }); @@ -1101,7 +1099,7 @@ describe('trace view', () => { mockQueryString('?node=span-span0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /Autogrouped/i); + await within(virtualizedContainer).findAllByText(/Autogrouped/i); const preferencesDropdownTrigger = screen.getByLabelText('Trace Preferences'); await userEvent.click(preferencesDropdownTrigger); @@ -1126,7 +1124,7 @@ describe('trace view', () => { mockQueryString('?node=span-span0&node=txn-1'); const {virtualizedContainer} = await completeTestSetup(); - await findAllByText(virtualizedContainer, /No Instrumentation/i); + await within(virtualizedContainer).findAllByText(/No Instrumentation/i); const preferencesDropdownTrigger = screen.getByLabelText('Trace Preferences'); await userEvent.click(preferencesDropdownTrigger); @@ -1289,7 +1287,7 @@ describe('trace view', () => { await userEvent.keyboard('{arrowup}'); expect( - await findByText(virtualizedContainer, /transaction-op-99/i) + await within(virtualizedContainer).findByText(/transaction-op-99/i) ).toBeInTheDocument(); await waitFor(() => { @@ -1370,7 +1368,7 @@ describe('trace view', () => { await userEvent.keyboard('{Shift>}{arrowdown}{/Shift}'); expect( - await findByText(virtualizedContainer, /transaction-op-99/i) + await within(virtualizedContainer).findByText(/transaction-op-99/i) ).toBeInTheDocument(); await waitFor(() => { rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR); @@ -1391,7 +1389,7 @@ describe('trace view', () => { await userEvent.keyboard('{Shift>}{arrowdown}{/Shift}'); expect( - await findByText(virtualizedContainer, /transaction-op-99/i) + await within(virtualizedContainer).findByText(/transaction-op-99/i) ).toBeInTheDocument(); await waitFor(() => { @@ -1402,7 +1400,7 @@ describe('trace view', () => { await userEvent.keyboard('{Shift>}{arrowup}{/Shift}'); expect( - await findByText(virtualizedContainer, /transaction-op-0/i) + await within(virtualizedContainer).findByText(/transaction-op-0/i) ).toBeInTheDocument(); await waitFor(() => { @@ -1692,7 +1690,7 @@ describe('trace view', () => { const {container} = render(, {router}); // Awaits for the placeholder rendering rows to be removed - await findByText(container, /transaction-op-0/i); + await within(container).findByText(/transaction-op-0/i); const searchInput = await screen.findByPlaceholderText('Search in trace'); await userEvent.type(searchInput, 'op-0'); diff --git a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx index ba784ff3cad2fd..f574f1e46aaf7e 100644 --- a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx @@ -4,12 +4,12 @@ import {TeamFixture} from 'sentry-fixture/team'; import {initializeOrg} from 'sentry-test/initializeOrg'; import { - findByLabelText, render, renderGlobalModal, screen, userEvent, waitFor, + within, } from 'sentry-test/reactTestingLibrary'; import OrganizationStore from 'sentry/stores/organizationStore'; @@ -939,11 +939,11 @@ describe('Performance > TransactionSummary', function () { await screen.findByText('Transaction Summary'); const pagination = await screen.findByTestId('pagination'); - expect(await findByLabelText(pagination, 'Previous')).toBeInTheDocument(); - expect(await findByLabelText(pagination, 'Next')).toBeInTheDocument(); + expect(await within(pagination).findByLabelText('Previous')).toBeInTheDocument(); + expect(await within(pagination).findByLabelText('Next')).toBeInTheDocument(); // Click the 'next' button - await userEvent.click(await findByLabelText(pagination, 'Next')); + await userEvent.click(await within(pagination).findByLabelText('Next')); // Check the navigation. expect(router.push).toHaveBeenCalledWith({ diff --git a/static/app/views/profiling/landing/slowestFunctionsWidget.spec.tsx b/static/app/views/profiling/landing/slowestFunctionsWidget.spec.tsx index 7a33c442595804..353dd5a812671f 100644 --- a/static/app/views/profiling/landing/slowestFunctionsWidget.spec.tsx +++ b/static/app/views/profiling/landing/slowestFunctionsWidget.spec.tsx @@ -1,6 +1,6 @@ import {ProjectFixture} from 'sentry-fixture/project'; -import {getAllByRole, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; import {SlowestFunctionsWidget} from 'sentry/views/profiling/landing/slowestFunctionsWidget'; @@ -246,7 +246,7 @@ describe('SlowestFunctionsWidget', function () { const items = screen.getAllByRole('listitem', {}); expect(items.length).toEqual(2); - const buttons = getAllByRole(items[0], 'button', {}); + const buttons = within(items[0]).getAllByRole('button'); expect(buttons.length).toEqual(2); await userEvent.click(buttons[1]); diff --git a/static/app/views/settings/projectPlugins/index.spec.tsx b/static/app/views/settings/projectPlugins/index.spec.tsx index db3baaf449986f..fb1f92c778dd82 100644 --- a/static/app/views/settings/projectPlugins/index.spec.tsx +++ b/static/app/views/settings/projectPlugins/index.spec.tsx @@ -4,7 +4,7 @@ import {PluginsFixture} from 'sentry-fixture/plugins'; import {ProjectFixture} from 'sentry-fixture/project'; import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixture'; -import {getByRole, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary'; import {disablePlugin, enablePlugin, fetchPlugins} from 'sentry/actionCreators/plugins'; import type {Plugin} from 'sentry/types/integrations'; @@ -91,7 +91,7 @@ describe('ProjectPluginsContainer', function () { if (!pluginItem) { return; } - const button = getByRole(pluginItem, 'checkbox'); + const button = within(pluginItem).getByRole('checkbox'); expect(enablePlugin).not.toHaveBeenCalled(); @@ -110,7 +110,7 @@ describe('ProjectPluginsContainer', function () { return; } - const button = getByRole(pluginItem, 'checkbox'); + const button = within(pluginItem).getByRole('checkbox'); expect(disablePlugin).not.toHaveBeenCalled(); diff --git a/tests/js/sentry-test/selectEvent.tsx b/tests/js/sentry-test/selectEvent.tsx index 735329df7a7428..df4a28566571af 100644 --- a/tests/js/sentry-test/selectEvent.tsx +++ b/tests/js/sentry-test/selectEvent.tsx @@ -23,7 +23,7 @@ import userEvent from '@testing-library/user-event'; // eslint-disable-line no-restricted-imports -import {findAllByText, findByText, type Matcher, waitFor} from './reactTestingLibrary'; +import {type Matcher, waitFor, within} from 'sentry-test/reactTestingLibrary'; /** * Find the react-select container from its input field @@ -107,7 +107,7 @@ const select = async ( } // only consider visible, interactive elements - const matchingElements = await findAllByText(container, option, { + const matchingElements = await within(container).findAllByText(option, { ignore: "[aria-live] *,[style*='visibility: hidden']", }); @@ -147,7 +147,7 @@ const create = async ( await select(input, createOptionText, {...config, user}); if (waitForElement) { - await findByText(getReactSelectContainerFromInput(input), option); + await within(getReactSelectContainerFromInput(input)).findByText(option); } }; From 1cc13064a204421b181c63c397018f995a509b7a Mon Sep 17 00:00:00 2001 From: mia hsu <55610339+ameliahsu@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:42:08 -0800 Subject: [PATCH 605/757] chore(onboarding): remove members invite members ff from backend (#82796) remove `members-invite-teammates` feature flag from backend --- src/sentry/api/bases/organizationmember.py | 6 +--- .../endpoints/organization_member/details.py | 8 ++--- .../api/bases/test_organizationmember.py | 29 ++++++++----------- .../test_organization_member_details.py | 6 ---- .../test_organization_member_index.py | 1 - 5 files changed, 15 insertions(+), 35 deletions(-) diff --git a/src/sentry/api/bases/organizationmember.py b/src/sentry/api/bases/organizationmember.py index df8bdefaca4386..c140bb06f73572 100644 --- a/src/sentry/api/bases/organizationmember.py +++ b/src/sentry/api/bases/organizationmember.py @@ -6,7 +6,6 @@ from rest_framework.fields import empty from rest_framework.request import Request -from sentry import features from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.permissions import StaffPermissionMixin from sentry.db.models.fields.bounded import BoundedAutoField @@ -44,10 +43,7 @@ def has_object_permission( is_role_above_member = "member:admin" in scopes or "member:write" in scopes if isinstance(organization, RpcUserOrganizationContext): organization = organization.organization - return is_role_above_member or ( - features.has("organizations:members-invite-teammates", organization) - and not organization.flags.disable_member_invite - ) + return is_role_above_member or not organization.flags.disable_member_invite class MemberAndStaffPermission(StaffPermissionMixin, MemberPermission): diff --git a/src/sentry/api/endpoints/organization_member/details.py b/src/sentry/api/endpoints/organization_member/details.py index 1564634770ea69..28a78b458a3545 100644 --- a/src/sentry/api/endpoints/organization_member/details.py +++ b/src/sentry/api/endpoints/organization_member/details.py @@ -226,10 +226,7 @@ def put( is_member = not ( request.access.has_scope("member:invite") and request.access.has_scope("member:admin") ) - enable_member_invite = ( - features.has("organizations:members-invite-teammates", organization) - and not organization.flags.disable_member_invite - ) + enable_member_invite = not organization.flags.disable_member_invite # Members can only resend invites reinvite_request_only = set(result.keys()).issubset({"reinvite", "regenerate"}) # Members can only resend invites that they sent @@ -470,8 +467,7 @@ def delete( if acting_member != member: if not request.access.has_scope("member:admin"): if ( - features.has("organizations:members-invite-teammates", organization) - and not organization.flags.disable_member_invite + not organization.flags.disable_member_invite and request.access.has_scope("member:invite") ): return self._handle_deletion_by_member( diff --git a/tests/sentry/api/bases/test_organizationmember.py b/tests/sentry/api/bases/test_organizationmember.py index 45a64f9b4f978c..42400aab11ee36 100644 --- a/tests/sentry/api/bases/test_organizationmember.py +++ b/tests/sentry/api/bases/test_organizationmember.py @@ -1,5 +1,4 @@ from sentry.api.bases.organizationmember import MemberAndStaffPermission, MemberPermission -from sentry.testutils.helpers import Feature from tests.sentry.api.bases.test_organization import PermissionBaseTestCase @@ -27,34 +26,30 @@ def test_org_member(self): self.create_member(user=member_user, organization=self.org, role="member") assert self.has_object_perm("GET", self.org, user=member_user) assert not self.has_object_perm("PUT", self.org, user=member_user) - assert not self.has_object_perm("POST", self.org, user=member_user) assert not self.has_object_perm("DELETE", self.org, user=member_user) - with Feature({"organizations:members-invite-teammates": True}): - self.org.flags.disable_member_invite = False - self.org.save() - assert self.has_object_perm("POST", self.org, user=member_user) + self.org.flags.disable_member_invite = False + self.org.save() + assert self.has_object_perm("POST", self.org, user=member_user) - self.org.flags.disable_member_invite = True - self.org.save() - assert not self.has_object_perm("POST", self.org, user=member_user) + self.org.flags.disable_member_invite = True + self.org.save() + assert not self.has_object_perm("POST", self.org, user=member_user) def test_org_admin(self): admin_user = self.create_user() self.create_member(user=admin_user, organization=self.org, role="admin") assert self.has_object_perm("GET", self.org, user=admin_user) assert not self.has_object_perm("PUT", self.org, user=admin_user) - assert not self.has_object_perm("POST", self.org, user=admin_user) assert not self.has_object_perm("DELETE", self.org, user=admin_user) - with Feature({"organizations:members-invite-teammates": True}): - self.org.flags.disable_member_invite = False - self.org.save() - assert self.has_object_perm("POST", self.org, user=admin_user) + self.org.flags.disable_member_invite = False + self.org.save() + assert self.has_object_perm("POST", self.org, user=admin_user) - self.org.flags.disable_member_invite = True - self.org.save() - assert not self.has_object_perm("POST", self.org, user=admin_user) + self.org.flags.disable_member_invite = True + self.org.save() + assert not self.has_object_perm("POST", self.org, user=admin_user) def test_org_manager(self): manager_user = self.create_user() diff --git a/tests/sentry/api/endpoints/test_organization_member_details.py b/tests/sentry/api/endpoints/test_organization_member_details.py index 2f1a7633ec8bf3..cd8df983a8eb69 100644 --- a/tests/sentry/api/endpoints/test_organization_member_details.py +++ b/tests/sentry/api/endpoints/test_organization_member_details.py @@ -186,7 +186,6 @@ def test_reinvite_pending_member(self, mock_send_invite_email): mock_send_invite_email.assert_called_once_with() @patch("sentry.models.OrganizationMember.send_invite_email") - @with_feature("organizations:members-invite-teammates") def test_member_reinvite_pending_member(self, mock_send_invite_email): self.login_as(self.curr_user) @@ -220,7 +219,6 @@ def test_member_reinvite_pending_member(self, mock_send_invite_email): assert not mock_send_invite_email.mock_calls @patch("sentry.models.OrganizationMember.send_invite_email") - @with_feature("organizations:members-invite-teammates") def test_member_can_only_reinvite(self, mock_send_invite_email): foo = self.create_team(organization=self.organization, name="Team Foo") self.login_as(self.curr_user) @@ -253,7 +251,6 @@ def test_member_can_only_reinvite(self, mock_send_invite_email): assert not mock_send_invite_email.mock_calls @patch("sentry.models.OrganizationMember.send_invite_email") - @with_feature("organizations:members-invite-teammates") def test_member_cannot_reinvite_non_pending_members(self, mock_send_invite_email): self.login_as(self.curr_user) @@ -340,7 +337,6 @@ def test_rate_limited(self, mock_send_invite_email, mock_rate_limit): assert not mock_send_invite_email.mock_calls @patch("sentry.models.OrganizationMember.send_invite_email") - @with_feature("organizations:members-invite-teammates") def test_member_cannot_regenerate_pending_invite(self, mock_send_invite_email): member_om = self.create_member( organization=self.organization, email="foo@example.com", role="member" @@ -1023,7 +1019,6 @@ def test_cannot_delete_partnership_member(self): self.get_error_response(self.organization.slug, member_om.id, status_code=403) - @with_feature("organizations:members-invite-teammates") def test_member_delete_pending_invite(self): curr_invite = self.create_member( organization=self.organization, @@ -1052,7 +1047,6 @@ def test_member_delete_pending_invite(self): self.get_success_response(self.organization.slug, curr_invite.id) self.get_error_response(self.organization.slug, other_invite.id, status_code=400) - @with_feature("organizations:members-invite-teammates") def test_member_cannot_delete_members(self): self.login_as(self.curr_user) diff --git a/tests/sentry/api/endpoints/test_organization_member_index.py b/tests/sentry/api/endpoints/test_organization_member_index.py index 9a4a14a8f3a3c3..e7eab35303a867 100644 --- a/tests/sentry/api/endpoints/test_organization_member_index.py +++ b/tests/sentry/api/endpoints/test_organization_member_index.py @@ -521,7 +521,6 @@ def test_cannot_invite_retired_role_with_flag(self): class OrganizationMemberPermissionRoleTest(OrganizationMemberListTestBase, HybridCloudTestMixin): method = "post" - @with_feature("organizations:members-invite-teammates") def invite_all_helper(self, role): invite_roles = ["owner", "manager", "member"] From 8f5df007969486456d3d43e63efbb5de04e565ae Mon Sep 17 00:00:00 2001 From: mia hsu <55610339+ameliahsu@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:44:01 -0800 Subject: [PATCH 606/757] chore(onboarding): remove members invite members ff from frontend (#82797) remove `members-invite-teammates` feature flag from frontend --- .../components/modals/inviteMembersModal/useInviteModal.tsx | 4 +--- static/app/components/roleSelectControl.tsx | 4 +--- static/app/data/forms/organizationMembershipSettings.tsx | 1 - .../organizationMembers/organizationMemberRow.spec.tsx | 4 ---- .../settings/organizationMembers/organizationMemberRow.tsx | 5 +---- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/static/app/components/modals/inviteMembersModal/useInviteModal.tsx b/static/app/components/modals/inviteMembersModal/useInviteModal.tsx index 3c5338b4534d90..ab23e5fbf5be75 100644 --- a/static/app/components/modals/inviteMembersModal/useInviteModal.tsx +++ b/static/app/components/modals/inviteMembersModal/useInviteModal.tsx @@ -29,9 +29,7 @@ function defaultInvite(): InviteRow { function canInvite(organization: Organization) { return ( organization.access?.includes('member:write') || - (organization.features.includes('members-invite-teammates') && - organization.allowMemberInvite && - organization.access?.includes('member:invite')) + (organization.allowMemberInvite && organization.access?.includes('member:invite')) ); } diff --git a/static/app/components/roleSelectControl.tsx b/static/app/components/roleSelectControl.tsx index 6f6222545701d8..d3b21790d7f707 100644 --- a/static/app/components/roleSelectControl.tsx +++ b/static/app/components/roleSelectControl.tsx @@ -25,9 +25,7 @@ type Props = Omit, 'onChange' | 'value'> & { function RoleSelectControl({roles, disableUnallowed, ...props}: Props) { const organization = useOrganization(); const isMemberInvite = - organization.features.includes('members-invite-teammates') && - organization.allowMemberInvite && - organization.access?.includes('member:invite'); + organization.allowMemberInvite && organization.access?.includes('member:invite'); return ( features.has('members-invite-teammates'), }, { name: 'allowMemberProjectCreation', diff --git a/static/app/views/settings/organizationMembers/organizationMemberRow.spec.tsx b/static/app/views/settings/organizationMembers/organizationMemberRow.spec.tsx index d8f14d3b112cff..443664d7ee1847 100644 --- a/static/app/views/settings/organizationMembers/organizationMemberRow.spec.tsx +++ b/static/app/views/settings/organizationMembers/organizationMemberRow.spec.tsx @@ -120,7 +120,6 @@ describe('OrganizationMemberRow', function () { it('has "Resend Invite" button if invite was sent from curr user and feature is on', function () { const org = OrganizationFixture({ - features: ['members-invite-teammates'], access: ['member:invite'], }); render(); @@ -131,7 +130,6 @@ describe('OrganizationMemberRow', function () { it('does not have "Resend Invite" button if invite was sent from other user and feature is on', function () { const org = OrganizationFixture({ - features: ['members-invite-teammates'], access: ['member:invite'], }); render( @@ -175,7 +173,6 @@ describe('OrganizationMemberRow', function () { it('has Remove button if invite was sent from curr user and feature is on', function () { const org = OrganizationFixture({ - features: ['members-invite-teammates'], access: ['member:invite'], }); render(); @@ -185,7 +182,6 @@ describe('OrganizationMemberRow', function () { it('has disabled Remove button if invite was sent from other user and feature is on', function () { const org = OrganizationFixture({ - features: ['members-invite-teammates'], access: ['member:invite'], }); render( diff --git a/static/app/views/settings/organizationMembers/organizationMemberRow.tsx b/static/app/views/settings/organizationMembers/organizationMemberRow.tsx index 9aa1bb0ac6805e..cc17d7b4351776 100644 --- a/static/app/views/settings/organizationMembers/organizationMemberRow.tsx +++ b/static/app/views/settings/organizationMembers/organizationMemberRow.tsx @@ -115,10 +115,7 @@ export default class OrganizationMemberRow extends PureComponent { const showRemoveButton = !isCurrentUser; const showLeaveButton = isCurrentUser; const isInviteFromCurrentUser = pending && inviterName === currentUser.name; - const canInvite = - organization.features?.includes('members-invite-teammates') && - organization.allowMemberInvite && - access.includes('member:invite'); + const canInvite = organization.allowMemberInvite && access.includes('member:invite'); // members can remove invites they sent if allowMemberInvite is true const canEditInvite = canInvite && isInviteFromCurrentUser; const canRemoveMember = From f5727d9a2f63e524e7b0ecfb05ed410b8fb0c8f4 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 31 Dec 2024 14:46:02 -0800 Subject: [PATCH 607/757] feat(devservices): Docker compose backend tests use devservices (#81941) This allows us to measure the stability of devservices for sentry backend tests. Changing the cadence of this scheduled job to hourly again, as we need more data here. Will check devinfra metrics to get a better sense of how well this is working in CI --- ...ckend.yml => test_devservices_backend.yml} | 85 +++++++------------ devservices/config.yml | 35 ++++++++ src/sentry/testutils/pytest/relay.py | 7 +- 3 files changed, 70 insertions(+), 57 deletions(-) rename .github/workflows/{test_docker_compose_backend.yml => test_devservices_backend.yml} (75%) diff --git a/.github/workflows/test_docker_compose_backend.yml b/.github/workflows/test_devservices_backend.yml similarity index 75% rename from .github/workflows/test_docker_compose_backend.yml rename to .github/workflows/test_devservices_backend.yml index 179b1efee17362..ec3f0ae5645eab 100644 --- a/.github/workflows/test_docker_compose_backend.yml +++ b/.github/workflows/test_devservices_backend.yml @@ -1,9 +1,8 @@ -name: test-docker-compose-backend +name: test-devservices-backend on: schedule: - - cron: '0 0 * * *' - + - cron: '0 * * * *' # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value concurrency: @@ -13,9 +12,11 @@ concurrency: # hack for https://github.com/actions/cache/issues/810#issuecomment-1222550359 env: SEGMENT_DOWNLOAD_TIMEOUT_MINS: 3 + USE_NEW_DEVSERVICES: 1 + IS_DEV: 1 jobs: - docker-compose-api-docs: + devservices-api-docs: name: api docs test runs-on: ubuntu-24.04 steps: @@ -31,9 +32,7 @@ jobs: id: setup - name: Bring up devservices - run: | - docker network create sentry - docker compose -f devservices/docker-compose-testing.yml up -d redis postgres snuba clickhouse + run: devservices up - name: Run API docs tests # install ts-node for ts build scripts to execute properly without potentially installing @@ -44,11 +43,9 @@ jobs: - name: Inspect failure if: failure() - run: | - docker compose -f devservices/docker-compose-testing.yml ps - docker compose -f devservices/docker-compose-testing.yml logs --tail 1000 + run: devservices logs - docker-compose-backend-test: + devservices-backend-test: name: backend test runs-on: ubuntu-24.04 timeout-minutes: 60 @@ -73,13 +70,11 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup sentry env + id: setup uses: ./.github/actions/test-setup-sentry-devservices - name: Bring up devservices - run: | - docker network create sentry - echo "BIGTABLE_EMULATOR_HOST=127.0.0.1:8086" >> $GITHUB_ENV - docker compose -f devservices/docker-compose-testing.yml up -d + run: devservices up --mode backend-ci - name: Run backend test (${{ steps.setup.outputs.matrix-instance-number }} of ${{ steps.setup.outputs.matrix-instance-total }}) run: | @@ -107,11 +102,9 @@ jobs: - name: Inspect failure if: failure() - run: | - docker compose -f devservices/docker-compose-testing.yml ps - docker compose -f devservices/docker-compose-testing.yml logs --tail 1000 + run: devservices logs - docker-compose-backend-migration-tests: + devservices-backend-migration-tests: name: backend migration tests runs-on: ubuntu-24.04 timeout-minutes: 30 @@ -127,9 +120,7 @@ jobs: id: setup - name: Bring up devservices - run: | - docker network create sentry - docker compose -f devservices/docker-compose-testing.yml up -d redis postgres snuba clickhouse + run: devservices up - name: run tests run: | @@ -146,11 +137,9 @@ jobs: - name: Inspect failure if: failure() - run: | - docker compose -f devservices/docker-compose-testing.yml ps - docker compose -f devservices/docker-compose-testing.yml logs --tail 1000 + run: devservices logs - docker-compose-cli: + devservices-cli: name: cli test runs-on: ubuntu-24.04 timeout-minutes: 10 @@ -165,9 +154,7 @@ jobs: id: setup - name: Bring up devservices - run: | - docker network create sentry - docker compose -f devservices/docker-compose-testing.yml up -d redis postgres + run: devservices up --mode migrations - name: Run test run: | @@ -184,11 +171,9 @@ jobs: - name: Inspect failure if: failure() - run: | - docker compose -f devservices/docker-compose-testing.yml ps - docker compose -f devservices/docker-compose-testing.yml logs --tail 1000 + run: devservices logs - docker-compose-migration: + devservices-migration: name: check migration runs-on: ubuntu-24.04 strategy: @@ -204,9 +189,7 @@ jobs: id: setup - name: Bring up devservices - run: | - docker network create sentry - docker compose -f devservices/docker-compose-testing.yml up -d redis postgres + run: devservices up --mode migrations - name: Migration & lockfile checks env: @@ -217,11 +200,9 @@ jobs: - name: Inspect failure if: failure() - run: | - docker compose -f devservices/docker-compose-testing.yml ps - docker compose -f devservices/docker-compose-testing.yml logs --tail 1000 + run: devservices logs - docker-compose-monolith-dbs: + devservices-monolith-dbs: name: monolith-dbs test runs-on: ubuntu-24.04 timeout-minutes: 20 @@ -236,9 +217,7 @@ jobs: id: setup - name: Bring up devservices - run: | - docker network create sentry - docker compose -f devservices/docker-compose-testing.yml up -d redis postgres + run: devservices up --mode migrations - name: Run test run: | @@ -265,24 +244,22 @@ jobs: - name: Inspect failure if: failure() - run: | - docker compose -f devservices/docker-compose-testing.yml ps - docker compose -f devservices/docker-compose-testing.yml logs --tail 1000 + run: devservices logs # This check runs once all dependent jobs have passed # It symbolizes that all required Backend checks have succesfully passed (Or skipped) # This step is the only required backend check - docker-compose-backend-required-check: + devservices-backend-required-check: needs: [ - docker-compose-api-docs, - docker-compose-backend-test, - docker-compose-backend-migration-tests, - docker-compose-cli, - docker-compose-migration, - docker-compose-monolith-dbs, + devservices-api-docs, + devservices-backend-test, + devservices-backend-migration-tests, + devservices-cli, + devservices-migration, + devservices-monolith-dbs, ] - name: Docker Compose Backend + name: Devservices Backend # This is necessary since a failed/skipped dependent job would cause this job to be skipped if: always() runs-on: ubuntu-24.04 diff --git a/devservices/config.yml b/devservices/config.yml index 37df3d3780a1b8..f4cf7a263b0160 100644 --- a/devservices/config.yml +++ b/devservices/config.yml @@ -25,6 +25,17 @@ x-sentry-service-config: repo_name: sentry-shared-redis branch: main repo_link: https://github.com/getsentry/sentry-shared-redis.git + symbolicator: + description: A symbolication service for native stacktraces and minidumps with symbol server support + remote: + repo_name: symbolicator + branch: master + repo_link: https://github.com/getsentry/symbolicator.git + mode: default + bigtable: + description: Bigtable emulator + redis-cluster: + description: Redis cluster used for testing chartcuterie: description: Chartcuterie is a service that generates charts remote: @@ -43,6 +54,7 @@ x-sentry-service-config: migrations: [postgres, redis] acceptance-ci: [postgres, snuba, chartcuterie] taskbroker: [snuba, postgres, relay, taskbroker] + backend-ci: [snuba, postgres, redis, bigtable, redis-cluster, symbolicator] services: postgres: @@ -76,6 +88,29 @@ services: labels: - orchestrator=devservices restart: unless-stopped + bigtable: + image: 'ghcr.io/getsentry/cbtemulator:d28ad6b63e461e8c05084b8c83f1c06627068c04' + ports: + - '127.0.0.1:8086:8086' + networks: + - devservices + extra_hosts: + - host.docker.internal:host-gateway + redis-cluster: + image: ghcr.io/getsentry/docker-redis-cluster:7.0.10 + ports: + - '127.0.0.1:7000:7000' + - '127.0.0.1:7001:7001' + - '127.0.0.1:7002:7002' + - '127.0.0.1:7003:7003' + - '127.0.0.1:7004:7004' + - '127.0.0.1:7005:7005' + networks: + - devservices + extra_hosts: + - host.docker.internal:host-gateway + environment: + - IP=0.0.0.0 networks: devservices: diff --git a/src/sentry/testutils/pytest/relay.py b/src/sentry/testutils/pytest/relay.py index 76dff65c2da813..5bff85dcf2cd53 100644 --- a/src/sentry/testutils/pytest/relay.py +++ b/src/sentry/testutils/pytest/relay.py @@ -69,6 +69,7 @@ def relay_server_setup(live_server, tmpdir_factory): relay_port = 33331 redis_db = TEST_REDIS_DB + use_new_dev_services = environ.get("USE_NEW_DEVSERVICES", "0") == "1" from sentry.relay import projectconfig_cache from sentry.relay.projectconfig_cache.redis import RedisProjectConfigCache @@ -80,8 +81,8 @@ def relay_server_setup(live_server, tmpdir_factory): template_vars = { "SENTRY_HOST": f"http://host.docker.internal:{port}/", "RELAY_PORT": relay_port, - "KAFKA_HOST": "sentry_kafka", - "REDIS_HOST": "sentry_redis", + "KAFKA_HOST": "kafka-kafka-1" if use_new_dev_services else "sentry_kafka", + "REDIS_HOST": "redis-redis-1" if use_new_dev_services else "sentry_redis", "REDIS_DB": redis_db, } @@ -106,7 +107,7 @@ def relay_server_setup(live_server, tmpdir_factory): options = { "image": RELAY_TEST_IMAGE, "ports": {"%s/tcp" % relay_port: relay_port}, - "network": "sentry", + "network": "devservices" if use_new_dev_services else "sentry", "detach": True, "name": container_name, "volumes": {config_path: {"bind": "/etc/relay"}}, From 707f353d2d1933fbf951478c414838f9e5956b38 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 31 Dec 2024 15:54:03 -0800 Subject: [PATCH 608/757] chore(devservices): Bump devservices to 1.0.8 (#82799) picks up https://github.com/getsentry/devservices/releases/tag/1.0.8 --- requirements-dev-frozen.txt | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev-frozen.txt b/requirements-dev-frozen.txt index 89a84c5b48d333..18227cc5072c89 100644 --- a/requirements-dev-frozen.txt +++ b/requirements-dev-frozen.txt @@ -37,7 +37,7 @@ cryptography==43.0.1 cssselect==1.0.3 cssutils==2.9.0 datadog==0.49.1 -devservices==1.0.7 +devservices==1.0.8 distlib==0.3.8 distro==1.8.0 django==5.1.4 diff --git a/requirements-dev.txt b/requirements-dev.txt index 5293cdc9317b38..0748993df7b1a1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ --index-url https://pypi.devinfra.sentry.io/simple sentry-devenv>=1.14.2 -devservices>=1.0.7 +devservices>=1.0.8 covdefaults>=2.3.0 sentry-covdefaults-disable-branch-coverage>=1.0.2 From cf8c5bdba70968e4e68a50cb3576ad0fd27761a7 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 2 Jan 2025 08:48:31 -0500 Subject: [PATCH 609/757] ref(tsc) fix nouncheckedindexedaccess (#82791) Precursor to enabling ts noUncheckedIndexedAccess rule. The fixes here dont impact the runtime and only add type assertions. I will followup with a PR to do the same for getsentry before enabling the flag --- build-utils/sentry-instrumentation.ts | 4 +- eslint.config.mjs | 1 + static/app/actionCreators/dashboards.tsx | 14 +-- static/app/actionCreators/members.tsx | 2 +- static/app/actionCreators/monitors.tsx | 2 +- static/app/actionCreators/organizations.tsx | 2 +- static/app/actionCreators/pageFilters.tsx | 2 +- static/app/actionCreators/projects.spec.tsx | 4 +- static/app/bootstrap/initializeSdk.tsx | 6 +- static/app/chartcuterie/discover.tsx | 2 +- static/app/components/acl/feature.tsx | 6 +- .../app/components/arithmeticInput/parser.tsx | 4 +- .../app/components/assigneeBadge.stories.tsx | 2 +- .../assigneeSelectorDropdown.spec.tsx | 2 +- .../components/assigneeSelectorDropdown.tsx | 17 ++-- .../app/components/assistant/guideAnchor.tsx | 2 +- static/app/components/autoComplete.spec.tsx | 4 +- .../app/components/avatar/avatarList.spec.tsx | 24 ++--- static/app/components/avatar/avatarList.tsx | 4 +- static/app/components/avatar/index.spec.tsx | 2 +- .../avatar/suggestedAvatarStack.tsx | 4 +- static/app/components/breadcrumbs.tsx | 2 +- static/app/components/carousel.spec.tsx | 12 +-- static/app/components/carousel.tsx | 4 +- static/app/components/charts/barChartZoom.tsx | 4 +- static/app/components/charts/baseChart.tsx | 2 +- static/app/components/charts/chartZoom.tsx | 2 +- .../charts/eventsAreaChart.spec.tsx | 2 +- static/app/components/charts/eventsChart.tsx | 4 +- .../app/components/charts/eventsRequest.tsx | 8 +- .../components/charts/intervalSelector.tsx | 6 +- static/app/components/charts/miniBarChart.tsx | 2 +- .../components/charts/percentageAreaChart.tsx | 4 +- static/app/components/charts/pieChart.tsx | 10 +- .../app/components/charts/releaseSeries.tsx | 2 +- static/app/components/charts/utils.tsx | 8 +- static/app/components/chevron.tsx | 2 +- static/app/components/collapsible.spec.tsx | 2 +- .../app/components/compactSelect/control.tsx | 4 +- static/app/components/compactSelect/list.tsx | 2 +- static/app/components/compactSelect/utils.tsx | 10 +- .../components/contextPickerModal.spec.tsx | 2 +- static/app/components/contextPickerModal.tsx | 14 +-- .../deprecatedAssigneeSelector.spec.tsx | 4 +- .../components/deprecatedAssigneeSelector.tsx | 7 +- .../deprecatedAssigneeSelectorDropdown.tsx | 8 +- .../deprecatedSmartSearchBar/index.tsx | 16 ++-- .../searchDropdown.tsx | 12 +-- .../deprecatedSmartSearchBar/utils.tsx | 2 +- static/app/components/deviceName.tsx | 2 +- .../featureFlags/featureFlagsPanel.tsx | 2 +- .../components/infiniteListItems.tsx | 2 +- .../components/releases/releasesPanel.tsx | 12 +-- .../components/discover/transactionsTable.tsx | 4 +- .../draggableTabs/draggableTabList.tsx | 4 +- .../components/dropdownAutoComplete/list.tsx | 6 +- .../components/events/autofix/autofixDiff.tsx | 10 +- .../autofixMessageBox.analytics.spec.tsx | 2 +- .../events/autofix/autofixRootCause.tsx | 2 +- .../events/autofix/autofixSteps.tsx | 34 +++---- .../breadcrumbsDataSection.spec.tsx | 12 +-- .../breadcrumbs/breadcrumbsTimeline.tsx | 2 +- .../events/breadcrumbs/testUtils.tsx | 2 +- .../components/events/breadcrumbs/utils.tsx | 4 +- .../events/contexts/contextCard.spec.tsx | 2 +- .../components/events/contexts/utils.spec.tsx | 8 +- .../app/components/events/contexts/utils.tsx | 6 +- .../app/components/events/errorItem.spec.tsx | 2 +- .../events/eventAttachments.spec.tsx | 10 +- static/app/components/events/eventEntries.tsx | 4 +- .../events/eventExtraData/index.spec.tsx | 2 +- .../aggregateSpanDiff.tsx | 8 +- .../eventComparison/eventDisplay.tsx | 6 +- .../eventThroughput.tsx | 6 +- .../spanOpBreakdown.tsx | 4 +- .../events/eventTags/eventTagsTree.tsx | 2 +- .../eventTagsAndScreenshot/index.spec.tsx | 10 +- .../screenshot/modal.tsx | 2 +- .../screenshot/screenshotDataSection.tsx | 2 +- static/app/components/events/eventVitals.tsx | 2 +- .../eventFeatureFlagList.spec.tsx | 16 ++-- .../featureFlags/featureFlagDrawer.spec.tsx | 22 ++--- .../featureFlagOnboardingSidebar.tsx | 4 +- .../groupingInfo/groupingInfoSection.spec.tsx | 2 +- .../highlights/editHighlightsModal.spec.tsx | 4 +- .../highlights/highlightsDataSection.spec.tsx | 2 +- .../highlights/highlightsDataSection.tsx | 2 +- .../events/highlights/util.spec.tsx | 12 +-- .../app/components/events/highlights/util.tsx | 2 +- .../events/interfaces/analyzeFrames.tsx | 4 +- .../interfaces/breadcrumbs/breadcrumbs.tsx | 6 +- .../events/interfaces/breadcrumbs/index.tsx | 18 ++-- .../exception/actionableItems.tsx | 2 +- .../crashContent/exception/content.spec.tsx | 14 +-- .../crashContent/exception/content.tsx | 2 +- .../exception/relatedExceptions.spec.tsx | 16 ++-- .../exception/sourceMapDebug.spec.tsx | 2 +- .../exception/stackTrace.spec.tsx | 8 +- .../exception/useSourceMapDebug.spec.tsx | 10 +- .../crashContent/exception/utils.tsx | 4 +- .../crashContent/stackTrace/content.spec.tsx | 40 ++++---- .../crashContent/stackTrace/content.tsx | 12 +-- .../stackTrace/nativeContent.spec.tsx | 28 +++--- .../crashContent/stackTrace/nativeContent.tsx | 8 +- .../interfaces/crons/cronTimelineSection.tsx | 2 +- .../events/interfaces/csp/index.spec.tsx | 2 +- .../events/interfaces/debugMeta/index.tsx | 8 +- .../events/interfaces/frame/context.tsx | 8 +- .../events/interfaces/frame/contextLine.tsx | 2 +- .../interfaces/frame/frameVariables.spec.tsx | 6 +- .../interfaces/frame/stacktraceLink.tsx | 6 +- .../interfaces/frame/stacktraceLinkModal.tsx | 10 +- .../frame/usePrismTokensSourceContext.tsx | 2 +- .../interfaces/keyValueList/index.spec.tsx | 16 ++-- .../performance/spanEvidenceKeyValueList.tsx | 39 ++++---- .../events/interfaces/request/index.spec.tsx | 20 ++-- .../interfaces/searchBarAction.spec.tsx | 4 +- .../interfaces/spans/measurementsPanel.tsx | 6 +- .../spans/newTraceDetailsSpanBar.tsx | 4 +- .../spans/newTraceDetailsSpanDetails.tsx | 2 +- .../spans/newTraceDetailsSpanTree.tsx | 8 +- .../events/interfaces/spans/spanBar.tsx | 2 +- .../events/interfaces/spans/spanDetail.tsx | 2 +- .../interfaces/spans/spanProfileDetails.tsx | 12 +-- .../interfaces/spans/spanSiblingGroupBar.tsx | 8 +- .../events/interfaces/spans/spanTree.tsx | 6 +- .../interfaces/spans/spanTreeModel.spec.tsx | 32 +++---- .../events/interfaces/spans/spanTreeModel.tsx | 30 +++--- .../interfaces/spans/traceView.spec.tsx | 4 +- .../events/interfaces/spans/utils.tsx | 20 ++-- .../interfaces/spans/waterfallModel.spec.tsx | 94 +++++++++---------- .../interfaces/spans/waterfallModel.tsx | 4 +- .../events/interfaces/threads.spec.tsx | 28 +++--- .../threadSelector/getThreadException.tsx | 4 +- .../components/events/interfaces/utils.tsx | 4 +- static/app/components/events/opsBreakdown.tsx | 2 +- static/app/components/events/searchBar.tsx | 10 +- .../events/viewHierarchy/index.spec.tsx | 4 +- .../components/events/viewHierarchy/utils.tsx | 10 +- .../events/viewHierarchy/wireframe.tsx | 16 ++-- .../feedback/feedbackOnboarding/sidebar.tsx | 4 +- .../components/feedback/feedbackSearch.tsx | 4 +- .../feedback/list/issueTrackingSignals.tsx | 2 +- .../forms/controls/selectControl.tsx | 2 +- .../forms/fields/choiceMapperField.tsx | 2 +- .../forms/fields/projectMapperField.spec.tsx | 2 +- .../forms/fields/projectMapperField.tsx | 2 +- .../sentryMemberTeamSelectorField.spec.tsx | 8 +- static/app/components/forms/jsonForm.spec.tsx | 24 ++--- static/app/components/forms/model.tsx | 2 +- static/app/components/gridEditable/index.tsx | 14 +-- static/app/components/group/assignedTo.tsx | 2 +- .../externalIssueActions.tsx | 10 +- .../hooks/useIntegrationExternalIssues.tsx | 10 +- .../components/group/releaseChart.spec.tsx | 4 +- static/app/components/group/releaseChart.tsx | 22 ++--- .../group/suggestedOwnerHovercard.tsx | 4 +- .../group/tagDistributionMeter.spec.tsx | 4 +- .../app/components/group/tagFacets/index.tsx | 10 +- .../tagFacets/tagFacetsDistributionMeter.tsx | 10 +- .../components/guidedSteps/guidedSteps.tsx | 2 +- .../app/components/idBadge/index.stories.tsx | 8 +- .../app/components/inactivePlugins.spec.tsx | 2 +- static/app/components/lastCommit.tsx | 2 +- static/app/components/letterAvatar.tsx | 6 +- static/app/components/metrics/chart/chart.tsx | 8 +- .../components/metrics/chart/useFocusArea.tsx | 16 ++-- .../metrics/chart/useMetricChartSamples.tsx | 5 +- .../metrics/chart/useMetricReleases.tsx | 2 +- static/app/components/metrics/chart/utils.tsx | 4 +- .../components/modals/commandPalette.spec.tsx | 2 +- .../modals/featureTourModal.spec.tsx | 6 +- .../components/modals/featureTourModal.tsx | 3 +- .../modals/inviteMembersModal/index.spec.tsx | 14 +-- .../modals/inviteMembersModal/index.tsx | 2 +- .../inviteMembersModal/inviteRowControl.tsx | 2 +- .../inviteRowControlNew.tsx | 2 +- .../inviteMembersModal/useInviteModal.tsx | 10 +- .../inviteMissingMembersModal/index.spec.tsx | 4 +- .../inviteMissingMembersModal/index.tsx | 12 +-- .../modals/metricWidgetViewerModal.tsx | 8 +- .../metricWidgetViewerModal/queries.tsx | 4 +- .../metricWidgetViewerModal/visualization.tsx | 2 +- .../widgetBuilder/addToDashboardModal.tsx | 8 +- .../components/modals/widgetViewerModal.tsx | 32 +++---- .../widgetViewerTableCell.tsx | 8 +- static/app/components/nav/utils.tsx | 2 +- .../forms/onCallServiceForm.tsx | 4 +- .../notificationActionManager.spec.tsx | 28 +++--- .../notificationActionManager.tsx | 6 +- .../onboarding/frameworkSuggestionModal.tsx | 6 +- .../onboardingCodeSnippet.spec.tsx | 6 +- .../onboarding/gettingStartedDoc/step.tsx | 4 +- .../utils/feedbackOnboarding.tsx | 12 +-- .../onboarding/platformOptionsControl.tsx | 6 +- .../onboardingWizard/newSidebar.spec.tsx | 12 +-- .../app/components/onboardingWizard/task.tsx | 2 +- .../onboardingWizard/taskConfig.tsx | 16 ++-- .../environmentPageFilter/trigger.tsx | 2 +- .../components/organizations/hybridFilter.tsx | 4 +- .../organizations/projectPageFilter/index.tsx | 4 +- .../projectPageFilter/trigger.tsx | 2 +- .../app/components/performance/searchBar.tsx | 4 +- .../performance/waterfall/utils.spec.tsx | 2 +- .../performance/waterfall/utils.tsx | 20 ++-- .../performanceOnboarding/sidebar.tsx | 2 +- .../app/components/pickProjectToContinue.tsx | 2 +- static/app/components/platformPicker.spec.tsx | 2 +- static/app/components/platformPicker.tsx | 4 +- .../flamegraph/continuousFlamegraph.tsx | 22 ++--- .../profiling/flamegraph/flamegraph.tsx | 10 +- .../flamegraph/flamegraphChartTooltip.tsx | 2 +- .../flamegraphDrawer/flamegraphDrawer.tsx | 2 +- .../profileDragDropImport.tsx | 2 +- .../flamegraph/flamegraphPreview.tsx | 2 +- .../flamegraphToolbar/flamegraphSearch.tsx | 2 +- .../projects/missingProjectMembership.tsx | 4 +- .../app/components/quickTrace/index.spec.tsx | 2 +- static/app/components/quickTrace/index.tsx | 14 +-- .../breadcrumbs/breadcrumbItem.spec.tsx | 4 +- .../breadcrumbs/replayTimelineEvents.tsx | 6 +- .../replays/canvasReplayerPlugin.tsx | 4 +- .../replays/header/errorCounts.spec.tsx | 4 +- static/app/components/replays/utils.spec.tsx | 8 +- static/app/components/replays/utils.tsx | 16 ++-- .../app/components/replays/videoReplayer.tsx | 12 +-- .../platformOptionDropdown.tsx | 4 +- .../components/replaysOnboarding/sidebar.tsx | 6 +- .../components/replaysOnboarding/utils.tsx | 2 +- static/app/components/resultGrid.tsx | 4 +- static/app/components/scrollCarousel.tsx | 6 +- .../components/search/sources/apiSource.tsx | 38 ++++---- .../components/search/sources/helpSource.tsx | 4 +- .../app/components/search/sources/index.tsx | 2 +- .../hooks/useQueryBuilderState.tsx | 8 +- .../hooks/useSelectOnDrag.tsx | 4 +- .../searchQueryBuilder/index.spec.tsx | 30 +++--- .../tokens/filter/filter.tsx | 4 +- .../tokens/filter/parametersCombobox.tsx | 6 +- .../filter/parsers/string/parser.spec.tsx | 42 ++++----- .../tokens/filter/valueListBox.tsx | 2 +- .../tokens/filter/valueSuggestions/date.tsx | 2 +- .../tokens/filterKeyListBox/index.tsx | 4 +- .../filterKeyListBox/useFilterKeyListBox.tsx | 8 +- .../tokens/useSortedFilterKeyItems.tsx | 2 +- .../components/searchQueryBuilder/utils.tsx | 4 +- .../app/components/searchSyntax/evaluator.tsx | 16 ++-- static/app/components/searchSyntax/parser.tsx | 2 +- static/app/components/sidebar/index.tsx | 2 +- static/app/components/slider/index.tsx | 28 +++--- static/app/components/slider/thumb.tsx | 2 +- static/app/components/stream/group.tsx | 6 +- .../recursiveStructuredData.tsx | 6 +- static/app/components/tabs/index.spec.tsx | 18 ++-- .../app/components/tagDistributionMeter.tsx | 6 +- static/app/components/teamRoleSelect.tsx | 2 +- static/app/components/teamSelector.spec.tsx | 4 +- .../components/timeRangeSelector/utils.tsx | 16 ++-- static/app/components/truncate.tsx | 2 +- static/app/components/updatedEmptyState.tsx | 6 +- static/app/components/userMisery.tsx | 2 +- static/app/constants/chartPalette.tsx | 2 +- .../gettingStartedDocs/javascript/astro.tsx | 2 +- static/app/locale.tsx | 2 +- static/app/plugins/registry.tsx | 2 +- static/app/routes.spec.tsx | 2 +- static/app/routes.tsx | 2 +- static/app/stores/alertStore.spec.tsx | 6 +- static/app/stores/alertStore.tsx | 2 +- static/app/stores/groupStore.spec.tsx | 2 +- static/app/stores/groupStore.tsx | 10 +- static/app/stores/groupingStore.tsx | 6 +- static/app/stores/guideStore.spec.tsx | 2 +- static/app/stores/guideStore.tsx | 4 +- static/app/stores/projectsStatsStore.tsx | 2 +- static/app/stores/tagStore.tsx | 4 +- .../app/utils/api/useFetchSequentialPages.tsx | 2 +- static/app/utils/consolidatedScopes.tsx | 4 +- static/app/utils/cursor.tsx | 6 +- static/app/utils/cursorPoller.tsx | 2 +- .../customMeasurementsProvider.tsx | 6 +- static/app/utils/dates.tsx | 6 +- static/app/utils/discover/arrayValue.tsx | 2 +- static/app/utils/discover/charts.spec.tsx | 14 +-- .../app/utils/discover/discoverQuery.spec.tsx | 4 +- static/app/utils/discover/eventView.spec.tsx | 28 +++--- static/app/utils/discover/eventView.tsx | 24 ++--- static/app/utils/discover/fields.tsx | 20 ++-- .../discover/teamKeyTransactionField.spec.tsx | 16 ++-- .../displayReprocessEventAction.spec.tsx | 12 +-- static/app/utils/duration/getPeriod.tsx | 2 +- .../utils/duration/intervalToMilliseconds.tsx | 2 +- .../utils/duration/parseClockToSeconds.tsx | 4 +- .../app/utils/duration/parsePeriodToHours.tsx | 2 +- static/app/utils/extractSlug.tsx | 4 +- static/app/utils/featureFlags.ts | 4 +- static/app/utils/fields/index.ts | 4 +- static/app/utils/getErrorDebugIds.ts | 4 +- static/app/utils/getMinMax.tsx | 4 +- static/app/utils/getProjectsByTeams.tsx | 2 +- static/app/utils/highlightFuseMatches.tsx | 4 +- static/app/utils/marked.spec.tsx | 2 +- static/app/utils/marked.tsx | 5 +- .../utils/metrics/dashboardImport.spec.tsx | 12 +-- static/app/utils/metrics/dashboardImport.tsx | 2 +- static/app/utils/metrics/mri.tsx | 4 +- static/app/utils/metrics/useBlockMetric.tsx | 2 +- .../useCardinalityLimitedMetricVolume.tsx | 2 +- static/app/utils/parseLinkHeader.tsx | 6 +- .../contexts/genericQueryBatcher.tsx | 18 ++-- .../app/utils/performance/histogram/index.tsx | 4 +- .../app/utils/performance/histogram/utils.tsx | 2 +- static/app/utils/prism.tsx | 2 +- .../utils/profiling/canvasScheduler.spec.tsx | 4 +- static/app/utils/profiling/colors/utils.tsx | 52 +++++----- .../profiling/differentialFlamegraph.spec.tsx | 4 +- .../profiling/differentialFlamegraph.tsx | 14 +-- .../profiling/filterFlamegraphTree.spec.tsx | 32 +++---- .../utils/profiling/filterFlamegraphTree.tsx | 2 +- .../app/utils/profiling/flamegraph.spec.tsx | 22 ++--- static/app/utils/profiling/flamegraph.ts | 18 ++-- .../profiling/flamegraph/flamegraphTheme.tsx | 12 +-- .../app/utils/profiling/flamegraphChart.tsx | 18 ++-- static/app/utils/profiling/frame.tsx | 2 +- static/app/utils/profiling/fzf/fzf.ts | 16 ++-- static/app/utils/profiling/gl/utils.spec.tsx | 8 +- static/app/utils/profiling/gl/utils.ts | 50 +++++----- .../utils/profiling/hooks/useContextMenu.tsx | 4 +- .../hooks/useCurrentProjectFromRouteParam.tsx | 2 +- .../hooks/useDifferentialFlamegraphModel.tsx | 4 +- .../VirtualizedTree.spec.tsx | 22 ++--- .../useVirtualizedTree/VirtualizedTree.tsx | 16 ++-- .../VirtualizedTreeNode.tsx | 6 +- .../useVirtualizedTree.spec.tsx | 34 +++---- .../useVirtualizedTree/useVirtualizedTree.tsx | 34 ++++--- .../virtualizedTreeUtils.tsx | 2 +- static/app/utils/profiling/hooks/utils.tsx | 2 +- .../app/utils/profiling/jsSelfProfiling.tsx | 2 +- .../profiling/profile/continuousProfile.tsx | 18 ++-- .../profiling/profile/eventedProfile.spec.tsx | 26 ++--- .../profiling/profile/eventedProfile.tsx | 8 +- .../utils/profiling/profile/importProfile.tsx | 16 ++-- .../profiling/profile/jsSelfProfile.spec.tsx | 18 ++-- .../utils/profiling/profile/jsSelfProfile.tsx | 30 +++--- .../app/utils/profiling/profile/profile.tsx | 8 +- .../profiling/profile/sampledProfile.spec.tsx | 60 ++++++------ .../profiling/profile/sampledProfile.tsx | 45 ++++----- .../profile/sentrySampledProfile.spec.tsx | 8 +- .../profile/sentrySampledProfile.tsx | 24 ++--- static/app/utils/profiling/profile/utils.tsx | 18 ++-- .../profiling/renderers/UIFramesRenderer.tsx | 2 +- .../profiling/renderers/chartRenderer.tsx | 32 +++---- .../renderers/flamegraphRenderer.tsx | 2 +- .../renderers/flamegraphRenderer2D.tsx | 6 +- .../renderers/flamegraphRendererDOM.tsx | 4 +- .../flamegraphRendererWebGL.spec.tsx | 6 +- .../renderers/flamegraphRendererWebGL.tsx | 4 +- .../renderers/flamegraphTextRenderer.tsx | 4 +- .../profiling/renderers/gridRenderer.tsx | 4 +- .../renderers/selectedFrameRenderer.tsx | 2 +- .../profiling/renderers/spansRenderer.tsx | 14 +-- .../profiling/renderers/spansTextRenderer.tsx | 4 +- .../renderers/uiFramesRendererWebGL.tsx | 2 +- static/app/utils/profiling/spanChart.spec.tsx | 22 ++--- static/app/utils/profiling/spanChart.tsx | 2 +- static/app/utils/profiling/spanTree.spec.tsx | 26 ++--- static/app/utils/profiling/spanTree.tsx | 10 +- static/app/utils/profiling/uiFrames.spec.tsx | 2 +- static/app/utils/profiling/uiFrames.tsx | 4 +- .../project/useSelectedProjectsHaveField.tsx | 2 +- static/app/utils/projects.tsx | 4 +- .../app/utils/reactRouter6Compat/location.tsx | 4 +- static/app/utils/recreateRoute.spec.tsx | 24 ++--- static/app/utils/replays/extractDomNodes.tsx | 2 +- static/app/utils/replays/fetchReplayList.tsx | 2 +- .../replays/getCurrentScreenName.spec.tsx | 3 +- .../app/utils/replays/getCurrentUrl.spec.tsx | 3 +- .../utils/replays/getDiffTimestamps.spec.tsx | 2 +- .../app/utils/replays/getReplayEvent.spec.tsx | 14 +-- .../replays/hooks/useExtractDiffMutations.tsx | 4 +- .../replays/hooks/useLoadReplayReader.tsx | 4 +- .../replays/hooks/useReplayData.spec.tsx | 10 +- static/app/utils/replays/replayReader.tsx | 2 +- static/app/utils/replays/replayerStepper.tsx | 2 +- .../app/utils/requestError/sanitizePath.tsx | 2 +- static/app/utils/sessions.spec.tsx | 4 +- static/app/utils/sessions.tsx | 28 +++--- static/app/utils/string/middleEllipsis.tsx | 6 +- static/app/utils/theme.tsx | 22 ++--- static/app/utils/tokenizeSearch.tsx | 14 +-- static/app/utils/touch.tsx | 2 +- static/app/utils/url/normalizeUrl.spec.tsx | 16 ++-- static/app/utils/useCombinedReducer.tsx | 2 +- static/app/utils/useExperiment.tsx | 2 +- static/app/utils/useHotkeys.spec.tsx | 22 ++--- static/app/utils/useIsStuck.tsx | 2 +- static/app/utils/useMembers.spec.tsx | 2 +- static/app/utils/useMembers.tsx | 4 +- static/app/utils/useOverlay.tsx | 10 +- static/app/utils/usePrismTokens.tsx | 6 +- static/app/utils/useProjects.spec.tsx | 2 +- static/app/utils/useProjects.tsx | 4 +- static/app/utils/useTeams.spec.tsx | 2 +- static/app/utils/useTeams.tsx | 4 +- static/app/utils/useTeamsById.spec.tsx | 4 +- static/app/utils/utils.spec.tsx | 2 +- .../views/admin/adminOverview/apiChart.tsx | 6 +- .../views/admin/adminOverview/eventChart.tsx | 20 ++-- static/app/views/admin/adminSettings.tsx | 2 +- .../app/views/admin/installWizard/index.tsx | 8 +- static/app/views/admin/options.tsx | 2 +- static/app/views/alerts/create.spec.tsx | 24 ++--- .../alerts/list/incidents/index.spec.tsx | 6 +- .../app/views/alerts/list/incidents/row.tsx | 2 +- .../rules/alertLastIncidentActivationInfo.tsx | 2 +- .../alerts/list/rules/alertRulesList.spec.tsx | 20 ++-- .../alerts/list/rules/combinedAlertBadge.tsx | 2 +- static/app/views/alerts/list/rules/row.tsx | 4 +- .../views/alerts/rules/issue/index.spec.tsx | 2 +- static/app/views/alerts/rules/issue/index.tsx | 20 ++-- .../views/alerts/rules/issue/ruleNodeList.tsx | 2 +- .../rules/issue/sentryAppRuleModal.spec.tsx | 10 +- .../issue/setupMessagingIntegrationButton.tsx | 4 +- .../views/alerts/rules/metric/constants.tsx | 4 +- .../alerts/rules/metric/details/index.tsx | 2 +- .../rules/metric/details/metricActivity.tsx | 2 +- .../rules/metric/details/metricChart.tsx | 10 +- .../metric/details/metricChartOption.tsx | 24 ++--- .../alerts/rules/metric/details/sidebar.tsx | 2 +- .../alerts/rules/metric/duplicate.spec.tsx | 2 +- .../alerts/rules/metric/metricRulePresets.tsx | 2 +- .../views/alerts/rules/metric/mriField.tsx | 2 +- .../rules/metric/ruleConditionsForm.spec.tsx | 2 +- .../rules/metric/ruleConditionsForm.tsx | 4 +- .../alerts/rules/metric/ruleForm.spec.tsx | 4 +- .../views/alerts/rules/metric/ruleForm.tsx | 8 +- .../metric/triggers/actionsPanel/index.tsx | 34 +++---- .../rules/metric/triggers/chart/index.tsx | 12 +-- .../metric/triggers/chart/thresholdsChart.tsx | 4 +- .../alerts/rules/metric/triggers/index.tsx | 4 +- .../alerts/rules/uptime/httpSnippet.spec.tsx | 2 +- .../rules/uptime/uptimeHeadersField.tsx | 12 ++- .../alerts/utils/getComparisonMarkLines.tsx | 8 +- static/app/views/alerts/utils/index.tsx | 8 +- static/app/views/dashboards/dashboard.tsx | 12 +-- .../datasetConfig/errorsAndTransactions.tsx | 10 +- .../dashboards/datasetConfig/releases.tsx | 2 +- .../utils/getSeriesRequestData.tsx | 2 +- static/app/views/dashboards/detail.spec.tsx | 14 +-- static/app/views/dashboards/detail.tsx | 2 +- static/app/views/dashboards/layoutUtils.tsx | 2 +- .../dashboards/manage/dashboardGrid.spec.tsx | 14 ++- .../dashboards/manage/dashboardTable.spec.tsx | 10 +- static/app/views/dashboards/manage/index.tsx | 6 +- .../views/dashboards/metrics/bigNumber.tsx | 4 +- .../views/dashboards/metrics/table.spec.tsx | 12 +-- static/app/views/dashboards/metrics/table.tsx | 6 +- static/app/views/dashboards/metrics/utils.tsx | 8 +- .../views/dashboards/metrics/widgetCard.tsx | 2 +- static/app/views/dashboards/utils.spec.tsx | 4 +- static/app/views/dashboards/utils.tsx | 14 +-- .../dashboards/utils/getWidgetExploreUrl.tsx | 6 +- .../buildSteps/columnsStep/columnFields.tsx | 2 +- .../buildSteps/filterResultsStep/index.tsx | 6 +- .../groupByStep/groupBySelector.tsx | 10 +- .../buildSteps/sortByStep/index.tsx | 10 +- .../components/groupBySelector.spec.tsx | 2 +- .../components/queryFilterBuilder.tsx | 4 +- .../components/sortBySelector.tsx | 10 +- .../widgetBuilder/components/typeSelector.tsx | 2 +- .../components/visualize.spec.tsx | 2 +- .../widgetBuilder/components/visualize.tsx | 22 +++-- .../components/widgetBuilderSlideout.tsx | 2 +- .../widgetBuilder/issueWidget/utils.tsx | 2 +- .../views/dashboards/widgetBuilder/utils.tsx | 10 +- .../convertBuilderStateToWidget.spec.tsx | 18 ++-- .../utils/convertBuilderStateToWidget.ts | 2 +- .../widgetBuilder/widgetBuilder.spec.tsx | 24 ++--- .../widgetBuilder/widgetBuilder.tsx | 52 +++++----- .../widgetBuilderDataset.spec.tsx | 20 ++-- .../widgetBuilderSortBy.spec.tsx | 36 +++---- .../widgetBuilder/widgetLibrary/index.tsx | 2 +- .../app/views/dashboards/widgetCard/chart.tsx | 19 ++-- .../widgetCard/genericWidgetQueries.tsx | 10 +- .../dashboards/widgetCard/index.spec.tsx | 20 ++-- .../app/views/dashboards/widgetCard/index.tsx | 10 +- .../widgetCard/issueWidgetCard.spec.tsx | 2 +- .../dashboards/widgetCard/issueWidgetCard.tsx | 4 +- .../widgetCard/releaseWidgetQueries.spec.tsx | 4 +- .../widgetCard/releaseWidgetQueries.tsx | 28 +++--- .../app/views/dashboards/widgetCard/utils.tsx | 2 +- .../widgetCard/widgetCardChartContainer.tsx | 2 +- .../widgetCard/widgetQueries.spec.tsx | 10 +- .../dashboards/widgetLegendSelectionState.tsx | 4 +- .../areaChartWidgetVisualization.tsx | 2 +- .../widgets/common/widgetFrame.spec.tsx | 2 +- .../dashboards/widgets/common/widgetFrame.tsx | 12 +-- .../lineChartWidgetVisualization.tsx | 2 +- static/app/views/discover/chartFooter.tsx | 2 +- .../views/discover/eventDetails/content.tsx | 2 +- .../discover/eventDetails/linkedIssue.tsx | 6 +- static/app/views/discover/landing.tsx | 4 +- static/app/views/discover/miniGraph.tsx | 2 +- static/app/views/discover/queryList.spec.tsx | 2 +- static/app/views/discover/results.spec.tsx | 4 +- static/app/views/discover/resultsChart.tsx | 2 +- .../savedQuery/datasetSelectorTabs.tsx | 2 +- .../discover/table/arithmeticInput.spec.tsx | 10 +- .../views/discover/table/arithmeticInput.tsx | 6 +- .../views/discover/table/cellAction.spec.tsx | 2 +- .../app/views/discover/table/cellAction.tsx | 4 +- .../discover/table/columnEditCollection.tsx | 22 ++--- .../discover/table/columnEditModal.spec.tsx | 66 ++++++------- static/app/views/discover/table/index.tsx | 2 +- .../views/discover/table/tableView.spec.tsx | 14 +-- static/app/views/discover/table/tableView.tsx | 6 +- static/app/views/discover/tags.tsx | 2 +- static/app/views/discover/utils.spec.tsx | 4 +- static/app/views/discover/utils.tsx | 16 ++-- static/app/views/explore/charts/index.tsx | 2 +- .../contexts/pageParamsContext/sortBys.tsx | 2 +- .../views/explore/hooks/useAddToDashboard.tsx | 2 +- .../views/explore/hooks/useChartInterval.tsx | 2 +- .../explore/hooks/useDragNDropColumns.tsx | 2 +- .../views/explore/tables/aggregatesTable.tsx | 4 +- .../explore/tables/columnEditorModal.spec.tsx | 28 +++--- .../explore/tables/fieldRenderer.spec.tsx | 10 +- .../app/views/explore/tables/spansTable.tsx | 2 +- .../explore/tables/tracesTable/index.tsx | 2 +- .../explore/tables/tracesTable/utils.tsx | 2 +- .../app/views/explore/toolbar/index.spec.tsx | 8 +- .../explore/toolbar/toolbarVisualize.tsx | 10 +- .../common/queries/useResourcesQuery.ts | 6 +- .../resources/components/sampleImages.tsx | 10 +- .../tables/resourceSummaryTable.tsx | 4 +- .../queries/useResourcePagesQuery.ts | 2 +- .../resources/utils/useResourceSort.ts | 2 +- .../resources/views/resourceSummaryPage.tsx | 9 +- .../views/resourcesLandingPage.spec.tsx | 4 +- .../charts/performanceScoreBreakdownChart.tsx | 2 +- .../components/performanceScoreRing.tsx | 4 +- .../performanceScoreRingWithTooltips.tsx | 10 +- .../components/webVitalDescription.tsx | 2 +- .../webVitals/components/webVitalMeters.tsx | 2 +- .../calculatePerformanceScoreFromStored.tsx | 10 +- ...TransactionSamplesWebVitalsScoresQuery.tsx | 18 ++-- .../useTransactionWebVitalsScoresQuery.tsx | 6 +- .../charts/transactionDurationChart.tsx | 2 +- .../insights/cache/components/samplePanel.tsx | 6 +- .../insights/cache/views/cacheLandingPage.tsx | 2 +- static/app/views/insights/colors.tsx | 14 +-- .../insights/common/components/chart.spec.tsx | 4 +- .../insights/common/components/chart.tsx | 22 ++--- .../insights/common/components/issues.tsx | 4 +- .../common/queries/useHasTtfdConfigured.tsx | 4 +- .../insights/common/queries/useReleases.tsx | 2 +- .../common/queries/useSortedTimeSeries.tsx | 6 +- .../queries/useSpanMetricsTopNSeries.tsx | 4 +- .../insights/common/queries/useSpansQuery.tsx | 2 +- .../common/utils/useModuleNameFromUrl.tsx | 2 +- .../sampleList/durationChart/index.tsx | 2 +- .../sampleList/sampleTable/sampleTable.tsx | 4 +- .../databaseSystemSelector.spec.tsx | 2 +- .../components/useSystemSelectorOptions.tsx | 4 +- .../database/utils/formatMongoDBQuery.tsx | 2 +- .../http/components/httpSamplesPanel.tsx | 2 +- .../http/views/httpDomainSummaryPage.tsx | 2 +- .../components/charts/llmMonitoringCharts.tsx | 12 +-- .../appStarts/components/appStartup.tsx | 4 +- .../components/charts/countChart.tsx | 2 +- .../charts/deviceClassBreakdownBarChart.tsx | 2 +- .../appStarts/components/spanOpSelector.tsx | 4 +- .../components/startDurationWidget.tsx | 2 +- .../components/systemApplicationBreakdown.tsx | 6 +- .../common/components/tables/screensTable.tsx | 2 +- .../components/charts/screenBarChart.tsx | 14 +-- .../components/charts/screenCharts.tsx | 26 ++--- .../screenload/components/eventSamples.tsx | 2 +- .../screenload/components/screensView.tsx | 14 +-- .../screenload/components/spanOpSelector.tsx | 4 +- .../components/tabbedCodeSnippets.tsx | 4 +- .../components/tables/eventSamplesTable.tsx | 2 +- .../tables/screenLoadSpansTable.spec.tsx | 4 +- .../insights/mobile/screenload/utils.tsx | 4 +- .../screens/components/screensOverview.tsx | 4 +- .../screens/views/screenDetailsPage.tsx | 2 +- .../screens/views/screensLandingPage.tsx | 2 +- .../mobile/ui/components/uiScreens.tsx | 2 +- .../insights/pages/ai/aiOverviewPage.tsx | 2 +- .../pages/backend/backendOverviewPage.tsx | 2 +- .../pages/frontend/frontendOverviewPage.tsx | 2 +- .../pages/mobile/mobileOverviewPage.tsx | 2 +- .../insights/queues/charts/latencyChart.tsx | 2 +- .../queues/charts/throughputChart.tsx | 2 +- .../queues/views/destinationSummaryPage.tsx | 2 +- .../views/issueDetails/groupActivityItem.tsx | 4 +- .../groupEventAttachments/index.tsx | 2 +- .../issueDetails/groupMerged/index.spec.tsx | 2 +- .../groupReplays/groupReplays.spec.tsx | 2 +- .../views/issueDetails/groupSidebar.spec.tsx | 2 +- .../similarStackTrace/index.tsx | 2 +- .../similarStackTrace/item.tsx | 2 +- .../streamline/sidebar/externalIssueList.tsx | 2 +- .../streamline/sidebar/groupActivityItem.tsx | 4 +- .../sidebar/participantList.spec.tsx | 2 +- .../issueDetails/traceDataSection.spec.tsx | 4 +- .../issueDetails/traceTimeline/traceIssue.tsx | 2 +- .../traceTimeline/traceTimelineTooltip.tsx | 2 +- static/app/views/issueDetails/utils.tsx | 6 +- .../issueList/customViewsHeader.spec.tsx | 74 +++++++-------- .../app/views/issueList/customViewsHeader.tsx | 10 +- .../groupSearchViewTabs/draggableTabBar.tsx | 4 +- .../groupSearchViewTabs/issueViews.tsx | 4 +- .../views/issueList/overview.actions.spec.tsx | 10 +- static/app/views/issueList/overview.spec.tsx | 2 +- static/app/views/issueList/overview.tsx | 8 +- .../issueList/utils/useFetchIssueTags.tsx | 24 ++--- static/app/views/metrics/codeLocations.tsx | 2 +- static/app/views/metrics/context.tsx | 8 +- static/app/views/metrics/createAlertModal.tsx | 6 +- .../metrics/metricFormulaContextMenu.tsx | 2 +- .../app/views/metrics/pageHeaderActions.tsx | 4 +- static/app/views/metrics/scratchpad.tsx | 10 +- .../app/views/metrics/useCreateDashboard.tsx | 6 +- static/app/views/metrics/utils/index.tsx | 2 +- .../metrics/utils/metricsChartPalette.tsx | 6 +- .../utils/parseMetricWidgetsQueryParam.tsx | 4 +- .../metrics/utils/useMetricsIntervalParam.tsx | 2 +- static/app/views/metrics/widget.tsx | 10 +- .../monitors/components/detailsTimeline.tsx | 4 +- .../views/monitors/components/monitorForm.tsx | 2 +- .../components/monitorQuickStartGuide.tsx | 8 +- .../monitors/components/monitorStats.tsx | 2 +- .../components/overviewTimeline/index.tsx | 4 +- .../monitorProcessingErrors.tsx | 2 +- .../components/timeline/checkInTimeline.tsx | 2 +- .../timeline/checkInTooltip.spec.tsx | 18 ++-- .../components/timeline/checkInTooltip.tsx | 6 +- .../timeline/utils/getAggregateStatus.tsx | 2 +- .../getAggregateStatusFromMultipleBuckets.tsx | 8 +- .../components/createProjectsFooter.tsx | 4 +- static/app/views/onboarding/onboarding.tsx | 18 ++-- .../app/views/onboarding/setupDocs.spec.tsx | 2 +- .../organizationStats/mapSeriesToChart.ts | 28 +++--- .../teamInsights/teamIssuesBreakdown.tsx | 10 +- .../teamInsights/teamStability.tsx | 6 +- .../teamInsights/teamUnresolvedIssues.tsx | 2 +- .../organizationStats/usageChart/index.tsx | 12 +-- .../organizationStats/usageChart/utils.tsx | 2 +- .../organizationStats/usageStatsPerMin.tsx | 2 +- .../organizationStats/usageStatsProjects.tsx | 14 +-- static/app/views/performance/charts/chart.tsx | 12 +-- static/app/views/performance/charts/index.tsx | 12 +-- static/app/views/performance/content.spec.tsx | 2 +- .../views/performance/landing/index.spec.tsx | 2 +- .../app/views/performance/landing/index.tsx | 8 +- .../landing/metricsDataSwitcherAlert.tsx | 2 +- .../performance/landing/samplingModal.tsx | 2 +- .../app/views/performance/landing/utils.tsx | 2 +- .../widgets/components/performanceWidget.tsx | 6 +- .../widgets/components/widgetChartRow.tsx | 8 +- .../transforms/transformEventsToArea.tsx | 2 +- .../transforms/transformEventsToVitals.tsx | 2 +- .../landing/widgets/widgetDefinitions.tsx | 64 ++++++------- .../widgets/widgets/histogramWidget.tsx | 4 +- .../widgets/widgets/lineChartListWidget.tsx | 28 +++--- .../mobileReleaseComparisonListWidget.tsx | 8 +- .../widgets/widgets/singleFieldAreaWidget.tsx | 8 +- .../widgets/stackedAreaChartListWidget.tsx | 2 +- .../landing/widgets/widgets/vitalWidget.tsx | 24 ++--- .../newTraceDetails/issuesTraceWaterfall.tsx | 4 +- .../newTraceDetails/trace.spec.tsx | 78 +++++++-------- .../performance/newTraceDetails/trace.tsx | 2 +- .../newTraceDetails/traceApi/useTrace.tsx | 2 +- .../newTraceDetails/traceConfigurations.tsx | 10 +- .../traceDrawer/details/issues/issues.tsx | 4 +- .../details/span/sections/http.tsx | 4 +- .../transaction/sections/cacheMetrics.tsx | 4 +- .../details/transaction/sections/entries.tsx | 2 +- .../traceDrawer/tabs/trace/tagsSummary.tsx | 2 +- .../traceDrawer/traceDrawer.tsx | 6 +- .../newTraceDetails/traceHeader/meta.tsx | 2 +- .../traceModels/issuesTraceTree.tsx | 10 +- .../traceModels/makeExampleTrace.tsx | 2 +- .../traceModels/parentAutogroupNode.tsx | 4 +- .../traceTree.autogrouping.spec.tsx | 35 +++---- .../traceTree.incremental.spec.tsx | 4 +- .../traceModels/traceTree.measurements.tsx | 4 +- .../traceTree.missinginstrumentation.spec.tsx | 16 ++-- .../traceModels/traceTree.spec.tsx | 92 +++++++++--------- .../traceModels/traceTree.ssr.spec.tsx | 6 +- .../newTraceDetails/traceModels/traceTree.tsx | 58 ++++++------ .../traceRenderers/traceVirtualizedList.tsx | 6 +- .../traceRenderers/virtualizedViewManager.tsx | 18 ++-- .../traceSearch/traceSearchEvaluator.tsx | 18 ++-- .../traceState/traceSearch.tsx | 24 ++--- .../newTraceDetails/traceState/traceTabs.tsx | 14 +-- .../usePerformanceUsageStats.tsx | 4 +- .../newTraceDetails/traceWaterfall.tsx | 11 ++- static/app/views/performance/table.spec.tsx | 10 +- static/app/views/performance/table.tsx | 2 +- .../traceDetails/TraceDetailsRouting.tsx | 2 +- .../performance/traceDetails/content.spec.tsx | 8 +- .../traceDetails/newTraceDetailsTraceView.tsx | 10 +- .../newTraceDetailsTransactionBar.tsx | 6 +- .../performance/traceDetails/traceView.tsx | 10 +- .../traceDetails/traceViewDetailPanel.tsx | 2 +- .../traceDetails/transactionBar.tsx | 4 +- .../traceDetails/transactionDetail.tsx | 2 +- .../views/performance/traceDetails/utils.tsx | 2 +- .../transactionDetails/content.tsx | 2 +- .../teamKeyTransactionButton.spec.tsx | 44 ++++----- .../transactionEvents/eventsTable.tsx | 10 +- .../transactionEvents/index.tsx | 2 +- .../transactionOverview/charts.tsx | 4 +- .../transactionOverview/content.tsx | 4 +- .../durationPercentileChart/utils.tsx | 12 +-- .../transactionOverview/index.spec.tsx | 2 +- .../metricEvents/metricsEventsDropdown.tsx | 2 +- .../transactionOverview/sidebarCharts.tsx | 6 +- .../transactionOverview/tagExplorer.tsx | 2 +- .../spanDetails/index.spec.tsx | 2 +- .../transactionSpans/spanSummary/content.tsx | 2 +- .../spanSummary/spanSummaryCharts.tsx | 2 +- .../spanSummary/spanSummaryTable.tsx | 4 +- .../suspectSpansTable.spec.tsx | 2 +- .../transactionSpans/utils.tsx | 2 +- .../transactionTags/tagValueTable.tsx | 2 +- .../transactionTags/tagsHeatMap.tsx | 15 +-- .../transactionTags/utils.tsx | 6 +- .../transactionVitals/utils.tsx | 22 ++--- .../transactionVitals/vitalCard.tsx | 2 +- .../transactionVitals/vitalsPanel.tsx | 2 +- .../performance/transactionSummary/utils.tsx | 6 +- .../app/views/performance/trends/content.tsx | 2 +- .../views/performance/trends/index.spec.tsx | 18 ++-- .../views/performance/trends/utils/index.tsx | 4 +- .../performance/utils/getIntervalLine.tsx | 6 +- .../views/performance/vitalDetail/table.tsx | 2 +- .../vitalDetail/vitalChartMetrics.tsx | 2 +- .../profiling/continuousProfileFlamegraph.tsx | 2 +- .../landing/functionTrendsWidget.tsx | 8 +- .../landing/slowestFunctionsWidget.spec.tsx | 4 +- .../landing/slowestFunctionsWidget.tsx | 6 +- .../app/views/profiling/profileFlamechart.tsx | 2 +- .../regressedProfileFunctions.tsx | 14 +-- .../app/views/profiling/profilesProvider.tsx | 10 +- .../charts/projectSessionsAnrRequest.tsx | 18 ++-- .../charts/projectSessionsChartRequest.tsx | 24 ++--- .../app/views/projectDetail/projectCharts.tsx | 6 +- .../projectDetail/projectDetail.spec.tsx | 2 +- .../app/views/projectDetail/projectDetail.tsx | 8 +- .../projectStabilityScoreCard.tsx | 4 +- .../views/projectInstall/createProject.tsx | 10 +- .../issueAlertNotificationOptions.tsx | 2 +- .../messagingIntegrationAlertRule.tsx | 2 +- .../projectInstall/otherPlatformsInfo.tsx | 2 +- .../views/projectInstall/platform.spec.tsx | 2 +- .../platformOrIntegration.spec.tsx | 2 +- .../views/projectsDashboard/index.spec.tsx | 40 ++++---- static/app/views/projectsDashboard/index.tsx | 2 +- .../views/projectsDashboard/projectCard.tsx | 2 +- .../detail/commitsAndFiles/filesChanged.tsx | 2 +- .../detail/header/releaseActions.spec.tsx | 8 +- .../releases/detail/header/releaseHeader.tsx | 2 +- static/app/views/releases/detail/index.tsx | 4 +- .../views/releases/detail/overview/index.tsx | 4 +- .../overview/releaseComparisonChart/index.tsx | 4 +- .../releaseEventsChart.tsx | 6 +- .../releaseSessionsChart.tsx | 12 +-- .../sidebar/commitAuthorBreakdown.tsx | 2 +- .../overview/sidebar/totalCrashFreeUsers.tsx | 2 +- .../app/views/releases/detail/utils.spec.tsx | 4 +- static/app/views/releases/detail/utils.tsx | 10 +- static/app/views/releases/list/index.tsx | 2 +- .../views/releases/list/releaseCard/index.tsx | 4 +- .../releaseCard/releaseCardProjectRow.tsx | 2 +- .../releases/list/releasesAdoptionChart.tsx | 10 +- .../views/releases/list/releasesRequest.tsx | 22 ++--- .../app/views/relocation/relocation.spec.tsx | 46 ++++----- static/app/views/relocation/relocation.tsx | 8 +- .../replays/deadRageClick/selectorTable.tsx | 2 +- .../replays/detail/breadcrumbs/index.tsx | 2 +- .../breadcrumbs/useBreadcrumbFilters.tsx | 4 +- .../views/replays/detail/console/format.tsx | 2 +- .../views/replays/detail/console/index.tsx | 2 +- .../detail/console/messageFormatter.spec.tsx | 24 ++--- .../detail/console/messageFormatter.tsx | 2 +- .../detail/console/useConsoleFilters.spec.tsx | 6 +- .../detail/errorList/errorHeaderCell.tsx | 2 +- .../detail/errorList/errorTableCell.tsx | 2 +- .../views/replays/detail/errorList/index.tsx | 2 +- .../detail/errorList/useErrorFilters.spec.tsx | 32 +++---- .../detail/errorList/useSortErrors.spec.tsx | 36 +++---- .../detail/network/details/content.spec.tsx | 12 +-- .../network/details/onboarding.spec.tsx | 10 +- .../views/replays/detail/network/index.tsx | 8 +- .../detail/network/networkHeaderCell.tsx | 2 +- .../detail/network/networkTableCell.tsx | 2 +- .../network/truncateJson/completeJson.ts | 2 +- .../network/truncateJson/evaluateJson.ts | 4 +- .../detail/network/useNetworkFilters.spec.tsx | 54 +++++------ .../detail/network/useSortNetwork.spec.tsx | 56 +++++------ .../replays/detail/tagPanel/useTagFilters.tsx | 2 +- .../detail/trace/replayTransactionContext.tsx | 2 +- .../replays/detail/trace/useReplayTraces.tsx | 4 +- .../replays/list/replayOnboardingPanel.tsx | 2 +- .../views/replays/list/replaySearchBar.tsx | 2 +- .../views/replays/replayTable/tableCell.tsx | 2 +- .../sentryAppExternalInstallation/index.tsx | 4 +- .../views/settings/account/accountDetails.tsx | 2 +- .../settings/account/accountEmails.spec.tsx | 6 +- .../account/accountNotificationFineTuning.tsx | 2 +- .../accountSecurity/accountSecurityEnroll.tsx | 6 +- .../account/accountSecurity/index.spec.tsx | 4 +- .../account/accountSubscriptions.spec.tsx | 2 +- .../settings/account/accountSubscriptions.tsx | 2 +- .../notificationSettings.spec.tsx | 4 +- .../notifications/notificationSettings.tsx | 2 +- .../notificationSettingsByEntity.tsx | 4 +- .../notificationSettingsByType.spec.tsx | 8 +- .../notificationSettingsByType.tsx | 8 +- .../settings/account/notifications/utils.tsx | 2 +- .../dataScrubbing/convertRelayPiiConfig.tsx | 2 +- .../components/dataScrubbing/index.spec.tsx | 4 +- .../components/dataScrubbing/index.tsx | 2 +- .../dataScrubbing/modals/add.spec.tsx | 10 +- .../dataScrubbing/modals/edit.spec.tsx | 22 ++--- .../modals/form/sourceField.spec.tsx | 18 ++-- .../dataScrubbing/modals/form/sourceField.tsx | 26 ++--- .../components/dataScrubbing/submitRules.tsx | 6 +- .../organizationCrumb.spec.tsx | 2 +- .../settings/components/settingsLayout.tsx | 2 +- .../dynamicSampling/projectSampling.tsx | 2 +- .../dynamicSampling/projectsEditTable.tsx | 4 +- .../dynamicSampling/samplingBreakdown.tsx | 2 +- .../utils/testScaleSapleRates.spec.tsx | 8 +- .../utils/useProjectSampleCounts.tsx | 2 +- .../utils/useSamplingProjectRates.tsx | 2 +- .../settings/earlyFeatures/settingsForm.tsx | 2 +- .../organizationAuth/organizationAuthList.tsx | 2 +- .../organizationAuth/providerItem.spec.tsx | 2 +- .../sentryApplicationDashboard/index.tsx | 2 +- .../organizationSettingsForm.spec.tsx | 2 +- .../organizationSettingsForm.tsx | 6 +- .../configureIntegration.tsx | 2 +- .../integrationCodeMappings.spec.tsx | 35 +++---- .../integrationDetailedView.tsx | 14 +-- .../integrationExternalMappingForm.spec.tsx | 6 +- .../integrationExternalMappings.spec.tsx | 2 +- .../integrationExternalMappings.tsx | 2 +- .../integrationListDirectory.tsx | 4 +- .../integrationServerlessFunctions.tsx | 2 +- .../pluginDetailedView.tsx | 8 +- .../sentryAppExternalForm.tsx | 2 +- .../organizationMembers/inviteBanner.spec.tsx | 2 +- .../inviteRequestRow.spec.tsx | 4 +- .../organizationMemberDetail.spec.tsx | 2 +- .../organizationMemberDetail.tsx | 2 +- .../organizationMembersList.spec.tsx | 24 ++--- .../organizationMembersList.tsx | 2 +- .../settings/organizationRelay/list/index.tsx | 2 +- .../settings/organizationRelay/list/utils.tsx | 4 +- .../organizationTeams.spec.tsx | 4 +- .../roleOverwriteWarning.tsx | 2 +- .../organizationTeams/teamMembers.spec.tsx | 34 +++---- .../organizationTeams/teamNotifications.tsx | 2 +- .../organizationTeams/teamProjects.spec.tsx | 4 +- .../organizationTeams/teamSettings/index.tsx | 2 +- .../settings/project/loaderScript.spec.tsx | 20 ++-- .../project/projectEnvironments.spec.tsx | 2 +- .../projectFilters/projectFiltersChart.tsx | 6 +- .../projectKeys/details/index.spec.tsx | 16 ++-- .../project/projectKeys/list/index.spec.tsx | 16 ++-- .../projectOwnership/addCodeOwnerModal.tsx | 4 +- .../project/projectOwnership/modal.spec.tsx | 2 +- .../settings/project/projectTeams.spec.tsx | 16 ++-- .../views/settings/projectAlerts/settings.tsx | 4 +- .../projectGeneralSettings/index.spec.tsx | 2 +- .../settings/projectGeneralSettings/index.tsx | 16 ++-- .../projectIssueGrouping/index.spec.tsx | 2 +- .../settings/projectIssueGrouping/index.tsx | 4 +- .../projectMetrics/customMetricsTable.tsx | 2 +- .../projectMetrics/projectMetricsDetails.tsx | 2 +- .../projectPerformance.spec.tsx | 4 +- .../projectPerformance/projectPerformance.tsx | 2 +- .../views/settings/projectProguard/index.tsx | 2 +- .../settings/projectSecurityHeaders/csp.tsx | 2 +- .../projectSecurityHeaders/expectCt.tsx | 8 +- .../settings/projectSecurityHeaders/hpkp.tsx | 4 +- .../settings/projectSecurityHeaders/index.tsx | 4 +- .../projectSecurityHeaders/reportUri.tsx | 2 +- .../views/settings/projectTags/index.spec.tsx | 2 +- static/app/views/setupWizard/index.tsx | 4 +- static/app/views/stories/storyTree.tsx | 2 +- .../app/views/traces/fieldRenderers.spec.tsx | 28 +++--- static/app/views/traces/tracesChart.tsx | 20 ++-- static/app/views/traces/tracesTable.tsx | 2 +- static/app/views/traces/utils.tsx | 2 +- static/app/views/unsubscribe/issue.tsx | 4 +- static/app/views/unsubscribe/project.tsx | 4 +- .../js/fixtures/routeComponentPropsFixture.ts | 2 +- tests/js/sentry-test/initializeOrg.tsx | 4 +- .../performance/initializePerformanceData.ts | 4 +- tests/js/sentry-test/performance/utils.ts | 2 +- tests/js/sentry-test/selectEvent.tsx | 4 +- tsconfig.json | 2 +- 907 files changed, 3639 insertions(+), 3553 deletions(-) diff --git a/build-utils/sentry-instrumentation.ts b/build-utils/sentry-instrumentation.ts index b680f98a34b263..ae502acd76f044 100644 --- a/build-utils/sentry-instrumentation.ts +++ b/build-utils/sentry-instrumentation.ts @@ -71,7 +71,7 @@ class SentryInstrumentation { sentry.setTag('arch', os.arch()); sentry.setTag( 'cpu', - cpus?.length ? `${cpus[0].model} (cores: ${cpus.length})}` : 'N/A' + cpus?.length ? `${cpus[0]!.model} (cores: ${cpus.length})}` : 'N/A' ); this.Sentry = sentry; @@ -96,7 +96,7 @@ class SentryInstrumentation { .filter(assetName => !assetName.endsWith('.map')) .forEach(assetName => { const asset = compilation.assets[assetName]; - const size = asset.size(); + const size = asset!.size(); const file = assetName; const body = JSON.stringify({ file, diff --git a/eslint.config.mjs b/eslint.config.mjs index cb696a686f424b..21e227016f2ced 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -779,6 +779,7 @@ export default typescript.config([ // corresponding configuration object where the plugin is initially included plugins: { ...react.configs.flat.plugins, + // @ts-ignore noUncheckedIndexedAccess ...react.configs.flat['jsx-runtime'].plugins, '@typescript-eslint': typescript.plugin, 'react-hooks': fixupPluginRules(reactHooks), diff --git a/static/app/actionCreators/dashboards.tsx b/static/app/actionCreators/dashboards.tsx index 846145e26b331c..3c7a6c902bde9b 100644 --- a/static/app/actionCreators/dashboards.tsx +++ b/static/app/actionCreators/dashboards.tsx @@ -27,7 +27,7 @@ export function fetchDashboards(api: Client, orgSlug: string) { if (errorResponse) { const errors = flattenErrors(errorResponse, {}); - addErrorMessage(errors[Object.keys(errors)[0]] as string); + addErrorMessage(errors[Object.keys(errors)[0]!] as string); } else { addErrorMessage(t('Unable to fetch dashboards')); } @@ -73,7 +73,7 @@ export function createDashboard( if (errorResponse) { const errors = flattenErrors(errorResponse, {}); - addErrorMessage(errors[Object.keys(errors)[0]] as string); + addErrorMessage(errors[Object.keys(errors)[0]!] as string); } else { addErrorMessage(t('Unable to create dashboard')); } @@ -118,7 +118,7 @@ export async function updateDashboardFavorite( const errorResponse = response?.responseJSON ?? null; if (errorResponse) { const errors = flattenErrors(errorResponse, {}); - addErrorMessage(errors[Object.keys(errors)[0]] as string); + addErrorMessage(errors[Object.keys(errors)[0]!]! as string); } else if (isFavorited) { addErrorMessage(t('Unable to favorite dashboard')); } else { @@ -145,7 +145,7 @@ export function fetchDashboard( if (errorResponse) { const errors = flattenErrors(errorResponse, {}); - addErrorMessage(errors[Object.keys(errors)[0]] as string); + addErrorMessage(errors[Object.keys(errors)[0]!] as string); } else { addErrorMessage(t('Unable to load dashboard')); } @@ -192,7 +192,7 @@ export function updateDashboard( if (errorResponse) { const errors = flattenErrors(errorResponse, {}); - addErrorMessage(errors[Object.keys(errors)[0]] as string); + addErrorMessage(errors[Object.keys(errors)[0]!] as string); } else { addErrorMessage(t('Unable to update dashboard')); } @@ -218,7 +218,7 @@ export function deleteDashboard( if (errorResponse) { const errors = flattenErrors(errorResponse, {}); - addErrorMessage(errors[Object.keys(errors)[0]] as string); + addErrorMessage(errors[Object.keys(errors)[0]!] as string); } else { addErrorMessage(t('Unable to delete dashboard')); } @@ -270,7 +270,7 @@ export function updateDashboardPermissions( if (errorResponse) { const errors = flattenErrors(errorResponse, {}); - addErrorMessage(errors[Object.keys(errors)[0]] as string); + addErrorMessage(errors[Object.keys(errors)[0]!]! as string); } else { addErrorMessage(t('Unable to update dashboard permissions')); } diff --git a/static/app/actionCreators/members.tsx b/static/app/actionCreators/members.tsx index 61bf7ef0ff92ed..27b48e37389a9e 100644 --- a/static/app/actionCreators/members.tsx +++ b/static/app/actionCreators/members.tsx @@ -67,7 +67,7 @@ export function indexMembersByProject(members: Member[]): IndexedMembersByProjec acc[project] = []; } if (member.user) { - acc[project].push(member.user); + acc[project]!.push(member.user); } } return acc; diff --git a/static/app/actionCreators/monitors.tsx b/static/app/actionCreators/monitors.tsx index 400dcaa98654c7..0a98c36450abad 100644 --- a/static/app/actionCreators/monitors.tsx +++ b/static/app/actionCreators/monitors.tsx @@ -70,7 +70,7 @@ export async function updateMonitor( // If we are updating a single value in the monitor we can read the // validation error for that key, otherwise fallback to the default error const validationError = - updateKeys.length === 1 ? respError.responseJSON?.[updateKeys[0]]?.[0] : undefined; + updateKeys.length === 1 ? respError.responseJSON?.[updateKeys[0]!]?.[0] : undefined; logException(err); addErrorMessage(validationError ?? t('Unable to update monitor.')); diff --git a/static/app/actionCreators/organizations.tsx b/static/app/actionCreators/organizations.tsx index b26bec04438edf..49e6b9d2f15758 100644 --- a/static/app/actionCreators/organizations.tsx +++ b/static/app/actionCreators/organizations.tsx @@ -45,7 +45,7 @@ export function redirectToRemainingOrganization({ } // Let's be smart and select the best org to redirect to - const firstRemainingOrg = allOrgs[0]; + const firstRemainingOrg = allOrgs[0]!; const route = `/organizations/${firstRemainingOrg.slug}/issues/`; if (USING_CUSTOMER_DOMAIN) { diff --git a/static/app/actionCreators/pageFilters.tsx b/static/app/actionCreators/pageFilters.tsx index 54783e899fcbb2..ecd873d685661f 100644 --- a/static/app/actionCreators/pageFilters.tsx +++ b/static/app/actionCreators/pageFilters.tsx @@ -319,7 +319,7 @@ export function initializeUrlState({ if (projects && projects.length > 0) { // If there is a list of projects from URL params, select first project // from that list - newProject = typeof projects === 'string' ? [Number(projects)] : [projects[0]]; + newProject = typeof projects === 'string' ? [Number(projects)] : [projects[0]!]; } else { // When we have finished loading the organization into the props, i.e. // the organization slug is consistent with the URL param--Sentry will diff --git a/static/app/actionCreators/projects.spec.tsx b/static/app/actionCreators/projects.spec.tsx index 6b3f3e3cb79b1f..c7eef90e1537de 100644 --- a/static/app/actionCreators/projects.spec.tsx +++ b/static/app/actionCreators/projects.spec.tsx @@ -14,7 +14,7 @@ describe('Projects ActionCreators', function () { expect(mock).not.toHaveBeenCalled(); _debouncedLoadStats(api, new Set([...Array(50)].map((_, i) => String(i))), { - projectId: project.id, + projectId: project!.id, orgId: organization.slug, }); @@ -38,7 +38,7 @@ describe('Projects ActionCreators', function () { expect(mock).not.toHaveBeenCalled(); _debouncedLoadStats(api, new Set(['1', '2', '3']), { - projectId: project.id, + projectId: project!.id, orgId: organization.slug, query: {transactionStats: '1'}, }); diff --git a/static/app/bootstrap/initializeSdk.tsx b/static/app/bootstrap/initializeSdk.tsx index f51a4b58835f36..222fda527d0715 100644 --- a/static/app/bootstrap/initializeSdk.tsx +++ b/static/app/bootstrap/initializeSdk.tsx @@ -217,7 +217,7 @@ export function initializeSdk(config: Config) { images.push({ type: 'sourcemap', code_file: filename, - debug_id: debugIdMap[filename], + debug_id: debugIdMap[filename]!, }); }); } catch (e) { @@ -310,7 +310,7 @@ function handlePossibleUndefinedResponseBodyErrors(event: Event): void { const causeErrorIsURBE = causeError?.type === 'UndefinedResponseBodyError'; if (mainErrorIsURBE || causeErrorIsURBE) { - mainError.type = 'UndefinedResponseBodyError'; + mainError!.type = 'UndefinedResponseBodyError'; event.tags = {...event.tags, undefinedResponseBody: true}; event.fingerprint = mainErrorIsURBE ? ['UndefinedResponseBodyError as main error'] @@ -319,7 +319,7 @@ function handlePossibleUndefinedResponseBodyErrors(event: Event): void { } export function addEndpointTagToRequestError(event: Event): void { - const errorMessage = event.exception?.values?.[0].value || ''; + const errorMessage = event.exception?.values?.[0]!.value || ''; // The capturing group here turns `GET /dogs/are/great 500` into just `GET /dogs/are/great` const requestErrorRegex = new RegExp('^([A-Za-z]+ (/[^/]+)+/) \\d+$'); diff --git a/static/app/chartcuterie/discover.tsx b/static/app/chartcuterie/discover.tsx index 50959c108139fa..13ec672ac75ad9 100644 --- a/static/app/chartcuterie/discover.tsx +++ b/static/app/chartcuterie/discover.tsx @@ -336,7 +336,7 @@ discoverCharts.push({ const previousPeriod = LineSeries({ name: t('previous %s', data.seriesName), data: previous.map(([_, countsForTimestamp], i) => [ - current[i][0] * 1000, + current[i]![0] * 1000, countsForTimestamp.reduce((acc, {count}) => acc + count, 0), ]), lineStyle: {color: theme.gray200, type: 'dotted'}, diff --git a/static/app/components/acl/feature.tsx b/static/app/components/acl/feature.tsx index 83547b2d78acbe..77a3f5b91061a0 100644 --- a/static/app/components/acl/feature.tsx +++ b/static/app/components/acl/feature.tsx @@ -141,12 +141,12 @@ class Feature extends Component { const shouldMatchOnlyProject = feature.match(/^projects:(.+)/); if (shouldMatchOnlyProject) { - return project.includes(shouldMatchOnlyProject[1]); + return project.includes(shouldMatchOnlyProject[1]!); } const shouldMatchOnlyOrg = feature.match(/^organizations:(.+)/); if (shouldMatchOnlyOrg) { - return organization.includes(shouldMatchOnlyOrg[1]); + return organization.includes(shouldMatchOnlyOrg[1]!); } // default, check all feature arrays @@ -186,7 +186,7 @@ class Feature extends Component { const hooks = HookStore.get(hookName); if (hooks.length > 0) { - customDisabledRender = hooks[0]; + customDisabledRender = hooks[0]!; } } const renderProps = { diff --git a/static/app/components/arithmeticInput/parser.tsx b/static/app/components/arithmeticInput/parser.tsx index 1c22966f4837ba..c73635d051f99f 100644 --- a/static/app/components/arithmeticInput/parser.tsx +++ b/static/app/components/arithmeticInput/parser.tsx @@ -54,7 +54,7 @@ export class TokenConverter { tokenTerm = (maybeFactor: Expression, remainingAdds: Array): Expression => { if (remainingAdds.length > 0) { - remainingAdds[0].lhs = maybeFactor; + remainingAdds[0]!.lhs = maybeFactor; return flatten(remainingAdds); } return maybeFactor; @@ -75,7 +75,7 @@ export class TokenConverter { }; tokenFactor = (primary: Expression, remaining: Array): Operation => { - remaining[0].lhs = primary; + remaining[0]!.lhs = primary; return flatten(remaining); }; diff --git a/static/app/components/assigneeBadge.stories.tsx b/static/app/components/assigneeBadge.stories.tsx index 3786ba961aaf48..d087d6e37455f4 100644 --- a/static/app/components/assigneeBadge.stories.tsx +++ b/static/app/components/assigneeBadge.stories.tsx @@ -42,7 +42,7 @@ export default storyBook('AssigneeBadge', story => { const [chevron2Toggle, setChevron2Toggle] = useState<'up' | 'down'>('down'); const team: Team = teams.length - ? teams[0] + ? teams[0]! : { id: '1', slug: 'team-slug', diff --git a/static/app/components/assigneeSelectorDropdown.spec.tsx b/static/app/components/assigneeSelectorDropdown.spec.tsx index 675aef098928c5..8c28fb3c99255b 100644 --- a/static/app/components/assigneeSelectorDropdown.spec.tsx +++ b/static/app/components/assigneeSelectorDropdown.spec.tsx @@ -572,7 +572,7 @@ describe('AssigneeSelectorDropdown', () => { // Suggested assignee initials expect(options[0]).toHaveTextContent('AB'); - await userEvent.click(options[0]); + await userEvent.click(options[0]!); await waitFor(() => expect(assignGroup2Mock).toHaveBeenCalledWith( diff --git a/static/app/components/assigneeSelectorDropdown.tsx b/static/app/components/assigneeSelectorDropdown.tsx index 3198df4a31a9d7..0615ae973d26e7 100644 --- a/static/app/components/assigneeSelectorDropdown.tsx +++ b/static/app/components/assigneeSelectorDropdown.tsx @@ -155,6 +155,7 @@ export function AssigneeAvatar({ } if (suggestedActors.length > 0) { + const actor = suggestedActors[0]!; return (
    {tct('Suggestion: [name]', { - name: - suggestedActors[0].type === 'team' - ? `#${suggestedActors[0].name}` - : suggestedActors[0].name, + name: actor.type === 'team' ? `#${actor.name}` : actor.name, })} {suggestedActors.length > 1 && tn(' + %s other', ' + %s others', suggestedActors.length - 1)}
    - - {suggestedReasons[suggestedActors[0].suggestedReason]} - + {suggestedReasons[actor.suggestedReason]} } /> @@ -265,7 +261,10 @@ export default function AssigneeSelectorDropdown({ const uniqueSuggestions = uniqBy(suggestedOwners, owner => owner.owner); return uniqueSuggestions .map(suggestion => { - const [suggestionType, suggestionId] = suggestion.owner.split(':'); + const [suggestionType, suggestionId] = suggestion.owner.split(':') as [ + string, + string, + ]; const suggestedReasonText = suggestedReasonTable[suggestion.type]; if (suggestionType === 'user') { const member = currentMemberList.find(user => user.id === suggestionId); @@ -322,7 +321,7 @@ export default function AssigneeSelectorDropdown({ } // See makeMemberOption and makeTeamOption for how the value is formatted const type = selectedOption.value.startsWith('user:') ? 'user' : 'team'; - const assigneeId = selectedOption.value.split(':')[1]; + const assigneeId = selectedOption.value.split(':')[1]!; let assignee: User | Actor; if (type === 'user') { diff --git a/static/app/components/assistant/guideAnchor.tsx b/static/app/components/assistant/guideAnchor.tsx index f5b9900c65c25d..efbd244f529460 100644 --- a/static/app/components/assistant/guideAnchor.tsx +++ b/static/app/components/assistant/guideAnchor.tsx @@ -155,7 +155,7 @@ class BaseGuideAnchor extends Component { const totalStepCount = currentGuide.steps.length; const currentStepCount = step + 1; - const currentStep = currentGuide.steps[step]; + const currentStep = currentGuide.steps[step]!; const lastStep = currentStepCount === totalStepCount; const hasManySteps = totalStepCount > 1; diff --git a/static/app/components/autoComplete.spec.tsx b/static/app/components/autoComplete.spec.tsx index b92558de4ae0d2..fb87a2fe581697 100644 --- a/static/app/components/autoComplete.spec.tsx +++ b/static/app/components/autoComplete.spec.tsx @@ -216,7 +216,7 @@ describe('AutoComplete', function () { expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument(); expect(screen.getAllByRole('option')).toHaveLength(3); - fireEvent.click(screen.getByText(items[1].name)); + fireEvent.click(screen.getByText(items[1]!.name)); expect(mocks.onSelect).toHaveBeenCalledWith( items[1], expect.objectContaining({inputValue: '', highlightedIndex: 0}), @@ -419,7 +419,7 @@ describe('AutoComplete', function () { createWrapper({isOpen: true}); expect(screen.getAllByRole('option')).toHaveLength(3); - fireEvent.click(screen.getByText(items[1].name)); + fireEvent.click(screen.getByText(items[1]!.name)); expect(mocks.onSelect).toHaveBeenCalledWith( items[1], expect.objectContaining({inputValue: '', highlightedIndex: 0}), diff --git a/static/app/components/avatar/avatarList.spec.tsx b/static/app/components/avatar/avatarList.spec.tsx index 9d8046e23da216..3e6adb97006c43 100644 --- a/static/app/components/avatar/avatarList.spec.tsx +++ b/static/app/components/avatar/avatarList.spec.tsx @@ -42,12 +42,12 @@ describe('AvatarList', () => { ]; renderComponent({users}); - expect(screen.getByText(users[0].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[1].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[2].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[3].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[4].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[5].name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[0]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[1]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[2]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[3]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[4]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[5]!.name.charAt(0))).toBeInTheDocument(); expect(screen.queryByTestId('avatarList-collapsedavatars')).not.toBeInTheDocument(); }); @@ -63,12 +63,12 @@ describe('AvatarList', () => { ]; renderComponent({users}); - expect(screen.getByText(users[0].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[1].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[2].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[3].name.charAt(0))).toBeInTheDocument(); - expect(screen.getByText(users[4].name.charAt(0))).toBeInTheDocument(); - expect(screen.queryByText(users[5].name.charAt(0))).not.toBeInTheDocument(); + expect(screen.getByText(users[0]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[1]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[2]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[3]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.getByText(users[4]!.name.charAt(0))).toBeInTheDocument(); + expect(screen.queryByText(users[5]!.name.charAt(0))).not.toBeInTheDocument(); expect(screen.getByTestId('avatarList-collapsedavatars')).toBeInTheDocument(); }); diff --git a/static/app/components/avatar/avatarList.tsx b/static/app/components/avatar/avatarList.tsx index df9932016668b3..e6234e62fd611f 100644 --- a/static/app/components/avatar/avatarList.tsx +++ b/static/app/components/avatar/avatarList.tsx @@ -69,9 +69,9 @@ function AvatarList({ if (numCollapsedAvatars === 1) { if (visibleTeamAvatars.length < teams.length) { - visibleTeamAvatars.unshift(teams[teams.length - 1]); + visibleTeamAvatars.unshift(teams[teams.length - 1]!); } else if (visibleUserAvatars.length < users.length) { - visibleUserAvatars.unshift(users[users.length - 1]); + visibleUserAvatars.unshift(users[users.length - 1]!); } numCollapsedAvatars = 0; } diff --git a/static/app/components/avatar/index.spec.tsx b/static/app/components/avatar/index.spec.tsx index 4959b79988bfad..e26f04fe0ccb10 100644 --- a/static/app/components/avatar/index.spec.tsx +++ b/static/app/components/avatar/index.spec.tsx @@ -293,7 +293,7 @@ describe('Avatar', function () { avatar2.unmount(); // avatarType of `default` - sentryApp.avatars![0].avatarType = 'default'; + sentryApp.avatars![0]!.avatarType = 'default'; render(); expect(screen.getByTestId('default-sentry-app-avatar')).toBeInTheDocument(); }); diff --git a/static/app/components/avatar/suggestedAvatarStack.tsx b/static/app/components/avatar/suggestedAvatarStack.tsx index ee03cdb641d307..cd869de11fd4ff 100644 --- a/static/app/components/avatar/suggestedAvatarStack.tsx +++ b/static/app/components/avatar/suggestedAvatarStack.tsx @@ -29,7 +29,7 @@ function SuggestedAvatarStack({ {suggestedOwners.slice(0, numAvatars - 1).map((owner, i) => ( ))} - childrenEls[visibility.findIndex(Boolean) - 1].scrollIntoView({ + childrenEls[visibility.findIndex(Boolean) - 1]!.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start', @@ -46,7 +46,7 @@ function Carousel({children, visibleRatio = 0.8}: CarouselProps) { const scrollRight = useCallback( () => - childrenEls[visibility.findLastIndex(Boolean) + 1].scrollIntoView({ + childrenEls[visibility.findLastIndex(Boolean) + 1]!.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'end', diff --git a/static/app/components/charts/barChartZoom.tsx b/static/app/components/charts/barChartZoom.tsx index a02995129bbd13..ece184c3faa10b 100644 --- a/static/app/components/charts/barChartZoom.tsx +++ b/static/app/components/charts/barChartZoom.tsx @@ -108,8 +108,8 @@ class BarChartZoom extends Component { if (startValue !== null && endValue !== null) { const {buckets, location, paramStart, paramEnd, minZoomWidth, onHistoryPush} = this.props; - const {start} = buckets[startValue]; - const {end} = buckets[endValue]; + const {start} = buckets[startValue]!; + const {end} = buckets[endValue]!; if (minZoomWidth === undefined || end - start > minZoomWidth) { const target = { diff --git a/static/app/components/charts/baseChart.tsx b/static/app/components/charts/baseChart.tsx index a98c80419dcafd..99a7d8e5d901c6 100644 --- a/static/app/components/charts/baseChart.tsx +++ b/static/app/components/charts/baseChart.tsx @@ -398,7 +398,7 @@ function BaseChartUnwrapped({ const resolvedSeries = useMemo(() => { const previousPeriodColors = - (previousPeriod?.length ?? 0) > 1 ? lightenHexToRgb(color) : undefined; + (previousPeriod?.length ?? 0) > 1 ? lightenHexToRgb(color as string[]) : undefined; const hasSinglePoints = (series as LineSeriesOption[] | undefined)?.every( s => Array.isArray(s.data) && s.data.length <= 1 diff --git a/static/app/components/charts/chartZoom.tsx b/static/app/components/charts/chartZoom.tsx index 160d0e43c6a0a5..01b85ba6bcf89a 100644 --- a/static/app/components/charts/chartZoom.tsx +++ b/static/app/components/charts/chartZoom.tsx @@ -243,7 +243,7 @@ class ChartZoom extends Component { return; } - this.setPeriod(this.history[0]); + this.setPeriod(this.history[0]!); // reset history this.history = []; diff --git a/static/app/components/charts/eventsAreaChart.spec.tsx b/static/app/components/charts/eventsAreaChart.spec.tsx index 38d4b6069775e8..d0113ed85c4141 100644 --- a/static/app/components/charts/eventsAreaChart.spec.tsx +++ b/static/app/components/charts/eventsAreaChart.spec.tsx @@ -52,6 +52,6 @@ describe('EventsChart with legend', function () { /> ); expect(await screen.findByTestId('area-chart')).toBeInTheDocument(); - expect(jest.mocked(BaseChart).mock.calls[0][0].legend).toHaveProperty('data'); + expect(jest.mocked(BaseChart).mock.calls[0]![0].legend).toHaveProperty('data'); }); }); diff --git a/static/app/components/charts/eventsChart.tsx b/static/app/components/charts/eventsChart.tsx index 07fd7b641dfb9c..2fd37ac0b1b9f2 100644 --- a/static/app/components/charts/eventsChart.tsx +++ b/static/app/components/charts/eventsChart.tsx @@ -314,7 +314,7 @@ class Chart extends Component { // Check to see if all series output types are the same. If not, then default to number. const outputType = new Set(Object.values(timeseriesResultsTypes)).size === 1 - ? timeseriesResultsTypes[yAxis] + ? timeseriesResultsTypes[yAxis]! : 'number'; return axisLabelFormatterUsingAggregateOutputType(value, outputType); } @@ -607,7 +607,7 @@ class EventsChart extends Component { additionalSeries={additionalSeries} previousSeriesTransformer={previousSeriesTransformer} stacked={this.isStacked()} - yAxis={yAxisArray[0]} + yAxis={yAxisArray[0]!} showDaily={showDaily} colors={colors} legendOptions={legendOptions} diff --git a/static/app/components/charts/eventsRequest.tsx b/static/app/components/charts/eventsRequest.tsx index 2462b5251c5793..f5f119ed5340ab 100644 --- a/static/app/components/charts/eventsRequest.tsx +++ b/static/app/components/charts/eventsRequest.tsx @@ -425,7 +425,7 @@ class EventsRequest extends PureComponent current[i][0] * 1000 + (_timestamp, _countArray, i) => current[i]![0] * 1000 ), stack: 'previous', }; @@ -573,7 +573,7 @@ class EventsRequest extends PureComponent { - const seriesData: EventsStats = timeseriesData[seriesName]; + const seriesData: EventsStats = timeseriesData[seriesName]!; const processedData = this.processData( seriesData, index, @@ -589,7 +589,7 @@ class EventsRequest extends PureComponent a[0] - b[0]); const timeseriesResultsTypes: Record = {}; Object.keys(timeseriesData).forEach(key => { - const fieldsMeta = timeseriesData[key].meta?.fields[getAggregateAlias(key)]; + const fieldsMeta = timeseriesData[key]!.meta?.fields[getAggregateAlias(key)]; if (fieldsMeta) { timeseriesResultsTypes[key] = fieldsMeta; } diff --git a/static/app/components/charts/intervalSelector.tsx b/static/app/components/charts/intervalSelector.tsx index 7a39386164b68e..34c35c8e2a0b71 100644 --- a/static/app/components/charts/intervalSelector.tsx +++ b/static/app/components/charts/intervalSelector.tsx @@ -123,12 +123,12 @@ function formatHoursToInterval(hours: number): [number, IntervalUnits] { function getIntervalOption(rangeHours: number): IntervalOption { for (const index in INTERVAL_OPTIONS) { - const currentOption = INTERVAL_OPTIONS[index]; + const currentOption = INTERVAL_OPTIONS[index]!; if (currentOption.rangeStart <= rangeHours) { return currentOption; } } - return INTERVAL_OPTIONS[0]; + return INTERVAL_OPTIONS[0]!; } function bindInterval( @@ -196,7 +196,7 @@ export default function IntervalSelector({ makeItem( amount, unit, - SUPPORTED_RELATIVE_PERIOD_UNITS[unit].label, + SUPPORTED_RELATIVE_PERIOD_UNITS[unit]!.label, results.length + 1 ) ); diff --git a/static/app/components/charts/miniBarChart.tsx b/static/app/components/charts/miniBarChart.tsx index 86549632b8a681..5125390e7724da 100644 --- a/static/app/components/charts/miniBarChart.tsx +++ b/static/app/components/charts/miniBarChart.tsx @@ -256,7 +256,7 @@ function MiniBarChart({ : [theme.gray200, theme.purple300, theme.purple300]; for (let i = 0; i < series.length; i++) { - const original = series[i]; + const original = series[i]!; const updated: BarChartSeries = { ...original, cursor: 'normal', diff --git a/static/app/components/charts/percentageAreaChart.tsx b/static/app/components/charts/percentageAreaChart.tsx index ac9916724a8eab..3697f14be07cfd 100644 --- a/static/app/components/charts/percentageAreaChart.tsx +++ b/static/app/components/charts/percentageAreaChart.tsx @@ -43,9 +43,9 @@ export default class PercentageAreaChart extends Component { const {series, getDataItemName, getValue} = this.props; const totalsArray: [string | number, number][] = series.length - ? series[0].data.map(({name}, i) => [ + ? series[0]!.data.map(({name}, i) => [ name, - series.reduce((sum, {data}) => sum + data[i].value, 0), + series.reduce((sum, {data}) => sum + data[i]!.value, 0), ]) : []; const totals = new Map(totalsArray); diff --git a/static/app/components/charts/pieChart.tsx b/static/app/components/charts/pieChart.tsx index 94dd202b984b56..0d80739112e579 100644 --- a/static/app/components/charts/pieChart.tsx +++ b/static/app/components/charts/pieChart.tsx @@ -77,7 +77,7 @@ class PieChart extends Component { .reduce( (acc, [name, value]) => ({ ...acc, - [name]: value, + [name!]: value, }), {} ); @@ -95,7 +95,7 @@ class PieChart extends Component { // Note, we only take the first series unit! const [firstSeries] = series; - const seriesPercentages = this.getSeriesPercentages(firstSeries); + const seriesPercentages = this.getSeriesPercentages(firstSeries!); return ( { if ( !this.isInitialSelected || !name || - firstSeries.data[this.selected].name === name + firstSeries!.data[this.selected]!.name === name ) { return; } @@ -159,8 +159,8 @@ class PieChart extends Component { }} series={[ PieSeries({ - name: firstSeries.seriesName, - data: firstSeries.data, + name: firstSeries!.seriesName, + data: firstSeries!.data, avoidLabelOverlap: false, label: { formatter: ({name, percent}) => `${name}\n${percent}%`, diff --git a/static/app/components/charts/releaseSeries.tsx b/static/app/components/charts/releaseSeries.tsx index b126c57ee68cc4..9fc3285d9acaf5 100644 --- a/static/app/components/charts/releaseSeries.tsx +++ b/static/app/components/charts/releaseSeries.tsx @@ -175,7 +175,7 @@ class ReleaseSeries extends Component { if (pageLinks) { const paginationObject = parseLinkHeader(pageLinks); hasMore = paginationObject?.next?.results ?? false; - conditions.cursor = paginationObject.next.cursor; + conditions.cursor = paginationObject.next!.cursor; } else { hasMore = false; } diff --git a/static/app/components/charts/utils.tsx b/static/app/components/charts/utils.tsx index ac03e159a78a1c..66c3c3b5461718 100644 --- a/static/app/components/charts/utils.tsx +++ b/static/app/components/charts/utils.tsx @@ -273,7 +273,7 @@ export const getDimensionValue = (dimension?: number | string | null) => { }; const RGB_LIGHTEN_VALUE = 30; -export const lightenHexToRgb = (colors: string[]) => +export const lightenHexToRgb = (colors: ReadonlyArray) => colors.map(hex => { const rgb = [ Math.min(parseInt(hex.slice(1, 3), 16) + RGB_LIGHTEN_VALUE, 255), @@ -292,7 +292,7 @@ export const processTableResults = (tableResults?: TableDataWithTitle[]) => { return DEFAULT_GEO_DATA; } - const tableResult = tableResults[0]; + const tableResult = tableResults[0]!; const {data} = tableResult; @@ -300,7 +300,7 @@ export const processTableResults = (tableResults?: TableDataWithTitle[]) => { return DEFAULT_GEO_DATA; } - const preAggregate = Object.keys(data[0]).find(column => { + const preAggregate = Object.keys(data[0]!).find(column => { return column !== 'geo.country_code'; }); @@ -309,7 +309,7 @@ export const processTableResults = (tableResults?: TableDataWithTitle[]) => { } return { - title: tableResult.title ?? '', + title: tableResult!.title ?? '', data: data.map(row => { return { name: row['geo.country_code'] as string, diff --git a/static/app/components/chevron.tsx b/static/app/components/chevron.tsx index d80b62050d3e22..dfa9f19cfae203 100644 --- a/static/app/components/chevron.tsx +++ b/static/app/components/chevron.tsx @@ -34,7 +34,7 @@ function getPath(direction: NonNullable) { [3.5, 5.5], [7, 9], [10.5, 5.5], - ]; + ] as const; switch (direction) { case 'right': diff --git a/static/app/components/collapsible.spec.tsx b/static/app/components/collapsible.spec.tsx index 5abc539696c81a..80ab6db5a0864f 100644 --- a/static/app/components/collapsible.spec.tsx +++ b/static/app/components/collapsible.spec.tsx @@ -10,7 +10,7 @@ describe('Collapsible', function () { render({items}); expect(screen.getAllByText(/Item/)).toHaveLength(5); - expect(screen.getAllByText(/Item/)[2].innerHTML).toBe('Item 3'); + expect(screen.getAllByText(/Item/)[2]!.innerHTML).toBe('Item 3'); expect(screen.getByLabelText('Show 2 hidden items')).toBeInTheDocument(); expect(screen.queryByLabelText('Collapse')).not.toBeInTheDocument(); diff --git a/static/app/components/compactSelect/control.tsx b/static/app/components/compactSelect/control.tsx index 141d148d7a4f77..77423cf6ee32d8 100644 --- a/static/app/components/compactSelect/control.tsx +++ b/static/app/components/compactSelect/control.tsx @@ -524,9 +524,9 @@ export function Control({ > ({ disallowEmptySelection: disallowEmptySelection ?? true, allowDuplicateSelectionEvents: true, onSelectionChange: selection => { - const selectedOption = getSelectedOptions(items, selection)[0] ?? null; + const selectedOption = getSelectedOptions(items, selection)[0]! ?? null; // Save selected options in SelectContext, to update the trigger label saveSelectedOptions(compositeIndex, selectedOption); onChange?.(selectedOption); diff --git a/static/app/components/compactSelect/utils.tsx b/static/app/components/compactSelect/utils.tsx index 768080ccf0f88a..b5f80ae9c6ec67 100644 --- a/static/app/components/compactSelect/utils.tsx +++ b/static/app/components/compactSelect/utils.tsx @@ -152,7 +152,7 @@ export function getHiddenOptions( let currentIndex = 0; while (currentIndex < remainingItems.length) { - const item = remainingItems[currentIndex]; + const item = remainingItems[currentIndex]!; const delta = 'options' in item ? item.options.length : 1; if (accumulator + delta > limit) { @@ -164,12 +164,12 @@ export function getHiddenOptions( currentIndex += 1; } - for (let i = threshold[0]; i < remainingItems.length; i++) { - const item = remainingItems[i]; + for (let i = threshold[0]!; i < remainingItems.length; i++) { + const item = remainingItems[i]!; if ('options' in item) { - const startingIndex = i === threshold[0] ? threshold[1] : 0; + const startingIndex = i === threshold[0] ? threshold[1]! : 0; for (let j = startingIndex; j < item.options.length; j++) { - hiddenOptionsSet.add(item.options[j].key); + hiddenOptionsSet.add(item.options[j]!.key); } } else { hiddenOptionsSet.add(item.key); diff --git a/static/app/components/contextPickerModal.spec.tsx b/static/app/components/contextPickerModal.spec.tsx index 942db40f2d41ee..3fc5eddd801c7f 100644 --- a/static/app/components/contextPickerModal.spec.tsx +++ b/static/app/components/contextPickerModal.spec.tsx @@ -166,7 +166,7 @@ describe('ContextPickerModal', function () { ]; const fetchProjectsForOrg = MockApiClient.addMockResponse({ url: `/organizations/${org2.slug}/projects/`, - body: organizations[1].projects, + body: organizations[1]!.projects, }); OrganizationsStore.load(organizations); diff --git a/static/app/components/contextPickerModal.tsx b/static/app/components/contextPickerModal.tsx index 7955357540a144..bb52bad7ba9363 100644 --- a/static/app/components/contextPickerModal.tsx +++ b/static/app/components/contextPickerModal.tsx @@ -147,7 +147,7 @@ class ContextPickerModal extends Component { // If there is only one org and we don't need a project slug, then call finish callback if (!needProject) { const newPathname = replaceRouterParams(pathname, { - orgId: organizations[0].slug, + orgId: organizations[0]!.slug, }); this.onFinishTimeout = onFinish( @@ -161,13 +161,13 @@ class ContextPickerModal extends Component { // Use latest org or if only 1 org, use that let org = latestOrg; if (!org && organizations.length === 1) { - org = organizations[0].slug; + org = organizations[0]!.slug; } const newPathname = replaceRouterParams(pathname, { orgId: org, - projectId: projects[0].slug, - project: this.props.projects.find(p => p.slug === projects[0].slug)?.id, + projectId: projects[0]!.slug, + project: this.props.projects.find(p => p.slug === projects[0]!.slug)?.id, }); this.onFinishTimeout = onFinish( @@ -268,7 +268,7 @@ class ContextPickerModal extends Component { const projectOptions = [ { label: t('My Projects'), - options: memberProjects.map(p => ({ + options: memberProjects!.map(p => ({ value: p.slug, label: p.slug, disabled: false, @@ -276,7 +276,7 @@ class ContextPickerModal extends Component { }, { label: t('All Projects'), - options: nonMemberProjects.map(p => ({ + options: nonMemberProjects!.map(p => ({ value: p.slug, label: p.slug, disabled: allowAllProjectsSelection ? false : !isSuperuser, @@ -317,7 +317,7 @@ class ContextPickerModal extends Component { const options = [ { label: tct('[providerName] Configurations', { - providerName: integrationConfigs[0].provider.name, + providerName: integrationConfigs[0]!.provider.name, }), options: integrationConfigs.map(config => ({ value: config.id, diff --git a/static/app/components/deprecatedAssigneeSelector.spec.tsx b/static/app/components/deprecatedAssigneeSelector.spec.tsx index c11f5d77679d8b..ad359ce728da1d 100644 --- a/static/app/components/deprecatedAssigneeSelector.spec.tsx +++ b/static/app/components/deprecatedAssigneeSelector.spec.tsx @@ -349,7 +349,7 @@ describe('DeprecatedAssigneeSelector', () => { const options = screen.getAllByTestId('assignee-option'); expect(options[5]).toHaveTextContent('JD'); - await userEvent.click(options[4]); + await userEvent.click(options[4]!); await waitFor(() => { expect(addMessageSpy).toHaveBeenCalledWith( @@ -379,7 +379,7 @@ describe('DeprecatedAssigneeSelector', () => { const options = screen.getAllByTestId('assignee-option'); // Suggested assignee initials expect(options[0]).toHaveTextContent('JB'); - await userEvent.click(options[0]); + await userEvent.click(options[0]!); await waitFor(() => expect(assignGroup2Mock).toHaveBeenCalledWith( diff --git a/static/app/components/deprecatedAssigneeSelector.tsx b/static/app/components/deprecatedAssigneeSelector.tsx index 4a9f9427693b60..029ec7ae6ce8e6 100644 --- a/static/app/components/deprecatedAssigneeSelector.tsx +++ b/static/app/components/deprecatedAssigneeSelector.tsx @@ -72,6 +72,7 @@ export function AssigneeAvatar({ } if (suggestedActors.length > 0) { + const firstActor = suggestedActors[0]!; return ( {tct('Suggestion: [name]', { name: - suggestedActors[0].type === 'team' - ? `#${suggestedActors[0].name}` - : suggestedActors[0].name, + firstActor.type === 'team' ? `#${firstActor.name}` : firstActor.name, })} {suggestedActors.length > 1 && tn(' + %s other', ' + %s others', suggestedActors.length - 1)} - {suggestedReasons[suggestedActors[0].suggestedReason]} + {suggestedReasons[firstActor.suggestedReason]} } diff --git a/static/app/components/deprecatedAssigneeSelectorDropdown.tsx b/static/app/components/deprecatedAssigneeSelectorDropdown.tsx index 0e2b351b38343e..66f34f86fe65d8 100644 --- a/static/app/components/deprecatedAssigneeSelectorDropdown.tsx +++ b/static/app/components/deprecatedAssigneeSelectorDropdown.tsx @@ -370,7 +370,7 @@ export class DeprecatedAssigneeSelectorDropdown extends Component< )[] = []; for (let i = 0; i < suggestedAssignees.length; i++) { - const assignee = suggestedAssignees[i]; + const assignee = suggestedAssignees[i]!; if (assignee.type !== 'user' && assignee.type !== 'team') { continue; } @@ -514,7 +514,7 @@ export class DeprecatedAssigneeSelectorDropdown extends Component< const member = memberList.find(user => user.id === id); if (member) { return { - id, + id: id!, type: 'user', name: member.name, suggestedReason: owner.type, @@ -528,7 +528,7 @@ export class DeprecatedAssigneeSelectorDropdown extends Component< ); if (matchingTeam) { return { - id, + id: id!, type: 'team', name: matchingTeam.team.name, suggestedReason: owner.type, @@ -606,7 +606,7 @@ export function putSessionUserFirst(members: User[] | undefined): User[] { return members; } - const arrangedMembers = [members[sessionUserIndex]].concat( + const arrangedMembers = [members[sessionUserIndex]!].concat( members.slice(0, sessionUserIndex), members.slice(sessionUserIndex + 1) ); diff --git a/static/app/components/deprecatedSmartSearchBar/index.tsx b/static/app/components/deprecatedSmartSearchBar/index.tsx index f3774f3784bf7b..4f3d3c024cc958 100644 --- a/static/app/components/deprecatedSmartSearchBar/index.tsx +++ b/static/app/components/deprecatedSmartSearchBar/index.tsx @@ -560,7 +560,7 @@ class DeprecatedSmartSearchBar extends Component { return; } - const entry = entries[0]; + const entry = entries[0]!; const {width} = entry.contentRect; const actionCount = this.props.actionBarItems?.length ?? 0; @@ -649,11 +649,11 @@ class DeprecatedSmartSearchBar extends Component { if (this.searchInput.current && filterTokens.length > 0) { maybeFocusInput(this.searchInput.current); - let offset = filterTokens[0].location.end.offset; + let offset = filterTokens[0]!.location.end.offset; if (token) { const tokenIndex = filterTokens.findIndex(tok => tok === token); if (tokenIndex !== -1 && tokenIndex + 1 < filterTokens.length) { - offset = filterTokens[tokenIndex + 1].location.end.offset; + offset = filterTokens[tokenIndex + 1]!.location.end.offset; } } @@ -958,12 +958,12 @@ class DeprecatedSmartSearchBar extends Component { : 0; // Clear previous selection - const prevItem = flatSearchItems[currIndex]; + const prevItem = flatSearchItems[currIndex]!; searchGroups = getSearchGroupWithItemMarkedActive(searchGroups, prevItem, false); // Set new selection const activeItem = flatSearchItems[nextActiveSearchItem]; - searchGroups = getSearchGroupWithItemMarkedActive(searchGroups, activeItem, true); + searchGroups = getSearchGroupWithItemMarkedActive(searchGroups, activeItem!, true); this.setState({searchGroups, activeSearchItem: nextActiveSearchItem}); } @@ -1055,7 +1055,7 @@ class DeprecatedSmartSearchBar extends Component { if (isSelectingDropdownItems) { searchGroups = getSearchGroupWithItemMarkedActive( searchGroups, - flatSearchItems[activeSearchItem], + flatSearchItems[activeSearchItem]!, false ); } @@ -1203,13 +1203,13 @@ class DeprecatedSmartSearchBar extends Component { const innerStart = cursorPosition - cursorToken.location.start.offset; let tokenStart = innerStart; - while (tokenStart > 0 && !LIMITER_CHARS.includes(cursorToken.text[tokenStart - 1])) { + while (tokenStart > 0 && !LIMITER_CHARS.includes(cursorToken.text[tokenStart - 1]!)) { tokenStart--; } let tokenEnd = innerStart; while ( tokenEnd < cursorToken.text.length && - !LIMITER_CHARS.includes(cursorToken.text[tokenEnd]) + !LIMITER_CHARS.includes(cursorToken.text[tokenEnd]!) ) { tokenEnd++; } diff --git a/static/app/components/deprecatedSmartSearchBar/searchDropdown.tsx b/static/app/components/deprecatedSmartSearchBar/searchDropdown.tsx index 12c04d9ce380e9..7361ad146242a0 100644 --- a/static/app/components/deprecatedSmartSearchBar/searchDropdown.tsx +++ b/static/app/components/deprecatedSmartSearchBar/searchDropdown.tsx @@ -22,7 +22,7 @@ import {invalidTypes, ItemType} from './types'; const getDropdownItemKey = (item: SearchItem) => `${item.value || item.desc || item.title}-${ - item.children && item.children.length > 0 ? getDropdownItemKey(item.children[0]) : '' + item.children && item.children.length > 0 ? getDropdownItemKey(item.children[0]!) : '' }`; type Props = { @@ -244,7 +244,7 @@ function ItemTitle({item, searchSubstring, isChild}: ItemTitleProps) { if (searchSubstring) { const idx = restWords.length === 0 - ? fullWord.toLowerCase().indexOf(searchSubstring.split('.')[0]) + ? fullWord.toLowerCase().indexOf(searchSubstring.split('.')[0]!) : fullWord.toLowerCase().indexOf(searchSubstring); // Below is the logic to make the current query bold inside the result. @@ -253,14 +253,14 @@ function ItemTitle({item, searchSubstring, isChild}: ItemTitleProps) { {!isFirstWordHidden && ( - {firstWord.slice(0, idx)} - {firstWord.slice(idx, idx + searchSubstring.length)} - {firstWord.slice(idx + searchSubstring.length)} + {firstWord!.slice(0, idx)} + {firstWord!.slice(idx, idx + searchSubstring.length)} + {firstWord!.slice(idx + searchSubstring.length)} )} {combinedRestWords && ( {names?.map(name => ( - + ))} diff --git a/static/app/components/devtoolbar/components/infiniteListItems.tsx b/static/app/components/devtoolbar/components/infiniteListItems.tsx index 772c1372c2a9d4..c025eb230ec3ca 100644 --- a/static/app/components/devtoolbar/components/infiniteListItems.tsx +++ b/static/app/components/devtoolbar/components/infiniteListItems.tsx @@ -79,7 +79,7 @@ export default function InfiniteListItems({ ? hasNextPage ? loadingMoreMessage() : loadingCompleteMessage() - : itemRenderer({item})} + : itemRenderer({item: item!})} ); })} diff --git a/static/app/components/devtoolbar/components/releases/releasesPanel.tsx b/static/app/components/devtoolbar/components/releases/releasesPanel.tsx index 5215f1b341846e..6bc3ad5067c1a6 100644 --- a/static/app/components/devtoolbar/components/releases/releasesPanel.tsx +++ b/static/app/components/devtoolbar/components/releases/releasesPanel.tsx @@ -53,7 +53,7 @@ function getCrashFreeRate(data: ApiResult): number { // assume it is 100%. // round to 2 decimal points return parseFloat( - ((data?.json.groups[0].totals['crash_free_rate(session)'] ?? 1) * 100).toFixed(2) + ((data?.json.groups[0]!.totals['crash_free_rate(session)'] ?? 1) * 100).toFixed(2) ); } @@ -101,7 +101,7 @@ function ReleaseSummary({orgSlug, release}: {orgSlug: string; release: Release}) {formatVersion(release.version)} @@ -244,14 +244,14 @@ export default function ReleasesPanel() { ) : (
    - + 1 ? releaseData.json[1].version : undefined + releaseData.json.length > 1 ? releaseData.json[1]!.version : undefined } /> - +
    )} diff --git a/static/app/components/discover/transactionsTable.tsx b/static/app/components/discover/transactionsTable.tsx index 87fc301df2ba96..7fbd410dde4174 100644 --- a/static/app/components/discover/transactionsTable.tsx +++ b/static/app/components/discover/transactionsTable.tsx @@ -76,7 +76,7 @@ function TransactionsTable(props: Props) { const tableTitles = getTitles(); const headers = tableTitles.map((title, index) => { - const column = columnOrder[index]; + const column = columnOrder[index]!; const align: Alignments = fieldAlignment(column.name, column.type, tableMeta); if (column.key === 'span_ops_breakdown.relative') { @@ -163,7 +163,7 @@ function TransactionsTable(props: Props) { } else if (target && !isEmptyObject(target)) { if (fields[index] === 'replayId') { rendered = ( - + {rendered} ); diff --git a/static/app/components/draggableTabs/draggableTabList.tsx b/static/app/components/draggableTabs/draggableTabList.tsx index 31df02af17428e..c921768f2d4d5e 100644 --- a/static/app/components/draggableTabs/draggableTabList.tsx +++ b/static/app/components/draggableTabs/draggableTabList.tsx @@ -62,9 +62,9 @@ function useOverflowingTabs({state}: {state: TabListState[] = []; for (let i = 0; i < tabsDimensions.length; i++) { - totalWidth += tabsDimensions[i].width + 1; // 1 extra pixel for the divider + totalWidth += tabsDimensions[i]!.width + 1; // 1 extra pixel for the divider if (totalWidth > availableWidth + 1) { - overflowing.push(persistentTabs[i]); + overflowing.push(persistentTabs[i]!); } } diff --git a/static/app/components/dropdownAutoComplete/list.tsx b/static/app/components/dropdownAutoComplete/list.tsx index 275762171ec8b8..37e43b5342fdb3 100644 --- a/static/app/components/dropdownAutoComplete/list.tsx +++ b/static/app/components/dropdownAutoComplete/list.tsx @@ -82,7 +82,7 @@ function List({ onScroll={onScroll} rowCount={items.length} rowHeight={({index}) => - items[index].groupLabel && virtualizedLabelHeight + items[index]!.groupLabel && virtualizedLabelHeight ? virtualizedLabelHeight : virtualizedHeight } @@ -90,8 +90,8 @@ function List({ )} diff --git a/static/app/components/events/autofix/autofixDiff.tsx b/static/app/components/events/autofix/autofixDiff.tsx index c076e645adeed3..d69b92e88c56e8 100644 --- a/static/app/components/events/autofix/autofixDiff.tsx +++ b/static/app/components/events/autofix/autofixDiff.tsx @@ -43,7 +43,7 @@ function makeTestIdFromLineType(lineType: DiffLineType) { function addChangesToDiffLines(lines: DiffLineWithChanges[]): DiffLineWithChanges[] { for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + const line = lines[i]!; if (line.line_type === DiffLineType.CONTEXT) { continue; } @@ -293,7 +293,7 @@ function DiffHunkContent({ // Update diff_line_no for all lines after the insertion for (let i = insertionIndex + newAddedLines.length; i < updatedLines.length; i++) { - updatedLines[i].diff_line_no = ++lastDiffLineNo; + updatedLines[i]!.diff_line_no = ++lastDiffLineNo; } updateHunk.mutate({hunkIndex, lines: updatedLines, repoId, fileName}); @@ -332,15 +332,15 @@ function DiffHunkContent({ }; const getStartLineNumber = (index: number, lineType: DiffLineType) => { - const line = linesWithChanges[index]; + const line = linesWithChanges[index]!; if (lineType === DiffLineType.REMOVED) { return line.source_line_no; } if (lineType === DiffLineType.ADDED) { // Find the first non-null target_line_no for (let i = index; i < linesWithChanges.length; i++) { - if (linesWithChanges[i].target_line_no !== null) { - return linesWithChanges[i].target_line_no; + if (linesWithChanges[i]!.target_line_no !== null) { + return linesWithChanges[i]!.target_line_no; } } } diff --git a/static/app/components/events/autofix/autofixMessageBox.analytics.spec.tsx b/static/app/components/events/autofix/autofixMessageBox.analytics.spec.tsx index 15c20464c7bb15..90c0902494c002 100644 --- a/static/app/components/events/autofix/autofixMessageBox.analytics.spec.tsx +++ b/static/app/components/events/autofix/autofixMessageBox.analytics.spec.tsx @@ -98,7 +98,7 @@ describe('AutofixMessageBox Analytics', () => { ); - await userEvent.click(screen.getAllByText('Propose your own root cause')[0]); + await userEvent.click(screen.getAllByText('Propose your own root cause')[0]!); const customInput = screen.getByPlaceholderText('Propose your own root cause...'); await userEvent.type(customInput, 'Custom root cause'); diff --git a/static/app/components/events/autofix/autofixRootCause.tsx b/static/app/components/events/autofix/autofixRootCause.tsx index d0a95855274952..53ff3ca1b014a1 100644 --- a/static/app/components/events/autofix/autofixRootCause.tsx +++ b/static/app/components/events/autofix/autofixRootCause.tsx @@ -383,7 +383,7 @@ function AutofixRootCauseDisplay({ rootCauseSelection, repos, }: AutofixRootCauseProps) { - const [selectedId, setSelectedId] = useState(() => causes[0].id); + const [selectedId, setSelectedId] = useState(() => causes[0]!.id); const {isPending, mutate: handleSelectFix} = useSelectCause({groupId, runId}); if (rootCauseSelection) { diff --git a/static/app/components/events/autofix/autofixSteps.tsx b/static/app/components/events/autofix/autofixSteps.tsx index d6e7d10b3b389f..490b02474edab4 100644 --- a/static/app/components/events/autofix/autofixSteps.tsx +++ b/static/app/components/events/autofix/autofixSteps.tsx @@ -136,11 +136,11 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { if (!steps) { return; } - const step = steps[steps.length - 1]; + const step = steps[steps.length - 1]!; if (step.type !== AutofixStepType.ROOT_CAUSE_ANALYSIS) { return; } - const cause = step.causes[0]; + const cause = step.causes[0]!; const id = cause.id; handleSelectFix({causeId: id, instruction: text}); } @@ -164,8 +164,8 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { - setIsBottomVisible(entry.isIntersecting); - if (entry.isIntersecting) { + setIsBottomVisible(entry!.isIntersecting); + if (entry!.isIntersecting) { setHasSeenBottom(true); } }, @@ -194,7 +194,7 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { const hasNewSteps = currentStepsLength > prevStepsLengthRef.current && - steps[currentStepsLength - 1].type !== AutofixStepType.DEFAULT; + steps[currentStepsLength - 1]!.type !== AutofixStepType.DEFAULT; const hasNewInsights = currentInsightsCount > prevInsightsCountRef.current; if (hasNewSteps || hasNewInsights) { @@ -217,16 +217,18 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) { } const lastStep = steps[steps.length - 1]; - const logs: AutofixProgressItem[] = lastStep.progress?.filter(isProgressLog) ?? []; + const logs: AutofixProgressItem[] = lastStep!.progress?.filter(isProgressLog) ?? []; const activeLog = - lastStep.completedMessage ?? replaceHeadersWithBold(logs.at(-1)?.message ?? '') ?? ''; + lastStep!.completedMessage ?? + replaceHeadersWithBold(logs.at(-1)?.message ?? '') ?? + ''; const isRootCauseSelectionStep = - lastStep.type === AutofixStepType.ROOT_CAUSE_ANALYSIS && - lastStep.status === 'COMPLETED'; + lastStep!.type === AutofixStepType.ROOT_CAUSE_ANALYSIS && + lastStep!.status === 'COMPLETED'; const isChangesStep = - lastStep.type === AutofixStepType.CHANGES && lastStep.status === 'COMPLETED'; + lastStep!.type === AutofixStepType.CHANGES && lastStep!.status === 'COMPLETED'; return (
    @@ -281,15 +283,15 @@ export function AutofixSteps({data, groupId, runId}: AutofixStepsProps) {
    ); })} - {lastStep.output_stream && ( - + {lastStep!.output_stream && ( + )} { const {breadcrumb, raw, title, meta, iconComponent, colorConfig, levelComponent} = - breadcrumbs[virtualizedRow.index]; + breadcrumbs[virtualizedRow.index]!; const isVirtualCrumb = !defined(raw); const timeDate = new Date(breadcrumb.timestamp ?? ''); diff --git a/static/app/components/events/breadcrumbs/testUtils.tsx b/static/app/components/events/breadcrumbs/testUtils.tsx index 677cebb470a448..6a5ea4a70fc3ed 100644 --- a/static/app/components/events/breadcrumbs/testUtils.tsx +++ b/static/app/components/events/breadcrumbs/testUtils.tsx @@ -49,7 +49,7 @@ export const MOCK_BREADCRUMBS = [ type: BreadcrumbType.DEFAULT, timestamp: oneMinuteBeforeEventFixture, }, -]; +] as const; const MOCK_BREADCRUMB_ENTRY = { type: EntryType.BREADCRUMBS, data: { diff --git a/static/app/components/events/breadcrumbs/utils.tsx b/static/app/components/events/breadcrumbs/utils.tsx index d14345302ef1e6..641b82e68d9ea5 100644 --- a/static/app/components/events/breadcrumbs/utils.tsx +++ b/static/app/components/events/breadcrumbs/utils.tsx @@ -143,9 +143,9 @@ export function useBreadcrumbFilters(crumbs: EnhancedCrumb[]) { options.forEach(optionValue => { const [indicator, value] = optionValue.split('-'); if (indicator === 'type') { - typeFilterSet.add(value); + typeFilterSet.add(value!); } else if (indicator === 'level') { - levelFilterSet.add(value); + levelFilterSet.add(value!); } }); diff --git a/static/app/components/events/contexts/contextCard.spec.tsx b/static/app/components/events/contexts/contextCard.spec.tsx index fa6e0fd93280c6..c716b4652f0e19 100644 --- a/static/app/components/events/contexts/contextCard.spec.tsx +++ b/static/app/components/events/contexts/contextCard.spec.tsx @@ -76,7 +76,7 @@ describe('ContextCard', function () { project={project} /> ); - expect(iconSpy.mock.calls[0][0]).toBe(browserContext.name); + expect(iconSpy.mock.calls[0]![0]).toBe(browserContext.name); expect(screen.getByRole('img')).toBeInTheDocument(); iconSpy.mockReset(); diff --git a/static/app/components/events/contexts/utils.spec.tsx b/static/app/components/events/contexts/utils.spec.tsx index 968d5ae8eb6a74..a5a93cb9222225 100644 --- a/static/app/components/events/contexts/utils.spec.tsx +++ b/static/app/components/events/contexts/utils.spec.tsx @@ -154,10 +154,10 @@ describe('contexts utils', function () { }; const knownStructuredData = getKnownStructuredData(knownData, errMeta); - expect(knownData[0].key).toEqual(knownStructuredData[0].key); - expect(knownData[0].subject).toEqual(knownStructuredData[0].subject); - render({knownStructuredData[0].value as React.ReactNode}); - expect(screen.getByText(`${knownData[0].value}`)).toBeInTheDocument(); + expect(knownData[0]!.key).toEqual(knownStructuredData[0]!.key); + expect(knownData[0]!.subject).toEqual(knownStructuredData[0]!.subject); + render({knownStructuredData[0]!.value as React.ReactNode}); + expect(screen.getByText(`${knownData[0]!.value}`)).toBeInTheDocument(); expect(screen.getByTestId('annotated-text-error-icon')).toBeInTheDocument(); }); }); diff --git a/static/app/components/events/contexts/utils.tsx b/static/app/components/events/contexts/utils.tsx index 8ad210103a7935..8d79c598b8f066 100644 --- a/static/app/components/events/contexts/utils.tsx +++ b/static/app/components/events/contexts/utils.tsx @@ -80,21 +80,21 @@ export function generateIconName( } const formattedName = name - .split(/\d/)[0] + .split(/\d/)[0]! .toLowerCase() .replace(/[^a-z0-9\-]+/g, '-') .replace(/\-+$/, '') .replace(/^\-+/, ''); if (formattedName === 'edge' && version) { - const majorVersion = version.split('.')[0]; + const majorVersion = version.split('.')[0]!; const isLegacyEdge = majorVersion >= '12' && majorVersion <= '18'; return isLegacyEdge ? 'legacy-edge' : 'edge'; } if (formattedName.endsWith('-mobile')) { - return formattedName.split('-')[0]; + return formattedName.split('-')[0]!; } return formattedName; diff --git a/static/app/components/events/errorItem.spec.tsx b/static/app/components/events/errorItem.spec.tsx index c3d2b89bf85bcf..d4a39b7a205bce 100644 --- a/static/app/components/events/errorItem.spec.tsx +++ b/static/app/components/events/errorItem.spec.tsx @@ -50,7 +50,7 @@ describe('Issue error item', function () { expect(screen.getByText('File Path')).toBeInTheDocument(); expect(screen.getAllByText(/redacted/)).toHaveLength(2); - await userEvent.hover(screen.getAllByText(/redacted/)[0]); + await userEvent.hover(screen.getAllByText(/redacted/)[0]!); expect( await screen.findByText( diff --git a/static/app/components/events/eventAttachments.spec.tsx b/static/app/components/events/eventAttachments.spec.tsx index 3f0a772cb3368c..113d9d8d350ced 100644 --- a/static/app/components/events/eventAttachments.spec.tsx +++ b/static/app/components/events/eventAttachments.spec.tsx @@ -27,11 +27,11 @@ describe('EventAttachments', function () { const props = { group: undefined, - project, + project: project!, event, }; - const attachmentsUrl = `/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/`; + const attachmentsUrl = `/projects/${organization.slug}/${project!.slug}/events/${event.id}/attachments/`; beforeEach(() => { ConfigStore.loadInitialData(ConfigFixture()); @@ -60,7 +60,7 @@ describe('EventAttachments', function () { expect(screen.getByRole('link', {name: 'configure limit'})).toHaveAttribute( 'href', - `/settings/org-slug/projects/${project.slug}/security-and-privacy/` + `/settings/org-slug/projects/${project!.slug}/security-and-privacy/` ); expect( @@ -144,7 +144,7 @@ describe('EventAttachments', function () { }); MockApiClient.addMockResponse({ - url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/1/?download`, + url: `/projects/${organization.slug}/${project!.slug}/events/${event.id}/attachments/1/?download`, body: 'file contents', }); @@ -180,7 +180,7 @@ describe('EventAttachments', function () { expect(await screen.findByText('Attachments (2)')).toBeInTheDocument(); - await userEvent.click(screen.getAllByRole('button', {name: 'Delete'})[0]); + await userEvent.click(screen.getAllByRole('button', {name: 'Delete'})[0]!); await userEvent.click( within(screen.getByRole('dialog')).getByRole('button', {name: /delete/i}) ); diff --git a/static/app/components/events/eventEntries.tsx b/static/app/components/events/eventEntries.tsx index 41283b321143d7..bdc4d64addcafa 100644 --- a/static/app/components/events/eventEntries.tsx +++ b/static/app/components/events/eventEntries.tsx @@ -195,12 +195,12 @@ export function Entries({ return ( {!hideBeforeReplayEntries && - beforeReplayEntries.map((entry, entryIdx) => ( + beforeReplayEntries!.map((entry, entryIdx) => ( ))} {!isShare && } {!isShare && } - {afterReplayEntries.map((entry, entryIdx) => { + {afterReplayEntries!.map((entry, entryIdx) => { if (hideBreadCrumbs && entry.type === EntryType.BREADCRUMBS) { return null; } diff --git a/static/app/components/events/eventExtraData/index.spec.tsx b/static/app/components/events/eventExtraData/index.spec.tsx index 0038ce1445fa49..1dd2c9d9bc429e 100644 --- a/static/app/components/events/eventExtraData/index.spec.tsx +++ b/static/app/components/events/eventExtraData/index.spec.tsx @@ -181,7 +181,7 @@ describe('EventExtraData', function () { await userEvent.click(screen.getByRole('button', {name: 'Expand'})); expect(await screen.findAllByText(/redacted/)).toHaveLength(10); - await userEvent.hover(screen.getAllByText(/redacted/)[0]); + await userEvent.hover(screen.getAllByText(/redacted/)[0]!); expect( await screen.findByText( diff --git a/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx b/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx index 90d06bf3cc21d9..e1013e44ebdc8b 100644 --- a/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx +++ b/static/app/components/events/eventStatisticalDetector/aggregateSpanDiff.tsx @@ -149,8 +149,8 @@ function AggregateSpanDiff({event, project}: AggregateSpanDiffProps) { }; if (causeType === 'throughput') { - const throughputBefore = row[`epm_by_timestamp(less,${breakpoint})`]; - const throughputAfter = row[`epm_by_timestamp(greater,${breakpoint})`]; + const throughputBefore = row[`epm_by_timestamp(less,${breakpoint})`]!; + const throughputAfter = row[`epm_by_timestamp(greater,${breakpoint})`]!; return { ...commonProps, throughputBefore, @@ -160,9 +160,9 @@ function AggregateSpanDiff({event, project}: AggregateSpanDiffProps) { } const durationBefore = - row[`avg_by_timestamp(span.self_time,less,${breakpoint})`] / 1e3; + row[`avg_by_timestamp(span.self_time,less,${breakpoint})`]! / 1e3; const durationAfter = - row[`avg_by_timestamp(span.self_time,greater,${breakpoint})`] / 1e3; + row[`avg_by_timestamp(span.self_time,greater,${breakpoint})`]! / 1e3; return { ...commonProps, durationBefore, diff --git a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx index 37a80abb359873..a4af44ae8ccc73 100644 --- a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx @@ -163,7 +163,7 @@ function EventDisplay({ useEffect(() => { if (defined(eventIds) && eventIds.length > 0 && !selectedEventId) { - setSelectedEventId(eventIds[0]); + setSelectedEventId(eventIds[0]!); } }, [eventIds, selectedEventId]); @@ -242,7 +242,7 @@ function EventDisplay({ icon={} onPaginate={() => { if (hasPrev) { - setSelectedEventId(eventIds[eventIdIndex - 1]); + setSelectedEventId(eventIds[eventIdIndex - 1]!); } }} /> @@ -252,7 +252,7 @@ function EventDisplay({ icon={} onPaginate={() => { if (hasNext) { - setSelectedEventId(eventIds[eventIdIndex + 1]); + setSelectedEventId(eventIds[eventIdIndex + 1]!); } }} /> diff --git a/static/app/components/events/eventStatisticalDetector/eventThroughput.tsx b/static/app/components/events/eventStatisticalDetector/eventThroughput.tsx index 40c86132577dc4..438b82d19ab8d0 100644 --- a/static/app/components/events/eventStatisticalDetector/eventThroughput.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventThroughput.tsx @@ -100,7 +100,7 @@ function EventThroughputInner({event, group}: EventThroughputProps) { const result = transformEventStats( stats.series.map(item => [item.timestamp, [{count: item.value / item.interval}]]), 'throughput()' - )[0]; + )[0]!; result.markLine = { data: [ @@ -293,7 +293,7 @@ function useThroughputStats({datetime, event, group}: UseThroughputStatsOptions) if (data.length < 2) { return null; } - return data[1][0] - data[0][0]; + return data[1]![0] - data[0]![0]; }, [transactionStats?.data]); const transactionData = useMemo(() => { @@ -305,7 +305,7 @@ function useThroughputStats({datetime, event, group}: UseThroughputStatsOptions) const timestamp = curr[0]; const bucket = Math.floor(timestamp / BUCKET_SIZE) * BUCKET_SIZE; const prev = acc[acc.length - 1]; - const value = curr[1][0].count; + const value = curr[1]![0]!.count; if (prev?.timestamp === bucket) { prev.value += value; diff --git a/static/app/components/events/eventStatisticalDetector/spanOpBreakdown.tsx b/static/app/components/events/eventStatisticalDetector/spanOpBreakdown.tsx index 2b84d78ec6405b..d61515d196f450 100644 --- a/static/app/components/events/eventStatisticalDetector/spanOpBreakdown.tsx +++ b/static/app/components/events/eventStatisticalDetector/spanOpBreakdown.tsx @@ -121,13 +121,13 @@ function EventSpanOpBreakdown({event}: {event: Event}) { const spanOpDiffs: SpanOpDiff[] = SPAN_OPS.map(op => { const preBreakpointValue = - (preBreakpointData?.data[0][`p95(spans.${op})`] as string) || undefined; + (preBreakpointData?.data[0]![`p95(spans.${op})`] as string) || undefined; const preBreakpointValueAsNumber = preBreakpointValue ? parseInt(preBreakpointValue, 10) : 0; const postBreakpointValue = - (postBreakpointData?.data[0][`p95(spans.${op})`] as string) || undefined; + (postBreakpointData?.data[0]![`p95(spans.${op})`] as string) || undefined; const postBreakpointValueAsNumber = postBreakpointValue ? parseInt(postBreakpointValue, 10) : 0; diff --git a/static/app/components/events/eventTags/eventTagsTree.tsx b/static/app/components/events/eventTags/eventTagsTree.tsx index bad50071e54f91..d607b2c0fd1679 100644 --- a/static/app/components/events/eventTags/eventTagsTree.tsx +++ b/static/app/components/events/eventTags/eventTagsTree.tsx @@ -100,7 +100,7 @@ function getTagTreeRows({ const branchRows = getTagTreeRows({ ...props, tagKey: tag, - content: content.subtree[tag], + content: content.subtree[tag]!, spacerCount: spacerCount + 1, isLast: i === subtreeTags.length - 1, // Encoding the trunk index with the branch index ensures uniqueness for the key diff --git a/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx b/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx index 2e1911efa54670..e33c346f48b647 100644 --- a/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx +++ b/static/app/components/events/eventTagsAndScreenshot/index.spec.tsx @@ -311,7 +311,7 @@ describe('EventTagsAndScreenshot', function () { expect(screen.getByText('View screenshot')).toBeInTheDocument(); expect(screen.getByTestId('image-viewer')).toHaveAttribute( 'src', - `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${attachments[1].id}/?download` + `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${attachments[1]!.id}/?download` ); // Display help text when hovering question element @@ -354,7 +354,7 @@ describe('EventTagsAndScreenshot', function () { expect(await screen.findByText('View screenshot')).toBeInTheDocument(); expect(screen.getByTestId('image-viewer')).toHaveAttribute( 'src', - `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${attachments[1].id}/?download` + `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${attachments[1]!.id}/?download` ); expect(screen.getByTestId('screenshot-data-section')?.textContent).toContain( @@ -403,7 +403,7 @@ describe('EventTagsAndScreenshot', function () { expect(screen.getByText('View screenshot')).toBeInTheDocument(); expect(screen.getByTestId('image-viewer')).toHaveAttribute( 'src', - `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${moreAttachments[1].id}/?download` + `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${moreAttachments[1]!.id}/?download` ); await userEvent.click(screen.getByRole('button', {name: 'Next Screenshot'})); @@ -415,7 +415,7 @@ describe('EventTagsAndScreenshot', function () { expect(screen.getByText('View screenshot')).toBeInTheDocument(); expect(screen.getByTestId('image-viewer')).toHaveAttribute( 'src', - `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${moreAttachments[2].id}/?download` + `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${moreAttachments[2]!.id}/?download` ); }); @@ -473,7 +473,7 @@ describe('EventTagsAndScreenshot', function () { expect(screen.getByText('View screenshot')).toBeInTheDocument(); expect(screen.getByTestId('image-viewer')).toHaveAttribute( 'src', - `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${attachments[1].id}/?download` + `/api/0/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/${attachments[1]!.id}/?download` ); }); }); diff --git a/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx b/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx index 7fd8a8e250f2e9..a301859b702c01 100644 --- a/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx +++ b/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx @@ -65,7 +65,7 @@ export default function ScreenshotModal({ if (attachments.length) { const newIndex = currentAttachmentIndex + delta; if (newIndex >= 0 && newIndex < attachments.length) { - setCurrentAttachment(attachments[newIndex]); + setCurrentAttachment(attachments[newIndex]!); } } }, diff --git a/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx b/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx index d19b2301cdfc56..6527ad69a9ff0f 100644 --- a/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx +++ b/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx @@ -63,7 +63,7 @@ export function ScreenshotDataSection({ const [screenshotInFocus, setScreenshotInFocus] = useState(0); const showScreenshot = !isShare && !!screenshots.length; - const screenshot = screenshots[screenshotInFocus]; + const screenshot = screenshots[screenshotInFocus]!; const handleDeleteScreenshot = (attachmentId: string) => { deleteAttachment({ diff --git a/static/app/components/events/eventVitals.tsx b/static/app/components/events/eventVitals.tsx index 05c335b22a1961..91b7501fec1849 100644 --- a/static/app/components/events/eventVitals.tsx +++ b/static/app/components/events/eventVitals.tsx @@ -119,7 +119,7 @@ interface EventVitalProps extends Props { } function EventVital({event, name, vital}: EventVitalProps) { - const value = event.measurements?.[name].value ?? null; + const value = event.measurements?.[name]!.value ?? null; if (value === null || !vital) { return null; } diff --git a/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx b/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx index 67663d1c778136..0e8c82b5ccd04f 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagList.spec.tsx @@ -176,8 +176,8 @@ describe('EventFeatureFlagList', function () { // expect enableReplay to be preceding webVitalsFlag expect( screen - .getByText(webVitalsFlag.flag) - .compareDocumentPosition(screen.getByText(enableReplay.flag)) + .getByText(webVitalsFlag!.flag) + .compareDocumentPosition(screen.getByText(enableReplay!.flag)) ).toBe(document.DOCUMENT_POSITION_PRECEDING); const sortControl = screen.getByRole('button', { @@ -189,8 +189,8 @@ describe('EventFeatureFlagList', function () { // expect enableReplay to be following webVitalsFlag expect( screen - .getByText(webVitalsFlag.flag) - .compareDocumentPosition(screen.getByText(enableReplay.flag)) + .getByText(webVitalsFlag!.flag) + .compareDocumentPosition(screen.getByText(enableReplay!.flag)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); await userEvent.click(sortControl); @@ -199,8 +199,8 @@ describe('EventFeatureFlagList', function () { // expect enableReplay to be preceding webVitalsFlag, A-Z sort by default expect( screen - .getByText(webVitalsFlag.flag) - .compareDocumentPosition(screen.getByText(enableReplay.flag)) + .getByText(webVitalsFlag!.flag) + .compareDocumentPosition(screen.getByText(enableReplay!.flag)) ).toBe(document.DOCUMENT_POSITION_PRECEDING); await userEvent.click(sortControl); @@ -209,8 +209,8 @@ describe('EventFeatureFlagList', function () { // expect enableReplay to be following webVitalsFlag expect( screen - .getByText(webVitalsFlag.flag) - .compareDocumentPosition(screen.getByText(enableReplay.flag)) + .getByText(webVitalsFlag!.flag) + .compareDocumentPosition(screen.getByText(enableReplay!.flag)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); }); diff --git a/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx b/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx index fe7d03a9646203..cff3ed55b90c33 100644 --- a/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx +++ b/static/app/components/events/featureFlags/featureFlagDrawer.spec.tsx @@ -79,16 +79,16 @@ describe('FeatureFlagDrawer', function () { const drawerScreen = await renderFlagDrawer(); const [webVitalsFlag, enableReplay] = MOCK_FLAGS.filter(f => f.result === true); - expect(within(drawerScreen).getByText(webVitalsFlag.flag)).toBeInTheDocument(); - expect(within(drawerScreen).getByText(enableReplay.flag)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(webVitalsFlag!.flag)).toBeInTheDocument(); + expect(within(drawerScreen).getByText(enableReplay!.flag)).toBeInTheDocument(); const searchInput = within(drawerScreen).getByRole('textbox', { name: 'Search Flags', }); - await userEvent.type(searchInput, webVitalsFlag.flag); + await userEvent.type(searchInput, webVitalsFlag!.flag); - expect(within(drawerScreen).getByText(webVitalsFlag.flag)).toBeInTheDocument(); - expect(within(drawerScreen).queryByText(enableReplay.flag)).not.toBeInTheDocument(); + expect(within(drawerScreen).getByText(webVitalsFlag!.flag)).toBeInTheDocument(); + expect(within(drawerScreen).queryByText(enableReplay!.flag)).not.toBeInTheDocument(); }); it('allows sort dropdown to affect displayed flags', async function () { @@ -99,8 +99,8 @@ describe('FeatureFlagDrawer', function () { // the flags are reversed by default, so webVitalsFlag should be following enableReplay expect( within(drawerScreen) - .getByText(enableReplay.flag) - .compareDocumentPosition(within(drawerScreen).getByText(webVitalsFlag.flag)) + .getByText(enableReplay!.flag) + .compareDocumentPosition(within(drawerScreen).getByText(webVitalsFlag!.flag)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); const sortControl = within(drawerScreen).getByRole('button', { @@ -114,8 +114,8 @@ describe('FeatureFlagDrawer', function () { // expect webVitalsFlag to be preceding enableReplay expect( within(drawerScreen) - .getByText(enableReplay.flag) - .compareDocumentPosition(within(drawerScreen).getByText(webVitalsFlag.flag)) + .getByText(enableReplay!.flag) + .compareDocumentPosition(within(drawerScreen).getByText(webVitalsFlag!.flag)) ).toBe(document.DOCUMENT_POSITION_PRECEDING); await userEvent.click(sortControl); @@ -128,8 +128,8 @@ describe('FeatureFlagDrawer', function () { // enableReplay follows webVitalsFlag in Z-A sort expect( within(drawerScreen) - .getByText(webVitalsFlag.flag) - .compareDocumentPosition(within(drawerScreen).getByText(enableReplay.flag)) + .getByText(webVitalsFlag!.flag) + .compareDocumentPosition(within(drawerScreen).getByText(enableReplay!.flag)) ).toBe(document.DOCUMENT_POSITION_FOLLOWING); }); diff --git a/static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx b/static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx index ea3a6dbd5551ad..8f1eca42a7dbc7 100644 --- a/static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx +++ b/static/app/components/events/featureFlags/featureFlagOnboardingSidebar.tsx @@ -173,7 +173,7 @@ function OnboardingContent({ value: string; label?: ReactNode; textValue?: string; - }>(openFeatureProviderOptions[0]); + }>(openFeatureProviderOptions[0]!); // Second dropdown: other SDK providers const sdkProviderOptions = sdkProviders.map(provider => { @@ -188,7 +188,7 @@ function OnboardingContent({ value: string; label?: ReactNode; textValue?: string; - }>(sdkProviderOptions[0]); + }>(sdkProviderOptions[0]!); const defaultTab: string = 'openFeature'; const {getParamValue: setupMode, setParamValue: setSetupMode} = useUrlParams( diff --git a/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx b/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx index 7b29d7e8e30b77..3e9094272447cd 100644 --- a/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx +++ b/static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx @@ -106,7 +106,7 @@ describe('EventGroupingInfo', function () { }, }); - await userEvent.click(screen.getAllByRole('button', {name: 'default:XXXX'})[0]); + await userEvent.click(screen.getAllByRole('button', {name: 'default:XXXX'})[0]!); await userEvent.click(screen.getByRole('option', {name: 'new:XXXX'})); // Should show new hash diff --git a/static/app/components/events/highlights/editHighlightsModal.spec.tsx b/static/app/components/events/highlights/editHighlightsModal.spec.tsx index fa50b3b3d16269..4b6c7b874fadbe 100644 --- a/static/app/components/events/highlights/editHighlightsModal.spec.tsx +++ b/static/app/components/events/highlights/editHighlightsModal.spec.tsx @@ -152,7 +152,7 @@ describe('EditHighlightsModal', function () { const previewCtxButtons = screen.queryAllByTestId('highlights-remove-ctx'); expect(previewCtxButtons).toHaveLength(highlightContextTitles.length); - await userEvent.click(previewTagButtons[0]); + await userEvent.click(previewTagButtons[0]!); expect(analyticsSpy).toHaveBeenCalledWith( 'highlights.edit_modal.remove_tag', expect.anything() @@ -161,7 +161,7 @@ describe('EditHighlightsModal', function () { previewTagButtons.length - 1 ); - await userEvent.click(previewCtxButtons[0]); + await userEvent.click(previewCtxButtons[0]!); expect(analyticsSpy).toHaveBeenCalledWith( 'highlights.edit_modal.remove_context_key', expect.anything() diff --git a/static/app/components/events/highlights/highlightsDataSection.spec.tsx b/static/app/components/events/highlights/highlightsDataSection.spec.tsx index 2f56f466900287..e4f93f1b117d08 100644 --- a/static/app/components/events/highlights/highlightsDataSection.spec.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.spec.tsx @@ -98,7 +98,7 @@ describe('HighlightsDataSection', function () { .closest('div[data-test-id=highlight-tag-row]') as HTMLElement; // If highlight is present on the event... if (eventTagMap.hasOwnProperty(tagKey)) { - expect(within(row).getByText(eventTagMap[tagKey])).toBeInTheDocument(); + expect(within(row).getByText(eventTagMap[tagKey]!)).toBeInTheDocument(); const highlightTagDropdown = within(row).getByLabelText('Tag Actions Menu'); expect(highlightTagDropdown).toBeInTheDocument(); await userEvent.click(highlightTagDropdown); diff --git a/static/app/components/events/highlights/highlightsDataSection.tsx b/static/app/components/events/highlights/highlightsDataSection.tsx index 72c9d75af74639..524e96fd436faa 100644 --- a/static/app/components/events/highlights/highlightsDataSection.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.tsx @@ -151,7 +151,7 @@ function HighlightsData({ // find the replayId from either context or tags, if it exists const contextReplayItem = highlightContextDataItems.find( - e => e.data.length && e.data[0].key === 'replay_id' + e => e.data.length && e.data[0]!.key === 'replay_id' ); const contextReplayId = contextReplayItem?.value ?? EMPTY_HIGHLIGHT_DEFAULT; diff --git a/static/app/components/events/highlights/util.spec.tsx b/static/app/components/events/highlights/util.spec.tsx index e46784cf47bed7..0ce2af90019dd6 100644 --- a/static/app/components/events/highlights/util.spec.tsx +++ b/static/app/components/events/highlights/util.spec.tsx @@ -29,14 +29,14 @@ describe('getHighlightContextData', function () { location: {query: {}} as Location, }); expect(highlightCtxData).toHaveLength(1); - expect(highlightCtxData[0].alias).toBe('keyboard'); - expect(highlightCtxData[0].type).toBe('default'); - expect(highlightCtxData[0].data).toHaveLength(highlightContext.keyboard.length); - const highlightCtxDataKeys = new Set(highlightCtxData[0].data.map(({key}) => key)); + expect(highlightCtxData[0]!.alias).toBe('keyboard'); + expect(highlightCtxData[0]!.type).toBe('default'); + expect(highlightCtxData[0]!.data).toHaveLength(highlightContext.keyboard.length); + const highlightCtxDataKeys = new Set(highlightCtxData[0]!.data.map(({key}) => key)); for (const ctxKey of highlightContext.keyboard) { expect(highlightCtxDataKeys.has(ctxKey)).toBe(true); } - const missingCtxHighlightFromEvent = highlightCtxData[0].data?.find( + const missingCtxHighlightFromEvent = highlightCtxData[0]!.data?.find( d => d.key === missingContextKey ); expect(missingCtxHighlightFromEvent?.value).toBe(EMPTY_HIGHLIGHT_DEFAULT); @@ -59,7 +59,7 @@ describe('getHighlightContextData', function () { location: {query: {}} as Location, }); expect(highlightCtxData).toHaveLength(1); - expect(highlightCtxData[0].type).toBe('os'); + expect(highlightCtxData[0]!.type).toBe('os'); }); }); diff --git a/static/app/components/events/highlights/util.tsx b/static/app/components/events/highlights/util.tsx index 233ba463aeb5bc..f952127f7869d5 100644 --- a/static/app/components/events/highlights/util.tsx +++ b/static/app/components/events/highlights/util.tsx @@ -61,7 +61,7 @@ function getFuzzyHighlightContext( }; } - const highlightContextKeys = highlightContextSets[highlightKey]; + const highlightContextKeys = highlightContextSets[highlightKey]!; const highlightItems: KeyValueListData = data.filter( ({key, subject}) => // We match on key (e.g. 'trace_id') and subject (e.g. 'Trace ID') diff --git a/static/app/components/events/interfaces/analyzeFrames.tsx b/static/app/components/events/interfaces/analyzeFrames.tsx index 60d6a2eb90e337..bc784f0b76b79e 100644 --- a/static/app/components/events/interfaces/analyzeFrames.tsx +++ b/static/app/components/events/interfaces/analyzeFrames.tsx @@ -171,7 +171,7 @@ function satisfiesFunctionCondition(frame: Frame, suspect: SuspectFrame) { return false; } for (let index = 0; index < suspect.functions.length; index++) { - const matchFuction = suspect.functions[index]; + const matchFuction = suspect.functions[index]!; const match = typeof matchFuction === 'string' ? frame.function === matchFuction @@ -219,7 +219,7 @@ export function analyzeFramesForRootCause(event: Event): { // iterating the frames in reverse order, because the topmost frames most like the root cause for (let index = exceptionFrames.length - 1; index >= 0; index--) { - const frame = exceptionFrames[index]; + const frame = exceptionFrames[index]!; const rootCause = analyzeFrameForRootCause(frame, currentThread); if (defined(rootCause)) { return rootCause; diff --git a/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.tsx b/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.tsx index 516b532fdf0e17..4945d94132178c 100644 --- a/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.tsx +++ b/static/app/components/events/interfaces/breadcrumbs/breadcrumbs.tsx @@ -67,15 +67,15 @@ function renderBreadCrumbRow({index, key, parent, style}: RenderBreadCrumbRowPro > f.value === `type-${breadcrumb.type}` + f => f.value === `type-${breadcrumb!.type}` ); if (foundFilterType === -1) { filterTypes.push({ - value: `type-${breadcrumb.type}`, - leadingItems: , - label: breadcrumb.description, - levels: breadcrumb?.level ? [breadcrumb.level] : [], + value: `type-${breadcrumb!.type}`, + leadingItems: , + label: breadcrumb!.description, + levels: breadcrumb!.level ? [breadcrumb!.level] : [], }); continue; } if ( breadcrumb?.level && - !filterTypes[foundFilterType].levels?.includes(breadcrumb.level) + !filterTypes[foundFilterType]!.levels?.includes(breadcrumb.level) ) { - filterTypes[foundFilterType].levels?.push(breadcrumb.level); + filterTypes[foundFilterType]!.levels?.push(breadcrumb!.level); } } @@ -176,8 +176,8 @@ function BreadcrumbsContainer({data, event, organization, hideTitle = false}: Pr const filterLevels: SelectOption[] = []; for (const indexType in types) { - for (const indexLevel in types[indexType].levels) { - const level = types[indexType].levels?.[indexLevel]; + for (const indexLevel in types[indexType]!.levels) { + const level = types[indexType]!.levels?.[indexLevel]; if (filterLevels.some(f => f.value === `level-${level}`)) { continue; diff --git a/static/app/components/events/interfaces/crashContent/exception/actionableItems.tsx b/static/app/components/events/interfaces/crashContent/exception/actionableItems.tsx index f6efae0defeff5..586eee6fd10fa1 100644 --- a/static/app/components/events/interfaces/crashContent/exception/actionableItems.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/actionableItems.tsx @@ -246,7 +246,7 @@ interface ExpandableErrorListProps { function ExpandableErrorList({handleExpandClick, errorList}: ExpandableErrorListProps) { const [expanded, setExpanded] = useState(false); - const firstError = errorList[0]; + const firstError = errorList[0]!; const {title, desc, type} = firstError; const numErrors = errorList.length; const errorDataList = errorList.map(error => error.data ?? {}); diff --git a/static/app/components/events/interfaces/crashContent/exception/content.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/content.spec.tsx index f4fc56d43c2b0e..5d05cd9e9a904b 100644 --- a/static/app/components/events/interfaces/crashContent/exception/content.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/content.spec.tsx @@ -136,7 +136,7 @@ describe('Exception Content', function () { newestFirst stackView={StackView.APP} event={event} - values={event.entries[0].data.values} + values={event.entries[0]!.data.values} meta={event._meta!.entries[0].data.values} projectSlug={project.slug} />, @@ -145,7 +145,7 @@ describe('Exception Content', function () { expect(screen.getAllByText(/redacted/)).toHaveLength(2); - await userEvent.hover(screen.getAllByText(/redacted/)[0]); + await userEvent.hover(screen.getAllByText(/redacted/)[0]!); expect( await screen.findByText( @@ -200,7 +200,7 @@ describe('Exception Content', function () { type={StackType.ORIGINAL} stackView={StackView.APP} event={event} - values={event.entries[0].data.values} + values={event.entries[0]!.data.values} projectSlug={project.slug} /> ); @@ -242,7 +242,7 @@ describe('Exception Content', function () { platform: 'python' as const, stackView: StackView.APP, event, - values: event.entries[0].data.values, + values: event.entries[0]!.data.values, projectSlug: project.slug, }; @@ -252,9 +252,9 @@ describe('Exception Content', function () { const exceptions = screen.getAllByTestId('exception-value'); // First exception should be the parent ExceptionGroup - expect(within(exceptions[0]).getByText('ExceptionGroup 1')).toBeInTheDocument(); + expect(within(exceptions[0]!).getByText('ExceptionGroup 1')).toBeInTheDocument(); expect( - within(exceptions[0]).getByRole('heading', {name: 'ExceptionGroup 1'}) + within(exceptions[0]!).getByRole('heading', {name: 'ExceptionGroup 1'}) ).toBeInTheDocument(); }); @@ -263,7 +263,7 @@ describe('Exception Content', function () { const exceptions = screen.getAllByTestId('exception-value'); - const exceptionGroupWithNoContext = exceptions[2]; + const exceptionGroupWithNoContext = exceptions[2]!; expect( within(exceptionGroupWithNoContext).getByText('Related Exceptions') ).toBeInTheDocument(); diff --git a/static/app/components/events/interfaces/crashContent/exception/content.tsx b/static/app/components/events/interfaces/crashContent/exception/content.tsx index 7ba4654d88c7bf..b98f29252e5517 100644 --- a/static/app/components/events/interfaces/crashContent/exception/content.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/content.tsx @@ -151,7 +151,7 @@ export function Content({ const frameSourceMapDebuggerData = sourceMapDebuggerData?.exceptions[ excIdx - ].frames.map(debuggerFrame => + ]!.frames.map(debuggerFrame => prepareSourceMapDebuggerFrameInformation( sourceMapDebuggerData, debuggerFrame, diff --git a/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx index 1f261acefc8570..986fb9fc2a042e 100644 --- a/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/relatedExceptions.spec.tsx @@ -31,14 +31,14 @@ describe('ExceptionGroupContext', function () { expect(items).toHaveLength(3); // ExceptionGroup should not link to itself - expect(within(items[0]).getByText('ExceptionGroup 1: parent')).toBeInTheDocument(); + expect(within(items[0]!).getByText('ExceptionGroup 1: parent')).toBeInTheDocument(); // Should have a link to TypeError exception expect( - within(items[1]).getByRole('button', {name: 'TypeError: nested'}) + within(items[1]!).getByRole('button', {name: 'TypeError: nested'}) ).toBeInTheDocument(); // Should have a link to child exception group expect( - within(items[2]).getByRole('button', {name: 'ExceptionGroup 2: child'}) + within(items[2]!).getByRole('button', {name: 'ExceptionGroup 2: child'}) ).toBeInTheDocument(); }); @@ -54,8 +54,8 @@ describe('ExceptionGroupContext', function () { const children = screen.getAllByRole('button'); // Order should be oldest to newest, opposite fo the previous test - expect(within(children[0]).getByText(/ExceptionGroup 2/i)).toBeInTheDocument(); - expect(within(children[1]).getByText(/TypeError/i)).toBeInTheDocument(); + expect(within(children[0]!).getByText(/ExceptionGroup 2/i)).toBeInTheDocument(); + expect(within(children[1]!).getByText(/TypeError/i)).toBeInTheDocument(); }); it('renders tree with child exception group', function () { @@ -66,13 +66,13 @@ describe('ExceptionGroupContext', function () { // Should show and link to parent exception group expect( - within(items[0]).getByRole('button', {name: 'ExceptionGroup 1: parent'}) + within(items[0]!).getByRole('button', {name: 'ExceptionGroup 1: parent'}) ).toBeInTheDocument(); // Should have a link to child exception group - expect(within(items[1]).getByText('ExceptionGroup 2: child')).toBeInTheDocument(); + expect(within(items[1]!).getByText('ExceptionGroup 2: child')).toBeInTheDocument(); // Show show and link to child exception expect( - within(items[2]).getByRole('button', {name: 'ValueError: test'}) + within(items[2]!).getByRole('button', {name: 'ValueError: test'}) ).toBeInTheDocument(); }); diff --git a/static/app/components/events/interfaces/crashContent/exception/sourceMapDebug.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/sourceMapDebug.spec.tsx index 7b2e52c3e70652..b9c6c3a3cd5bad 100644 --- a/static/app/components/events/interfaces/crashContent/exception/sourceMapDebug.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/sourceMapDebug.spec.tsx @@ -76,7 +76,7 @@ describe('SourceMapDebug', () => { it('should use unqiue in app frames', () => { expect(debugFrames).toHaveLength(1); - expect(debugFrames[0].filename).toBe( + expect(debugFrames[0]!.filename).toBe( './app/views/organizationStats/teamInsights/controls.tsx' ); }); diff --git a/static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx index 089dc288dc3362..124ff840ccc95a 100644 --- a/static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/stackTrace.spec.tsx @@ -121,7 +121,7 @@ describe('ExceptionStacktraceContent', function () { render( ); expect( @@ -143,7 +143,7 @@ describe('ExceptionStacktraceContent', function () { render( ); @@ -153,9 +153,9 @@ describe('ExceptionStacktraceContent', function () { expect(screen.getAllByRole('listitem')).toHaveLength(2); // inApp === true - expect(screen.getAllByRole('listitem')[1]).toHaveTextContent(frames[0].filename); + expect(screen.getAllByRole('listitem')[1]).toHaveTextContent(frames[0]!.filename); // inApp === false - expect(screen.getAllByRole('listitem')[0]).toHaveTextContent(frames[1].filename); + expect(screen.getAllByRole('listitem')[0]).toHaveTextContent(frames[1]!.filename); }); }); diff --git a/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebug.spec.tsx b/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebug.spec.tsx index c8177decf18622..952fcd60e859a6 100644 --- a/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebug.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebug.spec.tsx @@ -7,8 +7,8 @@ import {getUniqueFilesFromException} from './useSourceMapDebug'; function modifyEventFrames(event: Event, modify: any): Event { const modifiedEvent = cloneDeep(event); - modifiedEvent.entries[0].data.values[0].stacktrace.frames = - event.entries[0].data.values[0].stacktrace.frames.map(frame => ({ + modifiedEvent.entries[0]!.data.values[0].stacktrace.frames = + event.entries[0]!.data.values[0].stacktrace.frames.map(frame => ({ ...frame, ...modify, })); @@ -23,7 +23,7 @@ describe('getUniqueFilesFromException', () => { platform: 'javascript', }); const result = getUniqueFilesFromException( - (event.entries as EntryException[])[0].data.values!, + (event.entries as EntryException[])[0]!.data.values!, props ); @@ -48,7 +48,7 @@ describe('getUniqueFilesFromException', () => { {filename: ''} ); const result = getUniqueFilesFromException( - (event.entries as EntryException[])[0].data.values!, + (event.entries as EntryException[])[0]!.data.values!, props ); @@ -63,7 +63,7 @@ describe('getUniqueFilesFromException', () => { {absPath: '~/myfile.js', filename: '~/myfile.js'} ); const result = getUniqueFilesFromException( - (event.entries as EntryException[])[0].data.values!, + (event.entries as EntryException[])[0]!.data.values!, props ); diff --git a/static/app/components/events/interfaces/crashContent/exception/utils.tsx b/static/app/components/events/interfaces/crashContent/exception/utils.tsx index 7ed48748022b8b..c661baa6d43e32 100644 --- a/static/app/components/events/interfaces/crashContent/exception/utils.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/utils.tsx @@ -16,7 +16,7 @@ export function isFrameFilenamePathlike(frame: Frame): boolean { const parsedURL = safeURL(filename); if (parsedURL) { - filename = parsedURL.pathname.split('/').reverse()[0]; + filename = parsedURL.pathname.split('/').reverse()[0]!; } return ( @@ -58,7 +58,7 @@ export const renderLinksInText = ({ const urls = exceptionText.match(urlRegex) || []; const elements = parts.flatMap((part, index) => { - const url = urls[index]; + const url = urls[index]!; const isUrlValid = isUrl(url); let link: ReactElement | undefined; diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx index b93c928f79b8a2..082149b0f3f85b 100644 --- a/static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/stackTrace/content.spec.tsx @@ -116,15 +116,15 @@ describe('StackTrace', function () { const frameTitles = screen.getAllByTestId('title'); // collapse the expanded frame (by default) - await userEvent.click(frameTitles[0]); + await userEvent.click(frameTitles[0]!); // all frames are now collapsed expect(screen.queryByTestId('toggle-button-expanded')).not.toBeInTheDocument(); expect(screen.getAllByTestId('toggle-button-collapsed')).toHaveLength(5); // expand penultimate and last frame - await userEvent.click(frameTitles[frameTitles.length - 2]); - await userEvent.click(frameTitles[frameTitles.length - 1]); + await userEvent.click(frameTitles[frameTitles.length - 2]!); + await userEvent.click(frameTitles[frameTitles.length - 1]!); // two frames are now collapsed expect(screen.getAllByTestId('toggle-button-expanded')).toHaveLength(2); @@ -154,8 +154,8 @@ describe('StackTrace', function () { const collapsedToggleButtons = screen.getAllByTestId('toggle-button-collapsed'); // expand penultimate and last frame - await userEvent.click(collapsedToggleButtons[collapsedToggleButtons.length - 2]); - await userEvent.click(collapsedToggleButtons[collapsedToggleButtons.length - 1]); + await userEvent.click(collapsedToggleButtons[collapsedToggleButtons.length - 2]!); + await userEvent.click(collapsedToggleButtons[collapsedToggleButtons.length - 1]!); // two frames are now collapsed expect(screen.getAllByTestId('toggle-button-expanded')).toHaveLength(2); @@ -188,7 +188,7 @@ describe('StackTrace', function () { it('does not render non in app tags', function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: false}; + dataFrames[0] = {...dataFrames[0]!, inApp: false}; const newData = { ...data, @@ -204,7 +204,7 @@ describe('StackTrace', function () { it('displays a toggle button when there is more than one non-inapp frame', function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: true}; + dataFrames[0] = {...dataFrames[0]!, inApp: true}; const newData = { ...data, @@ -221,11 +221,11 @@ describe('StackTrace', function () { it('shows/hides frames when toggle button clicked', async function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: true}; - dataFrames[1] = {...dataFrames[1], function: 'non-in-app-frame'}; - dataFrames[2] = {...dataFrames[2], function: 'non-in-app-frame'}; - dataFrames[3] = {...dataFrames[3], function: 'non-in-app-frame'}; - dataFrames[4] = {...dataFrames[4], function: 'non-in-app-frame'}; + dataFrames[0] = {...dataFrames[0]!, inApp: true}; + dataFrames[1] = {...dataFrames[1]!, function: 'non-in-app-frame'}; + dataFrames[2] = {...dataFrames[2]!, function: 'non-in-app-frame'}; + dataFrames[3] = {...dataFrames[3]!, function: 'non-in-app-frame'}; + dataFrames[4] = {...dataFrames[4]!, function: 'non-in-app-frame'}; const newData = { ...data, @@ -244,9 +244,9 @@ describe('StackTrace', function () { it('does not display a toggle button when there is only one non-inapp frame', function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: true}; - dataFrames[2] = {...dataFrames[2], inApp: true}; - dataFrames[4] = {...dataFrames[4], inApp: true}; + dataFrames[0] = {...dataFrames[0]!, inApp: true}; + dataFrames[2] = {...dataFrames[2]!, inApp: true}; + dataFrames[4] = {...dataFrames[4]!, inApp: true}; const newData = { ...data, @@ -269,7 +269,7 @@ describe('StackTrace', function () { ...data, hasSystemFrames: true, frames: [ - {...dataFrames[0], inApp: true}, + {...dataFrames[0]!, inApp: true}, ...dataFrames.splice(1, dataFrames.length), ], }; @@ -304,7 +304,7 @@ describe('StackTrace', function () { registers: {}, frames: [ ...dataFrames.splice(0, dataFrames.length - 1), - {...dataFrames[dataFrames.length - 1], inApp: true}, + {...dataFrames[dataFrames.length - 1]!, inApp: true}, ], }; @@ -339,7 +339,7 @@ describe('StackTrace', function () { hasSystemFrames: true, frames: [ ...dataFrames.slice(0, 1), - {...dataFrames[1], inApp: true}, + {...dataFrames[1]!, inApp: true}, ...dataFrames.slice(2, dataFrames.length), ], }; @@ -375,7 +375,7 @@ describe('StackTrace', function () { ...data, hasSystemFrames: true, frames: [ - {...dataFrames[0], inApp: true}, + {...dataFrames[0]!, inApp: true}, ...dataFrames.splice(1, dataFrames.length), ], }; @@ -409,7 +409,7 @@ describe('StackTrace', function () { ...data, hasSystemFrames: true, frames: [ - {...dataFrames[0], inApp: true}, + {...dataFrames[0]!, inApp: true}, ...dataFrames.splice(1, dataFrames.length), ], }; diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/content.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/content.tsx index f3cc30ca29ba0e..620e727d8635b0 100644 --- a/static/app/components/events/interfaces/crashContent/stackTrace/content.tsx +++ b/static/app/components/events/interfaces/crashContent/stackTrace/content.tsx @@ -86,7 +86,7 @@ function Content({ function setInitialFrameMap(): {[frameIndex: number]: boolean} { const indexMap: Record = {}; (data.frames ?? []).forEach((frame, frameIdx) => { - const nextFrame = (data.frames ?? [])[frameIdx + 1]; + const nextFrame = (data.frames ?? [])[frameIdx + 1]!; const repeatedFrame = isRepeatedFrame(frame, nextFrame); if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { indexMap[frameIdx] = false; @@ -99,7 +99,7 @@ function Content({ let count = 0; const countMap: Record = {}; (data.frames ?? []).forEach((frame, frameIdx) => { - const nextFrame = (data.frames ?? [])[frameIdx + 1]; + const nextFrame = (data.frames ?? [])[frameIdx + 1]!; const repeatedFrame = isRepeatedFrame(frame, nextFrame); if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { countMap[frameIdx] = count; @@ -118,8 +118,8 @@ function Content({ return false; } - const lastFrame = frames[frames.length - 1]; - const penultimateFrame = frames[frames.length - 2]; + const lastFrame = frames[frames.length - 1]!; + const penultimateFrame = frames[frames.length - 2]!; return penultimateFrame.inApp && !lastFrame.inApp; } @@ -205,7 +205,7 @@ function Content({ let convertedFrames = frames .map((frame, frameIndex) => { const prevFrame = frames[frameIndex - 1]; - const nextFrame = frames[frameIndex + 1]; + const nextFrame = frames[frameIndex + 1]!; const repeatedFrame = isRepeatedFrame(frame, nextFrame); if (repeatedFrame) { @@ -284,7 +284,7 @@ function Content({ if (convertedFrames.length > 0 && registers) { const lastFrame = convertedFrames.length - 1; - convertedFrames[lastFrame] = cloneElement(convertedFrames[lastFrame], { + convertedFrames[lastFrame] = cloneElement(convertedFrames[lastFrame]!, { registers, }); } diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx index 8b35b2cca3a923..ee730e66b6d972 100644 --- a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx +++ b/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.spec.tsx @@ -67,7 +67,7 @@ describe('Native StackTrace', function () { }); it('does not render non in app tags', function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: false}; + dataFrames[0] = {...dataFrames[0]!, inApp: false}; const newData = { ...data, @@ -83,7 +83,7 @@ describe('Native StackTrace', function () { it('displays a toggle button when there is more than one non-inapp frame', function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: true}; + dataFrames[0] = {...dataFrames[0]!, inApp: true}; const newData = { ...data, @@ -100,11 +100,11 @@ describe('Native StackTrace', function () { it('shows/hides frames when toggle button clicked', async function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: true}; - dataFrames[1] = {...dataFrames[1], function: 'non-in-app-frame'}; - dataFrames[2] = {...dataFrames[2], function: 'non-in-app-frame'}; - dataFrames[3] = {...dataFrames[3], function: 'non-in-app-frame'}; - dataFrames[4] = {...dataFrames[4], function: 'non-in-app-frame'}; + dataFrames[0] = {...dataFrames[0]!, inApp: true}; + dataFrames[1] = {...dataFrames[1]!, function: 'non-in-app-frame'}; + dataFrames[2] = {...dataFrames[2]!, function: 'non-in-app-frame'}; + dataFrames[3] = {...dataFrames[3]!, function: 'non-in-app-frame'}; + dataFrames[4] = {...dataFrames[4]!, function: 'non-in-app-frame'}; const newData = { ...data, @@ -123,9 +123,9 @@ describe('Native StackTrace', function () { it('does not display a toggle button when there is only one non-inapp frame', function () { const dataFrames = [...data.frames]; - dataFrames[0] = {...dataFrames[0], inApp: true}; - dataFrames[2] = {...dataFrames[2], inApp: true}; - dataFrames[4] = {...dataFrames[4], inApp: true}; + dataFrames[0] = {...dataFrames[0]!, inApp: true}; + dataFrames[2] = {...dataFrames[2]!, inApp: true}; + dataFrames[4] = {...dataFrames[4]!, inApp: true}; const newData = { ...data, @@ -165,10 +165,12 @@ describe('Native StackTrace', function () { const frames = screen.getAllByTestId('stack-trace-frame'); - expect(within(frames[0]).getByTestId('symbolication-error-icon')).toBeInTheDocument(); expect( - within(frames[1]).getByTestId('symbolication-warning-icon') + within(frames[0]!).getByTestId('symbolication-error-icon') ).toBeInTheDocument(); - expect(within(frames[2]).queryByTestId(/symbolication/)).not.toBeInTheDocument(); + expect( + within(frames[1]!).getByTestId('symbolication-warning-icon') + ).toBeInTheDocument(); + expect(within(frames[2]!).queryByTestId(/symbolication/)).not.toBeInTheDocument(); }); }); diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx index 53becef22c7c10..b6032c1ff7bdb4 100644 --- a/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx +++ b/static/app/components/events/interfaces/crashContent/stackTrace/nativeContent.tsx @@ -74,7 +74,7 @@ export function NativeContent({ function setInitialFrameMap(): {[frameIndex: number]: boolean} { const indexMap = {}; (data.frames ?? []).forEach((frame, frameIdx) => { - const nextFrame = (data.frames ?? [])[frameIdx + 1]; + const nextFrame = (data.frames ?? [])[frameIdx + 1]!; const repeatedFrame = isRepeatedFrame(frame, nextFrame); if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { indexMap[frameIdx] = false; @@ -87,7 +87,7 @@ export function NativeContent({ let count = 0; const countMap = {}; (data.frames ?? []).forEach((frame, frameIdx) => { - const nextFrame = (data.frames ?? [])[frameIdx + 1]; + const nextFrame = (data.frames ?? [])[frameIdx + 1]!; const repeatedFrame = isRepeatedFrame(frame, nextFrame); if (frameIsVisible(frame, nextFrame) && !repeatedFrame && !frame.inApp) { countMap[frameIdx] = count; @@ -180,7 +180,7 @@ export function NativeContent({ let convertedFrames = frames .map((frame, frameIndex) => { const prevFrame = frames[frameIndex - 1]; - const nextFrame = frames[frameIndex + 1]; + const nextFrame = frames[frameIndex + 1]!; const repeatedFrame = isRepeatedFrame(frame, nextFrame); if (repeatedFrame) { @@ -260,7 +260,7 @@ export function NativeContent({ if (convertedFrames.length > 0 && registers) { const lastFrame = convertedFrames.length - 1; - convertedFrames[lastFrame] = cloneElement(convertedFrames[lastFrame], { + convertedFrames[lastFrame] = cloneElement(convertedFrames[lastFrame]!, { registers, }); } diff --git a/static/app/components/events/interfaces/crons/cronTimelineSection.tsx b/static/app/components/events/interfaces/crons/cronTimelineSection.tsx index 53bd9bc1d4eab7..de2d12be6c5446 100644 --- a/static/app/components/events/interfaces/crons/cronTimelineSection.tsx +++ b/static/app/components/events/interfaces/crons/cronTimelineSection.tsx @@ -120,7 +120,7 @@ export function CronTimelineSection({event, organization, project}: Props) { diff --git a/static/app/components/events/interfaces/csp/index.spec.tsx b/static/app/components/events/interfaces/csp/index.spec.tsx index 4d7b1efc0d91df..048f83cd478626 100644 --- a/static/app/components/events/interfaces/csp/index.spec.tsx +++ b/static/app/components/events/interfaces/csp/index.spec.tsx @@ -21,7 +21,7 @@ describe('Csp report entry', function () { }, }, }); - render(, { + render(, { organization: { relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()), }, diff --git a/static/app/components/events/interfaces/debugMeta/index.tsx b/static/app/components/events/interfaces/debugMeta/index.tsx index 9ee105aa9b2231..d48bec86431180 100644 --- a/static/app/components/events/interfaces/debugMeta/index.tsx +++ b/static/app/components/events/interfaces/debugMeta/index.tsx @@ -82,8 +82,8 @@ function applyImageFilters( if (term.indexOf('0x') === 0) { const needle = parseAddress(term); if (needle > 0 && image.image_addr !== '0x0') { - const [startAddress, endAddress] = getImageRange(image as any); // TODO(PRISCILA): remove any - return needle >= startAddress && needle < endAddress; + const [startAddress, endAddress] = getImageRange(image); + return needle >= startAddress! && needle < endAddress!; } } @@ -184,7 +184,7 @@ export function DebugMeta({data, projectSlug, groupId, event}: DebugMetaProps) { ]; const defaultFilterSelections = ( - 'options' in filterOptions[0] ? filterOptions[0].options : [] + 'options' in filterOptions[0]! ? filterOptions[0].options : [] ).filter(opt => opt.value !== ImageStatus.UNUSED); setFilterState({ @@ -320,7 +320,7 @@ export function DebugMeta({data, projectSlug, groupId, event}: DebugMetaProps) { > diff --git a/static/app/components/events/interfaces/frame/context.tsx b/static/app/components/events/interfaces/frame/context.tsx index 41c1d465915525..b536195bb65d65 100644 --- a/static/app/components/events/interfaces/frame/context.tsx +++ b/static/app/components/events/interfaces/frame/context.tsx @@ -141,7 +141,7 @@ function Context({ ) : null; } - const startLineNo = hasContextSource ? frame.context[0][0] : 0; + const startLineNo = hasContextSource ? frame.context[0]![0] : 0; const prismClassName = fileExtension ? `language-${fileExtension}` : ''; @@ -157,14 +157,14 @@ function Context({
                 
                   {lines.map((line, i) => {
    -                const contextLine = contextLines[i];
    -                const isActive = activeLineNumber === contextLine[0];
    +                const contextLine = contextLines[i]!;
    +                const isActive = activeLineNumber === contextLine[0]!;
     
                     return (
                       
                         
                           
    diff --git a/static/app/components/events/interfaces/frame/contextLine.tsx b/static/app/components/events/interfaces/frame/contextLine.tsx
    index 728f36bca89365..0173962dfd3ef4 100644
    --- a/static/app/components/events/interfaces/frame/contextLine.tsx
    +++ b/static/app/components/events/interfaces/frame/contextLine.tsx
    @@ -30,7 +30,7 @@ function ContextLine({line, isActive, children, coverage = ''}: Props) {
       let lineWs = '';
       let lineCode = '';
       if (typeof line[1] === 'string') {
    -    [, lineWs, lineCode] = line[1].match(/^(\s*)(.*?)$/m)!;
    +    [, lineWs, lineCode] = line[1].match(/^(\s*)(.*?)$/m)! as [string, string, string];
       }
     
       return (
    diff --git a/static/app/components/events/interfaces/frame/frameVariables.spec.tsx b/static/app/components/events/interfaces/frame/frameVariables.spec.tsx
    index e173bf7c25e1df..4bfd5660fffce9 100644
    --- a/static/app/components/events/interfaces/frame/frameVariables.spec.tsx
    +++ b/static/app/components/events/interfaces/frame/frameVariables.spec.tsx
    @@ -75,7 +75,7 @@ describe('Frame Variables', function () {
     
         expect(screen.getAllByText(/redacted/)).toHaveLength(2);
     
    -    await userEvent.hover(screen.getAllByText(/redacted/)[0]);
    +    await userEvent.hover(screen.getAllByText(/redacted/)[0]!);
     
         expect(
           await screen.findByText(
    @@ -147,8 +147,8 @@ describe('Frame Variables', function () {
     
         const nullValues = screen.getAllByTestId('value-null');
     
    -    expect(within(nullValues[0]).getByText('null')).toBeInTheDocument();
    -    expect(within(nullValues[1]).getByText('undefined')).toBeInTheDocument();
    +    expect(within(nullValues[0]!).getByText('null')).toBeInTheDocument();
    +    expect(within(nullValues[1]!).getByText('undefined')).toBeInTheDocument();
         expect(
           within(screen.getByTestId('value-boolean')).getByText('true')
         ).toBeInTheDocument();
    diff --git a/static/app/components/events/interfaces/frame/stacktraceLink.tsx b/static/app/components/events/interfaces/frame/stacktraceLink.tsx
    index de9c0abf74cfc3..89ec41f2e1795a 100644
    --- a/static/app/components/events/interfaces/frame/stacktraceLink.tsx
    +++ b/static/app/components/events/interfaces/frame/stacktraceLink.tsx
    @@ -200,7 +200,7 @@ export function StacktraceLink({frame, event, line}: StacktraceLinkProps) {
           const url = new URL(sourceLink);
           const hostname = url.hostname;
           const parts = hostname.split('.');
    -      const domain = parts.length > 1 ? parts[1] : '';
    +      const domain = parts.length > 1 ? parts[1]! : '';
           trackAnalytics(
             'integrations.non_inapp_stacktrace_link_clicked',
             {
    @@ -340,7 +340,7 @@ export function StacktraceLink({frame, event, line}: StacktraceLinkProps) {
               priority="link"
               icon={
                 sourceCodeProviders.length === 1
    -              ? getIntegrationIcon(sourceCodeProviders[0].provider.key, 'sm')
    +              ? getIntegrationIcon(sourceCodeProviders[0]!.provider.key, 'sm')
                   : undefined
               }
               onClick={() => {
    @@ -349,7 +349,7 @@ export function StacktraceLink({frame, event, line}: StacktraceLinkProps) {
                   {
                     view: 'stacktrace_issue_details',
                     platform: event.platform,
    -                provider: sourceCodeProviders[0]?.provider.key,
    +                provider: sourceCodeProviders[0]?.provider.key!,
                     setup_type: 'automatic',
                     organization,
                     ...getAnalyticsDataForEvent(event),
    diff --git a/static/app/components/events/interfaces/frame/stacktraceLinkModal.tsx b/static/app/components/events/interfaces/frame/stacktraceLinkModal.tsx
    index c4ef2bc695c8ba..b1cf126d46a630 100644
    --- a/static/app/components/events/interfaces/frame/stacktraceLinkModal.tsx
    +++ b/static/app/components/events/interfaces/frame/stacktraceLinkModal.tsx
    @@ -87,10 +87,10 @@ function StacktraceLinkModal({
       // If they have more than one, they'll have to navigate themselves
       const hasOneSourceCodeIntegration = sourceCodeProviders.length === 1;
       const sourceUrl = hasOneSourceCodeIntegration
    -    ? `https://${sourceCodeProviders[0].domainName}`
    +    ? `https://${sourceCodeProviders[0]!.domainName}`
         : undefined;
       const providerName = hasOneSourceCodeIntegration
    -    ? sourceCodeProviders[0].name
    +    ? sourceCodeProviders[0]!.name
         : t('source code');
     
       const onManualSetup = () => {
    @@ -99,7 +99,7 @@ function StacktraceLinkModal({
           setup_type: 'manual',
           provider:
             sourceCodeProviders.length === 1
    -          ? sourceCodeProviders[0].provider.name
    +          ? sourceCodeProviders[0]!.provider.name
               : 'unknown',
           organization,
         });
    @@ -171,7 +171,7 @@ function StacktraceLinkModal({
                               onClick={onManualSetup}
                               to={
                                 hasOneSourceCodeIntegration
    -                              ? `/settings/${organization.slug}/integrations/${sourceCodeProviders[0].provider.key}/${sourceCodeProviders[0].id}/`
    +                              ? `/settings/${organization.slug}/integrations/${sourceCodeProviders[0]!.provider.key}/${sourceCodeProviders[0]!.id}/`
                                   : `/settings/${organization.slug}/integrations/`
                               }
                             />
    @@ -200,7 +200,7 @@ function StacktraceLinkModal({
                         ? tct('Go to [link]', {
                             link: (
                               
    -                            {sourceCodeProviders[0].provider.name}
    +                            {sourceCodeProviders[0]!.provider.name}
                               
                             ),
                           })
    diff --git a/static/app/components/events/interfaces/frame/usePrismTokensSourceContext.tsx b/static/app/components/events/interfaces/frame/usePrismTokensSourceContext.tsx
    index 7fc3e56077d458..f82ece58927308 100644
    --- a/static/app/components/events/interfaces/frame/usePrismTokensSourceContext.tsx
    +++ b/static/app/components/events/interfaces/frame/usePrismTokensSourceContext.tsx
    @@ -253,7 +253,7 @@ export const usePrismTokensSourceContext = ({
     }) => {
       const organization = useOrganization({allowNull: true});
     
    -  const fullLanguage = getPrismLanguage(fileExtension);
    +  const fullLanguage = getPrismLanguage(fileExtension)!;
       const {preCode, executedCode, postCode} = convertContextLines(contextLines, lineNo);
       const code = preCode + executedCode + postCode;
     
    diff --git a/static/app/components/events/interfaces/keyValueList/index.spec.tsx b/static/app/components/events/interfaces/keyValueList/index.spec.tsx
    index 22f4670cac552e..f0e24cbc5b440d 100644
    --- a/static/app/components/events/interfaces/keyValueList/index.spec.tsx
    +++ b/static/app/components/events/interfaces/keyValueList/index.spec.tsx
    @@ -14,11 +14,11 @@ describe('KeyValueList', function () {
         const rows = screen.getAllByRole('row');
         expect(rows).toHaveLength(2);
     
    -    const firstColumn = within(rows[0]).getAllByRole('cell');
    +    const firstColumn = within(rows[0]!).getAllByRole('cell');
         expect(firstColumn[0]).toHaveTextContent('a');
         expect(firstColumn[1]).toHaveTextContent('x');
     
    -    const secondColumn = within(rows[1]).getAllByRole('cell');
    +    const secondColumn = within(rows[1]!).getAllByRole('cell');
         expect(secondColumn[0]).toHaveTextContent('b');
         expect(secondColumn[1]).toHaveTextContent('y');
       });
    @@ -33,11 +33,11 @@ describe('KeyValueList', function () {
     
         const rows = screen.getAllByRole('row');
     
    -    const firstColumn = within(rows[0]).getAllByRole('cell');
    +    const firstColumn = within(rows[0]!).getAllByRole('cell');
         expect(firstColumn[0]).toHaveTextContent('a');
         expect(firstColumn[1]).toHaveTextContent('x');
     
    -    const secondColumn = within(rows[1]).getAllByRole('cell');
    +    const secondColumn = within(rows[1]!).getAllByRole('cell');
         expect(secondColumn[0]).toHaveTextContent('b');
         expect(secondColumn[1]).toHaveTextContent('y');
       });
    @@ -52,11 +52,11 @@ describe('KeyValueList', function () {
     
         const rows = screen.getAllByRole('row');
     
    -    const firstColumn = within(rows[0]).getAllByRole('cell');
    +    const firstColumn = within(rows[0]!).getAllByRole('cell');
         expect(firstColumn[0]).toHaveTextContent('a');
         expect(firstColumn[1]).toHaveTextContent(''); // empty string
     
    -    const secondColumn = within(rows[1]).getAllByRole('cell');
    +    const secondColumn = within(rows[1]!).getAllByRole('cell');
         expect(secondColumn[0]).toHaveTextContent('b');
         expect(secondColumn[1]).toHaveTextContent('y');
       });
    @@ -72,10 +72,10 @@ describe('KeyValueList', function () {
         const rows = screen.getAllByRole('row');
     
         // Ignore values, more interested in if keys rendered + are sorted
    -    const firstColumn = within(rows[0]).getAllByRole('cell');
    +    const firstColumn = within(rows[0]!).getAllByRole('cell');
         expect(firstColumn[0]).toHaveTextContent('a');
     
    -    const secondColumn = within(rows[1]).getAllByRole('cell');
    +    const secondColumn = within(rows[1]!).getAllByRole('cell');
         expect(secondColumn[0]).toHaveTextContent('b');
       });
     
    diff --git a/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx b/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx
    index 59502e75f3b180..edde63cbac628b 100644
    --- a/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx
    +++ b/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx
    @@ -74,7 +74,7 @@ function ConsecutiveDBQueriesSpanEvidence({
             [
               makeTransactionNameRow(event, organization, location, projectSlug),
               causeSpans
    -            ? makeRow(t('Starting Span'), getSpanEvidenceValue(causeSpans[0]))
    +            ? makeRow(t('Starting Span'), getSpanEvidenceValue(causeSpans[0]!))
                 : null,
               makeRow('Parallelizable Spans', offendingSpans.map(getSpanEvidenceValue)),
               makeRow(
    @@ -124,11 +124,11 @@ function LargeHTTPPayloadSpanEvidence({
           data={
             [
               makeTransactionNameRow(event, organization, location, projectSlug),
    -          makeRow(t('Large HTTP Payload Span'), getSpanEvidenceValue(offendingSpans[0])),
    +          makeRow(t('Large HTTP Payload Span'), getSpanEvidenceValue(offendingSpans[0]!)),
               makeRow(
                 t('Payload Size'),
    -            getSpanFieldBytes(offendingSpans[0], 'http.response_content_length') ??
    -              getSpanFieldBytes(offendingSpans[0], 'Encoded Body Size')
    +            getSpanFieldBytes(offendingSpans[0]!, 'http.response_content_length') ??
    +              getSpanFieldBytes(offendingSpans[0]!, 'Encoded Body Size')
               ),
             ].filter(Boolean) as KeyValueListData
           }
    @@ -182,7 +182,7 @@ function NPlusOneDBQueriesSpanEvidence({
               makeTransactionNameRow(event, organization, location, projectSlug),
               parentSpan ? makeRow(t('Parent Span'), getSpanEvidenceValue(parentSpan)) : null,
               causeSpans.length > 0
    -            ? makeRow(t('Preceding Span'), getSpanEvidenceValue(causeSpans[0]))
    +            ? makeRow(t('Preceding Span'), getSpanEvidenceValue(causeSpans[0]!))
                 : null,
               ...repeatingSpanRows,
             ].filter(Boolean) as KeyValueListData
    @@ -202,7 +202,7 @@ function NPlusOneAPICallsSpanEvidence({
       const baseURL = requestEntry?.data?.url;
     
       const problemParameters = formatChangingQueryParameters(offendingSpans, baseURL);
    -  const commonPathPrefix = formatBasePath(offendingSpans[0], baseURL);
    +  const commonPathPrefix = formatBasePath(offendingSpans[0]!, baseURL);
     
       return (
         
    @@ -394,12 +394,15 @@ function RenderBlockingAssetSpanEvidence({
         
       );
    @@ -416,15 +419,15 @@ function UncompressedAssetSpanEvidence({
         
    @@ -444,7 +447,7 @@ function DefaultSpanEvidence({
             [
               makeTransactionNameRow(event, organization, location, projectSlug),
               offendingSpans.length > 0
    -            ? makeRow(t('Offending Span'), getSpanEvidenceValue(offendingSpans[0]))
    +            ? makeRow(t('Offending Span'), getSpanEvidenceValue(offendingSpans[0]!))
                 : null,
             ].filter(Boolean) as KeyValueListData
           }
    @@ -658,7 +661,7 @@ function formatChangingQueryParameters(spans: Span[], baseURL?: string): string[
     
       const pairs: string[] = [];
       for (const key in allQueryParameters) {
    -    const values = allQueryParameters[key];
    +    const values = allQueryParameters[key]!;
     
         // By definition, if the parameter only has one value that means it's not
         // changing between calls, so omit it!
    @@ -690,7 +693,7 @@ export const extractSpanURLString = (span: Span, baseURL?: string): URL | null =
         }
       }
     
    -  const [_method, _url] = (span?.description ?? '').split(' ', 2);
    +  const [_method, _url] = (span?.description ?? '').split(' ', 2) as [string, string];
     
       return safeURL(_url, baseURL) ?? null;
     };
    diff --git a/static/app/components/events/interfaces/request/index.spec.tsx b/static/app/components/events/interfaces/request/index.spec.tsx
    index 2ee61131f256e3..030807ee837cad 100644
    --- a/static/app/components/events/interfaces/request/index.spec.tsx
    +++ b/static/app/components/events/interfaces/request/index.spec.tsx
    @@ -172,7 +172,7 @@ describe('Request entry', function () {
           },
         });
     
    -    render(, {
    +    render(, {
           organization: {
             relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
           },
    @@ -186,7 +186,7 @@ describe('Request entry', function () {
     
         expect(screen.getAllByText(/redacted/)).toHaveLength(7);
     
    -    await userEvent.hover(screen.getAllByText(/redacted/)[0]);
    +    await userEvent.hover(screen.getAllByText(/redacted/)[0]!);
     
         expect(
           await screen.findByText(
    @@ -221,7 +221,7 @@ describe('Request entry', function () {
             ],
           });
     
    -      render(, {
    +      render(, {
             organization: {
               relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
             },
    @@ -255,7 +255,7 @@ describe('Request entry', function () {
             ],
           });
     
    -      render(, {
    +      render(, {
             organization: {
               relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
             },
    @@ -289,7 +289,7 @@ describe('Request entry', function () {
             ],
           });
     
    -      render(, {
    +      render(, {
             organization: {
               relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
             },
    @@ -325,7 +325,7 @@ describe('Request entry', function () {
           });
     
           expect(() =>
    -        render(, {
    +        render(, {
               organization: {
                 relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
               },
    @@ -357,7 +357,7 @@ describe('Request entry', function () {
             ],
           });
           expect(() =>
    -        render(, {
    +        render(, {
               organization: {
                 relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
               },
    @@ -388,7 +388,7 @@ describe('Request entry', function () {
           });
     
           expect(() =>
    -        render(, {
    +        render(, {
               organization: {
                 relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
               },
    @@ -418,7 +418,7 @@ describe('Request entry', function () {
               ],
             });
     
    -        render();
    +        render();
     
             expect(screen.getByText('query Test { test }')).toBeInTheDocument();
             expect(screen.getByRole('row', {name: 'operationName Test'})).toBeInTheDocument();
    @@ -456,7 +456,7 @@ describe('Request entry', function () {
             });
     
             const {container} = render(
    -          
    +          
             );
     
             expect(container.querySelector('.line-highlight')).toBeInTheDocument();
    diff --git a/static/app/components/events/interfaces/searchBarAction.spec.tsx b/static/app/components/events/interfaces/searchBarAction.spec.tsx
    index b1af6870ff031f..dbf5f58e269a99 100644
    --- a/static/app/components/events/interfaces/searchBarAction.spec.tsx
    +++ b/static/app/components/events/interfaces/searchBarAction.spec.tsx
    @@ -115,7 +115,7 @@ describe('SearchBarAction', () => {
       });
     
       it('With Option Type only', async () => {
    -    const typeOptions = options[0];
    +    const typeOptions = options[0]!;
         render(
            {
       });
     
       it('With Option Level only', async () => {
    -    const levelOptions = options[1];
    +    const levelOptions = options[1]!;
         render(
           
           {Array.from(measurements.values()).map(verticalMark => {
    -        const mark = Object.values(verticalMark.marks)[0];
    +        const mark = Object.values(verticalMark.marks)[0]!;
             const {timestamp} = mark;
             const bounds = getMeasurementBounds(timestamp, generateBounds);
     
    @@ -45,7 +45,7 @@ function MeasurementsPanel(props: Props) {
     
             const vitalLabels: VitalLabel[] = Object.keys(verticalMark.marks).map(name => ({
               vital: VITAL_DETAILS[`measurements.${name}`],
    -          isPoorValue: verticalMark.marks[name].failedThreshold,
    +          isPoorValue: verticalMark.marks[name]!.failedThreshold,
             }));
     
             if (vitalLabels.length > 1) {
    @@ -62,7 +62,7 @@ function MeasurementsPanel(props: Props) {
               
             );
           })}
    diff --git a/static/app/components/events/interfaces/spans/newTraceDetailsSpanBar.tsx b/static/app/components/events/interfaces/spans/newTraceDetailsSpanBar.tsx
    index 1aa192657fe6d4..97c38dccfdd1c3 100644
    --- a/static/app/components/events/interfaces/spans/newTraceDetailsSpanBar.tsx
    +++ b/static/app/components/events/interfaces/spans/newTraceDetailsSpanBar.tsx
    @@ -329,7 +329,7 @@ export class NewTraceDetailsSpanBar extends Component<
         return (
           
             {Array.from(spanMeasurements.values()).map(verticalMark => {
    -          const mark = Object.values(verticalMark.marks)[0];
    +          const mark = Object.values(verticalMark.marks)[0]!;
               const {timestamp} = mark;
               const bounds = getMeasurementBounds(timestamp, generateBounds);
     
    @@ -573,7 +573,7 @@ export class NewTraceDetailsSpanBar extends Component<
     
       connectObservers() {
         const observer = new IntersectionObserver(([entry]) =>
    -      this.setState({isIntersecting: entry.isIntersecting}, () => {
    +      this.setState({isIntersecting: entry!.isIntersecting}, () => {
             // Scrolls the next(invisible) bar from the virtualized list,
             // by its height. Allows us to look for anchored span bars occuring
             // at the bottom of the span tree.
    diff --git a/static/app/components/events/interfaces/spans/newTraceDetailsSpanDetails.tsx b/static/app/components/events/interfaces/spans/newTraceDetailsSpanDetails.tsx
    index a5e17c31216a18..3d870014281e6e 100644
    --- a/static/app/components/events/interfaces/spans/newTraceDetailsSpanDetails.tsx
    +++ b/static/app/components/events/interfaces/spans/newTraceDetailsSpanDetails.tsx
    @@ -599,7 +599,7 @@ function NewTraceDetailsSpanDetail(props: SpanDetailProps) {
     
     function SpanHTTPInfo({span}: {span: RawSpanType}) {
       if (span.op === 'http.client' && span.description) {
    -    const [method, url] = span.description.split(' ');
    +    const [method, url] = span.description.split(' ') as [string, string];
     
         const parsedURL = safeURL(url);
         const queryString = qs.parse(parsedURL?.search ?? '');
    diff --git a/static/app/components/events/interfaces/spans/newTraceDetailsSpanTree.tsx b/static/app/components/events/interfaces/spans/newTraceDetailsSpanTree.tsx
    index 0a626eb2ee50cf..3ae9bc742a86f6 100644
    --- a/static/app/components/events/interfaces/spans/newTraceDetailsSpanTree.tsx
    +++ b/static/app/components/events/interfaces/spans/newTraceDetailsSpanTree.tsx
    @@ -309,7 +309,7 @@ class NewTraceDetailsSpanTree extends Component {
         const showHiddenSpansMessage = !isCurrentSpanHidden && numOfSpansOutOfViewAbove > 0;
     
         if (showHiddenSpansMessage) {
    -      firstHiddenSpanId = getSpanID(outOfViewSpansAbove[0].span);
    +      firstHiddenSpanId = getSpanID(outOfViewSpansAbove[0]!.span);
           messages.push(
             
               {numOfSpansOutOfViewAbove} {t('spans out of view')}
    @@ -322,7 +322,7 @@ class NewTraceDetailsSpanTree extends Component {
           !isCurrentSpanFilteredOut && numOfFilteredSpansAbove > 0;
     
         if (showFilteredSpansMessage) {
    -      firstHiddenSpanId = getSpanID(filteredSpansAbove[0].span);
    +      firstHiddenSpanId = getSpanID(filteredSpansAbove[0]!.span);
           if (!isCurrentSpanHidden) {
             if (numOfFilteredSpansAbove === 1) {
               messages.push(
    @@ -499,7 +499,7 @@ class NewTraceDetailsSpanTree extends Component {
               // and it's a last child.
               const generationOffset =
                 parentContinuingDepths.length === 1 &&
    -            parentContinuingDepths[0].depth === 0 &&
    +            parentContinuingDepths[0]!.depth === 0 &&
                 parentGeneration > 2
                   ? 2
                   : 1;
    @@ -864,7 +864,7 @@ function SpanRow(props: SpanRowProps) {
       } = props;
     
       const rowRef = useRef(null);
    -  const spanNode = spanTree[index];
    +  const spanNode = spanTree[index]!;
     
       useEffect(() => {
         // Gap spans do not have IDs, so we can't really store them. This should not be a big deal, since
    diff --git a/static/app/components/events/interfaces/spans/spanBar.tsx b/static/app/components/events/interfaces/spans/spanBar.tsx
    index 7754f40a2a64f1..535c0c8834a60e 100644
    --- a/static/app/components/events/interfaces/spans/spanBar.tsx
    +++ b/static/app/components/events/interfaces/spans/spanBar.tsx
    @@ -425,7 +425,7 @@ export class SpanBar extends Component {
         return (
           
             {Array.from(measurements.values()).map(verticalMark => {
    -          const mark = Object.values(verticalMark.marks)[0];
    +          const mark = Object.values(verticalMark.marks)[0]!;
               const {timestamp} = mark;
               const bounds = getMeasurementBounds(timestamp, generateBounds);
     
    diff --git a/static/app/components/events/interfaces/spans/spanDetail.tsx b/static/app/components/events/interfaces/spans/spanDetail.tsx
    index 1fd175e17b2873..f0a1b3d99912a8 100644
    --- a/static/app/components/events/interfaces/spans/spanDetail.tsx
    +++ b/static/app/components/events/interfaces/spans/spanDetail.tsx
    @@ -196,7 +196,7 @@ function SpanDetail(props: Props) {
           return null;
         }
     
    -    const childTransaction = childTransactions[0];
    +    const childTransaction = childTransactions[0]!;
     
         const transactionResult: TransactionResult = {
           'project.name': childTransaction.project_slug,
    diff --git a/static/app/components/events/interfaces/spans/spanProfileDetails.tsx b/static/app/components/events/interfaces/spans/spanProfileDetails.tsx
    index 22f63b9582b4ca..197d5df1220176 100644
    --- a/static/app/components/events/interfaces/spans/spanProfileDetails.tsx
    +++ b/static/app/components/events/interfaces/spans/spanProfileDetails.tsx
    @@ -107,7 +107,7 @@ export function useSpanProfileDetails(event, span) {
         // find the number of nodes with the minimum number of samples
         let hasMinCount = 0;
         for (let i = 0; i < nodes.length; i++) {
    -      if (nodes[i].count >= TOP_NODE_MIN_COUNT) {
    +      if (nodes[i]!.count >= TOP_NODE_MIN_COUNT) {
             hasMinCount += 1;
           } else {
             break;
    @@ -125,7 +125,7 @@ export function useSpanProfileDetails(event, span) {
         }
     
         return {
    -      frames: extractFrames(nodes[index], event.platform || 'other'),
    +      frames: extractFrames(nodes[index]!, event.platform || 'other'),
           hasPrevious: index > 0,
           hasNext: index + 1 < maxNodes,
         };
    @@ -195,7 +195,7 @@ export function SpanProfileDetails({
         return null;
       }
     
    -  const percentage = formatPercentage(nodes[index].count / totalWeight);
    +  const percentage = formatPercentage(nodes[index]!.count / totalWeight);
     
       return (
         
    @@ -217,7 +217,7 @@ export function SpanProfileDetails({
               size="xs"
               title={t(
                 '%s out of %s (%s) of the call stacks collected during this span',
    -            nodes[index].count,
    +            nodes[index]!.count,
                 totalWeight,
                 percentage
               )}
    @@ -274,11 +274,11 @@ function getTopNodes(profile: Profile, startTimestamp, stopTimestamp): CallTreeN
       const callTree: CallTreeNode = new CallTreeNode(ProfilingFrame.Root, null);
     
       for (let i = 0; i < profile.samples.length; i++) {
    -    const sample = profile.samples[i];
    +    const sample = profile.samples[i]!;
         // TODO: should this take self times into consideration?
         const inRange = startTimestamp <= duration && duration < stopTimestamp;
     
    -    duration += profile.weights[i];
    +    duration += profile.weights[i]!;
     
         if (sample.isRoot || !inRange) {
           continue;
    diff --git a/static/app/components/events/interfaces/spans/spanSiblingGroupBar.tsx b/static/app/components/events/interfaces/spans/spanSiblingGroupBar.tsx
    index c65f4b88d04776..75fb10f64bf148 100644
    --- a/static/app/components/events/interfaces/spans/spanSiblingGroupBar.tsx
    +++ b/static/app/components/events/interfaces/spans/spanSiblingGroupBar.tsx
    @@ -72,8 +72,8 @@ export default function SpanSiblingGroupBar(props: SpanSiblingGroupBarProps) {
           return '';
         }
     
    -    const operation = spanGrouping[0].span.op;
    -    const description = spanGrouping[0].span.description;
    +    const operation = spanGrouping[0]!.span.op;
    +    const description = spanGrouping[0]!.span.description;
     
         if (!description || !operation) {
           if (description) {
    @@ -132,7 +132,7 @@ export default function SpanSiblingGroupBar(props: SpanSiblingGroupBarProps) {
               
             ))}
    @@ -155,7 +155,7 @@ export default function SpanSiblingGroupBar(props: SpanSiblingGroupBarProps) {
           spanNumber={spanNumber}
           generateBounds={generateBounds}
           toggleSpanGroup={() => {
    -        toggleSiblingSpanGroup?.(spanGrouping[0].span, occurrence);
    +        toggleSiblingSpanGroup?.(spanGrouping[0]!.span, occurrence);
             isEmbeddedSpanTree &&
               trackAnalytics('issue_details.performance.autogrouped_siblings_toggle', {
                 organization,
    diff --git a/static/app/components/events/interfaces/spans/spanTree.tsx b/static/app/components/events/interfaces/spans/spanTree.tsx
    index 41f653483eae9b..03960067aeba25 100644
    --- a/static/app/components/events/interfaces/spans/spanTree.tsx
    +++ b/static/app/components/events/interfaces/spans/spanTree.tsx
    @@ -305,7 +305,7 @@ class SpanTree extends Component {
         const showHiddenSpansMessage = !isCurrentSpanHidden && numOfSpansOutOfViewAbove > 0;
     
         if (showHiddenSpansMessage) {
    -      firstHiddenSpanId = getSpanID(outOfViewSpansAbove[0].span);
    +      firstHiddenSpanId = getSpanID(outOfViewSpansAbove[0]!.span);
           messages.push(
             
               {numOfSpansOutOfViewAbove} {t('spans out of view')}
    @@ -318,7 +318,7 @@ class SpanTree extends Component {
           !isCurrentSpanFilteredOut && numOfFilteredSpansAbove > 0;
     
         if (showFilteredSpansMessage) {
    -      firstHiddenSpanId = getSpanID(filteredSpansAbove[0].span);
    +      firstHiddenSpanId = getSpanID(filteredSpansAbove[0]!.span);
           if (!isCurrentSpanHidden) {
             if (numOfFilteredSpansAbove === 1) {
               messages.push(
    @@ -816,7 +816,7 @@ function SpanRow(props: SpanRowProps) {
       } = props;
     
       const rowRef = useRef(null);
    -  const spanNode = spanTree[index];
    +  const spanNode = spanTree[index]!;
     
       useEffect(() => {
         // Gap spans do not have IDs, so we can't really store them. This should not be a big deal, since
    diff --git a/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx b/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx
    index e536a47efb7b76..1096d6022a3b5d 100644
    --- a/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx
    +++ b/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx
    @@ -455,11 +455,11 @@ describe('SpanTreeModel', () => {
         );
     
         fullWaterfallExpected[0] = {
    -      ...fullWaterfallExpected[0],
    +      ...fullWaterfallExpected[0]!,
         };
    -    assert(fullWaterfallExpected[0].type === 'span');
    -    fullWaterfallExpected[0].numOfSpanChildren += 1;
    -    fullWaterfallExpected[0].showEmbeddedChildren = true;
    +    assert(fullWaterfallExpected[0]!.type === 'span');
    +    fullWaterfallExpected[0]!.numOfSpanChildren += 1;
    +    fullWaterfallExpected[0]!.showEmbeddedChildren = true;
     
         expect(spans).toEqual(fullWaterfallExpected);
     
    @@ -559,11 +559,11 @@ describe('SpanTreeModel', () => {
           },
         };
     
    -    if (!Array.isArray(event2.entries[0].data)) {
    +    if (!Array.isArray(event2.entries[0]!.data)) {
           throw new Error('event2.entries[0].data is not an array');
         }
     
    -    const data = event2.entries[0].data as RawSpanType[];
    +    const data = event2.entries[0]!.data as RawSpanType[];
         for (let i = 0; i < 5; i++) {
           data.push(spanTemplate);
         }
    @@ -603,11 +603,11 @@ describe('SpanTreeModel', () => {
         });
     
         expect(spans.length).toEqual(2);
    -    expect(spans[1].type).toEqual('span_group_siblings');
    +    expect(spans[1]!.type).toEqual('span_group_siblings');
     
         // If statement here is required to avoid TS linting issues
    -    if (spans[1].type === 'span_group_siblings') {
    -      expect(spans[1].spanSiblingGrouping!.length).toEqual(5);
    +    if (spans[1]!.type === 'span_group_siblings') {
    +      expect(spans[1]!.spanSiblingGrouping!.length).toEqual(5);
         }
       });
     
    @@ -641,11 +641,11 @@ describe('SpanTreeModel', () => {
           },
         };
     
    -    if (!Array.isArray(event2.entries[0].data)) {
    +    if (!Array.isArray(event2.entries[0]!.data)) {
           throw new Error('event2.entries[0].data is not an array');
         }
     
    -    const data = event2.entries[0].data as RawSpanType[];
    +    const data = event2.entries[0]!.data as RawSpanType[];
         for (let i = 0; i < 4; i++) {
           data.push(spanTemplate);
         }
    @@ -737,11 +737,11 @@ describe('SpanTreeModel', () => {
           },
         };
     
    -    if (!Array.isArray(event2.entries[0].data)) {
    +    if (!Array.isArray(event2.entries[0]!.data)) {
           throw new Error('event2.entries[0].data is not an array');
         }
     
    -    const data = event2.entries[0].data as RawSpanType[];
    +    const data = event2.entries[0]!.data as RawSpanType[];
         for (let i = 0; i < 7; i++) {
           data.push(groupableSpanTemplate);
         }
    @@ -788,8 +788,8 @@ describe('SpanTreeModel', () => {
         });
     
         expect(spans.length).toEqual(4);
    -    expect(spans[1].type).toEqual('span_group_siblings');
    -    expect(spans[2].type).toEqual('span');
    -    expect(spans[3].type).toEqual('span_group_siblings');
    +    expect(spans[1]!.type).toEqual('span_group_siblings');
    +    expect(spans[2]!.type).toEqual('span');
    +    expect(spans[3]!.type).toEqual('span_group_siblings');
       });
     });
    diff --git a/static/app/components/events/interfaces/spans/spanTreeModel.tsx b/static/app/components/events/interfaces/spans/spanTreeModel.tsx
    index 7de0a6c3ceaf99..41fd73af6d5898 100644
    --- a/static/app/components/events/interfaces/spans/spanTreeModel.tsx
    +++ b/static/app/components/events/interfaces/spans/spanTreeModel.tsx
    @@ -262,11 +262,11 @@ class SpanTreeModel {
           // we will need to reconstruct the tree depth information. This is only neccessary
           // when the span group chain is hidden/collapsed.
           if (spanNestedGrouping.length === 1) {
    -        const treeDepthEntry = isOrphanSpan(spanNestedGrouping[0].span)
    -          ? ({type: 'orphan', depth: spanNestedGrouping[0].treeDepth} as OrphanTreeDepth)
    -          : spanNestedGrouping[0].treeDepth;
    +        const treeDepthEntry = isOrphanSpan(spanNestedGrouping[0]!.span)
    +          ? ({type: 'orphan', depth: spanNestedGrouping[0]!.treeDepth} as OrphanTreeDepth)
    +          : spanNestedGrouping[0]!.treeDepth;
     
    -        if (!spanNestedGrouping[0].isLastSibling) {
    +        if (!spanNestedGrouping[0]!.isLastSibling) {
               continuingTreeDepths = [...continuingTreeDepths, treeDepthEntry];
             }
           }
    @@ -352,11 +352,11 @@ class SpanTreeModel {
         };
     
         if (descendantsSource?.length >= MIN_SIBLING_GROUP_SIZE) {
    -      let prevSpanModel = descendantsSource[0];
    +      let prevSpanModel = descendantsSource[0]!;
           let currentGroup = [prevSpanModel];
     
           for (let i = 1; i < descendantsSource.length; i++) {
    -        const currSpanModel = descendantsSource[i];
    +        const currSpanModel = descendantsSource[i]!;
     
             // We want to group siblings only if they share the same op and description, and if they have no children
             if (
    @@ -438,7 +438,7 @@ class SpanTreeModel {
               return acc;
             }
     
    -        const key = getSiblingGroupKey(group[0].span, occurrence);
    +        const key = getSiblingGroupKey(group[0]!.span, occurrence);
             if (this.expandedSiblingGroups.has(key)) {
               // This check is needed here, since it is possible that a user could be filtering for a specific span ID.
               // In this case, we must add only the specified span into the accumulator's descendants
    @@ -482,7 +482,7 @@ class SpanTreeModel {
                   });
     
                   const gapSpan = this.generateSpanGap(
    -                group[0].span,
    +                group[0]!.span,
                     event,
                     acc.previousSiblingEndTimestamp,
                     treeDepth + 1,
    @@ -512,7 +512,7 @@ class SpanTreeModel {
             // if the spans are filtered or out of bounds here
     
             if (
    -          this.isSpanFilteredOut(props, group[0]) ||
    +          this.isSpanFilteredOut(props, group[0]!) ||
               groupShouldBeHidden(group, focusedSpanIds)
             ) {
               group.forEach(spanModel => {
    @@ -525,8 +525,8 @@ class SpanTreeModel {
             }
     
             const bounds = generateBounds({
    -          startTimestamp: group[0].span.start_timestamp,
    -          endTimestamp: group[group.length - 1].span.timestamp,
    +          startTimestamp: group[0]!.span.start_timestamp,
    +          endTimestamp: group[group.length - 1]!.span.timestamp,
             });
     
             if (!bounds.isSpanVisibleInView) {
    @@ -540,7 +540,7 @@ class SpanTreeModel {
             }
     
             const gapSpan = this.generateSpanGap(
    -          group[0].span,
    +          group[0]!.span,
               event,
               acc.previousSiblingEndTimestamp,
               treeDepth + 1,
    @@ -590,7 +590,7 @@ class SpanTreeModel {
             };
     
             acc.previousSiblingEndTimestamp =
    -          wrappedSiblings[wrappedSiblings.length - 1].span.timestamp;
    +          wrappedSiblings[wrappedSiblings.length - 1]!.span.timestamp;
     
             acc.descendants.push(groupedSiblingsSpan);
             return acc;
    @@ -678,14 +678,14 @@ class SpanTreeModel {
           spanNestedGrouping.length === 1
         ) {
           if (!isNestedSpanGroupExpanded) {
    -        const parentSpan = spanNestedGrouping[0].span;
    +        const parentSpan = spanNestedGrouping[0]!.span;
             const parentSpanBounds = generateBounds({
               startTimestamp: parentSpan.start_timestamp,
               endTimestamp: parentSpan.timestamp,
             });
             const isParentSpanOutOfView = !parentSpanBounds.isSpanVisibleInView;
             if (!isParentSpanOutOfView) {
    -          return [spanNestedGrouping[0], wrappedSpan, ...descendants];
    +          return [spanNestedGrouping[0]!, wrappedSpan, ...descendants];
             }
           }
     
    diff --git a/static/app/components/events/interfaces/spans/traceView.spec.tsx b/static/app/components/events/interfaces/spans/traceView.spec.tsx
    index 6e08acbbbea14f..5285505fa72cce 100644
    --- a/static/app/components/events/interfaces/spans/traceView.spec.tsx
    +++ b/static/app/components/events/interfaces/spans/traceView.spec.tsx
    @@ -202,7 +202,7 @@ describe('TraceView', () => {
     
           expect(screen.queryAllByText('group me')).toHaveLength(2);
     
    -      const firstGroup = screen.queryAllByText('Autogrouped — http —')[0];
    +      const firstGroup = screen.queryAllByText('Autogrouped — http —')[0]!;
           await userEvent.click(firstGroup);
           expect(await screen.findAllByText('group me')).toHaveLength(6);
     
    @@ -210,7 +210,7 @@ describe('TraceView', () => {
           await userEvent.click(secondGroup);
           expect(await screen.findAllByText('group me')).toHaveLength(10);
     
    -      const firstRegroup = screen.queryAllByText('Regroup')[0];
    +      const firstRegroup = screen.queryAllByText('Regroup')[0]!;
           await userEvent.click(firstRegroup);
           expect(await screen.findAllByText('group me')).toHaveLength(6);
     
    diff --git a/static/app/components/events/interfaces/spans/utils.tsx b/static/app/components/events/interfaces/spans/utils.tsx
    index b3d25167f9212d..e5cade8519816b 100644
    --- a/static/app/components/events/interfaces/spans/utils.tsx
    +++ b/static/app/components/events/interfaces/spans/utils.tsx
    @@ -700,7 +700,7 @@ function hasFailedThreshold(marks: Measurements): boolean {
       );
     
       return records.some(record => {
    -    const {value} = marks[record.slug];
    +    const {value} = marks[record.slug]!;
         if (typeof value === 'number' && typeof record.poorThreshold === 'number') {
           return value >= record.poorThreshold;
         }
    @@ -733,7 +733,7 @@ export function getMeasurements(
       const measurements = Object.keys(event.measurements)
         .filter(name => allowedVitals.has(`measurements.${name}`))
         .map(name => {
    -      const associatedMeasurement = event.measurements![name];
    +      const associatedMeasurement = event.measurements![name]!;
           return {
             name,
             // Time timestamp is in seconds, but the measurement value is given in ms so convert it here
    @@ -947,8 +947,8 @@ export function getSpanGroupTimestamps(spanGroup: EnhancedSpan[]) {
           };
         },
         {
    -      startTimestamp: spanGroup[0].span.start_timestamp,
    -      endTimestamp: spanGroup[0].span.timestamp,
    +      startTimestamp: spanGroup[0]!.span.start_timestamp,
    +      endTimestamp: spanGroup[0]!.span.timestamp,
         }
       );
     }
    @@ -1065,22 +1065,22 @@ export function getFormattedTimeRangeWithLeadingAndTrailingZero(
         start: string[];
       }>(
         (acc, startString, index) => {
    -      if (startString.length > endStrings[index].length) {
    +      if (startString.length > endStrings[index]!.length) {
             acc.start.push(startString);
             acc.end.push(
               index === 0
    -            ? endStrings[index].padStart(startString.length, '0')
    -            : endStrings[index].padEnd(startString.length, '0')
    +            ? endStrings[index]!.padStart(startString.length, '0')
    +            : endStrings[index]!.padEnd(startString.length, '0')
             );
             return acc;
           }
     
           acc.start.push(
             index === 0
    -          ? startString.padStart(endStrings[index].length, '0')
    -          : startString.padEnd(endStrings[index].length, '0')
    +          ? startString.padStart(endStrings[index]!.length, '0')
    +          : startString.padEnd(endStrings[index]!.length, '0')
           );
    -      acc.end.push(endStrings[index]);
    +      acc.end.push(endStrings[index]!);
           return acc;
         },
         {start: [], end: []}
    diff --git a/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx b/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx
    index 6b2f210ab39afd..7ed9ae02119337 100644
    --- a/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx
    +++ b/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx
    @@ -668,12 +668,12 @@ describe('WaterfallModel', () => {
     
         expected[1] = {
           type: 'out_of_view',
    -      span: fullWaterfall[1].span,
    +      span: fullWaterfall[1]!!.span,
         } as EnhancedProcessedSpanType;
     
         expected[4] = {
           type: 'out_of_view',
    -      span: fullWaterfall[4].span,
    +      span: fullWaterfall[4]!.span,
         } as EnhancedProcessedSpanType;
     
         expect(spans).toEqual(expected);
    @@ -686,51 +686,51 @@ describe('WaterfallModel', () => {
         });
     
         assert(
    -      fullWaterfall[10].type === 'span_group_chain' &&
    -        fullWaterfall[10].spanNestedGrouping
    +      fullWaterfall[10]!.type === 'span_group_chain' &&
    +        fullWaterfall[10]!.spanNestedGrouping
         );
         expected = [
           {
             type: 'filtered_out',
    -        span: fullWaterfall[0].span,
    +        span: fullWaterfall[0]!.span,
           },
           {
             type: 'out_of_view',
    -        span: fullWaterfall[1].span,
    +        span: fullWaterfall[1]!!.span,
           },
           fullWaterfall[2],
           fullWaterfall[3],
           {
             type: 'filtered_out',
    -        span: fullWaterfall[4].span,
    +        span: fullWaterfall[4]!.span,
           },
           {
             type: 'filtered_out',
    -        span: fullWaterfall[5].span,
    +        span: fullWaterfall[5]!.span,
           },
           {
             type: 'filtered_out',
    -        span: fullWaterfall[6].span,
    +        span: fullWaterfall[6]!.span,
           },
           {
             type: 'filtered_out',
    -        span: fullWaterfall[7].span,
    +        span: fullWaterfall[7]!.span,
           },
           {
             type: 'filtered_out',
    -        span: fullWaterfall[9].span,
    +        span: fullWaterfall[9]!.span,
           },
           {
             type: 'filtered_out',
    -        span: fullWaterfall[10].spanNestedGrouping[0].span,
    +        span: fullWaterfall[10]!.spanNestedGrouping![0]!.span,
           },
           {
             type: 'filtered_out',
    -        span: fullWaterfall[10].spanNestedGrouping[1].span,
    +        span: fullWaterfall[10]!.spanNestedGrouping![1]!.span,
           },
           {
             type: 'filtered_out',
    -        span: fullWaterfall[11].span,
    +        span: fullWaterfall[11]!.span,
           },
         ] as EnhancedProcessedSpanType[];
     
    @@ -746,7 +746,7 @@ describe('WaterfallModel', () => {
           viewEnd: 0.65,
         });
     
    -    expected[1].type = 'filtered_out';
    +    expected[1]!.type = 'filtered_out';
     
         expect(spans).toEqual(expected);
       });
    @@ -814,7 +814,7 @@ describe('WaterfallModel', () => {
           ...event,
           entries: [
             {
    -          data: [event.entries[0].data[0]],
    +          data: [event.entries[0]!.data[0]],
               type: EntryType.SPANS,
             },
           ],
    @@ -834,7 +834,7 @@ describe('WaterfallModel', () => {
             toggleNestedSpanGroup: undefined,
           },
           {
    -        ...fullWaterfall[1],
    +        ...fullWaterfall[1]!,
             isLastSibling: true,
             numOfSpanChildren: 0,
             toggleNestedSpanGroup: undefined,
    @@ -848,10 +848,10 @@ describe('WaterfallModel', () => {
           entries: [
             {
               data: [
    -            event.entries[0].data[0],
    +            event.entries[0]!.data[0],
                 {
    -              ...event.entries[0].data[0],
    -              parent_span_id: event.entries[0].data[0].span_id,
    +              ...event.entries[0]!.data[0],
    +              parent_span_id: event.entries[0]!.data[0].span_id,
                   span_id: 'foo',
                 },
               ],
    @@ -875,17 +875,17 @@ describe('WaterfallModel', () => {
             toggleNestedSpanGroup: undefined,
           },
           {
    -        ...fullWaterfall[1],
    +        ...fullWaterfall[1]!,
             treeDepth: 1,
             isLastSibling: true,
             numOfSpanChildren: 1,
             toggleNestedSpanGroup: undefined,
           },
           {
    -        ...fullWaterfall[1],
    +        ...fullWaterfall[1]!,
             span: {
    -          ...fullWaterfall[1].span,
    -          parent_span_id: event.entries[0].data[0].span_id,
    +          ...fullWaterfall[1]!!.span,
    +          parent_span_id: event.entries[0]!.data[0]!.span_id,
               span_id: 'foo',
             },
             treeDepth: 2,
    @@ -902,14 +902,14 @@ describe('WaterfallModel', () => {
           entries: [
             {
               data: [
    -            event.entries[0].data[0],
    +            event.entries[0]!.data[0],
                 {
    -              ...event.entries[0].data[0],
    -              parent_span_id: event.entries[0].data[0].span_id,
    +              ...event.entries[0]!.data[0],
    +              parent_span_id: event.entries[0]!.data[0]!.span_id,
                   span_id: 'foo',
                 },
                 {
    -              ...event.entries[0].data[0],
    +              ...event.entries[0]!.data[0],
                   parent_span_id: 'foo',
                   span_id: 'bar',
                 },
    @@ -928,7 +928,7 @@ describe('WaterfallModel', () => {
         // expect 1 or more spans are grouped
         expect(spans).toHaveLength(3);
     
    -    assert(fullWaterfall[1].type === 'span');
    +    assert(fullWaterfall[1]!.type === 'span');
         const collapsedWaterfallExpected = [
           {
             ...fullWaterfall[0],
    @@ -938,24 +938,24 @@ describe('WaterfallModel', () => {
           {
             type: 'span_group_chain',
             treeDepth: 1,
    -        continuingTreeDepths: fullWaterfall[1].continuingTreeDepths,
    +        continuingTreeDepths: fullWaterfall[1]!.continuingTreeDepths,
             span: {
    -          ...fullWaterfall[1].span,
    +          ...fullWaterfall[1]!.span,
               parent_span_id: 'foo',
               span_id: 'bar',
             },
             spanNestedGrouping: [
               {
    -            ...fullWaterfall[1],
    +            ...fullWaterfall[1]!,
                 isLastSibling: true,
                 numOfSpanChildren: 1,
                 toggleNestedSpanGroup: undefined,
               },
               {
    -            ...fullWaterfall[1],
    +            ...fullWaterfall[1]!,
                 span: {
    -              ...fullWaterfall[1].span,
    -              parent_span_id: event.entries[0].data[0].span_id,
    +              ...fullWaterfall[1]!.span,
    +              parent_span_id: event.entries[0]!.data[0].span_id,
                   span_id: 'foo',
                 },
                 isLastSibling: true,
    @@ -967,9 +967,9 @@ describe('WaterfallModel', () => {
             toggleNestedSpanGroup: expect.anything(),
           },
           {
    -        ...fullWaterfall[1],
    +        ...fullWaterfall[1]!,
             span: {
    -          ...fullWaterfall[1].span,
    +          ...fullWaterfall[1]!.span,
               parent_span_id: 'foo',
               span_id: 'bar',
             },
    @@ -983,8 +983,8 @@ describe('WaterfallModel', () => {
         expect(spans).toEqual(collapsedWaterfallExpected);
     
         // Expand span group
    -    assert(spans[1].type === 'span' && spans[1].toggleNestedSpanGroup);
    -    spans[1].toggleNestedSpanGroup();
    +    assert(spans[1]!.type === 'span' && spans[1]!.toggleNestedSpanGroup);
    +    spans[1]!.toggleNestedSpanGroup();
     
         spans = waterfallModel.getWaterfall({
           viewStart: 0,
    @@ -1002,17 +1002,17 @@ describe('WaterfallModel', () => {
             toggleNestedSpanGroup: undefined,
           },
           {
    -        ...fullWaterfall[1],
    +        ...fullWaterfall[1]!,
             isLastSibling: true,
             numOfSpanChildren: 1,
             treeDepth: 1,
             toggleNestedSpanGroup: expect.anything(),
           },
           {
    -        ...fullWaterfall[1],
    +        ...fullWaterfall[1]!,
             span: {
    -          ...fullWaterfall[1].span,
    -          parent_span_id: event.entries[0].data[0].span_id,
    +          ...fullWaterfall[1]!.span,
    +          parent_span_id: event.entries[0]!.data[0].span_id,
               span_id: 'foo',
             },
             isLastSibling: true,
    @@ -1021,9 +1021,9 @@ describe('WaterfallModel', () => {
             toggleNestedSpanGroup: undefined,
           },
           {
    -        ...fullWaterfall[1],
    +        ...fullWaterfall[1]!,
             span: {
    -          ...fullWaterfall[1].span,
    +          ...fullWaterfall[1]!.span,
               parent_span_id: 'foo',
               span_id: 'bar',
             },
    @@ -1035,8 +1035,8 @@ describe('WaterfallModel', () => {
         ]);
     
         // Collapse span group
    -    assert(spans[1].type === 'span' && spans[1].toggleNestedSpanGroup);
    -    spans[1].toggleNestedSpanGroup();
    +    assert(spans[1]!.type === 'span' && spans[1]!.toggleNestedSpanGroup);
    +    spans[1]!.toggleNestedSpanGroup();
     
         spans = waterfallModel.getWaterfall({
           viewStart: 0,
    diff --git a/static/app/components/events/interfaces/spans/waterfallModel.tsx b/static/app/components/events/interfaces/spans/waterfallModel.tsx
    index a77d4c2d0d719c..2b0b446cbbb03a 100644
    --- a/static/app/components/events/interfaces/spans/waterfallModel.tsx
    +++ b/static/app/components/events/interfaces/spans/waterfallModel.tsx
    @@ -283,8 +283,8 @@ class WaterfallModel {
             };
           },
           {
    -        traceStartTimestamp: this.traceBounds[0].traceStartTimestamp,
    -        traceEndTimestamp: this.traceBounds[0].traceEndTimestamp,
    +        traceStartTimestamp: this.traceBounds[0]!.traceStartTimestamp,
    +        traceEndTimestamp: this.traceBounds[0]!.traceEndTimestamp,
           }
         );
       };
    diff --git a/static/app/components/events/interfaces/threads.spec.tsx b/static/app/components/events/interfaces/threads.spec.tsx
    index 44d1f1cbd18730..f96be912bf81cd 100644
    --- a/static/app/components/events/interfaces/threads.spec.tsx
    +++ b/static/app/components/events/interfaces/threads.spec.tsx
    @@ -218,7 +218,7 @@ describe('Threads', function () {
           };
     
           const props: React.ComponentProps = {
    -        data: event.entries[1].data as React.ComponentProps['data'],
    +        data: event.entries[1]!.data as React.ComponentProps['data'],
             event,
             groupingCurrentLevel: 0,
             projectSlug: project.slug,
    @@ -268,7 +268,7 @@ describe('Threads', function () {
             render(, {organization});
     
             expect(
    -          within(screen.getAllByTestId('line')[0]).getByText(
    +          within(screen.getAllByTestId('line')[0]!).getByText(
                 'sentry/controllers/welcome_controller.rb'
               )
             ).toBeInTheDocument();
    @@ -287,7 +287,7 @@ describe('Threads', function () {
     
             // Last frame is the first on the list
             expect(
    -          within(screen.getAllByTestId('line')[0]).getByText(
    +          within(screen.getAllByTestId('line')[0]!).getByText(
                 'puma (3.12.6) lib/puma/server.rb'
               )
             ).toBeInTheDocument();
    @@ -298,7 +298,7 @@ describe('Threads', function () {
     
             // First frame is the first on the list
             expect(
    -          within(screen.getAllByTestId('line')[0]).getByText(
    +          within(screen.getAllByTestId('line')[0]!).getByText(
                 'sentry/controllers/welcome_controller.rb'
               )
             ).toBeInTheDocument();
    @@ -896,7 +896,7 @@ describe('Threads', function () {
           };
     
           const props: React.ComponentProps = {
    -        data: event.entries[1].data as React.ComponentProps['data'],
    +        data: event.entries[1]!.data as React.ComponentProps['data'],
             event,
             groupingCurrentLevel: 0,
             projectSlug: project.slug,
    @@ -1013,7 +1013,7 @@ describe('Threads', function () {
     
           it('maps android vm states to java vm states', async function () {
             const newEvent = {...event};
    -        const threadsEntry = newEvent.entries[1].data as React.ComponentProps<
    +        const threadsEntry = newEvent.entries[1]!.data as React.ComponentProps<
               typeof Threads
             >['data'];
             const thread = {
    @@ -1089,7 +1089,7 @@ describe('Threads', function () {
             render(, {organization});
     
             expect(
    -          within(screen.getAllByTestId('stack-trace-frame')[0]).getByText(
    +          within(screen.getAllByTestId('stack-trace-frame')[0]!).getByText(
                 '-[SentryClient crash]'
               )
             ).toBeInTheDocument();
    @@ -1115,7 +1115,7 @@ describe('Threads', function () {
     
             // Last frame is the first on the list
             expect(
    -          within(screen.getAllByTestId('stack-trace-frame')[0]).getByText('UIKit')
    +          within(screen.getAllByTestId('stack-trace-frame')[0]!).getByText('UIKit')
             ).toBeInTheDocument();
     
             // Switch back to recent first
    @@ -1124,7 +1124,7 @@ describe('Threads', function () {
     
             // First frame is the first on the list
             expect(
    -          within(screen.getAllByTestId('stack-trace-frame')[0]).getByText(
    +          within(screen.getAllByTestId('stack-trace-frame')[0]!).getByText(
                 '-[SentryClient crash]'
               )
             ).toBeInTheDocument();
    @@ -1159,7 +1159,7 @@ describe('Threads', function () {
     
             // Function name is not verbose
             expect(
    -          within(screen.getAllByTestId('stack-trace-frame')[1]).getByText(
    +          within(screen.getAllByTestId('stack-trace-frame')[1]!).getByText(
                 'ViewController.causeCrash'
               )
             ).toBeInTheDocument();
    @@ -1169,14 +1169,14 @@ describe('Threads', function () {
     
             // Function name is now verbose
             expect(
    -          within(screen.getAllByTestId('stack-trace-frame')[1]).getByText(
    +          within(screen.getAllByTestId('stack-trace-frame')[1]!).getByText(
                 'ViewController.causeCrash(Any) -> ()'
               )
             ).toBeInTheDocument();
     
             // Address is not absolute
             expect(
    -          within(screen.getAllByTestId('stack-trace-frame')[1]).getByText('+0x085ac')
    +          within(screen.getAllByTestId('stack-trace-frame')[1]!).getByText('+0x085ac')
             ).toBeInTheDocument();
     
             // Click on absolute file paths option
    @@ -1184,7 +1184,7 @@ describe('Threads', function () {
     
             // Address is now absolute
             expect(
    -          within(screen.getAllByTestId('stack-trace-frame')[1]).getByText('0x10008c5ac')
    +          within(screen.getAllByTestId('stack-trace-frame')[1]!).getByText('0x10008c5ac')
             ).toBeInTheDocument();
     
             MockApiClient.addMockResponse({
    @@ -1215,7 +1215,7 @@ describe('Threads', function () {
     
           it('uses thread label in selector if name not available', async function () {
             const newEvent = {...event};
    -        const threadsEntry = newEvent.entries[1].data as React.ComponentProps<
    +        const threadsEntry = newEvent.entries[1]!.data as React.ComponentProps<
               typeof Threads
             >['data'];
             const thread = {
    diff --git a/static/app/components/events/interfaces/threads/threadSelector/getThreadException.tsx b/static/app/components/events/interfaces/threads/threadSelector/getThreadException.tsx
    index 23e490792c9033..c113527cda8533 100644
    --- a/static/app/components/events/interfaces/threads/threadSelector/getThreadException.tsx
    +++ b/static/app/components/events/interfaces/threads/threadSelector/getThreadException.tsx
    @@ -6,12 +6,12 @@ function getException(
       exceptionDataValues: ExceptionValue[],
       thread: Thread
     ) {
    -  if (exceptionDataValues.length === 1 && !exceptionDataValues[0].stacktrace) {
    +  if (exceptionDataValues.length === 1 && !exceptionDataValues[0]!.stacktrace) {
         return {
           ...exceptionData,
           values: [
             {
    -          ...exceptionDataValues[0],
    +          ...exceptionDataValues[0]!,
               stacktrace: thread.stacktrace,
               rawStacktrace: thread.rawStacktrace,
             },
    diff --git a/static/app/components/events/interfaces/utils.tsx b/static/app/components/events/interfaces/utils.tsx
    index 5020068be07e2b..62b175a4c10aa3 100644
    --- a/static/app/components/events/interfaces/utils.tsx
    +++ b/static/app/components/events/interfaces/utils.tsx
    @@ -89,7 +89,7 @@ export function getHiddenFrameIndices({
           const index = parseInt(indexString, 10);
           const indicesToBeAdded: number[] = [];
           let i = 1;
    -      let numHidden = frameCountMap[index];
    +      let numHidden = frameCountMap[index]!;
           while (numHidden > 0) {
             if (!repeatedIndeces.includes(index - i)) {
               indicesToBeAdded.push(index - i);
    @@ -305,7 +305,7 @@ export function parseAssembly(assembly: string | null) {
       }
     
       for (let i = 1; i < pieces.length; i++) {
    -    const [key, value] = pieces[i].trim().split('=');
    +    const [key, value] = pieces[i]!.trim().split('=');
     
         // eslint-disable-next-line default-case
         switch (key) {
    diff --git a/static/app/components/events/opsBreakdown.tsx b/static/app/components/events/opsBreakdown.tsx
    index d4fd56bc1e9cd8..e489c0edfa9ba1 100644
    --- a/static/app/components/events/opsBreakdown.tsx
    +++ b/static/app/components/events/opsBreakdown.tsx
    @@ -373,7 +373,7 @@ function mergeInterval(intervals: TimeWindowSpan[]): TimeWindowSpan[] {
           continue;
         }
     
    -    const lastInterval = merged[merged.length - 1];
    +    const lastInterval = merged[merged.length - 1]!;
         const lastIntervalEnd = lastInterval[1];
     
         const [currentIntervalStart, currentIntervalEnd] = currentInterval;
    diff --git a/static/app/components/events/searchBar.tsx b/static/app/components/events/searchBar.tsx
    index a990c611bbc047..dbfac9253a5b46 100644
    --- a/static/app/components/events/searchBar.tsx
    +++ b/static/app/components/events/searchBar.tsx
    @@ -98,19 +98,19 @@ const getSearchConfigFromCustomPerformanceMetrics = (
         numericKeys: [...defaultConfig.numericKeys],
       };
       Object.keys(customPerformanceMetrics).forEach(metricName => {
    -    const {fieldType} = customPerformanceMetrics[metricName];
    +    const {fieldType} = customPerformanceMetrics[metricName]!;
         switch (fieldType) {
           case 'size':
    -        searchConfigMap.sizeKeys.push(metricName);
    +        searchConfigMap.sizeKeys!.push(metricName);
             break;
           case 'duration':
    -        searchConfigMap.durationKeys.push(metricName);
    +        searchConfigMap.durationKeys!.push(metricName);
             break;
           case 'percentage':
    -        searchConfigMap.percentageKeys.push(metricName);
    +        searchConfigMap.percentageKeys!.push(metricName);
             break;
           default:
    -        searchConfigMap.numericKeys.push(metricName);
    +        searchConfigMap.numericKeys!.push(metricName);
         }
       });
       const searchConfig = {
    diff --git a/static/app/components/events/viewHierarchy/index.spec.tsx b/static/app/components/events/viewHierarchy/index.spec.tsx
    index 7ecfdd3c2321ed..a34df368f49bea 100644
    --- a/static/app/components/events/viewHierarchy/index.spec.tsx
    +++ b/static/app/components/events/viewHierarchy/index.spec.tsx
    @@ -96,7 +96,7 @@ describe('View Hierarchy', function () {
       it('can navigate with keyboard shortcuts after a selection', async function () {
         render();
     
    -    await userEvent.click(screen.getAllByText('Container - test_identifier')[0]);
    +    await userEvent.click(screen.getAllByText('Container - test_identifier')[0]!);
     
         await userEvent.keyboard('{ArrowDown}');
     
    @@ -107,7 +107,7 @@ describe('View Hierarchy', function () {
       it('can expand/collapse with the keyboard', async function () {
         render();
     
    -    await userEvent.click(screen.getAllByText('Nested Container - nested')[0]);
    +    await userEvent.click(screen.getAllByText('Nested Container - nested')[0]!);
     
         await userEvent.keyboard('{Enter}');
     
    diff --git a/static/app/components/events/viewHierarchy/utils.tsx b/static/app/components/events/viewHierarchy/utils.tsx
    index 6de7bd4aecdf3a..92a1017aafec52 100644
    --- a/static/app/components/events/viewHierarchy/utils.tsx
    +++ b/static/app/components/events/viewHierarchy/utils.tsx
    @@ -21,7 +21,7 @@ export function useResizeCanvasObserver(canvases: (HTMLCanvasElement | null)[]):
     
         const observer = watchForResize(canvases as HTMLCanvasElement[], entries => {
           const contentRect =
    -        entries[0].contentRect ?? entries[0].target.getBoundingClientRect();
    +        entries[0]!.contentRect ?? entries[0]!.target.getBoundingClientRect();
     
           setCanvasBounds(
             new Rect(
    @@ -52,7 +52,7 @@ export function getHierarchyDimensions(
       const nodes: ViewNode[] = [];
       const queue: [Rect | null, ViewHierarchyWindow][] = [];
       for (let i = hierarchies.length - 1; i >= 0; i--) {
    -    queue.push([null, hierarchies[i]]);
    +    queue.push([null, hierarchies[i]!]);
       }
     
       let maxWidth = Number.MIN_SAFE_INTEGER;
    @@ -77,7 +77,7 @@ export function getHierarchyDimensions(
           // output nodes should have early children before later children
           // i.e. we need to pop() off early children before ones that come after
           for (let i = child.children.length - 1; i >= 0; i--) {
    -        queue.push([node.rect, child.children[i]]);
    +        queue.push([node.rect, child.children[i]!]);
           }
         }
     
    @@ -114,8 +114,8 @@ export function getDeepestNodeAtPoint(
       vec2.scale(point, point, scale);
       const transformedPoint = vec2.transformMat3(vec2.create(), point, inverseMatrix);
       for (let i = 0; i < nodes.length; i++) {
    -    const node = nodes[i];
    -    if (node.rect.contains(transformedPoint)) {
    +    const node = nodes[i]!;
    +    if (node!.rect.contains(transformedPoint)) {
           clickedNode = node;
         }
       }
    diff --git a/static/app/components/events/viewHierarchy/wireframe.tsx b/static/app/components/events/viewHierarchy/wireframe.tsx
    index 9877dad5a743b5..26d32e686837df 100644
    --- a/static/app/components/events/viewHierarchy/wireframe.tsx
    +++ b/static/app/components/events/viewHierarchy/wireframe.tsx
    @@ -144,16 +144,16 @@ function Wireframe({hierarchy, selectedNode, onNodeSelect, project}: WireframePr
     
             for (let i = 0; i < hierarchyData.nodes.length; i++) {
               canvas.strokeRect(
    -            hierarchyData.nodes[i].rect.x,
    -            hierarchyData.nodes[i].rect.y,
    -            hierarchyData.nodes[i].rect.width,
    -            hierarchyData.nodes[i].rect.height
    +            hierarchyData.nodes[i]!.rect.x,
    +            hierarchyData.nodes[i]!.rect.y,
    +            hierarchyData.nodes[i]!.rect.width,
    +            hierarchyData.nodes[i]!.rect.height
               );
               canvas.fillRect(
    -            hierarchyData.nodes[i].rect.x,
    -            hierarchyData.nodes[i].rect.y,
    -            hierarchyData.nodes[i].rect.width,
    -            hierarchyData.nodes[i].rect.height
    +            hierarchyData.nodes[i]!.rect.x,
    +            hierarchyData.nodes[i]!.rect.y,
    +            hierarchyData.nodes[i]!.rect.width,
    +            hierarchyData.nodes[i]!.rect.height
               );
             }
           }
    diff --git a/static/app/components/feedback/feedbackOnboarding/sidebar.tsx b/static/app/components/feedback/feedbackOnboarding/sidebar.tsx
    index d2171d9a7d1ad7..caf622399b91d3 100644
    --- a/static/app/components/feedback/feedbackOnboarding/sidebar.tsx
    +++ b/static/app/components/feedback/feedbackOnboarding/sidebar.tsx
    @@ -163,7 +163,7 @@ function OnboardingContent({currentProject}: {currentProject: Project}) {
         value: PlatformKey;
         label?: ReactNode;
         textValue?: string;
    -  }>(jsFrameworkSelectOptions[0]);
    +  }>(jsFrameworkSelectOptions[0]!);
     
       const defaultTab = 'npm';
       const location = useLocation();
    @@ -195,7 +195,7 @@ function OnboardingContent({currentProject}: {currentProject: Project}) {
     
       const jsFrameworkPlatform =
         replayJsFrameworkOptions().find(p => p.id === jsFramework.value) ??
    -    replayJsFrameworkOptions()[0];
    +    replayJsFrameworkOptions()[0]!;
     
       const {
         isLoading,
    diff --git a/static/app/components/feedback/feedbackSearch.tsx b/static/app/components/feedback/feedbackSearch.tsx
    index 9d8e003dd37dc9..5a39973f83e02d 100644
    --- a/static/app/components/feedback/feedbackSearch.tsx
    +++ b/static/app/components/feedback/feedbackSearch.tsx
    @@ -66,7 +66,7 @@ function getFeedbackFilterKeys(supportedTags: TagCollection) {
             .map(key => [
               key,
               {
    -            ...supportedTags[key],
    +            ...supportedTags[key]!,
                 kind: getFeedbackFieldDefinition(key)?.kind ?? FieldKind.TAG,
               },
             ])
    @@ -79,7 +79,7 @@ function getFeedbackFilterKeys(supportedTags: TagCollection) {
       // To guarantee ordering, we need to implement filterKeySections.
       const keys = Object.keys(allTags);
       keys.sort();
    -  return Object.fromEntries(keys.map(key => [key, allTags[key]]));
    +  return Object.fromEntries(keys.map(key => [key, allTags[key]!]));
     }
     
     const getFilterKeySections = (tags: TagCollection): FilterKeySection[] => {
    diff --git a/static/app/components/feedback/list/issueTrackingSignals.tsx b/static/app/components/feedback/list/issueTrackingSignals.tsx
    index 00ca8a6e8bc7bf..a733eddc484271 100644
    --- a/static/app/components/feedback/list/issueTrackingSignals.tsx
    +++ b/static/app/components/feedback/list/issueTrackingSignals.tsx
    @@ -69,7 +69,7 @@ export default function IssueTrackingSignals({group}: Props) {
         );
       }
     
    -  const issue = linkedIssues[0];
    +  const issue = linkedIssues[0]!;
       const {name, icon} = {
         'plugin-issue': getPluginNames,
         'plugin-actions': getPluginNames,
    diff --git a/static/app/components/forms/controls/selectControl.tsx b/static/app/components/forms/controls/selectControl.tsx
    index b5242fdd6e90e9..081032fa8d9be1 100644
    --- a/static/app/components/forms/controls/selectControl.tsx
    +++ b/static/app/components/forms/controls/selectControl.tsx
    @@ -41,7 +41,7 @@ function isGroupedOptions(
       if (!maybe || maybe.length === 0) {
         return false;
       }
    -  return (maybe as GroupedOptionsType)[0].options !== undefined;
    +  return (maybe as GroupedOptionsType)[0]!.options !== undefined;
     }
     
     function ClearIndicator(
    diff --git a/static/app/components/forms/fields/choiceMapperField.tsx b/static/app/components/forms/fields/choiceMapperField.tsx
    index 1b81df22741436..97af15f23c04c2 100644
    --- a/static/app/components/forms/fields/choiceMapperField.tsx
    +++ b/static/app/components/forms/fields/choiceMapperField.tsx
    @@ -219,7 +219,7 @@ export default class ChoiceMapperField extends Component
                     
                        {
             ]}
           />
         );
    -    await userEvent.click(screen.getAllByLabelText('Delete')[0]);
    +    await userEvent.click(screen.getAllByLabelText('Delete')[0]!);
     
         expect(defaultProps.onBlur).toHaveBeenCalledWith([[24, 1]], []);
         expect(defaultProps.onChange).toHaveBeenCalledWith([[24, 1]], []);
    diff --git a/static/app/components/forms/fields/projectMapperField.tsx b/static/app/components/forms/fields/projectMapperField.tsx
    index 92f79bebb25c17..33096362a79ca2 100644
    --- a/static/app/components/forms/fields/projectMapperField.tsx
    +++ b/static/app/components/forms/fields/projectMapperField.tsx
    @@ -67,7 +67,7 @@ export class RenderField extends Component {
     
         if (newProjects.length === 1) {
           this.setState({
    -        selectedSentryProjectId: newProjects[0],
    +        selectedSentryProjectId: newProjects[0]!,
           });
         }
       }
    diff --git a/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx b/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx
    index 75cd94a07ee326..a68cda0ca8d1c7 100644
    --- a/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx
    +++ b/static/app/components/forms/fields/sentryMemberTeamSelectorField.spec.tsx
    @@ -50,7 +50,7 @@ describe('SentryMemberTeamSelectorField', () => {
     
         await selectEvent.select(
           screen.getByRole('textbox', {name: 'Select Owner'}),
    -      `#${mockTeams[0].slug}`
    +      `#${mockTeams[0]!.slug}`
         );
     
         expect(mock).toHaveBeenCalledWith('team:1', expect.anything());
    @@ -92,7 +92,7 @@ describe('SentryMemberTeamSelectorField', () => {
     
         await selectEvent.select(
           screen.getByRole('textbox', {name: 'Select Owner'}),
    -      mockUsers[0].name
    +      mockUsers[0]!.name
         );
     
         expect(mock).toHaveBeenCalledWith('user:1', expect.anything());
    @@ -114,11 +114,11 @@ describe('SentryMemberTeamSelectorField', () => {
     
         await selectEvent.select(
           screen.getByRole('textbox', {name: 'Select Owner'}),
    -      mockUsers[0].name
    +      mockUsers[0]!.name
         );
         await selectEvent.select(
           screen.getByRole('textbox', {name: 'Select Owner'}),
    -      `#${mockTeams[0].slug}`
    +      `#${mockTeams[0]!.slug}`
         );
     
         expect(mock).toHaveBeenCalledWith(['user:1', 'team:1'], expect.anything());
    diff --git a/static/app/components/forms/jsonForm.spec.tsx b/static/app/components/forms/jsonForm.spec.tsx
    index 2f3ce623d1a5e8..7dc25a8d75fd6c 100644
    --- a/static/app/components/forms/jsonForm.spec.tsx
    +++ b/static/app/components/forms/jsonForm.spec.tsx
    @@ -6,7 +6,7 @@ import JsonForm from 'sentry/components/forms/jsonForm';
     import accountDetailsFields from 'sentry/data/forms/accountDetails';
     import {fields} from 'sentry/data/forms/projectGeneralSettings';
     
    -import type {JsonFormObject} from './types';
    +import type {FieldObject, JsonFormObject} from './types';
     
     const user = UserFixture();
     
    @@ -167,7 +167,7 @@ describe('JsonForm', function () {
       });
     
       describe('fields prop', function () {
    -    const jsonFormFields = [fields.name, fields.platform];
    +    const jsonFormFields = [fields.name, fields.platform] as FieldObject[];
     
         it('default', function () {
           render();
    @@ -178,7 +178,7 @@ describe('JsonForm', function () {
           try {
             render(
                !!test.email}]}
    +            fields={[{...jsonFormFields[0]!, visible: ({test}) => !!test.email}]}
               />
             );
           } catch (error) {
    @@ -190,7 +190,7 @@ describe('JsonForm', function () {
     
         it('should NOT hide panel, if at least one field has visible set to true - no visible prop', function () {
           // slug and platform have no visible prop, that means they will be always visible
    -      render();
    +      render();
     
           expect(screen.getByText('Account Details')).toBeInTheDocument();
           expect(screen.getAllByRole('textbox')).toHaveLength(2);
    @@ -200,8 +200,8 @@ describe('JsonForm', function () {
           // slug and platform have no visible prop, that means they will be always visible
           render(
              ({...field, visible: true}))}
    +          title={accountDetailsFields[0]!.title}
    +          fields={jsonFormFields.map(field => ({...field!, visible: true}))}
             />
           );
     
    @@ -213,8 +213,8 @@ describe('JsonForm', function () {
           // slug and platform have no visible prop, that means they will be always visible
           render(
              ({...field, visible: () => true}))}
    +          title={accountDetailsFields[0]!.title}
    +          fields={jsonFormFields.map(field => ({...field!, visible: () => true}))}
             />
           );
     
    @@ -226,8 +226,8 @@ describe('JsonForm', function () {
           // slug and platform have no visible prop, that means they will be always visible
           render(
              ({...field, visible: false}))}
    +          title={accountDetailsFields[0]!.title}
    +          fields={jsonFormFields.map(field => ({...field!, visible: false}))}
             />
           );
     
    @@ -238,8 +238,8 @@ describe('JsonForm', function () {
           // slug and platform have no visible prop, that means they will be always visible
           render(
              ({...field, visible: () => false}))}
    +          title={accountDetailsFields[0]!.title}
    +          fields={jsonFormFields.map(field => ({...field!, visible: () => false}))}
             />
           );
     
    diff --git a/static/app/components/forms/model.tsx b/static/app/components/forms/model.tsx
    index 7b4d98a2e54689..bc506b1267e37b 100644
    --- a/static/app/components/forms/model.tsx
    +++ b/static/app/components/forms/model.tsx
    @@ -484,7 +484,7 @@ class FormModel {
         }
     
         this.snapshots.shift();
    -    this.fields.replace(this.snapshots[0]);
    +    this.fields.replace(this.snapshots[0]!);
     
         return true;
       }
    diff --git a/static/app/components/gridEditable/index.tsx b/static/app/components/gridEditable/index.tsx
    index e28ef18e1e8f55..66411bdac8168e 100644
    --- a/static/app/components/gridEditable/index.tsx
    +++ b/static/app/components/gridEditable/index.tsx
    @@ -178,7 +178,7 @@ class GridEditable<
     
       clearWindowLifecycleEvents() {
         Object.keys(this.resizeWindowLifecycleEvents).forEach(e => {
    -      this.resizeWindowLifecycleEvents[e].forEach(c => window.removeEventListener(e, c));
    +      this.resizeWindowLifecycleEvents[e]!.forEach(c => window.removeEventListener(e, c));
           this.resizeWindowLifecycleEvents[e] = [];
         });
       }
    @@ -188,7 +188,7 @@ class GridEditable<
     
         const nextColumnOrder = [...this.props.columnOrder];
         nextColumnOrder[i] = {
    -      ...nextColumnOrder[i],
    +      ...nextColumnOrder[i]!,
           width: COL_WIDTH_UNDEFINED,
         };
         this.setGridTemplateColumns(nextColumnOrder);
    @@ -196,7 +196,7 @@ class GridEditable<
         const onResizeColumn = this.props.grid.onResizeColumn;
         if (onResizeColumn) {
           onResizeColumn(i, {
    -        ...nextColumnOrder[i],
    +        ...nextColumnOrder[i]!,
             width: COL_WIDTH_UNDEFINED,
           });
         }
    @@ -224,10 +224,10 @@ class GridEditable<
         };
     
         window.addEventListener('mousemove', this.onResizeMouseMove);
    -    this.resizeWindowLifecycleEvents.mousemove.push(this.onResizeMouseMove);
    +    this.resizeWindowLifecycleEvents.mousemove!.push(this.onResizeMouseMove);
     
         window.addEventListener('mouseup', this.onResizeMouseUp);
    -    this.resizeWindowLifecycleEvents.mouseup.push(this.onResizeMouseUp);
    +    this.resizeWindowLifecycleEvents.mouseup!.push(this.onResizeMouseUp);
       };
     
       onResizeMouseUp = (e: MouseEvent) => {
    @@ -239,7 +239,7 @@ class GridEditable<
           const widthChange = e.clientX - metadata.cursorX;
     
           onResizeColumn(metadata.columnIndex, {
    -        ...columnOrder[metadata.columnIndex],
    +        ...columnOrder[metadata.columnIndex]!,
             width: metadata.columnWidth + widthChange,
           });
         }
    @@ -267,7 +267,7 @@ class GridEditable<
     
         const nextColumnOrder = [...this.props.columnOrder];
         nextColumnOrder[metadata.columnIndex] = {
    -      ...nextColumnOrder[metadata.columnIndex],
    +      ...nextColumnOrder[metadata.columnIndex]!,
           width: Math.max(metadata.columnWidth + widthChange, 0),
         };
     
    diff --git a/static/app/components/group/assignedTo.tsx b/static/app/components/group/assignedTo.tsx
    index 5bb43bfa54c18f..a7b576a583342c 100644
    --- a/static/app/components/group/assignedTo.tsx
    +++ b/static/app/components/group/assignedTo.tsx
    @@ -92,7 +92,7 @@ function getSuggestedReason(owner: IssueOwner) {
       }
     
       if (owner.rules?.length) {
    -    const firstRule = owner.rules[0];
    +    const firstRule = owner.rules[0]!;
         return `${toTitleCase(firstRule[0])}:${firstRule[1]}`;
       }
     
    diff --git a/static/app/components/group/externalIssuesList/externalIssueActions.tsx b/static/app/components/group/externalIssuesList/externalIssueActions.tsx
    index 115a8799da0420..cb4c8182250b34 100644
    --- a/static/app/components/group/externalIssuesList/externalIssueActions.tsx
    +++ b/static/app/components/group/externalIssuesList/externalIssueActions.tsx
    @@ -75,7 +75,7 @@ function ExternalIssueActions({configurations, group, onChange}: Props) {
         const {externalIssues} = integration;
         // Currently we do not support a case where there is multiple external issues.
         // For example, we shouldn't have more than 1 jira ticket created for an issue for each jira configuration.
    -    const issue = externalIssues[0];
    +    const issue = externalIssues[0]!;
         const {id} = issue;
         const endpoint = `/organizations/${organization.slug}/issues/${group.id}/integrations/${integration.id}/?externalIssue=${id}`;
     
    @@ -97,7 +97,7 @@ function ExternalIssueActions({configurations, group, onChange}: Props) {
         
           {linked.map(config => {
             const {provider, externalIssues} = config;
    -        const issue = externalIssues[0];
    +        const issue = externalIssues[0]!;
             return (
                0 && (
             
                   {unlinked.map(config => (
    @@ -148,7 +148,7 @@ function ExternalIssueActions({configurations, group, onChange}: Props) {
                   ? () =>
                       doOpenExternalIssueModal({
                         group,
    -                    integration: unlinked[0],
    +                    integration: unlinked[0]!,
                         onChange,
                         organization,
                       })
    diff --git a/static/app/components/group/externalIssuesList/hooks/useIntegrationExternalIssues.tsx b/static/app/components/group/externalIssuesList/hooks/useIntegrationExternalIssues.tsx
    index aa10ecaec532a7..74b860ed629809 100644
    --- a/static/app/components/group/externalIssuesList/hooks/useIntegrationExternalIssues.tsx
    +++ b/static/app/components/group/externalIssuesList/hooks/useIntegrationExternalIssues.tsx
    @@ -84,11 +84,11 @@ export function useIntegrationExternalIssues({
           ...configurations
             .filter(config => config.externalIssues.length > 0)
             .map(config => ({
    -          key: config.externalIssues[0].id,
    -          displayName: config.externalIssues[0].key,
    +          key: config.externalIssues[0]!.id,
    +          displayName: config.externalIssues[0]!.key,
               displayIcon,
    -          url: config.externalIssues[0].url,
    -          title: config.externalIssues[0].title,
    +          url: config.externalIssues[0]!.url,
    +          title: config.externalIssues[0]!.title,
               onUnlink: () => {
                 // Currently we do not support a case where there is multiple external issues.
                 // For example, we shouldn't have more than 1 jira ticket created for an issue for each jira configuration.
    @@ -98,7 +98,7 @@ export function useIntegrationExternalIssues({
                   `/organizations/${organization.slug}/issues/${group.id}/integrations/${config.id}/`,
                   {
                     method: 'DELETE',
    -                query: {externalIssue: issue.id},
    +                query: {externalIssue: issue!.id},
                     success: () => {
                       addSuccessMessage(t('Successfully unlinked issue.'));
                       refetchIntegrations();
    diff --git a/static/app/components/group/releaseChart.spec.tsx b/static/app/components/group/releaseChart.spec.tsx
    index 2b07253fe25551..4dd8ae1d4d1ad3 100644
    --- a/static/app/components/group/releaseChart.spec.tsx
    +++ b/static/app/components/group/releaseChart.spec.tsx
    @@ -20,6 +20,6 @@ it('should set marker before first bucket', () => {
       const markers = getGroupReleaseChartMarkers(theme as any, data, firstSeen, lastSeen)!.data!;
     
       expect((markers[0] as any).displayValue).toBe(new Date(firstSeen).getTime());
    -  expect(markers[0].coord![0]).toBe(1659533400000);
    -  expect(markers[1].coord![0]).toBe(new Date(lastSeen).getTime());
    +  expect(markers[0]!.coord![0]).toBe(1659533400000);
    +  expect(markers[1]!.coord![0]).toBe(new Date(lastSeen).getTime());
     });
    diff --git a/static/app/components/group/releaseChart.tsx b/static/app/components/group/releaseChart.tsx
    index aefd6becd43798..38d08d207e29e0 100644
    --- a/static/app/components/group/releaseChart.tsx
    +++ b/static/app/components/group/releaseChart.tsx
    @@ -47,7 +47,7 @@ export function getGroupReleaseChartMarkers(
     ): BarChartSeries['markPoint'] {
       const markers: Marker[] = [];
       // Get the timestamp of the first point.
    -  const firstGraphTime = stats[0][0] * 1000;
    +  const firstGraphTime = stats[0]![0] * 1000;
     
       const firstSeenX = new Date(firstSeen ?? 0).getTime();
       const lastSeenX = new Date(lastSeen ?? 0).getTime();
    @@ -67,9 +67,9 @@ export function getGroupReleaseChartMarkers(
         let bucketStart: number | undefined;
         if (firstBucket > 0) {
           // The size of the data interval in ms
    -      const halfBucketSize = ((stats[1][0] - stats[0][0]) * 1000) / 2;
    +      const halfBucketSize = ((stats[1]![0] - stats[0]![0]) * 1000) / 2;
           // Display the marker in front of the first bucket
    -      bucketStart = stats[firstBucket - 1][0] * 1000 - halfBucketSize;
    +      bucketStart = stats[firstBucket - 1]![0] * 1000 - halfBucketSize;
         }
     
         markers.push({
    @@ -156,26 +156,26 @@ function GroupReleaseChart(props: Props) {
     
       series.push({
         seriesName: t('Events in %s', environmentLabel),
    -    data: environmentStats[statsPeriod].map(point => ({
    -      name: point[0] * 1000,
    -      value: point[1],
    +    data: environmentStats[statsPeriod]!.map(point => ({
    +      name: point![0] * 1000,
    +      value: point![1],
         })),
       });
     
       if (release && releaseStats) {
         series.push({
           seriesName: t('Events in release %s', formatVersion(release.version)),
    -      data: releaseStats[statsPeriod].map(point => ({
    -        name: point[0] * 1000,
    -        value: point[1],
    +      data: releaseStats[statsPeriod]!.map(point => ({
    +        name: point![0] * 1000,
    +        value: point![1],
           })),
         });
       }
     
       const totalSeries =
         environment && environmentStats ? environmentStats[statsPeriod] : stats;
    -  const totalEvents = totalSeries.reduce((acc, current) => acc + current[1], 0);
    -  series[0].markPoint = getGroupReleaseChartMarkers(theme, stats, firstSeen, lastSeen);
    +  const totalEvents = totalSeries!.reduce((acc, current) => acc + current[1], 0);
    +  series[0]!.markPoint = getGroupReleaseChartMarkers(theme, stats, firstSeen, lastSeen);
     
       return (
         
    diff --git a/static/app/components/group/suggestedOwnerHovercard.tsx b/static/app/components/group/suggestedOwnerHovercard.tsx
    index 3bde36f6ca542c..040ccd18990772 100644
    --- a/static/app/components/group/suggestedOwnerHovercard.tsx
    +++ b/static/app/components/group/suggestedOwnerHovercard.tsx
    @@ -142,8 +142,8 @@ function SuggestedOwnerHovercard(props: Props) {
                               
                             ),
                           release: (
    diff --git a/static/app/components/group/tagDistributionMeter.spec.tsx b/static/app/components/group/tagDistributionMeter.spec.tsx
    index 049b55037310d7..62ddf09d5991b3 100644
    --- a/static/app/components/group/tagDistributionMeter.spec.tsx
    +++ b/static/app/components/group/tagDistributionMeter.spec.tsx
    @@ -35,8 +35,8 @@ describe('TagDistributionMeter', function () {
             group={GroupFixture({id: '1337'})}
             organization={organization}
             projectId="456"
    -        totalValues={tags[0].totalValues}
    -        topValues={tags[0].topValues}
    +        totalValues={tags[0]!.totalValues}
    +        topValues={tags[0]!.topValues}
           />
         );
         expect(
    diff --git a/static/app/components/group/tagFacets/index.tsx b/static/app/components/group/tagFacets/index.tsx
    index bc00bffee094ab..56549f9545ee86 100644
    --- a/static/app/components/group/tagFacets/index.tsx
    +++ b/static/app/components/group/tagFacets/index.tsx
    @@ -51,8 +51,8 @@ export function TAGS_FORMATTER(tagsData: Record) {
       Object.keys(tagsData).forEach(tagKey => {
         if (tagKey === 'release') {
           transformedTagsData[tagKey] = {
    -        ...tagsData[tagKey],
    -        topValues: tagsData[tagKey].topValues.map(topValue => {
    +        ...tagsData[tagKey]!,
    +        topValues: tagsData[tagKey]!.topValues.map(topValue => {
               return {
                 ...topValue,
                 name: formatVersion(topValue.name),
    @@ -61,8 +61,8 @@ export function TAGS_FORMATTER(tagsData: Record) {
           };
         } else if (tagKey === 'device') {
           transformedTagsData[tagKey] = {
    -        ...tagsData[tagKey],
    -        topValues: tagsData[tagKey].topValues.map(topValue => {
    +        ...tagsData[tagKey]!,
    +        topValues: tagsData[tagKey]!.topValues.map(topValue => {
               return {
                 ...topValue,
                 name: topValue.readable ?? topValue.name,
    @@ -70,7 +70,7 @@ export function TAGS_FORMATTER(tagsData: Record) {
             }),
           };
         } else {
    -      transformedTagsData[tagKey] = tagsData[tagKey];
    +      transformedTagsData[tagKey] = tagsData[tagKey]!;
         }
       });
     
    diff --git a/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx b/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx
    index 0f4ddddcefd2e2..6e1e79ed39ddfe 100644
    --- a/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx
    +++ b/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx
    @@ -67,9 +67,9 @@ function TagFacetsDistributionMeter({
             
    -          {topSegments[0].name || t('n/a')}
    +          {topSegments[0]!.name || t('n/a')}
             
             
                   ) : (
                     
                       {/* if the first segment is 6% or less, the label won't fit cleanly into the segment, so don't show the label */}
    @@ -180,7 +180,7 @@ function TagFacetsDistributionMeter({
                         onMouseLeave={() => setHoveredValue(null)}
                       >
                         
                         
    diff --git a/static/app/components/guidedSteps/guidedSteps.tsx b/static/app/components/guidedSteps/guidedSteps.tsx
    index e5c5c1395d1bb4..491b00777e54e7 100644
    --- a/static/app/components/guidedSteps/guidedSteps.tsx
    +++ b/static/app/components/guidedSteps/guidedSteps.tsx
    @@ -67,7 +67,7 @@ function useGuidedStepsContentValue({
       // render and that step order does not change.
       const registerStep = useCallback((props: RegisterStepInfo) => {
         if (registeredStepsRef.current[props.stepKey]) {
    -      registeredStepsRef.current[props.stepKey].isCompleted = props.isCompleted;
    +      registeredStepsRef.current[props.stepKey]!.isCompleted = props.isCompleted;
           return;
         }
         const numRegisteredSteps = Object.keys(registeredStepsRef.current).length + 1;
    diff --git a/static/app/components/idBadge/index.stories.tsx b/static/app/components/idBadge/index.stories.tsx
    index a15eb439f03c03..abcf2d8d3430d0 100644
    --- a/static/app/components/idBadge/index.stories.tsx
    +++ b/static/app/components/idBadge/index.stories.tsx
    @@ -42,7 +42,7 @@ export default storyBook(IdBadge, story => {
           return ;
         }
     
    -    return ;
    +    return ;
       });
     
       story('Project', () => {
    @@ -53,7 +53,7 @@ export default storyBook(IdBadge, story => {
           return ;
         }
     
    -    return ;
    +    return ;
       });
     
       story('User', () => {
    @@ -116,8 +116,8 @@ export default storyBook(IdBadge, story => {
     
         const teamActor: Actor = {
           type: 'team',
    -      id: teams[0].id,
    -      name: teams[0].name,
    +      id: teams[0]!.id,
    +      name: teams[0]!.name,
         };
     
         return (
    diff --git a/static/app/components/inactivePlugins.spec.tsx b/static/app/components/inactivePlugins.spec.tsx
    index 89067a6d7c52be..32f4b76699a8cf 100644
    --- a/static/app/components/inactivePlugins.spec.tsx
    +++ b/static/app/components/inactivePlugins.spec.tsx
    @@ -20,7 +20,7 @@ describe('InactivePlugins', function () {
         const enableFn = jest.fn();
         const plugins = PluginsFixture();
         render();
    -    await userEvent.click(screen.getByRole('button', {name: plugins[0].name}));
    +    await userEvent.click(screen.getByRole('button', {name: plugins[0]!.name}));
         expect(enableFn).toHaveBeenCalledWith(expect.objectContaining(plugins[0]));
       });
     });
    diff --git a/static/app/components/lastCommit.tsx b/static/app/components/lastCommit.tsx
    index e6cd82bd56b0e9..aef8965844fa39 100644
    --- a/static/app/components/lastCommit.tsx
    +++ b/static/app/components/lastCommit.tsx
    @@ -38,7 +38,7 @@ function LastCommit({commit}: Props) {
           );
         }
     
    -    let finalMessage = message.split(/\n/)[0];
    +    let finalMessage = message.split(/\n/)[0]!;
         if (finalMessage.length > 100) {
           let truncated = finalMessage.substring(0, 90);
           const words = truncated.split(/ /);
    diff --git a/static/app/components/letterAvatar.tsx b/static/app/components/letterAvatar.tsx
    index 93a25bdc0768c3..e98bfb8433c84b 100644
    --- a/static/app/components/letterAvatar.tsx
    +++ b/static/app/components/letterAvatar.tsx
    @@ -37,7 +37,7 @@ function getColor(identifier: string | undefined): Color {
       }
     
       const id = hashIdentifier(identifier);
    -  return COLORS[id % COLORS.length];
    +  return COLORS[id % COLORS.length]!;
     }
     
     function getInitials(displayName: string | undefined) {
    @@ -46,9 +46,9 @@ function getInitials(displayName: string | undefined) {
       );
       // Use Array.from as slicing and substring() work on ucs2 segments which
       // results in only getting half of any 4+ byte character.
    -  let initials = Array.from(names[0])[0];
    +  let initials = Array.from(names[0]!)[0]!;
       if (names.length > 1) {
    -    initials += Array.from(names[names.length - 1])[0];
    +    initials += Array.from(names[names.length - 1]!)[0]!;
       }
       return initials.toUpperCase();
     }
    diff --git a/static/app/components/metrics/chart/chart.tsx b/static/app/components/metrics/chart/chart.tsx
    index e2052fd3f626a9..c238100b511fbe 100644
    --- a/static/app/components/metrics/chart/chart.tsx
    +++ b/static/app/components/metrics/chart/chart.tsx
    @@ -86,7 +86,7 @@ function isNonZeroValue(value: number | null) {
     function addSeriesPadding(data: Series['data']) {
       const hasNonZeroSibling = (index: number) => {
         return (
    -      isNonZeroValue(data[index - 1]?.value) || isNonZeroValue(data[index + 1]?.value)
    +      isNonZeroValue(data[index - 1]!?.value) || isNonZeroValue(data[index + 1]!?.value)
         );
       };
       const paddingIndices = new Set();
    @@ -142,9 +142,9 @@ export const MetricChart = memo(
             }
           });
     
    -      const bucketSize = series[0]?.data[1]?.name - series[0]?.data[0]?.name;
    +      const bucketSize = series[0]!?.data[1]!?.name - series[0]!?.data[0]!?.name;
           const isSubMinuteBucket = bucketSize < 60_000;
    -      const lastBucketTimestamp = series[0]?.data?.[series[0]?.data?.length - 1]?.name;
    +      const lastBucketTimestamp = series[0]!?.data?.[series[0]!?.data?.length - 1]!?.name;
           const ingestionBuckets = useMemo(
             () => getIngestionDelayBucketCount(bucketSize, lastBucketTimestamp),
             [bucketSize, lastBucketTimestamp]
    @@ -245,7 +245,7 @@ export const MetricChart = memo(
     
                       // Filter padding datapoints from tooltip
                       if (param.value[1] === 0) {
    -                    const currentSeries = seriesToShow[param.seriesIndex];
    +                    const currentSeries = seriesToShow[param.seriesIndex]!;
                         const paddingIndices =
                           'paddingIndices' in currentSeries
                             ? currentSeries.paddingIndices
    diff --git a/static/app/components/metrics/chart/useFocusArea.tsx b/static/app/components/metrics/chart/useFocusArea.tsx
    index f381fc3f3b8674..8fbbe03d4682e0 100644
    --- a/static/app/components/metrics/chart/useFocusArea.tsx
    +++ b/static/app/components/metrics/chart/useFocusArea.tsx
    @@ -278,16 +278,16 @@ function FocusAreaOverlay({
           return;
         }
     
    -    const widthPx = bottomRight[0] - topLeft[0];
    -    const heightPx = bottomRight[1] - topLeft[1];
    +    const widthPx = bottomRight[0]! - topLeft[0]!;
    +    const heightPx = bottomRight[1]! - topLeft[1]!;
     
    -    const resultTop = useFullYAxis ? '0px' : `${topLeft[1].toPrecision(5)}px`;
    +    const resultTop = useFullYAxis ? '0px' : `${topLeft[1]!.toPrecision(5)}px`;
         const resultHeight = useFullYAxis
           ? `${CHART_HEIGHT}px`
           : `${heightPx.toPrecision(5)}px`;
     
         // Ensure the focus area rect is always within the chart bounds
    -    const left = Math.max(topLeft[0], 0);
    +    const left = Math.max(topLeft[0]!, 0);
         const width = Math.min(widthPx, chartInstance.getWidth() - left);
     
         const newPosition = {
    @@ -347,14 +347,14 @@ const getSelectionRange = (
       useFullYAxis: boolean,
       boundingRect: ValueRect
     ): SelectionRange => {
    -  const startTimestamp = Math.min(...rect.coordRange[0]);
    -  const endTimestamp = Math.max(...rect.coordRange[0]);
    +  const startTimestamp = Math.min(...rect.coordRange![0]!);
    +  const endTimestamp = Math.max(...rect.coordRange![0]!);
     
       const startDate = getDateString(Math.max(startTimestamp, boundingRect.xMin));
       const endDate = getDateString(Math.min(endTimestamp, boundingRect.xMax));
     
    -  const min = useFullYAxis ? NaN : Math.min(...rect.coordRange[1]);
    -  const max = useFullYAxis ? NaN : Math.max(...rect.coordRange[1]);
    +  const min = useFullYAxis ? NaN : Math.min(...rect.coordRange[1]!);
    +  const max = useFullYAxis ? NaN : Math.max(...rect.coordRange[1]!);
     
       return {
         start: startDate,
    diff --git a/static/app/components/metrics/chart/useMetricChartSamples.tsx b/static/app/components/metrics/chart/useMetricChartSamples.tsx
    index 83cf415ba77f96..e7a25ab26796d1 100644
    --- a/static/app/components/metrics/chart/useMetricChartSamples.tsx
    +++ b/static/app/components/metrics/chart/useMetricChartSamples.tsx
    @@ -164,7 +164,10 @@ export function useMetricChartSamples({
               const value = getSummaryValueForAggregation(sample.summary, aggregation);
               const yValue = value;
     
    -          const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
    +          const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect) as [
    +            number,
    +            number,
    +          ];
     
               return {
                 seriesName: sample.id,
    diff --git a/static/app/components/metrics/chart/useMetricReleases.tsx b/static/app/components/metrics/chart/useMetricReleases.tsx
    index 932e9a5c5ab193..2b2b9b03f7f41c 100644
    --- a/static/app/components/metrics/chart/useMetricReleases.tsx
    +++ b/static/app/components/metrics/chart/useMetricReleases.tsx
    @@ -86,7 +86,7 @@ export function useReleases() {
             if (pageLinks) {
               const paginationObject = parseLinkHeader(pageLinks);
               hasMore = paginationObject?.next?.results ?? false;
    -          queryObj.cursor = paginationObject.next.cursor;
    +          queryObj.cursor = paginationObject.next!.cursor;
             } else {
               hasMore = false;
             }
    diff --git a/static/app/components/metrics/chart/utils.tsx b/static/app/components/metrics/chart/utils.tsx
    index af86decedbdfed..42da326d82ab18 100644
    --- a/static/app/components/metrics/chart/utils.tsx
    +++ b/static/app/components/metrics/chart/utils.tsx
    @@ -53,8 +53,8 @@ export function getValueRect(chartRef?: RefObject): ValueRect {
     
       const xMin = moment(topLeft[0]).valueOf();
       const xMax = moment(bottomRight[0]).valueOf();
    -  const yMin = Math.max(0, bottomRight[1]);
    -  const yMax = topLeft[1];
    +  const yMin = Math.max(0, bottomRight[1]!);
    +  const yMax = topLeft[1]!;
     
       return {
         xMin,
    diff --git a/static/app/components/modals/commandPalette.spec.tsx b/static/app/components/modals/commandPalette.spec.tsx
    index fb752b1d3cecc3..2c7e087e16bd62 100644
    --- a/static/app/components/modals/commandPalette.spec.tsx
    +++ b/static/app/components/modals/commandPalette.spec.tsx
    @@ -113,7 +113,7 @@ describe('Command Palette Modal', function () {
         expect(badges[0]).toHaveTextContent('billy-org Dashboard');
         expect(badges[1]).toHaveTextContent('billy-org Settings');
     
    -    await userEvent.click(badges[0]);
    +    await userEvent.click(badges[0]!);
     
         expect(navigateTo).toHaveBeenCalledWith('/billy-org/', expect.anything(), undefined);
       });
    diff --git a/static/app/components/modals/featureTourModal.spec.tsx b/static/app/components/modals/featureTourModal.spec.tsx
    index 8cf37f754562ec..7c2764c4fbb0c7 100644
    --- a/static/app/components/modals/featureTourModal.spec.tsx
    +++ b/static/app/components/modals/featureTourModal.spec.tsx
    @@ -71,13 +71,13 @@ describe('FeatureTourModal', function () {
         await clickModal();
     
         // Should start on the first step.
    -    expect(screen.getByRole('heading')).toHaveTextContent(steps[0].title);
    +    expect(screen.getByRole('heading')).toHaveTextContent(steps[0]!.title);
     
         // Advance to the next step.
         await userEvent.click(screen.getByRole('button', {name: 'Next'}));
     
         // Should move to next step.
    -    expect(screen.getByRole('heading')).toHaveTextContent(steps[1].title);
    +    expect(screen.getByRole('heading')).toHaveTextContent(steps[1]!.title);
         expect(onAdvance).toHaveBeenCalled();
       });
     
    @@ -87,7 +87,7 @@ describe('FeatureTourModal', function () {
         await clickModal();
     
         // Should show title, image and actions
    -    expect(screen.getByRole('heading')).toHaveTextContent(steps[0].title);
    +    expect(screen.getByRole('heading')).toHaveTextContent(steps[0]!.title);
         expect(screen.getByTestId('step-image')).toBeInTheDocument();
         expect(screen.getByTestId('step-action')).toBeInTheDocument();
         expect(screen.getByText('1 of 2')).toBeInTheDocument();
    diff --git a/static/app/components/modals/featureTourModal.tsx b/static/app/components/modals/featureTourModal.tsx
    index e0d07789f4356b..960f64fcc6329b 100644
    --- a/static/app/components/modals/featureTourModal.tsx
    +++ b/static/app/components/modals/featureTourModal.tsx
    @@ -156,7 +156,8 @@ class ModalContents extends Component {
         const {Body, steps, doneText, doneUrl, closeModal} = this.props;
         const {current} = this.state;
     
    -    const step = steps[current] !== undefined ? steps[current] : steps[steps.length - 1];
    +    const step =
    +      steps[current] !== undefined ? steps[current]! : steps[steps.length - 1]!;
         const hasNext = steps[current + 1] !== undefined;
     
         return (
    diff --git a/static/app/components/modals/inviteMembersModal/index.spec.tsx b/static/app/components/modals/inviteMembersModal/index.spec.tsx
    index 339967db5086c5..1953f0949d0837 100644
    --- a/static/app/components/modals/inviteMembersModal/index.spec.tsx
    +++ b/static/app/components/modals/inviteMembersModal/index.spec.tsx
    @@ -96,12 +96,12 @@ describe('InviteMembersModal', function () {
         const emailInputs = screen.getAllByRole('textbox', {name: 'Email Addresses'});
         const roleInputs = screen.getAllByRole('textbox', {name: 'Role'});
     
    -    await userEvent.type(emailInputs[0], 'test1@test.com');
    +    await userEvent.type(emailInputs[0]!, 'test1@test.com');
         await userEvent.tab();
     
    -    await selectEvent.select(roleInputs[0], 'Admin');
    +    await selectEvent.select(roleInputs[0]!, 'Admin');
     
    -    await userEvent.type(emailInputs[1], 'test2@test.com');
    +    await userEvent.type(emailInputs[1]!, 'test2@test.com');
         await userEvent.tab();
       };
     
    @@ -162,10 +162,10 @@ describe('InviteMembersModal', function () {
         await userEvent.click(screen.getByRole('button', {name: 'Add another'}));
     
         const emailInputs = screen.getAllByRole('textbox', {name: 'Email Addresses'});
    -    await userEvent.type(emailInputs[0], 'test@test.com');
    +    await userEvent.type(emailInputs[0]!, 'test@test.com');
         await userEvent.tab();
     
    -    await userEvent.type(emailInputs[1], 'test@test.com');
    +    await userEvent.type(emailInputs[1]!, 'test@test.com');
         await userEvent.tab();
     
         expect(screen.getByText('Duplicate emails between invite rows.')).toBeInTheDocument();
    @@ -211,8 +211,8 @@ describe('InviteMembersModal', function () {
         await setupMemberInviteState();
     
         const teamInputs = screen.getAllByRole('textbox', {name: 'Add to Team'});
    -    await selectEvent.select(teamInputs[0], '#team-slug');
    -    await selectEvent.select(teamInputs[1], '#team-slug');
    +    await selectEvent.select(teamInputs[0]!, '#team-slug');
    +    await selectEvent.select(teamInputs[1]!, '#team-slug');
     
         await userEvent.click(screen.getByRole('button', {name: 'Send invites (2)'}));
     
    diff --git a/static/app/components/modals/inviteMembersModal/index.tsx b/static/app/components/modals/inviteMembersModal/index.tsx
    index d32a3f20e192c5..e50f4c5246a6fb 100644
    --- a/static/app/components/modals/inviteMembersModal/index.tsx
    +++ b/static/app/components/modals/inviteMembersModal/index.tsx
    @@ -100,7 +100,7 @@ function InviteMembersModal({
                     sendInvites: inviteModalSendInvites,
                     reset,
                     inviteStatus,
    -                pendingInvites: pendingInvites[0],
    +                pendingInvites: pendingInvites[0]!,
                     sendingInvites,
                     complete,
                     error,
    diff --git a/static/app/components/modals/inviteMembersModal/inviteRowControl.tsx b/static/app/components/modals/inviteMembersModal/inviteRowControl.tsx
    index cd73db279ef038..dc524d6bfbfdef 100644
    --- a/static/app/components/modals/inviteMembersModal/inviteRowControl.tsx
    +++ b/static/app/components/modals/inviteMembersModal/inviteRowControl.tsx
    @@ -96,7 +96,7 @@ function InviteRowControl({
             value={emails}
             components={{
               MultiValue: (props: MultiValueProps) => (
    -            
    +            
               ),
               DropdownIndicator: () => null,
             }}
    diff --git a/static/app/components/modals/inviteMembersModal/inviteRowControlNew.tsx b/static/app/components/modals/inviteMembersModal/inviteRowControlNew.tsx
    index 4326a144ba9a8f..61aebc3da2ca18 100644
    --- a/static/app/components/modals/inviteMembersModal/inviteRowControlNew.tsx
    +++ b/static/app/components/modals/inviteMembersModal/inviteRowControlNew.tsx
    @@ -94,7 +94,7 @@ function InviteRowControl({roleDisabledUnallowed, roleOptions}: Props) {
               value={emails}
               components={{
                 MultiValue: (props: MultiValueProps) => (
    -              
    +              
                 ),
                 DropdownIndicator: () => null,
               }}
    diff --git a/static/app/components/modals/inviteMembersModal/useInviteModal.tsx b/static/app/components/modals/inviteMembersModal/useInviteModal.tsx
    index ab23e5fbf5be75..98bd2faceee03b 100644
    --- a/static/app/components/modals/inviteMembersModal/useInviteModal.tsx
    +++ b/static/app/components/modals/inviteMembersModal/useInviteModal.tsx
    @@ -163,7 +163,7 @@ export default function useInviteModal({organization, initialData, source}: Prop
     
       const removeSentInvites = useCallback(() => {
         setState(prev => {
    -      const emails = prev.pendingInvites[0].emails;
    +      const emails = prev.pendingInvites[0]!.emails;
           const filteredEmails = Array.from(emails).filter(
             email => !prev.inviteStatus[email]?.sent
           );
    @@ -171,7 +171,7 @@ export default function useInviteModal({organization, initialData, source}: Prop
             ...prev,
             pendingInvites: [
               {
    -            ...prev.pendingInvites[0],
    +            ...prev.pendingInvites[0]!,
                 emails: new Set(filteredEmails),
               },
             ],
    @@ -218,7 +218,7 @@ export default function useInviteModal({organization, initialData, source}: Prop
       const setEmails = useCallback((emails: string[], index: number) => {
         setState(prev => {
           const pendingInvites = [...prev.pendingInvites];
    -      pendingInvites[index] = {...pendingInvites[index], emails: new Set(emails)};
    +      pendingInvites[index] = {...pendingInvites[index]!, emails: new Set(emails)};
     
           return {...prev, pendingInvites};
         });
    @@ -227,7 +227,7 @@ export default function useInviteModal({organization, initialData, source}: Prop
       const setTeams = useCallback((teams: string[], index: number) => {
         setState(prev => {
           const pendingInvites = [...prev.pendingInvites];
    -      pendingInvites[index] = {...pendingInvites[index], teams: new Set(teams)};
    +      pendingInvites[index] = {...pendingInvites[index]!, teams: new Set(teams)};
     
           return {...prev, pendingInvites};
         });
    @@ -236,7 +236,7 @@ export default function useInviteModal({organization, initialData, source}: Prop
       const setRole = useCallback((role: string, index: number) => {
         setState(prev => {
           const pendingInvites = [...prev.pendingInvites];
    -      pendingInvites[index] = {...pendingInvites[index], role};
    +      pendingInvites[index] = {...pendingInvites[index]!, role};
     
           return {...prev, pendingInvites};
         });
    diff --git a/static/app/components/modals/inviteMissingMembersModal/index.spec.tsx b/static/app/components/modals/inviteMissingMembersModal/index.spec.tsx
    index 8a462fcfbe2316..620f8a7b72d3bf 100644
    --- a/static/app/components/modals/inviteMissingMembersModal/index.spec.tsx
    +++ b/static/app/components/modals/inviteMissingMembersModal/index.spec.tsx
    @@ -188,12 +188,12 @@ describe('InviteMissingMembersModal', function () {
         const teamInputs = screen.getAllByRole('textbox', {name: 'Add to Team'});
     
         await userEvent.click(screen.getByLabelText('Select hello@sentry.io'));
    -    await selectEvent.select(roleInputs[0], 'Admin', {
    +    await selectEvent.select(roleInputs[0]!, 'Admin', {
           container: document.body,
         });
     
         await userEvent.click(screen.getByLabelText('Select abcd@sentry.io'));
    -    await selectEvent.select(teamInputs[1], '#team-slug', {
    +    await selectEvent.select(teamInputs[1]!, '#team-slug', {
           container: document.body,
         });
     
    diff --git a/static/app/components/modals/inviteMissingMembersModal/index.tsx b/static/app/components/modals/inviteMissingMembersModal/index.tsx
    index 9c89de5ec73a09..4ba3e48f86a8b7 100644
    --- a/static/app/components/modals/inviteMissingMembersModal/index.tsx
    +++ b/static/app/components/modals/inviteMissingMembersModal/index.tsx
    @@ -67,9 +67,9 @@ export function InviteMissingMembersModal({
         (role: string, index: number) => {
           setMemberInvites(prevInvites => {
             const invites = prevInvites.map(i => ({...i}));
    -        invites[index].role = role;
    -        if (!allowedRolesMap[role].isTeamRolesAllowed) {
    -          invites[index].teamSlugs = new Set([]);
    +        invites[index]!.role = role;
    +        if (!allowedRolesMap[role]!.isTeamRolesAllowed) {
    +          invites[index]!.teamSlugs = new Set([]);
             }
             return invites;
           });
    @@ -80,7 +80,7 @@ export function InviteMissingMembersModal({
       const setTeams = useCallback((teamSlugs: string[], index: number) => {
         setMemberInvites(prevInvites => {
           const invites = prevInvites.map(i => ({...i}));
    -      invites[index].teamSlugs = new Set(teamSlugs);
    +      invites[index]!.teamSlugs = new Set(teamSlugs);
           return invites;
         });
       }, []);
    @@ -96,7 +96,7 @@ export function InviteMissingMembersModal({
       const toggleCheckbox = useCallback(
         (checked: boolean, index: number) => {
           const selectedMembers = [...memberInvites];
    -      selectedMembers[index].selected = checked;
    +      selectedMembers[index]!.selected = checked;
           setMemberInvites(selectedMembers);
         },
         [memberInvites]
    @@ -234,7 +234,7 @@ export function InviteMissingMembersModal({
             stickyHeaders
           >
             {memberInvites?.map((member, i) => {
    -          const checked = memberInvites[i].selected;
    +          const checked = memberInvites[i]!.selected;
               const username = member.externalId.split(':').pop();
               const isTeamRolesAllowed =
                 allowedRolesMap[member.role]?.isTeamRolesAllowed ?? true;
    diff --git a/static/app/components/modals/metricWidgetViewerModal.tsx b/static/app/components/modals/metricWidgetViewerModal.tsx
    index c3bb40feeedba8..960085fb4c27ce 100644
    --- a/static/app/components/modals/metricWidgetViewerModal.tsx
    +++ b/static/app/components/modals/metricWidgetViewerModal.tsx
    @@ -120,7 +120,7 @@ function MetricWidgetViewerModal({
               if (!updatedQuery.alias) {
                 updatedQuery.alias = updatedAlias;
               }
    -          if (isVirtualAlias(currentQuery.alias) && isVirtualAlias(updatedQuery.alias)) {
    +          if (isVirtualAlias(currentQuery!.alias) && isVirtualAlias(updatedQuery.alias)) {
                 updatedQuery.alias = updatedAlias;
               }
             }
    @@ -182,7 +182,7 @@ function MetricWidgetViewerModal({
                 ? curr.map(q => ({...q, isHidden: true}))
                 : curr),
               {
    -            ...query,
    +            ...query!,
                 id: generateQueryId(),
               },
             ];
    @@ -241,7 +241,7 @@ function MetricWidgetViewerModal({
             updated.splice(index, 1);
             // Make sure the last query is visible for big number widgets
             if (displayType === DisplayType.BIG_NUMBER && filteredEquations.length === 0) {
    -          updated[updated.length - 1].isHidden = false;
    +          updated[updated.length - 1]!.isHidden = false;
             }
             return updated;
           });
    @@ -308,7 +308,7 @@ function MetricWidgetViewerModal({
         closeModal();
       }, [userHasModified, closeModal, organization]);
     
    -  const {mri, aggregation, query, condition} = metricQueries[0];
    +  const {mri, aggregation, query, condition} = metricQueries[0]!;
     
       if (isLoading) {
         return ;
    diff --git a/static/app/components/modals/metricWidgetViewerModal/queries.tsx b/static/app/components/modals/metricWidgetViewerModal/queries.tsx
    index f17bc9d99b367c..08a88d7135d61d 100644
    --- a/static/app/components/modals/metricWidgetViewerModal/queries.tsx
    +++ b/static/app/components/modals/metricWidgetViewerModal/queries.tsx
    @@ -86,7 +86,7 @@ export const Queries = memo(function Queries({
     
       const handleEditQueryAlias = useCallback(
         (index: number) => {
    -      const query = metricQueries[index];
    +      const query = metricQueries[index]!;
           const alias = getMetricQueryName(query);
     
           onQueryChange({alias}, index);
    @@ -96,7 +96,7 @@ export const Queries = memo(function Queries({
     
       const handleEditEquationAlias = useCallback(
         (index: number) => {
    -      const equation = metricEquations[index];
    +      const equation = metricEquations[index]!;
           const alias = getMetricQueryName(equation);
     
           onEquationChange({alias: alias ?? ''}, index);
    diff --git a/static/app/components/modals/metricWidgetViewerModal/visualization.tsx b/static/app/components/modals/metricWidgetViewerModal/visualization.tsx
    index c8501a6cb5b123..c5b7fab35b396d 100644
    --- a/static/app/components/modals/metricWidgetViewerModal/visualization.tsx
    +++ b/static/app/components/modals/metricWidgetViewerModal/visualization.tsx
    @@ -97,7 +97,7 @@ function useFocusedSeries({
       const setSeriesVisibility = useCallback(
         (series: FocusedMetricsSeries) => {
           onChange?.();
    -      if (focusedSeries?.length === 1 && focusedSeries[0].id === series.id) {
    +      if (focusedSeries?.length === 1 && focusedSeries[0]!.id === series.id) {
             setFocusedSeries([]);
             return;
           }
    diff --git a/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx b/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx
    index 7da42feb17ae34..0de52ea3a08f65 100644
    --- a/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx
    +++ b/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx
    @@ -171,11 +171,11 @@ function AddToDashboardModal({
           return;
         }
     
    -    let orderby = widget.queries[0].orderby;
    -    if (!(DisplayType.AREA && widget.queries[0].columns.length)) {
    +    let orderby = widget.queries[0]!.orderby;
    +    if (!(DisplayType.AREA && widget.queries[0]!.columns.length)) {
           orderby = ''; // Clear orderby if its not a top n visualization.
         }
    -    const query = widget.queries[0];
    +    const query = widget.queries[0]!;
     
         const title =
           // Metric widgets have their default title derived from the query
    @@ -278,7 +278,7 @@ function AddToDashboardModal({
             
               
    diff --git a/static/app/components/modals/widgetViewerModal.tsx b/static/app/components/modals/widgetViewerModal.tsx
    index f343822c3a8797..af4f53478e452c 100644
    --- a/static/app/components/modals/widgetViewerModal.tsx
    +++ b/static/app/components/modals/widgetViewerModal.tsx
    @@ -297,16 +297,16 @@ function WidgetViewerModal(props: Props) {
     
       // Create Table widget
       const tableWidget = {
    -    ...cloneDeep({...widget, queries: [sortedQueries[selectedQueryIndex]]}),
    +    ...cloneDeep({...widget, queries: [sortedQueries[selectedQueryIndex]!]}),
         displayType: DisplayType.TABLE,
       };
    -  const {aggregates, columns} = tableWidget.queries[0];
    -  const {orderby} = widget.queries[0];
    +  const {aggregates, columns} = tableWidget.queries[0]!;
    +  const {orderby} = widget.queries[0]!;
       const order = orderby.startsWith('-');
       const rawOrderby = trimStart(orderby, '-');
     
    -  const fields = defined(tableWidget.queries[0].fields)
    -    ? tableWidget.queries[0].fields
    +  const fields = defined(tableWidget.queries[0]!.fields)
    +    ? tableWidget.queries[0]!.fields
         : [...columns, ...aggregates];
     
       // Some Discover Widgets (Line, Area, Bar) allow the user to specify an orderby
    @@ -339,8 +339,8 @@ function WidgetViewerModal(props: Props) {
     
       // Need to set the orderby of the eventsv2 query to equation[index] format
       // since eventsv2 does not accept the raw equation as a valid sort payload
    -  if (isEquation(rawOrderby) && tableWidget.queries[0].orderby === orderby) {
    -    tableWidget.queries[0].orderby = `${order ? '-' : ''}equation[${
    +  if (isEquation(rawOrderby) && tableWidget.queries[0]!.orderby === orderby) {
    +    tableWidget.queries[0]!.orderby = `${order ? '-' : ''}equation[${
           getNumEquations(fields) - 1
         }]`;
       }
    @@ -381,8 +381,8 @@ function WidgetViewerModal(props: Props) {
         switch (widget.widgetType) {
           case WidgetType.DISCOVER:
             if (fields.length === 1) {
    -          tableWidget.queries[0].orderby =
    -            tableWidget.queries[0].orderby || `-${fields[0]}`;
    +          tableWidget.queries[0]!.orderby =
    +            tableWidget.queries[0]!.orderby || `-${fields[0]}`;
             }
             fields.unshift('title');
             columns.unshift('title');
    @@ -398,7 +398,7 @@ function WidgetViewerModal(props: Props) {
     
       const eventView = eventViewFromWidget(
         tableWidget.title,
    -    tableWidget.queries[0],
    +    tableWidget.queries[0]!,
         modalTableSelection
       );
     
    @@ -410,7 +410,7 @@ function WidgetViewerModal(props: Props) {
       const columnSortBy = eventView.getSorts();
       columnOrder = columnOrder.map((column, index) => ({
         ...column,
    -    width: parseInt(widths[index], 10) || -1,
    +    width: parseInt(widths[index]!, 10) || -1,
       }));
     
       const getOnDemandFilterWarning = createOnDemandFilterWarning(
    @@ -685,7 +685,7 @@ function WidgetViewerModal(props: Props) {
                 onResizeColumn,
               }}
             />
    -        {!tableWidget.queries[0].orderby.match(/^-?release$/) &&
    +        {!tableWidget.queries[0]!.orderby.match(/^-?release$/) &&
               (links?.previous?.results || links?.next?.results) && (
                 
             )}
    -        {(widget.queries.length > 1 || widget.queries[0].conditions) && (
    +        {(widget.queries.length > 1 || widget.queries[0]!.conditions) && (
               
                 
    -                      {queryOptions[selectedQueryIndex].getHighlightedQuery({
    +                      {queryOptions[selectedQueryIndex]!.getHighlightedQuery({
                             display: 'block',
                           }) ??
    -                        (queryOptions[selectedQueryIndex].label || (
    +                        (queryOptions[selectedQueryIndex]!.label || (
                               {EMPTY_QUERY_NAME}
                             ))}
                         
    @@ -1161,7 +1161,7 @@ function OpenButton({
         default:
           openLabel = t('Open in Discover');
           path = getWidgetDiscoverUrl(
    -        {...widget, queries: [widget.queries[selectedQueryIndex]]},
    +        {...widget, queries: [widget.queries[selectedQueryIndex]!]},
             selection,
             organization,
             0,
    diff --git a/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx b/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx
    index f9899302ae5767..f0f7dfa3d3bd3c 100644
    --- a/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx
    +++ b/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx
    @@ -80,7 +80,7 @@ export const renderIssueGridHeaderCell = ({
           {column.name}}
    -        direction={widget.queries[0].orderby === sortField ? 'desc' : undefined}
    +        direction={widget.queries[0]!.orderby === sortField ? 'desc' : undefined}
             canSort={!!sortField}
             generateSortLink={() => ({
               ...location,
    @@ -118,14 +118,14 @@ export const renderDiscoverGridHeaderCell = ({
         column: TableColumn,
         _columnIndex: number
       ): React.ReactNode {
    -    const {orderby} = widget.queries[0];
    +    const {orderby} = widget.queries[0]!;
         // Need to convert orderby to aggregate alias because eventView still uses aggregate alias format
         const aggregateAliasOrderBy = `${
           orderby.startsWith('-') ? '-' : ''
         }${getAggregateAlias(trimStart(orderby, '-'))}`;
         const eventView = eventViewFromWidget(
           widget.title,
    -      {...widget.queries[0], orderby: aggregateAliasOrderBy},
    +      {...widget.queries[0]!, orderby: aggregateAliasOrderBy},
           selection
         );
         const tableMeta = tableData?.meta;
    @@ -318,7 +318,7 @@ export const renderReleaseGridHeaderCell = ({
       ): React.ReactNode {
         const tableMeta = tableData?.meta;
         const align = fieldAlignment(column.name, column.type, tableMeta);
    -    const widgetOrderBy = widget.queries[0].orderby;
    +    const widgetOrderBy = widget.queries[0]!.orderby;
         const sort: Sort = {
           kind: widgetOrderBy.startsWith('-') ? 'desc' : 'asc',
           field: widgetOrderBy.startsWith('-') ? widgetOrderBy.slice(1) : widgetOrderBy,
    diff --git a/static/app/components/nav/utils.tsx b/static/app/components/nav/utils.tsx
    index 498162e743828d..5b509e01e8bb90 100644
    --- a/static/app/components/nav/utils.tsx
    +++ b/static/app/components/nav/utils.tsx
    @@ -165,7 +165,7 @@ export function resolveNavItemTo(
         return undefined;
       }
       if (isSidebarItem(item) && isNonEmptyArray(item.submenu)) {
    -    return item.submenu[0].to;
    +    return item.submenu[0]!.to;
       }
       return undefined;
     }
    diff --git a/static/app/components/notificationActions/forms/onCallServiceForm.tsx b/static/app/components/notificationActions/forms/onCallServiceForm.tsx
    index 21aa0696c994ce..5dc690069abc34 100644
    --- a/static/app/components/notificationActions/forms/onCallServiceForm.tsx
    +++ b/static/app/components/notificationActions/forms/onCallServiceForm.tsx
    @@ -41,7 +41,7 @@ function OnCallServiceForm({
     }: OnCallServiceFormProps) {
       const [selectedAccount, setSelectedAccount] = useState(
         action.integrationId
    -      ? Integrations[action.integrationId][0].action.integrationName
    +      ? Integrations[action.integrationId]![0]!.action.integrationName
           : ''
       );
       const [selectedDisplay, setSelectedDisplay] = useState(action.targetDisplay ?? '');
    @@ -67,7 +67,7 @@ function OnCallServiceForm({
         if (!action.integrationId) {
           return [];
         }
    -    const services = Integrations[action.integrationId];
    +    const services = Integrations[action.integrationId]!;
         return services.map(service => ({
           key: service.action.targetDisplay ?? '',
           label: service.action.targetDisplay,
    diff --git a/static/app/components/notificationActions/notificationActionManager.spec.tsx b/static/app/components/notificationActions/notificationActionManager.spec.tsx
    index f0cafde15aa975..c5cdea2ff4de98 100644
    --- a/static/app/components/notificationActions/notificationActionManager.spec.tsx
    +++ b/static/app/components/notificationActions/notificationActionManager.spec.tsx
    @@ -97,7 +97,7 @@ describe('Adds, deletes, and updates notification actions', function () {
         render(
            {
               // Add notification action
    -          const updatedActions = [...notificationActions, validActions[0].action];
    +          const updatedActions = [...notificationActions, validActions[0]!.action];
               setNotificationActions(updatedActions);
             },
           });
    diff --git a/static/app/components/onboarding/frameworkSuggestionModal.tsx b/static/app/components/onboarding/frameworkSuggestionModal.tsx
    index a35999e131881e..187ff72a4cfc13 100644
    --- a/static/app/components/onboarding/frameworkSuggestionModal.tsx
    +++ b/static/app/components/onboarding/frameworkSuggestionModal.tsx
    @@ -313,7 +313,7 @@ function TopFrameworksImage({frameworks}: {frameworks: PlatformIntegration[]}) {
         
           
           
           SENTRY_AUTH_TOKEN='
         );
         expect(tokenNodes).toHaveLength(1);
    -    expect(element.contains(tokenNodes[0])).toBe(true);
    +    expect(element.contains(tokenNodes[0]!)).toBe(true);
       });
     
       it('replaces multiple ___ORG_AUTH_TOKEN___ tokens', function () {
    @@ -29,7 +29,7 @@ const assetUrl = '';
     `
         );
         expect(tokenNodes).toHaveLength(2);
    -    expect(element.contains(tokenNodes[0])).toBe(true);
    -    expect(element.contains(tokenNodes[1])).toBe(true);
    +    expect(element.contains(tokenNodes[0]!)).toBe(true);
    +    expect(element.contains(tokenNodes[1]!)).toBe(true);
       });
     });
    diff --git a/static/app/components/onboarding/gettingStartedDoc/step.tsx b/static/app/components/onboarding/gettingStartedDoc/step.tsx
    index 862d864e8afdad..2233c1a4e45490 100644
    --- a/static/app/components/onboarding/gettingStartedDoc/step.tsx
    +++ b/static/app/components/onboarding/gettingStartedDoc/step.tsx
    @@ -53,8 +53,8 @@ export function TabbedCodeSnippet({
       onSelectAndCopy,
       partialLoading,
     }: TabbedCodeSnippetProps) {
    -  const [selectedTabValue, setSelectedTabValue] = useState(tabs[0].value);
    -  const selectedTab = tabs.find(tab => tab.value === selectedTabValue) ?? tabs[0];
    +  const [selectedTabValue, setSelectedTabValue] = useState(tabs[0]!.value);
    +  const selectedTab = tabs.find(tab => tab.value === selectedTabValue) ?? tabs[0]!;
       const {code, language, filename} = selectedTab;
     
       return (
    diff --git a/static/app/components/onboarding/gettingStartedDoc/utils/feedbackOnboarding.tsx b/static/app/components/onboarding/gettingStartedDoc/utils/feedbackOnboarding.tsx
    index 72e15c3a8dba34..abc989a265a5d5 100644
    --- a/static/app/components/onboarding/gettingStartedDoc/utils/feedbackOnboarding.tsx
    +++ b/static/app/components/onboarding/gettingStartedDoc/utils/feedbackOnboarding.tsx
    @@ -155,13 +155,13 @@ export function getCrashReportSDKInstallFirstStep(params: DocsParams) {
         params.sourcePackageRegistries && !params.sourcePackageRegistries.isLoading;
       const version =
         (dataLoaded &&
    -      params.sourcePackageRegistries.data?.['sentry.javascript.browser'].version) ??
    +      params.sourcePackageRegistries.data?.['sentry.javascript.browser']!.version) ??
         '';
       const hash =
         (dataLoaded &&
    -      params.sourcePackageRegistries.data?.['sentry.javascript.browser'].files[
    +      params.sourcePackageRegistries.data?.['sentry.javascript.browser']!.files[
             'bundle.min.js'
    -      ].checksums['sha384-base64']) ??
    +      ]!.checksums['sha384-base64']) ??
         '';
     
       return {
    @@ -242,13 +242,13 @@ export function getCrashReportSDKInstallFirstStepRails(params: DocsParams) {
         params.sourcePackageRegistries && !params.sourcePackageRegistries.isLoading;
       const version =
         (dataLoaded &&
    -      params.sourcePackageRegistries.data?.['sentry.javascript.browser'].version) ??
    +      params.sourcePackageRegistries.data?.['sentry.javascript.browser']!.version) ??
         '';
       const hash =
         (dataLoaded &&
    -      params.sourcePackageRegistries.data?.['sentry.javascript.browser'].files[
    +      params.sourcePackageRegistries.data?.['sentry.javascript.browser']!.files[
             'bundle.min.js'
    -      ].checksums['sha384-base64']) ??
    +      ]!.checksums['sha384-base64']) ??
         '';
     
       return {
    diff --git a/static/app/components/onboarding/platformOptionsControl.tsx b/static/app/components/onboarding/platformOptionsControl.tsx
    index 7d7588781a6fde..1253ec46ad5822 100644
    --- a/static/app/components/onboarding/platformOptionsControl.tsx
    +++ b/static/app/components/onboarding/platformOptionsControl.tsx
    @@ -26,8 +26,8 @@ export function useUrlPlatformOptions {
    -      const defaultValue = platformOptions[key].defaultValue;
    -      const values = platformOptions[key].items.map(({value}) => value);
    +      const defaultValue = platformOptions[key]!.defaultValue;
    +      const values = platformOptions[key]!.items.map(({value}) => value);
           acc[key as keyof PlatformOptions] = values.includes(query[key])
             ? query[key]
             : defaultValue ?? values[0];
    @@ -100,7 +100,7 @@ export function PlatformOptionsControl({
              handleChange(key, value)}
             />
           ))}
    diff --git a/static/app/components/onboardingWizard/newSidebar.spec.tsx b/static/app/components/onboardingWizard/newSidebar.spec.tsx
    index bf0dece9649e89..937f13dfefdac0 100644
    --- a/static/app/components/onboardingWizard/newSidebar.spec.tsx
    +++ b/static/app/components/onboardingWizard/newSidebar.spec.tsx
    @@ -63,20 +63,20 @@ describe('NewSidebar', function () {
         expect(screen.getByText('0 out of 2 tasks completed')).toBeInTheDocument();
         // This means that the group is expanded
         expect(screen.getByRole('button', {name: 'Collapse'})).toBeInTheDocument();
    -    expect(screen.getByText(gettingStartedTasks[0].title)).toBeInTheDocument();
    -    expect(screen.getByText(gettingStartedTasks[0].description)).toBeInTheDocument();
    +    expect(screen.getByText(gettingStartedTasks[0]!.title)).toBeInTheDocument();
    +    expect(screen.getByText(gettingStartedTasks[0]!.description)).toBeInTheDocument();
         expect(screen.queryByRole('button', {name: 'Skip Task'})).not.toBeInTheDocument();
     
         // Group 2
         expect(screen.getByText('Beyond the Basics')).toBeInTheDocument();
         expect(screen.getByText('0 out of 1 task completed')).toBeInTheDocument();
         // This means that the group is not expanded
    -    expect(screen.queryByText(beyondBasicsTasks[0].title)).not.toBeInTheDocument();
    +    expect(screen.queryByText(beyondBasicsTasks[0]!.title)).not.toBeInTheDocument();
     
         // Manually expand second group
         await userEvent.click(screen.getByRole('button', {name: 'Expand'}));
         // Tasks from the second group should be visible
    -    expect(await screen.findByText(beyondBasicsTasks[0].title)).toBeInTheDocument();
    +    expect(await screen.findByText(beyondBasicsTasks[0]!.title)).toBeInTheDocument();
         // task from second group are skippable
         expect(screen.getByRole('button', {name: 'Skip Task'})).toBeInTheDocument();
       });
    @@ -101,7 +101,7 @@ describe('NewSidebar', function () {
     
         // Group 2
         // This means that the group is expanded
    -    expect(screen.getByText(beyondBasicsTasks[0].title)).toBeInTheDocument();
    +    expect(screen.getByText(beyondBasicsTasks[0]!.title)).toBeInTheDocument();
       });
     
       it('show skipable confirmation when skipping a task', async function () {
    @@ -128,7 +128,7 @@ describe('NewSidebar', function () {
         // Manually expand second group
         await userEvent.click(screen.getByRole('button', {name: 'Expand'}));
         // Tasks from the second group should be visible
    -    expect(await screen.findByText(beyondBasicsTasks[0].title)).toBeInTheDocument();
    +    expect(await screen.findByText(beyondBasicsTasks[0]!.title)).toBeInTheDocument();
     
         await userEvent.click(screen.getByRole('button', {name: 'Skip Task'}));
     
    diff --git a/static/app/components/onboardingWizard/task.tsx b/static/app/components/onboardingWizard/task.tsx
    index 76bb18b5ce06fa..21e3495b2e0b21 100644
    --- a/static/app/components/onboardingWizard/task.tsx
    +++ b/static/app/components/onboardingWizard/task.tsx
    @@ -128,7 +128,7 @@ function Task(props: Props) {
         
           
    diff --git a/static/app/components/onboardingWizard/taskConfig.tsx b/static/app/components/onboardingWizard/taskConfig.tsx
    index 6900c8ac85a7f4..1e62e7db20d483 100644
    --- a/static/app/components/onboardingWizard/taskConfig.tsx
    +++ b/static/app/components/onboardingWizard/taskConfig.tsx
    @@ -58,7 +58,7 @@ function getIssueAlertUrl({projects, organization}: Options) {
       }
       // pick the first project with events if we have that, otherwise just pick the first project
       const firstProjectWithEvents = projects.find(project => !!project.firstEvent);
    -  const project = firstProjectWithEvents ?? projects[0];
    +  const project = firstProjectWithEvents ?? projects[0]!;
       return `/organizations/${organization.slug}/alerts/${project.slug}/wizard/`;
     }
     
    @@ -81,7 +81,7 @@ function getOnboardingInstructionsUrl({projects, organization}: Options) {
       const firstProjectWithoutError = projects.find(project => !project.firstEvent);
       // If all projects contain errors, this step will not be visible to the user,
       // but if the user falls into this case for some reason, we pick the first project
    -  const project = firstProjectWithoutError ?? projects[0];
    +  const project = firstProjectWithoutError ?? projects[0]!;
     
       let url = `/${organization.slug}/${project.slug}/getting-started/`;
     
    @@ -100,7 +100,7 @@ function getMetricAlertUrl({projects, organization}: Options) {
       const firstProjectWithEvents = projects.find(
         project => !!project.firstTransactionEvent
       );
    -  const project = firstProjectWithEvents ?? projects[0];
    +  const project = firstProjectWithEvents ?? projects[0]!;
       return {
         pathname: `/organizations/${organization.slug}/alerts/${project.slug}/wizard/`,
         query: {
    @@ -230,7 +230,7 @@ export function getOnboardingTasks({
                !taskIsDone(task) && onCompleteTask?.()}
               >
    @@ -372,14 +372,14 @@ export function getOnboardingTasks({
     
             if (projectsForOnboarding.length) {
               navigateTo(
    -            `${performanceUrl}?project=${projectsForOnboarding[0].id}#performance-sidequest`,
    +            `${performanceUrl}?project=${projectsForOnboarding[0]!.id}#performance-sidequest`,
                 router
               );
               return;
             }
     
             navigateTo(
    -          `${performanceUrl}?project=${projectsWithoutFirstTransactionEvent[0].id}#performance-sidequest`,
    +          `${performanceUrl}?project=${projectsWithoutFirstTransactionEvent[0]!.id}#performance-sidequest`,
               router
             );
           },
    @@ -408,7 +408,7 @@ export function getOnboardingTasks({
                !taskIsDone(task) && onCompleteTask?.()}
               >
    @@ -485,7 +485,7 @@ export function getOnboardingTasks({
                !taskIsDone(task) && onCompleteTask?.()}
               >
    diff --git a/static/app/components/organizations/environmentPageFilter/trigger.tsx b/static/app/components/organizations/environmentPageFilter/trigger.tsx
    index 7146b4b5e9a937..b37703f0156056 100644
    --- a/static/app/components/organizations/environmentPageFilter/trigger.tsx
    +++ b/static/app/components/organizations/environmentPageFilter/trigger.tsx
    @@ -27,7 +27,7 @@ function BaseEnvironmentPageFilterTrigger(
       // Show 2 environments only if the combined string's length does not exceed 25.
       // Otherwise show only 1 environment.
       const envsToShow =
    -    value[0]?.length + value[1]?.length <= 23 ? value.slice(0, 2) : value.slice(0, 1);
    +    value[0]!?.length + value[1]!?.length <= 23 ? value.slice(0, 2) : value.slice(0, 1);
     
       // e.g. "production, staging"
       const enumeratedLabel = envsToShow.map(env => trimSlug(env, 25)).join(', ');
    diff --git a/static/app/components/organizations/hybridFilter.tsx b/static/app/components/organizations/hybridFilter.tsx
    index 82ac8be78b7aee..eb9f17e961dc58 100644
    --- a/static/app/components/organizations/hybridFilter.tsx
    +++ b/static/app/components/organizations/hybridFilter.tsx
    @@ -324,12 +324,12 @@ export function HybridFilter({
           // A modifier key is being pressed --> enter multiple selection mode
           if (multiple && modifierKeyPressed) {
             !modifierTipSeen && setModifierTipSeen(true);
    -        toggleOption(diff[0]);
    +        toggleOption(diff[0]!);
             return;
           }
     
           // Only one option was clicked on --> use single, direct selection mode
    -      onReplace?.(diff[0]);
    +      onReplace?.(diff[0]!);
           commit(diff);
         },
         [
    diff --git a/static/app/components/organizations/projectPageFilter/index.tsx b/static/app/components/organizations/projectPageFilter/index.tsx
    index 6917dd03b8dec6..393d75d57e1ec8 100644
    --- a/static/app/components/organizations/projectPageFilter/index.tsx
    +++ b/static/app/components/organizations/projectPageFilter/index.tsx
    @@ -171,10 +171,10 @@ export function ProjectPageFilter({
           if (!val.length) {
             return allowMultiple
               ? memberProjects.map(p => parseInt(p.id, 10))
    -          : [parseInt(memberProjects[0]?.id, 10)];
    +          : [parseInt(memberProjects[0]!?.id, 10)];
           }
     
    -      return allowMultiple ? val : [val[0]];
    +      return allowMultiple ? val : [val[0]!];
         },
         [memberProjects, allowMultiple]
       );
    diff --git a/static/app/components/organizations/projectPageFilter/trigger.tsx b/static/app/components/organizations/projectPageFilter/trigger.tsx
    index 93b09ff1281862..64fdf90744d256 100644
    --- a/static/app/components/organizations/projectPageFilter/trigger.tsx
    +++ b/static/app/components/organizations/projectPageFilter/trigger.tsx
    @@ -54,7 +54,7 @@ function BaseProjectPageFilterTrigger(
       // Show 2 projects only if the combined string does not exceed maxTitleLength.
       // Otherwise show only 1 project.
       const projectsToShow =
    -    selectedProjects[0]?.slug?.length + selectedProjects[1]?.slug?.length <= 23
    +    selectedProjects[0]!?.slug?.length + selectedProjects[1]!?.slug?.length <= 23
           ? selectedProjects.slice(0, 2)
           : selectedProjects.slice(0, 1);
     
    diff --git a/static/app/components/performance/searchBar.tsx b/static/app/components/performance/searchBar.tsx
    index f26ef55e9aa1af..a7293c7458bcde 100644
    --- a/static/app/components/performance/searchBar.tsx
    +++ b/static/app/components/performance/searchBar.tsx
    @@ -97,12 +97,12 @@ function SearchBar(props: SearchBarProps) {
           isDropdownOpen &&
           transactionCount > 0
         ) {
    -      const currentHighlightedItem = searchResults[0].children[highlightedItemIndex];
    +      const currentHighlightedItem = searchResults[0]!.children[highlightedItemIndex];
           const nextHighlightedItemIndex =
             (highlightedItemIndex + transactionCount + (key === 'ArrowUp' ? -1 : 1)) %
             transactionCount;
           setHighlightedItemIndex(nextHighlightedItemIndex);
    -      const nextHighlightedItem = searchResults[0].children[nextHighlightedItemIndex];
    +      const nextHighlightedItem = searchResults[0]!.children[nextHighlightedItemIndex];
     
           let newSearchResults = searchResults;
           if (currentHighlightedItem) {
    diff --git a/static/app/components/performance/waterfall/utils.spec.tsx b/static/app/components/performance/waterfall/utils.spec.tsx
    index c284110f42a07c..e4664d79369cfd 100644
    --- a/static/app/components/performance/waterfall/utils.spec.tsx
    +++ b/static/app/components/performance/waterfall/utils.spec.tsx
    @@ -16,7 +16,7 @@ describe('pickBarColor()', function () {
       });
     
       it('returns a random color when no predefined option is available', function () {
    -    const colorsAsArray = Object.keys(CHART_PALETTE).map(key => CHART_PALETTE[17][key]);
    +    const colorsAsArray = Object.keys(CHART_PALETTE).map(key => CHART_PALETTE[17]![key]);
     
         let randomColor = pickBarColor('a normal string');
         expect(colorsAsArray).toContain(randomColor);
    diff --git a/static/app/components/performance/waterfall/utils.tsx b/static/app/components/performance/waterfall/utils.tsx
    index 63b7fed34ff53c..3589403bf789e5 100644
    --- a/static/app/components/performance/waterfall/utils.tsx
    +++ b/static/app/components/performance/waterfall/utils.tsx
    @@ -238,13 +238,13 @@ const getLetterIndex = (letter: string): number => {
       return index === -1 ? 0 : index;
     };
     
    -const colorsAsArray = Object.keys(CHART_PALETTE).map(key => CHART_PALETTE[17][key]);
    +const colorsAsArray = Object.keys(CHART_PALETTE).map(key => CHART_PALETTE[17]![key]);
     
     export const barColors = {
    -  default: CHART_PALETTE[17][4],
    -  transaction: CHART_PALETTE[17][8],
    -  http: CHART_PALETTE[17][10],
    -  db: CHART_PALETTE[17][17],
    +  default: CHART_PALETTE[17]![4],
    +  transaction: CHART_PALETTE[17]![8],
    +  http: CHART_PALETTE[17]![10],
    +  db: CHART_PALETTE[17]![17],
     };
     
     export const pickBarColor = (input: string | undefined): string => {
    @@ -252,17 +252,17 @@ export const pickBarColor = (input: string | undefined): string => {
       // That way colors stay consistent between transactions.
     
       if (!input || input.length < 3) {
    -    return CHART_PALETTE[17][4];
    +    return CHART_PALETTE[17]![4]!;
       }
     
       if (barColors[input]) {
         return barColors[input];
       }
     
    -  const letterIndex1 = getLetterIndex(input[0]);
    -  const letterIndex2 = getLetterIndex(input[1]);
    -  const letterIndex3 = getLetterIndex(input[2]);
    -  const letterIndex4 = getLetterIndex(input[3]);
    +  const letterIndex1 = getLetterIndex(input[0]!);
    +  const letterIndex2 = getLetterIndex(input[1]!);
    +  const letterIndex3 = getLetterIndex(input[2]!);
    +  const letterIndex4 = getLetterIndex(input[3]!);
     
       return colorsAsArray[
         (letterIndex1 + letterIndex2 + letterIndex3 + letterIndex4) % colorsAsArray.length
    diff --git a/static/app/components/performanceOnboarding/sidebar.tsx b/static/app/components/performanceOnboarding/sidebar.tsx
    index 207dfd170afcf9..23a88d7fe47b63 100644
    --- a/static/app/components/performanceOnboarding/sidebar.tsx
    +++ b/static/app/components/performanceOnboarding/sidebar.tsx
    @@ -85,7 +85,7 @@ function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
     
           const priorityProjects: Project[] = [];
           priorityProjectIds.forEach(projectId => {
    -        priorityProjects.push(projectMap[String(projectId)]);
    +        priorityProjects.push(projectMap[String(projectId)]!);
           });
     
           // Among the project selection, find a project that has performance onboarding docs support, and has not sent
    diff --git a/static/app/components/pickProjectToContinue.tsx b/static/app/components/pickProjectToContinue.tsx
    index b15d0fb082a4f1..b04309d63a6871 100644
    --- a/static/app/components/pickProjectToContinue.tsx
    +++ b/static/app/components/pickProjectToContinue.tsx
    @@ -49,7 +49,7 @@ function PickProjectToContinue({
     
       // if the project in URL is missing, but this release belongs to only one project, redirect there
       if (projects.length === 1) {
    -    router.replace(path + projects[0].id);
    +    router.replace(path + projects[0]!.id);
         return null;
       }
     
    diff --git a/static/app/components/platformPicker.spec.tsx b/static/app/components/platformPicker.spec.tsx
    index a0a31b356c4904..6e127f16ce96c8 100644
    --- a/static/app/components/platformPicker.spec.tsx
    +++ b/static/app/components/platformPicker.spec.tsx
    @@ -104,7 +104,7 @@ describe('PlatformPicker', function () {
         const platformNames = screen.getAllByRole('heading', {level: 3});
     
         platformNames.forEach((platform, index) => {
    -      expect(platform).toHaveTextContent(alphabeticallyOrderedPlatformNames[index]);
    +      expect(platform).toHaveTextContent(alphabeticallyOrderedPlatformNames[index]!);
         });
       });
     
    diff --git a/static/app/components/platformPicker.tsx b/static/app/components/platformPicker.tsx
    index edee2eda9fe73c..c1bba265ab8e1d 100644
    --- a/static/app/components/platformPicker.tsx
    +++ b/static/app/components/platformPicker.tsx
    @@ -72,8 +72,8 @@ class PlatformPicker extends Component {
       };
     
       state: State = {
    -    category: this.props.defaultCategory ?? categoryList[0].id,
    -    filter: this.props.noAutoFilter ? '' : (this.props.platform || '').split('-')[0],
    +    category: this.props.defaultCategory ?? categoryList[0]!.id,
    +    filter: this.props.noAutoFilter ? '' : (this.props.platform || '').split('-')[0]!,
       };
     
       get platformList() {
    diff --git a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx
    index cbe1d3931fe460..a55ddbacc11dcf 100644
    --- a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx
    +++ b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx
    @@ -165,11 +165,11 @@ function convertContinuousProfileMeasurementsToUIFrames(
       };
     
       for (let i = 0; i < measurement.values.length; i++) {
    -    const value = measurement.values[i];
    +    const value = measurement.values[i]!;
         const next = measurement.values[i + 1] ?? value;
     
         measurements.values.push({
    -      elapsed: next.timestamp - value.timestamp,
    +      elapsed: next!.timestamp - value.timestamp,
           value: value.value,
         });
       }
    @@ -207,7 +207,7 @@ function findLongestMatchingFrame(
         }
     
         for (let i = 0; i < frame.children.length; i++) {
    -      frames.push(frame.children[i]);
    +      frames.push(frame.children[i]!);
         }
       }
     
    @@ -425,8 +425,8 @@ export function ContinuousFlamegraph(): ReactElement {
     
             let offset = 0;
             for (let i = 0; i < measurements.values.length; i++) {
    -          const value = measurements.values[i];
    -          const next = measurements.values[i + 1] ?? value;
    +          const value = measurements.values[i]!;
    +          const next = measurements.values[i + 1]! ?? value;
               offset += (next.timestamp - value.timestamp) * 1e3;
     
               values.push({
    @@ -467,8 +467,8 @@ export function ContinuousFlamegraph(): ReactElement {
     
             let offset = 0;
             for (let i = 0; i < measurements.values.length; i++) {
    -          const value = measurements.values[i];
    -          const next = measurements.values[i + 1] ?? value;
    +          const value = measurements.values[i]!;
    +          const next = measurements.values[i + 1]! ?? value;
               offset += (next.timestamp - value.timestamp) * 1e3;
     
               values.push({
    @@ -502,8 +502,8 @@ export function ContinuousFlamegraph(): ReactElement {
     
           let offset = 0;
           for (let i = 0; i < memory_footprint.values.length; i++) {
    -        const value = memory_footprint.values[i];
    -        const next = memory_footprint.values[i + 1] ?? value;
    +        const value = memory_footprint.values[i]!;
    +        const next = memory_footprint.values[i + 1]! ?? value;
             offset += (next.timestamp - value.timestamp) * 1e3;
     
             values.push({
    @@ -525,8 +525,8 @@ export function ContinuousFlamegraph(): ReactElement {
     
           let offset = 0;
           for (let i = 0; i < native_memory_footprint.values.length; i++) {
    -        const value = native_memory_footprint.values[i];
    -        const next = native_memory_footprint.values[i + 1] ?? value;
    +        const value = native_memory_footprint.values[i]!;
    +        const next = native_memory_footprint.values[i + 1]! ?? value;
             offset += (next.timestamp - value.timestamp) * 1e3;
     
             values.push({
    diff --git a/static/app/components/profiling/flamegraph/flamegraph.tsx b/static/app/components/profiling/flamegraph/flamegraph.tsx
    index a02ce26d2a4fd4..3e35201a064573 100644
    --- a/static/app/components/profiling/flamegraph/flamegraph.tsx
    +++ b/static/app/components/profiling/flamegraph/flamegraph.tsx
    @@ -132,7 +132,7 @@ function convertProfileMeasurementsToUIFrames(
       };
     
       for (let i = 0; i < measurement.values.length; i++) {
    -    const value = measurement.values[i];
    +    const value = measurement.values[i]!;
     
         measurements.values.push({
           elapsed: value.elapsed_since_start_ns,
    @@ -173,7 +173,7 @@ function findLongestMatchingFrame(
         }
     
         for (let i = 0; i < frame.children.length; i++) {
    -      frames.push(frame.children[i]);
    +      frames.push(frame.children[i]!);
         }
       }
     
    @@ -449,7 +449,7 @@ function Flamegraph(): ReactElement {
             const values: ProfileSeriesMeasurement['values'] = [];
     
             for (let i = 0; i < measurements.values.length; i++) {
    -          const value = measurements.values[i];
    +          const value = measurements.values[i]!;
               values.push({
                 value: value.value,
                 elapsed: value.elapsed_since_start_ns,
    @@ -478,7 +478,7 @@ function Flamegraph(): ReactElement {
           const values: ProfileSeriesMeasurement['values'] = [];
     
           for (let i = 0; i < memory_footprint.values.length; i++) {
    -        const value = memory_footprint.values[i];
    +        const value = memory_footprint.values[i]!;
             values.push({
               value: value.value,
               elapsed: value.elapsed_since_start_ns,
    @@ -497,7 +497,7 @@ function Flamegraph(): ReactElement {
           const values: ProfileSeriesMeasurement['values'] = [];
     
           for (let i = 0; i < native_memory_footprint.values.length; i++) {
    -        const value = native_memory_footprint.values[i];
    +        const value = native_memory_footprint.values[i]!;
             values.push({
               value: value.value,
               elapsed: value.elapsed_since_start_ns,
    diff --git a/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx b/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx
    index 02ac3aadbec950..328d1202c37539 100644
    --- a/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx
    +++ b/static/app/components/profiling/flamegraph/flamegraphChartTooltip.tsx
    @@ -59,7 +59,7 @@ export function FlamegraphChartTooltip({
                   />
                   {p.name}: 
                   
    -                {chart.tooltipFormatter(p.points[0].y)}
    +                {chart.tooltipFormatter(p.points[0]!.y)}
                   
                 
               
    diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx
    index 75aad0f8a04977..00e15b320e4034 100644
    --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx
    +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx
    @@ -264,7 +264,7 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP
                   ? props.profileTransaction.data
                   : null
               }
    -          projectId={params.projectId}
    +          projectId={params.projectId!}
               profileGroup={props.profileGroup}
             />
           ) : null}
    diff --git a/static/app/components/profiling/flamegraph/flamegraphOverlays/profileDragDropImport.tsx b/static/app/components/profiling/flamegraph/flamegraphOverlays/profileDragDropImport.tsx
    index e7495f1be38133..fd7fe5745e52d9 100644
    --- a/static/app/components/profiling/flamegraph/flamegraphOverlays/profileDragDropImport.tsx
    +++ b/static/app/components/profiling/flamegraph/flamegraphOverlays/profileDragDropImport.tsx
    @@ -25,7 +25,7 @@ function ProfileDragDropImport({
           evt.preventDefault();
           evt.stopPropagation();
     
    -      const file = evt.dataTransfer.items[0].getAsFile();
    +      const file = evt.dataTransfer.items[0]!.getAsFile();
     
           if (file) {
             setDropState('processing');
    diff --git a/static/app/components/profiling/flamegraph/flamegraphPreview.tsx b/static/app/components/profiling/flamegraph/flamegraphPreview.tsx
    index e38af70780bf2d..5bcb5c362ef468 100644
    --- a/static/app/components/profiling/flamegraph/flamegraphPreview.tsx
    +++ b/static/app/components/profiling/flamegraph/flamegraphPreview.tsx
    @@ -332,7 +332,7 @@ export function computePreviewConfigView(
         }
     
         for (let i = 0; i < frame.children.length; i++) {
    -      frames.push(frame.children[i]);
    +      frames.push(frame.children[i]!);
         }
       }
     
    diff --git a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx
    index f9571c828f8f06..b6322a43ac6e2a 100644
    --- a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx
    +++ b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx
    @@ -171,7 +171,7 @@ function yieldingRafFrameSearch(
     
       const searchFramesFunction = isRegExpSearch ? searchFrameRegExp : searchFrameFzf;
       const searchSpansFunction = isRegExpSearch ? searchSpanRegExp : searchSpanFzf;
    -  const searchQuery = isRegExpSearch ? lookup : lowercaseQuery;
    +  const searchQuery = isRegExpSearch ? lookup! : lowercaseQuery;
     
       function searchFramesAndSpans() {
         const start = performance.now();
    diff --git a/static/app/components/projects/missingProjectMembership.tsx b/static/app/components/projects/missingProjectMembership.tsx
    index 94912788222c8b..8110ca41bac49b 100644
    --- a/static/app/components/projects/missingProjectMembership.tsx
    +++ b/static/app/components/projects/missingProjectMembership.tsx
    @@ -126,14 +126,14 @@ class MissingProjectMembership extends Component {
         const teamAccess = [
           {
             label: t('Request Access'),
    -        options: this.getTeamsForAccess()[0].map(request => ({
    +        options: this.getTeamsForAccess()[0]!.map(request => ({
               value: request,
               label: `#${request}`,
             })),
           },
           {
             label: t('Pending Requests'),
    -        options: this.getTeamsForAccess()[1].map(pending =>
    +        options: this.getTeamsForAccess()[1]!.map(pending =>
               this.getPendingTeamOption(pending)
             ),
           },
    diff --git a/static/app/components/quickTrace/index.spec.tsx b/static/app/components/quickTrace/index.spec.tsx
    index 254aa2acefc80b..353e3a2e8788e8 100644
    --- a/static/app/components/quickTrace/index.spec.tsx
    +++ b/static/app/components/quickTrace/index.spec.tsx
    @@ -411,7 +411,7 @@ describe('Quick Trace', function () {
             makeTransactionHref('p4', 'e4', 't4'),
             makeTransactionHref('p5', 'e5', 't5'),
           ].forEach((target, i) => {
    -        const linkNode = nodes[i].children[0];
    +        const linkNode = nodes[i]!.children[0];
             if (target) {
               expect(linkNode).toHaveAttribute('href', target);
             } else {
    diff --git a/static/app/components/quickTrace/index.tsx b/static/app/components/quickTrace/index.tsx
    index 78a914d8796ebd..03cd05ef3fd4ff 100644
    --- a/static/app/components/quickTrace/index.tsx
    +++ b/static/app/components/quickTrace/index.tsx
    @@ -394,20 +394,20 @@ function EventNodeSelector({
         const hoverText = totalErrors ? (
           t('View the error for this Transaction')
         ) : (
    -      
    +      
         );
         const target = errors.length
    -      ? generateSingleErrorTarget(errors[0], organization, location, errorDest)
    +      ? generateSingleErrorTarget(errors[0]!, organization, location, errorDest)
           : perfIssues.length
    -        ? generateSingleErrorTarget(perfIssues[0], organization, location, errorDest)
    +        ? generateSingleErrorTarget(perfIssues[0]!, organization, location, errorDest)
             : generateLinkToEventInTraceView({
                 traceSlug,
    -            eventId: events[0].event_id,
    -            projectSlug: events[0].project_slug,
    -            timestamp: events[0].timestamp,
    +            eventId: events[0]!.event_id,
    +            projectSlug: events[0]!.project_slug,
    +            timestamp: events[0]!.timestamp,
                 location,
                 organization,
    -            transactionName: events[0].transaction,
    +            transactionName: events[0]!.transaction,
                 type: transactionDest,
               });
         return (
    diff --git a/static/app/components/replays/breadcrumbs/breadcrumbItem.spec.tsx b/static/app/components/replays/breadcrumbs/breadcrumbItem.spec.tsx
    index d2ab3d84f476a6..57ca7b981ebae2 100644
    --- a/static/app/components/replays/breadcrumbs/breadcrumbItem.spec.tsx
    +++ b/static/app/components/replays/breadcrumbs/breadcrumbItem.spec.tsx
    @@ -22,12 +22,12 @@ describe('BreadcrumbItem', function () {
         const mockMouseLeave = jest.fn();
         render(
            {}}
    -        startTimestampMs={MOCK_FRAME.timestampMs}
    +        startTimestampMs={MOCK_FRAME!.timestampMs}
           />,
           {organization}
         );
    diff --git a/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx b/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx
    index 0a701c42a5e0f1..eed579105eafe8 100644
    --- a/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx
    +++ b/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx
    @@ -163,9 +163,9 @@ const getBackgroundGradient = ({
       frameCount: number;
       theme: Theme;
     }) => {
    -  const c0 = theme[colors[0]] ?? colors[0];
    -  const c1 = theme[colors[1]] ?? colors[1] ?? c0;
    -  const c2 = theme[colors[2]] ?? colors[2] ?? c1;
    +  const c0 = theme[colors[0]!] ?? colors[0]!;
    +  const c1 = theme[colors[1]!] ?? colors[1]! ?? c0;
    +  const c2 = theme[colors[2]!] ?? colors[2]! ?? c1;
     
       if (frameCount === 1) {
         return `background: ${c0};`;
    diff --git a/static/app/components/replays/canvasReplayerPlugin.tsx b/static/app/components/replays/canvasReplayerPlugin.tsx
    index 55fec93e00cdaa..a026dcdf0b1367 100644
    --- a/static/app/components/replays/canvasReplayerPlugin.tsx
    +++ b/static/app/components/replays/canvasReplayerPlugin.tsx
    @@ -51,7 +51,7 @@ function findIndex(
       const mid = Math.floor((start + end) / 2);
     
       // Search lower half
    -  if (event.timestamp <= arr[mid].timestamp) {
    +  if (event.timestamp <= arr[mid]!.timestamp) {
         return findIndex(arr, event, start, mid - 1);
       }
     
    @@ -106,7 +106,7 @@ export function CanvasReplayerPlugin(events: eventWithTime[]): ReplayPlugin {
         while (eventsToPrune.length) {
           // Peek top of queue and see if event should be pruned, otherwise we can break out of the loop
           if (
    -        Math.abs(event.timestamp - eventsToPrune[0].timestamp) <= BUFFER_TIME &&
    +        Math.abs(event.timestamp - eventsToPrune[0]!.timestamp) <= BUFFER_TIME &&
             eventsToPrune.length <= PRELOAD_SIZE
           ) {
             break;
    diff --git a/static/app/components/replays/header/errorCounts.spec.tsx b/static/app/components/replays/header/errorCounts.spec.tsx
    index 659e2e89912e92..00a97e2765eb0d 100644
    --- a/static/app/components/replays/header/errorCounts.spec.tsx
    +++ b/static/app/components/replays/header/errorCounts.spec.tsx
    @@ -94,11 +94,11 @@ describe('ErrorCounts', () => {
         const pyIcon = await screen.findByTestId('platform-icon-python');
         expect(pyIcon).toBeInTheDocument();
     
    -    expect(countNodes[0].parentElement).toHaveAttribute(
    +    expect(countNodes[0]!.parentElement).toHaveAttribute(
           'href',
           '/mock-pathname/?f_e_project=my-js-app&t_main=errors'
         );
    -    expect(countNodes[1].parentElement).toHaveAttribute(
    +    expect(countNodes[1]!.parentElement).toHaveAttribute(
           'href',
           '/mock-pathname/?f_e_project=my-py-backend&t_main=errors'
         );
    diff --git a/static/app/components/replays/utils.spec.tsx b/static/app/components/replays/utils.spec.tsx
    index 7e9928dfb91948..e402b281c5f883 100644
    --- a/static/app/components/replays/utils.spec.tsx
    +++ b/static/app/components/replays/utils.spec.tsx
    @@ -110,11 +110,11 @@ describe('getFramesByColumn', () => {
     
       it('should put a crumbs in the first and last buckets', () => {
         const columnCount = 3;
    -    const columns = getFramesByColumn(durationMs, [CRUMB_1, CRUMB_5], columnCount);
    +    const columns = getFramesByColumn(durationMs, [CRUMB_1!, CRUMB_5!], columnCount);
         expect(columns).toEqual(
           new Map([
    -        [1, [CRUMB_1]],
    -        [3, [CRUMB_5]],
    +        [1, [CRUMB_1!]],
    +        [3, [CRUMB_5!]],
           ])
         );
       });
    @@ -124,7 +124,7 @@ describe('getFramesByColumn', () => {
         const columnCount = 6;
         const columns = getFramesByColumn(
           durationMs,
    -      [CRUMB_1, CRUMB_2, CRUMB_3, CRUMB_4, CRUMB_5],
    +      [CRUMB_1!, CRUMB_2!, CRUMB_3!, CRUMB_4!, CRUMB_5!],
           columnCount
         );
         expect(columns).toEqual(
    diff --git a/static/app/components/replays/utils.tsx b/static/app/components/replays/utils.tsx
    index a750ed5b279590..5c1ee7851f79d9 100644
    --- a/static/app/components/replays/utils.tsx
    +++ b/static/app/components/replays/utils.tsx
    @@ -134,17 +134,17 @@ export function flattenFrames(frames: SpanFrame[]): FlattenedSpanRange[] {
         };
       });
     
    -  const flattened = [first];
    +  const flattened = [first!];
     
       for (const span of rest) {
         let overlap = false;
         for (const range of flattened) {
    -      if (doesOverlap(range, span)) {
    +      if (doesOverlap(range!, span)) {
             overlap = true;
    -        range.frameCount += 1;
    -        range.startTimestamp = Math.min(range.startTimestamp, span.startTimestamp);
    -        range.endTimestamp = Math.max(range.endTimestamp, span.endTimestamp);
    -        range.duration = range.endTimestamp - range.startTimestamp;
    +        range!.frameCount += 1;
    +        range!.startTimestamp = Math.min(range!.startTimestamp, span.startTimestamp);
    +        range!.endTimestamp = Math.max(range!.endTimestamp, span.endTimestamp);
    +        range!.duration = range!.endTimestamp - range!.startTimestamp;
             break;
           }
         }
    @@ -178,11 +178,11 @@ export function findVideoSegmentIndex(
     
       const mid = Math.floor((start + end) / 2);
     
    -  const [ts, index] = trackList[mid];
    +  const [ts, index] = trackList[mid]!;
       const segment = segments[index];
     
       // Segment match found
    -  if (targetTimestamp >= ts && targetTimestamp <= ts + segment.duration) {
    +  if (targetTimestamp >= ts && targetTimestamp <= ts + segment!.duration) {
         return index;
       }
     
    diff --git a/static/app/components/replays/videoReplayer.tsx b/static/app/components/replays/videoReplayer.tsx
    index 1583fc9172238d..b16ceb6444ef40 100644
    --- a/static/app/components/replays/videoReplayer.tsx
    +++ b/static/app/components/replays/videoReplayer.tsx
    @@ -141,7 +141,7 @@ export class VideoReplayer {
         const handleLoadedData = event => {
           // Used to correctly set the dimensions of the first frame
           if (index === 0) {
    -        this._callbacks.onLoaded(event);
    +        this._callbacks.onLoaded!(event);
           }
     
           // Only call this for current segment as we preload multiple
    @@ -159,7 +159,7 @@ export class VideoReplayer {
     
         const handlePlay = event => {
           if (index === this._currentIndex) {
    -        this._callbacks.onLoaded(event);
    +        this._callbacks.onLoaded!(event);
           }
         };
     
    @@ -168,13 +168,13 @@ export class VideoReplayer {
           if (index === this._currentIndex) {
             // Theoretically we could have different orientations and they should
             // only happen in different segments
    -        this._callbacks.onLoaded(event);
    +        this._callbacks.onLoaded!(event);
           }
         };
     
         const handleSeeking = event => {
           // Centers the video when seeking (and video is not playing)
    -      this._callbacks.onLoaded(event);
    +      this._callbacks.onLoaded!(event);
         };
     
         el.addEventListener('ended', handleEnded);
    @@ -284,12 +284,12 @@ export class VideoReplayer {
           this.resumeTimer();
         }
     
    -    this._callbacks.onBuffer(isBuffering);
    +    this._callbacks.onBuffer!(isBuffering);
       }
     
       private stopReplay() {
         this._timer.stop();
    -    this._callbacks.onFinished();
    +    this._callbacks.onFinished!();
         this._isPlaying = false;
       }
     
    diff --git a/static/app/components/replaysOnboarding/platformOptionDropdown.tsx b/static/app/components/replaysOnboarding/platformOptionDropdown.tsx
    index 780a9fc75f8924..e735456aa1acdb 100644
    --- a/static/app/components/replaysOnboarding/platformOptionDropdown.tsx
    +++ b/static/app/components/replaysOnboarding/platformOptionDropdown.tsx
    @@ -41,7 +41,7 @@ function OptionControl({option, value, onChange, disabled}: OptionControlProps)
       return (
          v.value === value)?.label ?? option.items[0].label
    +        option.items.find(v => v.value === value)?.label ?? option.items[0]!.label
           }
           value={value}
           onChange={onChange}
    @@ -81,7 +81,7 @@ export function PlatformOptionDropdown({
            handleChange('siblingOption', v.value)}
             disabled={disabled}
           />
    diff --git a/static/app/components/replaysOnboarding/sidebar.tsx b/static/app/components/replaysOnboarding/sidebar.tsx
    index f2c6054f997d19..e40f0a2e5b30af 100644
    --- a/static/app/components/replaysOnboarding/sidebar.tsx
    +++ b/static/app/components/replaysOnboarding/sidebar.tsx
    @@ -181,7 +181,7 @@ function OnboardingContent({
         value: PlatformKey;
         label?: ReactNode;
         textValue?: string;
    -  }>(jsFrameworkSelectOptions[0]);
    +  }>(jsFrameworkSelectOptions[0]!);
     
       const backendPlatform =
         currentProject.platform && replayBackendPlatforms.includes(currentProject.platform);
    @@ -215,7 +215,7 @@ function OnboardingContent({
         platform:
           showJsFrameworkInstructions && setupMode() === 'npm'
             ? replayJsFrameworkOptions().find(p => p.id === jsFramework.value) ??
    -          replayJsFrameworkOptions()[0]
    +          replayJsFrameworkOptions()[0]!
             : currentPlatform,
         projSlug: currentProject.slug,
         orgSlug: organization.slug,
    @@ -226,7 +226,7 @@ function OnboardingContent({
       const {docs: jsFrameworkDocs} = useLoadGettingStarted({
         platform:
           replayJsFrameworkOptions().find(p => p.id === jsFramework.value) ??
    -      replayJsFrameworkOptions()[0],
    +      replayJsFrameworkOptions()[0]!,
         projSlug: currentProject.slug,
         orgSlug: organization.slug,
         productType: 'replay',
    diff --git a/static/app/components/replaysOnboarding/utils.tsx b/static/app/components/replaysOnboarding/utils.tsx
    index 96273c99b035ec..1f057421d3b950 100644
    --- a/static/app/components/replaysOnboarding/utils.tsx
    +++ b/static/app/components/replaysOnboarding/utils.tsx
    @@ -7,7 +7,7 @@ export function replayJsFrameworkOptions(): PlatformIntegration[] {
       // at the front so that it shows up by default in the onboarding.
       const frameworks = platforms.filter(p => replayFrontendPlatforms.includes(p.id));
       const jsPlatformIdx = frameworks.findIndex(p => p.id === 'javascript');
    -  const jsPlatform = frameworks[jsPlatformIdx];
    +  const jsPlatform = frameworks[jsPlatformIdx]!;
     
       // move javascript to the front
       frameworks.splice(jsPlatformIdx, 1);
    diff --git a/static/app/components/resultGrid.tsx b/static/app/components/resultGrid.tsx
    index e14af9c402250d..d4376e5e5ac85c 100644
    --- a/static/app/components/resultGrid.tsx
    +++ b/static/app/components/resultGrid.tsx
    @@ -204,7 +204,7 @@ class ResultGrid extends Component {
         this.setState(
           {
             query: queryParams.query ?? '',
    -        sortBy: queryParams.sortBy ?? this.props.defaultSort,
    +        sortBy: queryParams.sortBy ?? this.props.defaultSort!,
             filters: {...queryParams},
             pageLinks: null,
             loading: true,
    @@ -358,7 +358,7 @@ class ResultGrid extends Component {
                  {
     
         const [searchResults, directResults] = await Promise.all([
           this.getSearchableResults([
    -        organizations,
    -        projects,
    -        teams,
    -        members,
    -        plugins,
    -        integrations,
    -        sentryApps,
    -        docIntegrations,
    +        organizations!,
    +        projects!,
    +        teams!,
    +        members!,
    +        plugins!,
    +        integrations!,
    +        sentryApps!,
    +        docIntegrations!,
           ]),
    -      this.getDirectResults([shortIdLookup, eventIdLookup]),
    +      this.getDirectResults([shortIdLookup!, eventIdLookup!]),
         ]);
     
         // TODO(XXX): Might consider adding logic to maintain consistent ordering
    @@ -468,14 +468,14 @@ class ApiSource extends Component {
           docIntegrations,
         ] = requests;
         const searchResults = await Promise.all([
    -      createOrganizationResults(organizations),
    -      createProjectResults(projects, orgId),
    -      createTeamResults(teams, orgId),
    -      createMemberResults(members, orgId),
    -      createIntegrationResults(integrations, orgId),
    -      createPluginResults(plugins, orgId),
    -      createSentryAppResults(sentryApps, orgId),
    -      createDocIntegrationResults(docIntegrations, orgId),
    +      createOrganizationResults(organizations!),
    +      createProjectResults(projects!, orgId),
    +      createTeamResults(teams!, orgId),
    +      createMemberResults(members!, orgId),
    +      createIntegrationResults(integrations!, orgId),
    +      createPluginResults(plugins!, orgId),
    +      createSentryAppResults(sentryApps!, orgId),
    +      createDocIntegrationResults(docIntegrations!, orgId),
         ]);
     
         return searchResults.flat();
    @@ -488,8 +488,8 @@ class ApiSource extends Component {
     
         const directResults = (
           await Promise.all([
    -        createShortIdLookupResult(shortIdLookup),
    -        createEventIdLookupResult(eventIdLookup),
    +        createShortIdLookupResult(shortIdLookup!),
    +        createEventIdLookupResult(eventIdLookup!),
           ])
         ).filter(defined);
     
    diff --git a/static/app/components/search/sources/helpSource.tsx b/static/app/components/search/sources/helpSource.tsx
    index 95f6623df24f16..4f72b1a7c234dc 100644
    --- a/static/app/components/search/sources/helpSource.tsx
    +++ b/static/app/components/search/sources/helpSource.tsx
    @@ -121,8 +121,8 @@ function mapSearchResults(results: SearchResult[]) {
     
         // The first element should indicate the section.
         if (sectionItems.length > 0) {
    -      sectionItems[0].item.sectionHeading = section.name;
    -      sectionItems[0].item.sectionCount = sectionItems.length;
    +      sectionItems[0]!.item.sectionHeading = section.name;
    +      sectionItems[0]!.item.sectionCount = sectionItems.length;
     
           items.push(...sectionItems);
           return;
    diff --git a/static/app/components/search/sources/index.tsx b/static/app/components/search/sources/index.tsx
    index 24b98acbb0d5f5..523c99cd7f9190 100644
    --- a/static/app/components/search/sources/index.tsx
    +++ b/static/app/components/search/sources/index.tsx
    @@ -53,7 +53,7 @@ function SearchSources(props: Props) {
           if (idx >= sources.length) {
             return renderResults(results);
           }
    -      const Source = sources[idx];
    +      const Source = sources[idx]!;
           return (
             
               {(args: SourceResult) => {
    diff --git a/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx b/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx
    index 3296faf061fe89..8f9157cb041935 100644
    --- a/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx
    +++ b/static/app/components/searchQueryBuilder/hooks/useQueryBuilderState.tsx
    @@ -131,7 +131,7 @@ function removeQueryTokensFromQuery(
       }
     
       return removeExcessWhitespaceFromParts(
    -    query.substring(0, tokens[0].location.start.offset),
    +    query.substring(0, tokens[0]!.location.start.offset),
         query.substring(tokens.at(-1)!.location.end.offset)
       );
     }
    @@ -243,7 +243,7 @@ function replaceQueryTokens(
         return query;
       }
     
    -  const start = query.substring(0, tokens[0].location.start.offset);
    +  const start = query.substring(0, tokens[0]!.location.start.offset);
       const end = query.substring(tokens.at(-1)!.location.end.offset);
     
       return start + value + end;
    @@ -296,7 +296,7 @@ export function replaceTokensWithPadding(
         return query;
       }
     
    -  const start = query.substring(0, tokens[0].location.start.offset);
    +  const start = query.substring(0, tokens[0]!.location.start.offset);
       const end = query.substring(tokens.at(-1)!.location.end.offset);
     
       return removeExcessWhitespaceFromParts(start, value, end);
    @@ -367,7 +367,7 @@ function updateFilterMultipleValues(
       const newValue =
         uniqNonEmptyValues.length > 1
           ? `[${uniqNonEmptyValues.join(',')}]`
    -      : uniqNonEmptyValues[0];
    +      : uniqNonEmptyValues[0]!;
     
       return {...state, query: replaceQueryToken(state.query, token.value, newValue)};
     }
    diff --git a/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx b/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx
    index 83c37de6c73d45..1cc93d905ef84d 100644
    --- a/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx
    +++ b/static/app/components/searchQueryBuilder/hooks/useSelectOnDrag.tsx
    @@ -87,8 +87,8 @@ function getItemIndexAtPosition(
       y: number
     ) {
       for (let i = 0; i < keys.length; i++) {
    -    const key = keys[i];
    -    const coords = coordinates[key];
    +    const key = keys[i]!;
    +    const coords = coordinates[key]!;
     
         // If we are above this item, we must be in between this and the
         // previous item on the row above it.
    diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx
    index ada4b162131fcf..a36e631d1b5110 100644
    --- a/static/app/components/searchQueryBuilder/index.spec.tsx
    +++ b/static/app/components/searchQueryBuilder/index.spec.tsx
    @@ -321,7 +321,7 @@ describe('SearchQueryBuilder', function () {
           expect(groups).toHaveLength(3);
     
           // First group (Field) should have age, assigned, browser.name
    -      const group1 = groups[0];
    +      const group1 = groups[0]!;
           expect(within(group1).getByRole('option', {name: 'age'})).toBeInTheDocument();
           expect(within(group1).getByRole('option', {name: 'assigned'})).toBeInTheDocument();
           expect(
    @@ -329,13 +329,13 @@ describe('SearchQueryBuilder', function () {
           ).toBeInTheDocument();
     
           // Second group (Tag) should have custom_tag_name
    -      const group2 = groups[1];
    +      const group2 = groups[1]!;
           expect(
             within(group2).getByRole('option', {name: 'custom_tag_name'})
           ).toBeInTheDocument();
     
           // There should be a third group for uncategorized keys
    -      const group3 = groups[2];
    +      const group3 = groups[2]!;
           expect(
             within(group3).getByRole('option', {name: 'uncategorized_tag'})
           ).toBeInTheDocument();
    @@ -398,7 +398,7 @@ describe('SearchQueryBuilder', function () {
             expect(recentFilterKeys[1]).toHaveTextContent('browser');
             expect(recentFilterKeys[2]).toHaveTextContent('is');
     
    -        await userEvent.click(recentFilterKeys[0]);
    +        await userEvent.click(recentFilterKeys[0]!);
     
             expect(await screen.findByRole('row', {name: 'assigned:""'})).toBeInTheDocument();
           });
    @@ -460,7 +460,7 @@ describe('SearchQueryBuilder', function () {
             await waitFor(() => {
               expect(getLastInput()).toHaveAttribute(
                 'aria-activedescendant',
    -            recentFilterKeys[0].id
    +            recentFilterKeys[0]!.id
               );
             });
     
    @@ -469,7 +469,7 @@ describe('SearchQueryBuilder', function () {
             await waitFor(() => {
               expect(getLastInput()).toHaveAttribute(
                 'aria-activedescendant',
    -            recentFilterKeys[1].id
    +            recentFilterKeys[1]!.id
               );
             });
     
    @@ -487,7 +487,7 @@ describe('SearchQueryBuilder', function () {
             await waitFor(() => {
               expect(getLastInput()).toHaveAttribute(
                 'aria-activedescendant',
    -            recentFilterKeys[0].id
    +            recentFilterKeys[0]!.id
               );
             });
           });
    @@ -695,7 +695,7 @@ describe('SearchQueryBuilder', function () {
           // jsdom does not support getBoundingClientRect, so we need to mock it for each item
     
           // First freeText area is 5px wide
    -      freeText1.getBoundingClientRect = () => {
    +      freeText1!.getBoundingClientRect = () => {
             return {
               top: 0,
               left: 10,
    @@ -706,7 +706,7 @@ describe('SearchQueryBuilder', function () {
             } as DOMRect;
           };
           // "is:unresolved" filter is 100px wide
    -      filter.getBoundingClientRect = () => {
    +      filter!.getBoundingClientRect = () => {
             return {
               top: 0,
               left: 15,
    @@ -717,7 +717,7 @@ describe('SearchQueryBuilder', function () {
             } as DOMRect;
           };
           // Last freeText area is 200px wide
    -      freeText2.getBoundingClientRect = () => {
    +      freeText2!.getBoundingClientRect = () => {
             return {
               top: 0,
               left: 115,
    @@ -990,7 +990,7 @@ describe('SearchQueryBuilder', function () {
     
           // Put focus into the first input (before the token)
           await userEvent.click(
    -        screen.getAllByRole('combobox', {name: 'Add a search term'})[0]
    +        screen.getAllByRole('combobox', {name: 'Add a search term'})[0]!
           );
     
           // Pressing delete once should focus the previous token
    @@ -1461,17 +1461,17 @@ describe('SearchQueryBuilder', function () {
           expect(within(screen.getByRole('listbox')).getByText('All')).toBeInTheDocument();
     
           // First group is the selected "me"
    -      expect(within(groups[0]).getByRole('option', {name: 'me'})).toBeInTheDocument();
    +      expect(within(groups[0]!).getByRole('option', {name: 'me'})).toBeInTheDocument();
           // Second group is the remaining option in the "Suggested" section
           expect(
    -        within(groups[1]).getByRole('option', {name: 'unassigned'})
    +        within(groups[1]!).getByRole('option', {name: 'unassigned'})
           ).toBeInTheDocument();
           // Third group are the options under the "All" section
           expect(
    -        within(groups[2]).getByRole('option', {name: 'person1@sentry.io'})
    +        within(groups[2]!).getByRole('option', {name: 'person1@sentry.io'})
           ).toBeInTheDocument();
           expect(
    -        within(groups[2]).getByRole('option', {name: 'person2@sentry.io'})
    +        within(groups[2]!).getByRole('option', {name: 'person2@sentry.io'})
           ).toBeInTheDocument();
         });
     
    diff --git a/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx b/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx
    index af001328692cca..15665bb382cbdd 100644
    --- a/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx
    +++ b/static/app/components/searchQueryBuilder/tokens/filter/filter.tsx
    @@ -51,10 +51,10 @@ export function FilterValueText({token}: {token: TokenResult}) {
         case Token.VALUE_NUMBER_LIST:
           const items = token.value.items;
     
    -      if (items.length === 1 && items[0].value) {
    +      if (items.length === 1 && items[0]!.value) {
             return (
               
    -            {formatFilterValue(items[0].value)}
    +            {formatFilterValue(items[0]!.value)}
               
             );
           }
    diff --git a/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx
    index 642ded7066b69c..bcbdd2935b45a1 100644
    --- a/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx
    +++ b/static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx
    @@ -44,9 +44,9 @@ function getParameterAtCursorPosition(
     
       let characterCount = 0;
       for (let i = 0; i < items.length; i++) {
    -    characterCount += items[i].length + 1;
    +    characterCount += items[i]!.length + 1;
         if (characterCount > cursorPosition) {
    -      return {parameterIndex: i, textValue: items[i].trim()};
    +      return {parameterIndex: i, textValue: items[i]!.trim()};
         }
       }
     
    @@ -58,7 +58,7 @@ function getCursorPositionAtEndOfParameter(text: string, parameterIndex: number)
       const charactersBefore =
         items.slice(0, parameterIndex).join('').length + parameterIndex;
     
    -  return charactersBefore + items[parameterIndex].length;
    +  return charactersBefore + items[parameterIndex]!.length;
     }
     
     function useSelectionIndex({
    diff --git a/static/app/components/searchQueryBuilder/tokens/filter/parsers/string/parser.spec.tsx b/static/app/components/searchQueryBuilder/tokens/filter/parsers/string/parser.spec.tsx
    index aa1333237fb8d8..4e3a6a1f7c40a5 100644
    --- a/static/app/components/searchQueryBuilder/tokens/filter/parsers/string/parser.spec.tsx
    +++ b/static/app/components/searchQueryBuilder/tokens/filter/parsers/string/parser.spec.tsx
    @@ -7,7 +7,7 @@ describe('parseMultiSelectValue', function () {
         expect(result).not.toBeNull();
     
         expect(result!.items).toHaveLength(1);
    -    expect(result?.items[0].value?.value).toEqual('a');
    +    expect(result!.items[0]!.value?.value).toEqual('a');
       });
     
       it('multiple value', function () {
    @@ -16,9 +16,9 @@ describe('parseMultiSelectValue', function () {
         expect(result).not.toBeNull();
     
         expect(result!.items).toHaveLength(3);
    -    expect(result?.items[0].value?.value).toEqual('a');
    -    expect(result?.items[1].value?.value).toEqual('b');
    -    expect(result?.items[2].value?.value).toEqual('c');
    +    expect(result!?.items[0]!.value?.value).toEqual('a');
    +    expect(result!?.items[1]!.value?.value).toEqual('b');
    +    expect(result!?.items[2]!.value?.value).toEqual('c');
       });
     
       it('quoted value', function () {
    @@ -27,13 +27,13 @@ describe('parseMultiSelectValue', function () {
         expect(result).not.toBeNull();
     
         expect(result!.items).toHaveLength(3);
    -    expect(result?.items[0].value?.value).toEqual('a');
    +    expect(result!?.items[0]!.value?.value).toEqual('a');
     
    -    expect(result?.items[1].value?.value).toEqual('b');
    -    expect(result?.items[1].value?.text).toEqual('"b"');
    -    expect(result?.items[1].value?.quoted).toBe(true);
    +    expect(result!?.items[1]!.value?.value).toEqual('b');
    +    expect(result!?.items[1]!.value?.text).toEqual('"b"');
    +    expect(result!?.items[1]!.value?.quoted).toBe(true);
     
    -    expect(result?.items[2].value?.value).toEqual('c');
    +    expect(result!.items[2]!.value?.value).toEqual('c');
       });
     
       it('just quotes', function () {
    @@ -44,9 +44,9 @@ describe('parseMultiSelectValue', function () {
         expect(result!.items).toHaveLength(1);
         const item = result!.items[0];
     
    -    expect(item.value?.value).toEqual('');
    -    expect(item.value?.text).toEqual('""');
    -    expect(item.value?.quoted).toBe(true);
    +    expect(item!.value!?.value).toEqual('');
    +    expect(item!.value!?.text).toEqual('""');
    +    expect(item!.value!?.quoted).toBe(true);
       });
     
       it('single empty value', function () {
    @@ -57,7 +57,7 @@ describe('parseMultiSelectValue', function () {
         expect(result!.items).toHaveLength(1);
         const item = result!.items[0];
     
    -    expect(item.value!.value).toBe('');
    +    expect(item!.value!.value).toBe('');
       });
     
       it('multiple empty value', function () {
    @@ -67,9 +67,9 @@ describe('parseMultiSelectValue', function () {
     
         expect(result!.items).toHaveLength(3);
     
    -    expect(result?.items[0].value?.value).toEqual('a');
    -    expect(result?.items[1].value?.value).toBe('');
    -    expect(result?.items[2].value?.value).toEqual('b');
    +    expect(result!?.items[0]!.value!?.value).toEqual('a');
    +    expect(result!?.items[1]!.value!?.value).toBe('');
    +    expect(result!?.items[2]!.value!?.value).toEqual('b');
       });
     
       it('trailing comma', function () {
    @@ -79,8 +79,8 @@ describe('parseMultiSelectValue', function () {
     
         expect(result!.items).toHaveLength(2);
     
    -    expect(result?.items[0].value?.value).toEqual('a');
    -    expect(result?.items[1].value?.value).toBe('');
    +    expect(result!?.items[0]!.value!?.value).toEqual('a');
    +    expect(result!?.items[1]!.value!?.value).toBe('');
       });
     
       it('spaces', function () {
    @@ -90,8 +90,8 @@ describe('parseMultiSelectValue', function () {
     
         expect(result!.items).toHaveLength(3);
     
    -    expect(result?.items[0].value?.value).toEqual('a');
    -    expect(result?.items[1].value?.value).toEqual('b c');
    -    expect(result?.items[2].value?.value).toEqual('d');
    +    expect(result!?.items[0]!.value!?.value).toEqual('a');
    +    expect(result!?.items[1]!.value!?.value).toEqual('b c');
    +    expect(result!?.items[2]!.value!?.value).toEqual('d');
       });
     });
    diff --git a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx
    index 6da3c0416ffff8..7cd8c2c9b53ed4 100644
    --- a/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx
    +++ b/static/app/components/searchQueryBuilder/tokens/filter/valueListBox.tsx
    @@ -82,7 +82,7 @@ export function ValueListBox>({
                   overlayIsOpen={isOpen}
                   showSectionHeaders={!filterValue}
                   size="sm"
    -              style={{maxWidth: overlayProps.style.maxWidth}}
    +              style={{maxWidth: overlayProps.style!.maxWidth}}
                 />