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.
+ `)}
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+}