|
2 | 2 |
|
3 | 3 | import logging |
4 | 4 | from collections.abc import Mapping, Sequence |
5 | | -from typing import Any, TypedDict |
| 5 | +from typing import Any, Self, TypedDict |
6 | 6 | from urllib.parse import urlencode |
7 | 7 |
|
8 | 8 | import sentry_sdk |
|
28 | 28 | from sentry.pipeline.views.nested import NestedPipelineView |
29 | 29 | from sentry.projects.services.project.model import RpcProject |
30 | 30 | from sentry.projects.services.project_key import project_key_service |
| 31 | +from sentry.projects.services.project_key.model import RpcProjectKey |
31 | 32 | from sentry.sentry_apps.logic import SentryAppCreator |
32 | 33 | from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation |
33 | 34 | from sentry.sentry_apps.models.sentry_app_installation_for_provider import ( |
@@ -107,6 +108,7 @@ class VercelEnvVarDefinition(TypedDict): |
107 | 108 | target: list[str] |
108 | 109 |
|
109 | 110 |
|
| 111 | +# TODO: Remove this function and use the new VercelEnvVarMapBuilder class instead |
110 | 112 | def get_env_var_map( |
111 | 113 | organization: RpcOrganization, |
112 | 114 | project: RpcProject, |
@@ -153,6 +155,107 @@ def get_env_var_map( |
153 | 155 | } |
154 | 156 |
|
155 | 157 |
|
| 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 | + |
156 | 259 | class VercelIntegration(IntegrationInstallation): |
157 | 260 | @property |
158 | 261 | def metadata(self): |
@@ -264,27 +367,28 @@ def update_organization_config(self, data): |
264 | 367 | [sentry_project_id, vercel_project_id] = mapping |
265 | 368 | sentry_project = sentry_projects[sentry_project_id] |
266 | 369 |
|
267 | | - enabled_dsn = project_key_service.get_default_project_key( |
| 370 | + project_key = project_key_service.get_default_project_key( |
268 | 371 | organization_id=self.organization_id, project_id=sentry_project_id |
269 | 372 | ) |
270 | | - if not enabled_dsn: |
| 373 | + if not project_key: |
271 | 374 | raise ValidationError( |
272 | 375 | {"project_mappings": ["You must have an enabled DSN to continue!"]} |
273 | 376 | ) |
274 | 377 |
|
275 | | - sentry_project_dsn = enabled_dsn.dsn_public |
276 | 378 | vercel_project = vercel_client.get_project(vercel_project_id) |
277 | 379 | sentry_auth_token = SentryAppInstallationToken.objects.get_token( |
278 | 380 | sentry_project.organization_id, |
279 | 381 | "vercel", |
280 | 382 | ) |
281 | 383 |
|
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() |
288 | 392 | ) |
289 | 393 |
|
290 | 394 | for env_var, details in env_var_map.items(): |
|
0 commit comments