Skip to content

Commit 55c3d76

Browse files
committed
Merge branch 'master' of github.com:getsentry/sentry into txiao/chore/remove-visibility-explore-aggregate-editor-flag-backend
2 parents 7d32fd2 + 4136362 commit 55c3d76

File tree

58 files changed

+1927
-391
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1927
-391
lines changed

src/sentry/conf/server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ def env(
195195
SENTRY_HYBRIDCLOUD_DELETIONS_REDIS_CLUSTER = "default"
196196
SENTRY_SESSION_STORE_REDIS_CLUSTER = "default"
197197
SENTRY_AUTH_IDPMIGRATION_REDIS_CLUSTER = "default"
198-
SENTRY_SNOWFLAKE_REDIS_CLUSTER = "default"
199198

200199
# Hosts that are allowed to use system token authentication.
201200
# http://en.wikipedia.org/wiki/Reserved_IP_addresses

src/sentry/features/temporary.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,6 @@ def register_temporary_features(manager: FeatureManager) -> None:
329329
manager.add("organizations:relay-otlp-traces-endpoint", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
330330
# Enables OTLP Log ingestion in Relay for an entire org.
331331
manager.add("organizations:relay-otel-logs-endpoint", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
332-
# Enables Vercel Log Drain ingestion in Relay for an entire org.
333-
manager.add("organizations:relay-vercel-log-drain-endpoint", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
334332
# Enables Prevent AI in the Sentry UI
335333
manager.add("organizations:prevent-ai", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
336334
# Enables Prevent AI Configuration Page in the Sentry UI

src/sentry/incidents/endpoints/serializers/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
OFFSET = 10**9
1+
OFFSET = 10**10
22

33

44
def get_fake_id_from_object_id(obj_id: int) -> int:

src/sentry/integrations/cursor/client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
CursorAgentLaunchRequestWebhook,
1212
CursorAgentLaunchResponse,
1313
CursorAgentSource,
14+
CursorApiKeyMetadata,
1415
)
1516
from sentry.seer.autofix.utils import CodingAgentProviderType, CodingAgentState, CodingAgentStatus
1617

@@ -30,6 +31,24 @@ def __init__(self, api_key: str, webhook_secret: str):
3031
def _get_auth_headers(self) -> dict[str, str]:
3132
return {"Authorization": f"Bearer {self.api_key}"}
3233

34+
def get_api_key_metadata(self) -> CursorApiKeyMetadata:
35+
"""Fetch metadata about the API key from Cursor's /v0/me endpoint."""
36+
logger.info(
37+
"coding_agent.cursor.get_api_key_metadata",
38+
extra={"agent_type": self.__class__.__name__},
39+
)
40+
41+
api_response = self.get(
42+
"/v0/me",
43+
headers={
44+
"content-type": "application/json;charset=utf-8",
45+
**self._get_auth_headers(),
46+
},
47+
timeout=30,
48+
)
49+
50+
return CursorApiKeyMetadata.validate(api_response.json)
51+
3352
def launch(self, webhook_url: str, request: CodingAgentLaunchRequest) -> CodingAgentState:
3453
"""Launch coding agent with webhook callback."""
3554
payload = CursorAgentLaunchRequestBody(

src/sentry/integrations/cursor/integration.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from django.http.request import HttpRequest
99
from django.http.response import HttpResponseBase
1010
from django.utils.translation import gettext_lazy as _
11-
from pydantic import BaseModel
11+
from pydantic import BaseModel, ValidationError
12+
from requests import HTTPError
1213

1314
from sentry.integrations.base import (
1415
FeatureDescription,
@@ -26,7 +27,7 @@
2627
from sentry.integrations.services.integration import integration_service
2728
from sentry.integrations.services.integration.model import RpcIntegration
2829
from sentry.models.apitoken import generate_token
29-
from sentry.shared_integrations.exceptions import IntegrationConfigurationError
30+
from sentry.shared_integrations.exceptions import ApiError, IntegrationConfigurationError
3031

3132
DESCRIPTION = "Connect your Sentry organization with Cursor Cloud Agents."
3233

@@ -42,6 +43,8 @@ class CursorIntegrationMetadata(BaseModel):
4243
api_key: str
4344
webhook_secret: str
4445
domain_name: Literal["cursor.sh"] = "cursor.sh"
46+
api_key_name: str | None = None
47+
user_email: str | None = None
4548

4649

4750
metadata = IntegrationMetadata(
@@ -106,19 +109,44 @@ def build_integration(self, state: Mapping[str, Any]) -> IntegrationData:
106109
raise IntegrationConfigurationError("Missing configuration data")
107110

108111
webhook_secret = generate_token()
112+
api_key = config["api_key"]
113+
114+
api_key_name = None
115+
user_email = None
116+
try:
117+
client = CursorAgentClient(api_key=api_key, webhook_secret=webhook_secret)
118+
cursor_metadata = client.get_api_key_metadata()
119+
api_key_name = cursor_metadata.apiKeyName
120+
user_email = cursor_metadata.userEmail
121+
except (HTTPError, ApiError):
122+
self.get_logger().exception(
123+
"cursor.build_integration.metadata_fetch_failed",
124+
)
125+
except ValidationError:
126+
self.get_logger().exception(
127+
"cursor.build_integration.metadata_validation_failed",
128+
)
129+
130+
integration_name = (
131+
f"Cursor Cloud Agent - {user_email}/{api_key_name}"
132+
if user_email and api_key_name
133+
else "Cursor Cloud Agent"
134+
)
109135

110136
metadata = CursorIntegrationMetadata(
111137
domain_name="cursor.sh",
112-
api_key=config["api_key"],
138+
api_key=api_key,
113139
webhook_secret=webhook_secret,
140+
api_key_name=api_key_name,
141+
user_email=user_email,
114142
)
115143

116144
return {
117145
# NOTE(jennmueng): We need to create a unique ID for each integration installation. Because of this, new installations will yield a unique external_id and integration.
118146
# Why UUIDs? We use UUIDs here for each integration installation because we don't know how many times this USER-LEVEL API key will be used, or if the same org can have multiple cursor agents (in the near future)
119147
# or if the same user can have multiple installations across multiple orgs. So just a UUID per installation is the best approach. Re-configuring an existing installation will still maintain this external id
120148
"external_id": uuid.uuid4().hex,
121-
"name": "Cursor Agent",
149+
"name": integration_name,
122150
"metadata": metadata.dict(),
123151
}
124152

@@ -170,6 +198,18 @@ def get_client(self):
170198
webhook_secret=self.webhook_secret,
171199
)
172200

201+
def get_dynamic_display_information(self) -> Mapping[str, Any] | None:
202+
"""Return metadata to display in the configurations list."""
203+
metadata = CursorIntegrationMetadata.parse_obj(self.model.metadata or {})
204+
205+
display_info = {}
206+
if metadata.api_key_name:
207+
display_info["api_key_name"] = metadata.api_key_name
208+
if metadata.user_email:
209+
display_info["user_email"] = metadata.user_email
210+
211+
return display_info if display_info else None
212+
173213
@property
174214
def webhook_secret(self) -> str:
175215
return CursorIntegrationMetadata.parse_obj(self.model.metadata).webhook_secret

src/sentry/integrations/cursor/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
from pydantic import BaseModel
44

55

6+
class CursorApiKeyMetadata(BaseModel):
7+
apiKeyName: str
8+
createdAt: str
9+
userEmail: str
10+
11+
612
class CursorAgentLaunchRequestPrompt(BaseModel):
713
text: str
814
images: list[dict] = []

src/sentry/integrations/vercel/integration.py

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
from collections.abc import Mapping, Sequence
5-
from typing import Any, TypedDict
5+
from typing import Any, Self, TypedDict
66
from urllib.parse import urlencode
77

88
import sentry_sdk
@@ -28,6 +28,7 @@
2828
from sentry.pipeline.views.nested import NestedPipelineView
2929
from sentry.projects.services.project.model import RpcProject
3030
from sentry.projects.services.project_key import project_key_service
31+
from sentry.projects.services.project_key.model import RpcProjectKey
3132
from sentry.sentry_apps.logic import SentryAppCreator
3233
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
3334
from sentry.sentry_apps.models.sentry_app_installation_for_provider import (
@@ -107,6 +108,7 @@ class VercelEnvVarDefinition(TypedDict):
107108
target: list[str]
108109

109110

111+
# TODO: Remove this function and use the new VercelEnvVarMapBuilder class instead
110112
def get_env_var_map(
111113
organization: RpcOrganization,
112114
project: RpcProject,
@@ -153,6 +155,107 @@ def get_env_var_map(
153155
}
154156

155157

158+
class VercelEnvVarMapBuilder:
159+
"""
160+
Builder for creating Vercel environment variable maps.
161+
162+
env_var_map = (
163+
VercelEnvVarMapBuilder()
164+
.with_organization(organization)
165+
.with_project(project)
166+
.with_project_key(project_key)
167+
.with_auth_token(auth_token)
168+
.with_framework(framework)
169+
.build()
170+
)
171+
"""
172+
173+
def __init__(self) -> None:
174+
self._organization: RpcOrganization | None = None
175+
self._project: RpcProject | None = None
176+
self._project_key: RpcProjectKey | None = None
177+
self._auth_token: str | None = None
178+
self._framework: str | None = None
179+
180+
def with_organization(self, organization: RpcOrganization) -> Self:
181+
self._organization = organization
182+
return self
183+
184+
def with_project(self, project: RpcProject) -> Self:
185+
self._project = project
186+
return self
187+
188+
def with_project_key(self, project_key: RpcProjectKey) -> Self:
189+
self._project_key = project_key
190+
return self
191+
192+
def with_auth_token(self, auth_token: str | None) -> Self:
193+
self._auth_token = auth_token
194+
return self
195+
196+
def with_framework(self, framework: str | None) -> Self:
197+
self._framework = framework
198+
return self
199+
200+
def build(self) -> dict[str, VercelEnvVarDefinition]:
201+
if self._organization is None:
202+
raise ValueError("organization is required")
203+
if self._project is None:
204+
raise ValueError("project is required")
205+
if self._project_key is None:
206+
raise ValueError("project_key is required")
207+
208+
is_next_js = self._framework == "nextjs"
209+
dsn_env_name = "NEXT_PUBLIC_SENTRY_DSN" if is_next_js else "SENTRY_DSN"
210+
211+
return {
212+
"SENTRY_ORG": {
213+
"type": "encrypted",
214+
"value": self._organization.slug,
215+
"target": ["production", "preview"],
216+
},
217+
"SENTRY_PROJECT": {
218+
"type": "encrypted",
219+
"value": self._project.slug,
220+
"target": ["production", "preview"],
221+
},
222+
dsn_env_name: {
223+
"type": "encrypted",
224+
"value": self._project_key.dsn_public,
225+
"target": [
226+
"production",
227+
"preview",
228+
"development", # The DSN is the only value that makes sense to have available locally via Vercel CLI's `vercel dev` command
229+
],
230+
},
231+
"SENTRY_AUTH_TOKEN": {
232+
"type": "encrypted",
233+
"value": self._auth_token,
234+
"target": ["production", "preview"],
235+
},
236+
"VERCEL_GIT_COMMIT_SHA": {
237+
"type": "system",
238+
"value": "VERCEL_GIT_COMMIT_SHA",
239+
"target": ["production", "preview"],
240+
},
241+
"SENTRY_VERCEL_LOG_DRAIN_URL": {
242+
"type": "encrypted",
243+
"value": f"{self._project_key.integration_endpoint}vercel/logs/",
244+
"target": ["production", "preview"],
245+
},
246+
"SENTRY_OTLP_TRACES_URL": {
247+
"type": "encrypted",
248+
"value": f"{self._project_key.integration_endpoint}otlp/v1/traces",
249+
"target": ["production", "preview"],
250+
},
251+
"SENTRY_PUBLIC_KEY": {
252+
"type": "encrypted",
253+
"value": self._project_key.public_key,
254+
"target": ["production", "preview"],
255+
},
256+
}
257+
258+
156259
class VercelIntegration(IntegrationInstallation):
157260
@property
158261
def metadata(self):
@@ -264,27 +367,28 @@ def update_organization_config(self, data):
264367
[sentry_project_id, vercel_project_id] = mapping
265368
sentry_project = sentry_projects[sentry_project_id]
266369

267-
enabled_dsn = project_key_service.get_default_project_key(
370+
project_key = project_key_service.get_default_project_key(
268371
organization_id=self.organization_id, project_id=sentry_project_id
269372
)
270-
if not enabled_dsn:
373+
if not project_key:
271374
raise ValidationError(
272375
{"project_mappings": ["You must have an enabled DSN to continue!"]}
273376
)
274377

275-
sentry_project_dsn = enabled_dsn.dsn_public
276378
vercel_project = vercel_client.get_project(vercel_project_id)
277379
sentry_auth_token = SentryAppInstallationToken.objects.get_token(
278380
sentry_project.organization_id,
279381
"vercel",
280382
)
281383

282-
env_var_map = get_env_var_map(
283-
organization=self.organization,
284-
project=sentry_project,
285-
project_dsn=sentry_project_dsn,
286-
auth_token=sentry_auth_token,
287-
framework=vercel_project.get("framework"),
384+
env_var_map = (
385+
VercelEnvVarMapBuilder()
386+
.with_organization(self.organization)
387+
.with_project(sentry_project)
388+
.with_project_key(project_key)
389+
.with_auth_token(sentry_auth_token)
390+
.with_framework(vercel_project.get("framework"))
391+
.build()
288392
)
289393

290394
for env_var, details in env_var_map.items():

src/sentry/notifications/notification_action/action_handler_registry/sentry_app_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class SentryAppActionHandler(ActionHandler):
3333

3434
data_schema = {
3535
"$schema": "https://json-schema.org/draft/2020-12/schema",
36+
"description": "The data schema for a Sentry App Action",
3637
"type": "object",
3738
"properties": {
3839
"settings": {"type": ["array", "object"]},

0 commit comments

Comments
 (0)