diff --git a/src/sentry/preprod/size_analysis/issues.py b/src/sentry/preprod/size_analysis/issues.py new file mode 100644 index 00000000000000..dc2c263d4062a3 --- /dev/null +++ b/src/sentry/preprod/size_analysis/issues.py @@ -0,0 +1,113 @@ +from collections.abc import Iterator +from datetime import datetime, timezone +from typing import Any +from uuid import uuid4 + +from sentry.issues.grouptype import PreprodDeltaGroupType +from sentry.issues.issue_occurrence import IssueOccurrence + + +class SizeRegressionOccurrenceBuilder: + def __init__(self): + self.issue_title = None + self.issue_subtitle = None + self.project_id = None + + def build(self) -> tuple[IssueOccurrence, dict[str, Any]]: + id = uuid4() + assert self.issue_title is not None, "issue_title must be set" + assert self.issue_title is not None, "issue_subtitle must be set" + assert self.project_id is not None, "project_id must be set" + + current_timestamp = datetime.now(timezone.utc) + id = uuid4().hex + event_id = uuid4().hex + + event_data = { + "event_id": event_id, + "platform": "other", + "project_id": self.project_id, + "received": current_timestamp.isoformat(), + "sdk": None, + "tags": {}, + "timestamp": current_timestamp.isoformat(), + "environment": "prod", + } + + return ( + IssueOccurrence( + id=id, + event_id=event_id, + issue_title=self.issue_title, + subtitle=self.issue_subtitle, + project_id=self.project_id, + fingerprint=uuid4().hex, + type=PreprodDeltaGroupType, + detection_time=current_timestamp, + level="error", + resource_id="", + evidence_data={ + "head_install_size_bytes": self.head_install_size_bytes, + "base_install_size_bytes": self.base_install_size_bytes, + "head_artifact_id": self.head_artifact_id, + "base_artifact_id": self.base_artifact_id, + }, + evidence_display={}, + culprit="", + ), + event_data, + ) + + +# from sentry.issues.grouptype import PreprodStaticGroupType +# class PreprodStaticIssueOccurrenceBuilder: +# def __init__(self): +# self.issue_title = None +# self.project_id = None +# +# def build(self) -> tuple[IssueOccurrence, dict[str, Any]]: +# id = uuid4() +# assert self.issue_title is not None, "issue_title must be set" +# assert self.project_id is not None, "project_id must be set" +# +# current_timestamp = datetime.now(timezone.utc) +# id = uuid4().hex +# event_id = uuid4().hex +# +# event_data = { +# "event_id": event_id, +# "platform": "other", +# "project_id": self.project_id, +# "received": current_timestamp.isoformat(), +# "sdk": None, +# "tags": {}, +# "timestamp": current_timestamp.isoformat(), +# # "contexts": {"monitor": get_monitor_environment_context(monitor_env)}, +# "environment": "prod", +# # "fingerprint": [incident.grouphash], +# } +# +# return ( +# IssueOccurrence( +# id=id, +# event_id=event_id, +# issue_title=self.issue_title, +# subtitle="", +# project_id=self.project_id, +# # TODO: fix +# fingerprint=uuid4().hex, +# type=PreprodStaticIssueOccurrenceBuilder, +# # Now? +# detection_time=current_timestamp, +# level="error", +# resource_id="", +# evidence_data={}, +# evidence_display={}, +# culprit="", +# ), +# event_data, +# ) +# def insight_to_occurrences(name: str, insight: dict[str, any]) -> list[SizeIssueOccurrenceBuilder]: +# builder = SizeIssueOccurrenceBuilder() +# builder.issue_title = f"Bad size {name}" +# return [builder] diff --git a/src/sentry/preprod/size_analysis/tasks.py b/src/sentry/preprod/size_analysis/tasks.py index 2f597d86783364..83a2de7ebaeaf7 100644 --- a/src/sentry/preprod/size_analysis/tasks.py +++ b/src/sentry/preprod/size_analysis/tasks.py @@ -6,6 +6,7 @@ from django.db import router, transaction from django.utils import timezone +from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka from sentry.models.files.file import File from sentry.preprod.models import ( PreprodArtifact, @@ -22,6 +23,8 @@ from sentry.utils import metrics from sentry.utils.json import dumps_htmlsafe +from .issues import SizeRegressionOccurrenceBuilder + logger = logging.getLogger(__name__) @@ -477,6 +480,39 @@ def _run_size_analysis_comparison( comparison.state = PreprodArtifactSizeComparison.State.SUCCESS comparison.save() + ################################################################ + + head_project = head_size_metric.preprod_artifact.project + threshold = head_project.get_option("sentry:preprod_size_issues_delta_install_threshhold_kb") + diff_item = comparison_results.size_metric_diff_item + actual = diff_item.head_install_size - diff_item.base_install_size + + logger.warning( + "!!!!!!!!!!!!!!!!? issue threshold: %d actual: %d base: %d head: %d", + threshold, + actual, + diff_item.base_install_size, + diff_item.head_install_size, + ) + if actual >= threshold * 1024: + logger.warning("!!!! trigger issue") + builder = SizeRegressionOccurrenceBuilder() + builder.project_id = head_project.id + builder.issue_title = f"Install size regression" + builder.issue_subtitle = f"Size increased {actual // 1024}kb" + builder.head_install_size_bytes = diff_item.head_install_size + builder.base_install_size_bytes = diff_item.base_install_size + builder.base_artifact_id = base_size_metric.preprod_artifact.id + builder.head_artifact_id = head_size_metric.preprod_artifact.id + occurrence, event_data = builder.build() + produce_occurrence_to_kafka( + payload_type=PayloadType.OCCURRENCE, + occurrence=occurrence, + event_data=event_data, + ) + logger.warning("!!!! done") + ################################################################ + logger.info( "preprod.size_analysis.compare.success", extra={"comparison_id": comparison.id}, diff --git a/src/sentry/projectoptions/defaults.py b/src/sentry/projectoptions/defaults.py index 7eef7184ff99a8..26e87625d8f47d 100644 --- a/src/sentry/projectoptions/defaults.py +++ b/src/sentry/projectoptions/defaults.py @@ -182,3 +182,6 @@ # Should seer scanner run automatically on new issues register(key="sentry:seer_scanner_automation", default=True) + +register(key="sentry:preprod_size_issues_delta_install_threshhold_kb", default=500) +register(key="sentry:preprod_size_issues_is_16kb_ready", default=True) diff --git a/static/app/router/routes.tsx b/static/app/router/routes.tsx index b537ddc81bf214..e6861b9c7c6881 100644 --- a/static/app/router/routes.tsx +++ b/static/app/router/routes.tsx @@ -692,6 +692,11 @@ function buildRoutes(): RouteObject[] { name: t('PlayStation'), component: make(() => import('sentry/views/settings/project/tempest')), }, + { + path: 'preprod/', + name: t('Preprod'), + component: make(() => import('sentry/views/settings/project/preprod')), + }, { path: 'keys/', name: t('Client Keys'), diff --git a/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx b/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx index 423967ab8318e7..b190458c59c6ab 100644 --- a/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx +++ b/static/app/views/preprod/buildDetails/header/buildDetailsHeaderContent.tsx @@ -6,8 +6,10 @@ import {Button} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; +import Feature from 'sentry/components/acl/feature'; import {Breadcrumbs, type Crumb} from 'sentry/components/breadcrumbs'; import ConfirmDelete from 'sentry/components/confirmDelete'; +import {LinkButton} from 'sentry/components/core/button/linkButton'; import DropdownButton from 'sentry/components/dropdownButton'; import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu'; import FeedbackButton from 'sentry/components/feedbackButton/feedbackButton'; @@ -19,6 +21,7 @@ import { IconDownload, IconEllipsis, IconRefresh, + IconSettings, IconTelescope, } from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -156,6 +159,14 @@ export function BuildDetailsHeaderContent(props: BuildDetailsHeaderContentProps) {t('Compare Build')} + + } + aria-label={t('Settings')} + to={`/settings/${organization.slug}/projects/${projectId}/preprod/`} + /> + Builds + + + } + aria-label={t('Settings')} + to={`/settings/${organization.slug}/projects/${projectId}/preprod/`} + /> + + diff --git a/static/app/views/settings/project/navigationConfiguration.tsx b/static/app/views/settings/project/navigationConfiguration.tsx index 8c506155ab61f1..fecf9a535e2580 100644 --- a/static/app/views/settings/project/navigationConfiguration.tsx +++ b/static/app/views/settings/project/navigationConfiguration.tsx @@ -134,6 +134,13 @@ export default function getConfiguration({ title: t('PlayStation'), show: () => !!(organization && hasTempestAccess(organization)) && !isSelfHosted, }, + { + path: `${pathPrefix}/preprod/`, + title: t('Preprod'), + show: () => !!organization?.features?.includes('preprod-issues'), + badge: () => 'beta', + description: t('Size analysis and build distribution configuration.'), + }, ], }, { diff --git a/static/app/views/settings/project/preprod/index.tsx b/static/app/views/settings/project/preprod/index.tsx new file mode 100644 index 00000000000000..33e11be7f59b2b --- /dev/null +++ b/static/app/views/settings/project/preprod/index.tsx @@ -0,0 +1,93 @@ +import {Fragment} from 'react'; + +import TextBlock from 'sentry/views/settings/components/text/textBlock'; +import Feature from 'sentry/components/acl/feature'; +import {ButtonBar} from 'sentry/components/core/button/buttonBar'; +import FeedbackButton from 'sentry/components/feedbackButton/feedbackButton'; +import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; +import {t} from 'sentry/locale'; +import ProjectsStore from 'sentry/stores/projectsStore'; +import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; +import {useProjectSettingsOutlet} from 'sentry/views/settings/project/projectSettingsLayout'; +import Access from 'sentry/components/acl/access'; +import {ProjectPermissionAlert} from 'sentry/views/settings/project/projectPermissionAlert'; +import Form from 'sentry/components/forms/form'; +import JsonForm from 'sentry/components/forms/jsonForm'; +import useOrganization from 'sentry/utils/useOrganization'; +import type {JsonFormObject} from 'sentry/components/forms/types'; + +const scopeForEdit = 'project:write'; + + +//const defaultInstallSizeAbsoluteDeltaIssueThresholdKb = 500; + +export default function PreprodSettings() { + const organization = useOrganization(); + const {project} = useProjectSettingsOutlet(); + + const formGroups: JsonFormObject[] = [ + { + title: t('Size Analysis Issues'), + fields: [ + { + name: 'sentry:preprod_size_issues_is_16kb_ready', + type: 'boolean', + label: t('Create 16kb ready issues'), + help: t('Toggles whether or not to create issues for 16kb readiness.'), + getData: data => ({options: data}), + }, + { + name: 'sentry:preprod_size_issues_delta_install_threshhold_kb', + type: 'number', + label: t('Install size regression threshold (kb)'), + help: t('If there is an regression in install size larger than the threshold an issue will be created.'), + getData: data => ({options: data}), + }, + ], + }, + ]; + + return ( + + + + + + + } + /> + + {({hasAccess}) => ( + + + {t(` + Configure size analysis and build distribution. + `)} + + + + +
ProjectsStore.onUpdateSuccess(response)} + > + + + +
+ )} +
+
+
+ ); +}