1
- import logging
2
1
from collections .abc import Sequence
3
2
from dataclasses import dataclass , field
4
3
from typing import Annotated , Any , TypedDict
5
4
from urllib .parse import urlencode , urlparse , urlunparse
6
5
from uuid import uuid4
7
6
8
7
from django .utils .functional import cached_property
8
+ from requests import RequestException
9
9
10
10
from sentry .http import safe_urlread
11
11
from sentry .sentry_apps .external_requests .utils import send_and_save_sentry_app_request , validate
12
+ from sentry .sentry_apps .metrics import (
13
+ SentryAppEventType ,
14
+ SentryAppExternalRequestFailureReason ,
15
+ SentryAppExternalRequestHaltReason ,
16
+ SentryAppInteractionEvent ,
17
+ SentryAppInteractionType ,
18
+ )
12
19
from sentry .sentry_apps .services .app import RpcSentryAppInstallation
13
20
from sentry .sentry_apps .services .app .model import RpcSentryApp
14
- from sentry .sentry_apps .utils .errors import SentryAppIntegratorError
21
+ from sentry .sentry_apps .utils .errors import SentryAppIntegratorError , SentryAppSentryError
15
22
from sentry .utils import json
16
23
17
- logger = logging . getLogger ( "sentry.sentry_apps.external_requests" )
24
+ FAILURE_REASON_BASE = f" { SentryAppEventType . SELECT_OPTIONS_REQUESTED } .{{}}"
18
25
19
26
20
27
class SelectRequesterResult (TypedDict , total = False ):
@@ -41,66 +48,90 @@ class SelectRequester:
41
48
dependent_data : str | None = field (default = None )
42
49
43
50
def run (self ) -> SelectRequesterResult :
51
+
44
52
response : list [dict [str , str ]] = []
45
53
url = None
46
- try :
47
- url = self ._build_url ()
48
- body = safe_urlread (
49
- send_and_save_sentry_app_request (
50
- url ,
51
- self .sentry_app ,
52
- self .install .organization_id ,
53
- "select_options.requested" ,
54
- headers = self ._build_headers (),
55
- )
56
- )
57
54
58
- response = json .loads (body )
59
- except Exception as e :
60
- extra = {
55
+ with SentryAppInteractionEvent (
56
+ operation_type = SentryAppInteractionType .EXTERNAL_REQUEST ,
57
+ event_type = SentryAppEventType .SELECT_OPTIONS_REQUESTED ,
58
+ ).capture () as lifecycle :
59
+ extras : dict [str , Any ] = {
61
60
"sentry_app_slug" : self .sentry_app .slug ,
62
61
"install_uuid" : self .install .uuid ,
63
62
"project_slug" : self .project_slug ,
64
63
}
64
+ lifecycle .add_extras (extras )
65
+
66
+ try :
67
+ url = self ._build_url ()
68
+ extras .update ({"url" : url })
69
+
70
+ body = safe_urlread (
71
+ send_and_save_sentry_app_request (
72
+ url ,
73
+ self .sentry_app ,
74
+ self .install .organization_id ,
75
+ SentryAppEventType .SELECT_OPTIONS_REQUESTED ,
76
+ headers = self ._build_headers (),
77
+ )
78
+ )
65
79
66
- if not url :
67
- extra .update (
68
- {
69
- "uri" : self .uri ,
70
- "dependent_data" : self .dependent_data ,
71
- "webhook_url" : self .sentry_app .webhook_url ,
72
- }
80
+ response = json .loads (body )
81
+ extras .update ({"response" : response })
82
+ except RequestException as e :
83
+ halt_reason = FAILURE_REASON_BASE .format (
84
+ SentryAppExternalRequestHaltReason .BAD_RESPONSE
85
+ )
86
+ lifecycle .record_halt (halt_reason = e , extra = {"reason" : halt_reason , ** extras })
87
+
88
+ raise SentryAppIntegratorError (
89
+ message = f"Something went wrong while getting options for Select FormField from { self .sentry_app .slug } " ,
90
+ webhook_context = {"error_type" : halt_reason , ** extras },
91
+ status_code = 500 ,
92
+ ) from e
93
+
94
+ except Exception as e :
95
+ failure_reason = FAILURE_REASON_BASE .format (
96
+ SentryAppExternalRequestFailureReason .UNEXPECTED_ERROR
97
+ )
98
+
99
+ if not url :
100
+ failure_reason = FAILURE_REASON_BASE .format (
101
+ SentryAppExternalRequestFailureReason .MISSING_URL
102
+ )
103
+ extras .update (
104
+ {
105
+ "uri" : self .uri ,
106
+ "dependent_data" : self .dependent_data ,
107
+ "webhook_url" : self .sentry_app .webhook_url ,
108
+ }
109
+ )
110
+
111
+ raise SentryAppSentryError (
112
+ message = "Something went wrong while preparing to get Select FormField options" ,
113
+ webhook_context = {"error_type" : failure_reason , ** extras },
114
+ ) from e
115
+
116
+ if not self ._validate_response (response ):
117
+ halt_reason = FAILURE_REASON_BASE .format (
118
+ SentryAppExternalRequestHaltReason .MISSING_FIELDS
119
+ )
120
+ lifecycle .record_halt (halt_reason = halt_reason , extra = extras )
121
+
122
+ raise SentryAppIntegratorError (
123
+ message = f"Invalid response format for Select FormField in { self .sentry_app .slug } from uri: { self .uri } " ,
124
+ webhook_context = {
125
+ "error_type" : halt_reason ,
126
+ ** extras ,
127
+ },
73
128
)
74
- message = "select-requester.missing-url"
75
- else :
76
- extra .update ({"url" : url })
77
- message = "select-requester.request-failed"
78
-
79
- logger .info (message , exc_info = e , extra = extra )
80
- raise SentryAppIntegratorError (
81
- message = f"Something went wrong while getting options for Select FormField from { self .sentry_app .slug } " ,
82
- webhook_context = {"error_type" : message , ** extra },
83
- status_code = 500 ,
84
- ) from e
85
-
86
- if not self ._validate_response (response ):
87
- extras = {
88
- "response" : response ,
89
- "sentry_app_slug" : self .sentry_app .slug ,
90
- "install_uuid" : self .install .uuid ,
91
- "project_slug" : self .project_slug ,
92
- "url" : url ,
93
- }
94
- logger .info ("select-requester.invalid-response" , extra = extras )
95
129
96
- raise SentryAppIntegratorError (
97
- message = f"Invalid response format for Select FormField in { self .sentry_app .slug } from uri: { self .uri } " ,
98
- webhook_context = {
99
- "error_type" : "select-requester.invalid-integrator-response" ,
100
- ** extras ,
101
- },
102
- )
103
- return self ._format_response (response )
130
+ try :
131
+ return self ._format_response (response )
132
+ except SentryAppIntegratorError as e :
133
+ lifecycle .record_halt (halt_reason = e , extra = {** extras })
134
+ raise
104
135
105
136
def _build_url (self ) -> str :
106
137
urlparts : list [str ] = [url_part for url_part in urlparse (self .sentry_app .webhook_url )]
@@ -137,7 +168,9 @@ def _format_response(self, resp: Sequence[dict[str, Any]]) -> SelectRequesterRes
137
168
raise SentryAppIntegratorError (
138
169
message = "Missing `value` or `label` in option data for Select FormField" ,
139
170
webhook_context = {
140
- "error_type" : "select-requester.missing-fields" ,
171
+ "error_type" : FAILURE_REASON_BASE .format (
172
+ SentryAppExternalRequestHaltReason .MISSING_FIELDS
173
+ ),
141
174
"response" : resp ,
142
175
},
143
176
status_code = 500 ,
0 commit comments