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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import Any

import jsonschema
Expand Down Expand Up @@ -104,6 +105,10 @@
**VCS_ERROR_MESSAGES,
}

_SENTRY_CLI_UA_PREFIX = "sentry-cli/"
_GRADLE_PLUGIN_UA_PREFIX = "sentry-gradle-plugin/"
_FASTLANE_PLUGIN_UA_PREFIX = "sentry-fastlane-plugin/"

_COMPACT_FIELDS = {"display_name", "image_file_name", "group", "description"}
_COMPACT_IMAGE_LIST_KEYS = ("images", "added", "removed", "unchanged", "skipped")
_COMPACT_PAIR_LIST_KEYS = ("changed", "renamed", "errored")
Expand Down Expand Up @@ -134,6 +139,32 @@ def build_snapshot_image_response(
)


@dataclass
class _ToolVersions:
cli_version: str | None = None
gradle_plugin_version: str | None = None
fastlane_plugin_version: str | None = None


def _parse_tool_versions(user_agent: str) -> _ToolVersions:
"""Extract tool versions from a user-agent string.

Formats:
``sentry-cli/X.Y.Z``
``sentry-cli/X.Y.Z sentry-gradle-plugin/A.B.C``
``sentry-cli/X.Y.Z sentry-fastlane-plugin/A.B.C``
"""
versions = _ToolVersions()
for part in user_agent.split():
if part.startswith(_SENTRY_CLI_UA_PREFIX):
versions.cli_version = part[len(_SENTRY_CLI_UA_PREFIX) :] or None
elif part.startswith(_GRADLE_PLUGIN_UA_PREFIX):
versions.gradle_plugin_version = part[len(_GRADLE_PLUGIN_UA_PREFIX) :] or None
elif part.startswith(_FASTLANE_PLUGIN_UA_PREFIX):
versions.fastlane_plugin_version = part[len(_FASTLANE_PLUGIN_UA_PREFIX) :] or None
return versions


def validate_preprod_snapshot_post_schema(
request_body: bytes,
) -> tuple[dict[str, Any], str | None]:
Expand Down Expand Up @@ -613,11 +644,16 @@ def post(self, request: Request, project: Project) -> Response:
},
)

tool_versions = _parse_tool_versions(request.META.get("HTTP_USER_AGENT", ""))

artifact = PreprodArtifact.objects.create(
project=project,
state=PreprodArtifact.ArtifactState.UPLOADED,
app_id=app_id,
commit_comparison=commit_comparison,
cli_version=tool_versions.cli_version,
gradle_plugin_version=tool_versions.gradle_plugin_version,
fastlane_plugin_version=tool_versions.fastlane_plugin_version,
)

manifest_key = f"{project.organization_id}/{project.id}/{artifact.id}/manifest.json"
Expand Down
104 changes: 104 additions & 0 deletions tests/sentry/preprod/api/endpoints/test_preprod_artifact_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,110 @@ def test_successful_snapshot_upload(self) -> None:
assert snapshot_metrics.preprod_artifact == artifact
assert snapshot_metrics.image_count == 1

def test_snapshot_upload_stores_cli_version_from_user_agent(self) -> None:
url = self._get_create_url()
data = {
"app_id": "com.example.app",
"images": {
"abc123": {
"content_hash": "abc123",
"display_name": "Screen",
"image_file_name": "screen.png",
"width": 100,
"height": 200,
},
},
}

with self.feature("organizations:preprod-snapshots"):
response = self.client.post(
url, data, format="json", HTTP_USER_AGENT="sentry-cli/2.40.0"
)

assert response.status_code == 200
artifact = PreprodArtifact.objects.get(id=response.data["artifactId"])
assert artifact.cli_version == "2.40.0"

def test_snapshot_upload_stores_gradle_plugin_version(self) -> None:
url = self._get_create_url()
data = {
"app_id": "com.example.app",
"images": {
"abc123": {
"content_hash": "abc123",
"display_name": "Screen",
"image_file_name": "screen.png",
"width": 100,
"height": 200,
},
},
}

with self.feature("organizations:preprod-snapshots"):
response = self.client.post(
url,
data,
format="json",
HTTP_USER_AGENT="sentry-cli/2.40.0 sentry-gradle-plugin/5.0.0",
)

assert response.status_code == 200
artifact = PreprodArtifact.objects.get(id=response.data["artifactId"])
assert artifact.cli_version == "2.40.0"
assert artifact.gradle_plugin_version == "5.0.0"
assert artifact.fastlane_plugin_version is None

def test_snapshot_upload_stores_fastlane_plugin_version(self) -> None:
url = self._get_create_url()
data = {
"app_id": "com.example.app",
"images": {
"abc123": {
"content_hash": "abc123",
"display_name": "Screen",
"image_file_name": "screen.png",
"width": 100,
"height": 200,
},
},
}

with self.feature("organizations:preprod-snapshots"):
response = self.client.post(
url,
data,
format="json",
HTTP_USER_AGENT="sentry-cli/2.40.0 sentry-fastlane-plugin/2.5.1",
)

assert response.status_code == 200
artifact = PreprodArtifact.objects.get(id=response.data["artifactId"])
assert artifact.cli_version == "2.40.0"
assert artifact.fastlane_plugin_version == "2.5.1"
assert artifact.gradle_plugin_version is None

def test_snapshot_upload_no_cli_version_for_unknown_user_agent(self) -> None:
url = self._get_create_url()
data = {
"app_id": "com.example.app",
"images": {
"abc123": {
"content_hash": "abc123",
"display_name": "Screen",
"image_file_name": "screen.png",
"width": 100,
"height": 200,
},
},
}

with self.feature("organizations:preprod-snapshots"):
response = self.client.post(url, data, format="json", HTTP_USER_AGENT="curl/8.0")

assert response.status_code == 200
artifact = PreprodArtifact.objects.get(id=response.data["artifactId"])
assert artifact.cli_version is None

def test_snapshot_upload_creates_commit_comparison(self) -> None:
url = self._get_create_url()
data = {
Expand Down
Loading