Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/sentry/objectstore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ def distribution(
_ATTACHMENTS_CLIENT: Client | None = None
_ATTACHMENTS_USECASE = Usecase("attachments", expiration_policy=TimeToLive(timedelta(days=30)))

_PREPROD_CLIENT: Client | None = None
_PREPROD_USECASE = Usecase("preprod", expiration_policy=TimeToLive(timedelta(days=30)))


def get_attachments_session(org: int, project: int) -> Session:
global _ATTACHMENTS_CLIENT
Expand All @@ -49,3 +52,17 @@ def get_attachments_session(org: int, project: int) -> Session:
)

return _ATTACHMENTS_CLIENT.session(_ATTACHMENTS_USECASE, org=org, project=project)


def get_preprod_session(org: int, project: int) -> Session:
global _PREPROD_CLIENT
if not _PREPROD_CLIENT:
from sentry import options as options_store

options = options_store.get("objectstore.config")
_PREPROD_CLIENT = Client(
options["base_url"],
metrics_backend=SentryMetricsBackend(),
)

return _PREPROD_CLIENT.session(_PREPROD_USECASE, org=org, project=project)
31 changes: 16 additions & 15 deletions src/sentry/preprod/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ class PreprodArtifactApiAssembleGenericEvent(analytics.Event):
project_id: int


@analytics.eventclass("preprod_artifact.api.size_analysis_download")
class PreprodArtifactApiSizeAnalysisDownloadEvent(analytics.Event):
@analytics.eventclass("preprod_artifact.api.get_build_details")
class PreprodArtifactApiGetBuildDetailsEvent(analytics.Event):
organization_id: int
project_id: int
user_id: int | None = None
artifact_id: str


@analytics.eventclass("preprod_artifact.api.get_build_details")
class PreprodArtifactApiGetBuildDetailsEvent(analytics.Event):
@analytics.eventclass("preprod_artifact.api.list_builds")
class PreprodArtifactApiListBuildsEvent(analytics.Event):
organization_id: int
project_id: int
user_id: int | None = None
artifact_id: str


@analytics.eventclass("preprod_artifact.api.list_builds")
class PreprodArtifactApiListBuildsEvent(analytics.Event):
@analytics.eventclass("preprod_artifact.api.install_details")
class PreprodArtifactApiInstallDetailsEvent(analytics.Event):
organization_id: int
project_id: int
user_id: int | None = None
artifact_id: str


@analytics.eventclass("preprod_artifact.api.admin_rerun_analysis")
Expand Down Expand Up @@ -77,6 +77,15 @@ class PreprodArtifactApiDeleteEvent(analytics.Event):
artifact_id: str


# Size analysis
@analytics.eventclass("preprod_artifact.api.size_analysis_download")
class PreprodArtifactApiSizeAnalysisDownloadEvent(analytics.Event):
organization_id: int
project_id: int
user_id: int | None = None
artifact_id: str


@analytics.eventclass("preprod_artifact.api.size_analysis_compare.get")
class PreprodArtifactApiSizeAnalysisCompareGetEvent(analytics.Event):
organization_id: int
Expand All @@ -95,14 +104,6 @@ class PreprodArtifactApiSizeAnalysisComparePostEvent(analytics.Event):
base_artifact_id: str


@analytics.eventclass("preprod_artifact.api.install_details")
class PreprodArtifactApiInstallDetailsEvent(analytics.Event):
organization_id: int
project_id: int
user_id: int | None = None
artifact_id: str


@analytics.eventclass("preprod_artifact.api.size_analysis_compare_download")
class PreprodArtifactApiSizeAnalysisCompareDownloadEvent(analytics.Event):
organization_id: int
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

import logging

from django.http import HttpResponse
from objectstore_client import ClientError
from rest_framework.request import Request

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.project import ProjectEndpoint
from sentry.models.project import Project
from sentry.objectstore import get_preprod_session

logger = logging.getLogger(__name__)


@region_silo_endpoint
class ProjectPreprodArtifactImageEndpoint(ProjectEndpoint):
owner = ApiOwner.EMERGE_TOOLS
publish_status = {
"GET": ApiPublishStatus.EXPERIMENTAL,
}

def get(
self,
_: Request,
project: Project,
image_id: str,
) -> HttpResponse:

organization_id = project.organization_id
project_id = project.id

object_key = f"{organization_id}/{project_id}/{image_id}"
session = get_preprod_session(organization_id, project_id)

try:
result = session.get(object_key)
# Read the entire stream at once (necessary for content_type)
image_data = result.payload.read()

# Detect content type from the image data
return HttpResponse(image_data, content_type=result.metadata.content_type)

except ClientError as e:
if e.status == 404:
logger.warning(
"Image not found in objectstore",
extra={
"organization_id": organization_id,
"project_id": project_id,
"image_id": image_id,
},
)

# Upload failed, return appropriate error
return HttpResponse({"error": "Not found"}, status=404)

logger.warning(
"Failed to retrieve image from objectstore",
extra={
"organization_id": organization_id,
"project_id": project_id,
"image_id": image_id,
"error": str(e),
"status": e.status,
},
)
return HttpResponse({"error": "Failed to retrieve image"}, status=500)

except Exception:
logger.exception(
"Unexpected error retrieving image",
extra={
"organization_id": organization_id,
"project_id": project_id,
"image_id": image_id,
},
)
return HttpResponse({"error": "Internal server error"}, status=500)
8 changes: 8 additions & 0 deletions src/sentry/preprod/api/endpoints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from django.urls import re_path

from sentry.preprod.api.endpoints.project_preprod_artifact_image import (
ProjectPreprodArtifactImageEndpoint,
)
from sentry.preprod.api.endpoints.size_analysis.project_preprod_size_analysis_compare import (
ProjectPreprodArtifactSizeAnalysisCompareEndpoint,
)
Expand Down Expand Up @@ -81,6 +84,11 @@
ProjectInstallablePreprodArtifactDownloadEndpoint.as_view(),
name="sentry-api-0-installable-preprod-artifact-download",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/(?P<project_id_or_slug>[^/]+)/files/images/(?P<image_id>[^/]+)/$",
ProjectPreprodArtifactImageEndpoint.as_view(),
name="sentry-api-0-project-preprod-artifact-image",
),
# Size analysis
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/(?P<project_id_or_slug>[^/]+)/preprodartifacts/size-analysis/compare/(?P<head_artifact_id>[^/]+)/(?P<base_artifact_id>[^/]+)/$",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class BuildDetailsAppInfo(BaseModel):
platform: Platform | None = None
is_installable: bool
build_configuration: str | None = None
app_icon_id: str | None = None
apple_app_info: AppleAppInfo | None = None
android_app_info: AndroidAppInfo | None = None

Expand Down Expand Up @@ -213,6 +214,7 @@ def transform_preprod_artifact_to_build_details(
build_configuration=(
artifact.build_configuration.name if artifact.build_configuration else None
),
app_icon_id=artifact.app_icon_id,
apple_app_info=apple_app_info,
android_app_info=android_app_info,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Feature from 'sentry/components/acl/feature';
import {IconClock, IconFile, IconJson, IconLink, IconMobile} from 'sentry/icons';
import {t} from 'sentry/locale';
import {getFormat, getFormattedDate, getUtcToSystem} from 'sentry/utils/dates';
import useOrganization from 'sentry/utils/useOrganization';
import {openInstallModal} from 'sentry/views/preprod/components/installModal';
import {type BuildDetailsAppInfo} from 'sentry/views/preprod/types/buildDetailsTypes';
import {
Expand All @@ -27,19 +28,26 @@ interface BuildDetailsSidebarAppInfoProps {
}

export function BuildDetailsSidebarAppInfo(props: BuildDetailsSidebarAppInfoProps) {
const organization = useOrganization();
const labels = getLabels(props.appInfo.platform ?? undefined);

const datetimeFormat = getFormat({
seconds: true,
timeZone: true,
});

let iconUrl = null;
if (props.appInfo.app_icon_id) {
iconUrl = `/api/0/projects/${organization.slug}/${props.projectId}/files/images/${props.appInfo.app_icon_id}/`;
}

return (
<Flex direction="column" gap="xl">
<Flex align="center" gap="sm">
<AppIcon>
{iconUrl && <img src={iconUrl} alt="App Icon" width={24} height={24} />}
{!iconUrl && (
<AppIconPlaceholder>{props.appInfo.name?.charAt(0) || ''}</AppIconPlaceholder>
</AppIcon>
)}
{props.appInfo.name && <Heading as="h3">{props.appInfo.name}</Heading>}
</Flex>

Expand Down Expand Up @@ -131,19 +139,16 @@ export function BuildDetailsSidebarAppInfo(props: BuildDetailsSidebarAppInfoProp
);
}

const AppIcon = styled('div')`
const AppIconPlaceholder = styled('div')`
width: 24px;
height: 24px;
border-radius: 4px;
background: #ff6600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
`;

const AppIconPlaceholder = styled('div')`
color: white;
background: ${p => p.theme.purple400};
color: ${p => p.theme.white};
font-weight: ${p => p.theme.fontWeight.bold};
font-size: ${p => p.theme.fontSize.sm};
`;
Expand Down
1 change: 1 addition & 0 deletions static/app/views/preprod/types/buildDetailsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface BuildDetailsApiResponse {
}

export interface BuildDetailsAppInfo {
app_icon_id?: string | null;
android_app_info?: AndroidAppInfo | null;
app_id?: string | null;
apple_app_info?: AppleAppInfo | null;
Expand Down
Loading
Loading