Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/integrations/flask.py: 12%

132 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from __future__ import absolute_import 

2 

3from sentry_sdk._types import MYPY 

4from sentry_sdk.hub import Hub, _should_send_default_pii 

5from sentry_sdk.integrations import DidNotEnable, Integration 

6from sentry_sdk.integrations._wsgi_common import RequestExtractor 

7from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware 

8from sentry_sdk.scope import Scope 

9from sentry_sdk.tracing import SOURCE_FOR_STYLE 

10from sentry_sdk.utils import ( 

11 capture_internal_exceptions, 

12 event_from_exception, 

13) 

14 

15if MYPY: 15 ↛ 16line 15 didn't jump to line 16, because the condition on line 15 was never true

16 from typing import Any, Callable, Dict, Union 

17 

18 from sentry_sdk._types import EventProcessor 

19 from sentry_sdk.integrations.wsgi import _ScopedResponse 

20 from werkzeug.datastructures import FileStorage, ImmutableMultiDict 

21 

22 

23try: 

24 import flask_login # type: ignore 

25except ImportError: 

26 flask_login = None 

27 

28try: 

29 from flask import Flask, Markup, Request # type: ignore 

30 from flask import __version__ as FLASK_VERSION 

31 from flask import _app_ctx_stack, _request_ctx_stack 

32 from flask.signals import ( 

33 before_render_template, 

34 got_request_exception, 

35 request_started, 

36 ) 

37except ImportError: 

38 raise DidNotEnable("Flask is not installed") 

39 

40try: 

41 import blinker # noqa 

42except ImportError: 

43 raise DidNotEnable("blinker is not installed") 

44 

45TRANSACTION_STYLE_VALUES = ("endpoint", "url") 

46 

47 

48class FlaskIntegration(Integration): 

49 identifier = "flask" 

50 

51 transaction_style = "" 

52 

53 def __init__(self, transaction_style="endpoint"): 

54 # type: (str) -> None 

55 if transaction_style not in TRANSACTION_STYLE_VALUES: 

56 raise ValueError( 

57 "Invalid value for transaction_style: %s (must be in %s)" 

58 % (transaction_style, TRANSACTION_STYLE_VALUES) 

59 ) 

60 self.transaction_style = transaction_style 

61 

62 @staticmethod 

63 def setup_once(): 

64 # type: () -> None 

65 

66 # This version parsing is absolutely naive but the alternative is to 

67 # import pkg_resources which slows down the SDK a lot. 

68 try: 

69 version = tuple(map(int, FLASK_VERSION.split(".")[:3])) 

70 except (ValueError, TypeError): 

71 # It's probably a release candidate, we assume it's fine. 

72 pass 

73 else: 

74 if version < (0, 10): 

75 raise DidNotEnable("Flask 0.10 or newer is required.") 

76 

77 before_render_template.connect(_add_sentry_trace) 

78 request_started.connect(_request_started) 

79 got_request_exception.connect(_capture_exception) 

80 

81 old_app = Flask.__call__ 

82 

83 def sentry_patched_wsgi_app(self, environ, start_response): 

84 # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse 

85 if Hub.current.get_integration(FlaskIntegration) is None: 

86 return old_app(self, environ, start_response) 

87 

88 return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))( 

89 environ, start_response 

90 ) 

91 

92 Flask.__call__ = sentry_patched_wsgi_app 

93 

94 

95def _add_sentry_trace(sender, template, context, **extra): 

96 # type: (Flask, Any, Dict[str, Any], **Any) -> None 

97 

98 if "sentry_trace" in context: 

99 return 

100 

101 sentry_span = Hub.current.scope.span 

102 context["sentry_trace"] = ( 

103 Markup( 

104 '<meta name="sentry-trace" content="%s" />' 

105 % (sentry_span.to_traceparent(),) 

106 ) 

107 if sentry_span 

108 else "" 

109 ) 

110 

111 

112def _set_transaction_name_and_source(scope, transaction_style, request): 

113 # type: (Scope, str, Request) -> None 

114 try: 

115 name_for_style = { 

116 "url": request.url_rule.rule, 

117 "endpoint": request.url_rule.endpoint, 

118 } 

119 scope.set_transaction_name( 

120 name_for_style[transaction_style], 

121 source=SOURCE_FOR_STYLE[transaction_style], 

122 ) 

123 except Exception: 

124 pass 

125 

126 

127def _request_started(sender, **kwargs): 

128 # type: (Flask, **Any) -> None 

129 hub = Hub.current 

130 integration = hub.get_integration(FlaskIntegration) 

131 if integration is None: 

132 return 

133 

134 app = _app_ctx_stack.top.app 

135 with hub.configure_scope() as scope: 

136 request = _request_ctx_stack.top.request 

137 

138 # Set the transaction name and source here, 

139 # but rely on WSGI middleware to actually start the transaction 

140 _set_transaction_name_and_source(scope, integration.transaction_style, request) 

141 evt_processor = _make_request_event_processor(app, request, integration) 

142 scope.add_event_processor(evt_processor) 

143 

144 

145class FlaskRequestExtractor(RequestExtractor): 

146 def env(self): 

147 # type: () -> Dict[str, str] 

148 return self.request.environ 

149 

150 def cookies(self): 

151 # type: () -> Dict[Any, Any] 

152 return { 

153 k: v[0] if isinstance(v, list) and len(v) == 1 else v 

154 for k, v in self.request.cookies.items() 

155 } 

156 

157 def raw_data(self): 

158 # type: () -> bytes 

159 return self.request.get_data() 

160 

161 def form(self): 

162 # type: () -> ImmutableMultiDict[str, Any] 

163 return self.request.form 

164 

165 def files(self): 

166 # type: () -> ImmutableMultiDict[str, Any] 

167 return self.request.files 

168 

169 def is_json(self): 

170 # type: () -> bool 

171 return self.request.is_json 

172 

173 def json(self): 

174 # type: () -> Any 

175 return self.request.get_json() 

176 

177 def size_of_file(self, file): 

178 # type: (FileStorage) -> int 

179 return file.content_length 

180 

181 

182def _make_request_event_processor(app, request, integration): 

183 # type: (Flask, Callable[[], Request], FlaskIntegration) -> EventProcessor 

184 

185 def inner(event, hint): 

186 # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] 

187 

188 # if the request is gone we are fine not logging the data from 

189 # it. This might happen if the processor is pushed away to 

190 # another thread. 

191 if request is None: 

192 return event 

193 

194 with capture_internal_exceptions(): 

195 FlaskRequestExtractor(request).extract_into_event(event) 

196 

197 if _should_send_default_pii(): 

198 with capture_internal_exceptions(): 

199 _add_user_to_event(event) 

200 

201 return event 

202 

203 return inner 

204 

205 

206def _capture_exception(sender, exception, **kwargs): 

207 # type: (Flask, Union[ValueError, BaseException], **Any) -> None 

208 hub = Hub.current 

209 if hub.get_integration(FlaskIntegration) is None: 

210 return 

211 

212 # If an integration is there, a client has to be there. 

213 client = hub.client # type: Any 

214 

215 event, hint = event_from_exception( 

216 exception, 

217 client_options=client.options, 

218 mechanism={"type": "flask", "handled": False}, 

219 ) 

220 

221 hub.capture_event(event, hint=hint) 

222 

223 

224def _add_user_to_event(event): 

225 # type: (Dict[str, Any]) -> None 

226 if flask_login is None: 

227 return 

228 

229 user = flask_login.current_user 

230 if user is None: 

231 return 

232 

233 with capture_internal_exceptions(): 

234 # Access this object as late as possible as accessing the user 

235 # is relatively costly 

236 

237 user_info = event.setdefault("user", {}) 

238 

239 try: 

240 user_info.setdefault("id", user.get_id()) 

241 # TODO: more configurable user attrs here 

242 except AttributeError: 

243 # might happen if: 

244 # - flask_login could not be imported 

245 # - flask_login is not configured 

246 # - no user is logged in 

247 pass 

248 

249 # The following attribute accesses are ineffective for the general 

250 # Flask-Login case, because the User interface of Flask-Login does not 

251 # care about anything but the ID. However, Flask-User (based on 

252 # Flask-Login) documents a few optional extra attributes. 

253 # 

254 # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names 

255 

256 try: 

257 user_info.setdefault("email", user.email) 

258 except Exception: 

259 pass 

260 

261 try: 

262 user_info.setdefault("username", user.username) 

263 user_info.setdefault("username", user.email) 

264 except Exception: 

265 pass