sentry-sdk 0.7.5__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -30
  2. sentry_sdk/_compat.py +74 -61
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +289 -0
  8. sentry_sdk/_types.py +338 -0
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +134 -0
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +141 -0
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +112 -68
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/_types.py ADDED
@@ -0,0 +1,338 @@
1
+ from typing import TYPE_CHECKING, TypeVar, Union
2
+
3
+
4
+ # Re-exported for compat, since code out there in the wild might use this variable.
5
+ MYPY = TYPE_CHECKING
6
+
7
+
8
+ SENSITIVE_DATA_SUBSTITUTE = "[Filtered]"
9
+
10
+
11
+ class AnnotatedValue:
12
+ """
13
+ Meta information for a data field in the event payload.
14
+ This is to tell Relay that we have tampered with the fields value.
15
+ See:
16
+ https://github.com/getsentry/relay/blob/be12cd49a0f06ea932ed9b9f93a655de5d6ad6d1/relay-general/src/types/meta.rs#L407-L423
17
+ """
18
+
19
+ __slots__ = ("value", "metadata")
20
+
21
+ def __init__(self, value, metadata):
22
+ # type: (Optional[Any], Dict[str, Any]) -> None
23
+ self.value = value
24
+ self.metadata = metadata
25
+
26
+ def __eq__(self, other):
27
+ # type: (Any) -> bool
28
+ if not isinstance(other, AnnotatedValue):
29
+ return False
30
+
31
+ return self.value == other.value and self.metadata == other.metadata
32
+
33
+ def __str__(self):
34
+ # type: (AnnotatedValue) -> str
35
+ return str({"value": str(self.value), "metadata": str(self.metadata)})
36
+
37
+ def __len__(self):
38
+ # type: (AnnotatedValue) -> int
39
+ if self.value is not None:
40
+ return len(self.value)
41
+ else:
42
+ return 0
43
+
44
+ @classmethod
45
+ def removed_because_raw_data(cls):
46
+ # type: () -> AnnotatedValue
47
+ """The value was removed because it could not be parsed. This is done for request body values that are not json nor a form."""
48
+ return AnnotatedValue(
49
+ value="",
50
+ metadata={
51
+ "rem": [ # Remark
52
+ [
53
+ "!raw", # Unparsable raw data
54
+ "x", # The fields original value was removed
55
+ ]
56
+ ]
57
+ },
58
+ )
59
+
60
+ @classmethod
61
+ def removed_because_over_size_limit(cls, value=""):
62
+ # type: (Any) -> AnnotatedValue
63
+ """
64
+ The actual value was removed because the size of the field exceeded the configured maximum size,
65
+ for example specified with the max_request_body_size sdk option.
66
+ """
67
+ return AnnotatedValue(
68
+ value=value,
69
+ metadata={
70
+ "rem": [ # Remark
71
+ [
72
+ "!config", # Because of configured maximum size
73
+ "x", # The fields original value was removed
74
+ ]
75
+ ]
76
+ },
77
+ )
78
+
79
+ @classmethod
80
+ def substituted_because_contains_sensitive_data(cls):
81
+ # type: () -> AnnotatedValue
82
+ """The actual value was removed because it contained sensitive information."""
83
+ return AnnotatedValue(
84
+ value=SENSITIVE_DATA_SUBSTITUTE,
85
+ metadata={
86
+ "rem": [ # Remark
87
+ [
88
+ "!config", # Because of SDK configuration (in this case the config is the hard coded removal of certain django cookies)
89
+ "s", # The fields original value was substituted
90
+ ]
91
+ ]
92
+ },
93
+ )
94
+
95
+
96
+ T = TypeVar("T")
97
+ Annotated = Union[AnnotatedValue, T]
98
+
99
+
100
+ if TYPE_CHECKING:
101
+ from collections.abc import Container, MutableMapping, Sequence
102
+
103
+ from datetime import datetime
104
+
105
+ from types import TracebackType
106
+ from typing import Any
107
+ from typing import Callable
108
+ from typing import Dict
109
+ from typing import Mapping
110
+ from typing import NotRequired
111
+ from typing import Optional
112
+ from typing import Tuple
113
+ from typing import Type
114
+ from typing_extensions import Literal, TypedDict
115
+
116
+ class SDKInfo(TypedDict):
117
+ name: str
118
+ version: str
119
+ packages: Sequence[Mapping[str, str]]
120
+
121
+ # "critical" is an alias of "fatal" recognized by Relay
122
+ LogLevelStr = Literal["fatal", "critical", "error", "warning", "info", "debug"]
123
+
124
+ DurationUnit = Literal[
125
+ "nanosecond",
126
+ "microsecond",
127
+ "millisecond",
128
+ "second",
129
+ "minute",
130
+ "hour",
131
+ "day",
132
+ "week",
133
+ ]
134
+
135
+ InformationUnit = Literal[
136
+ "bit",
137
+ "byte",
138
+ "kilobyte",
139
+ "kibibyte",
140
+ "megabyte",
141
+ "mebibyte",
142
+ "gigabyte",
143
+ "gibibyte",
144
+ "terabyte",
145
+ "tebibyte",
146
+ "petabyte",
147
+ "pebibyte",
148
+ "exabyte",
149
+ "exbibyte",
150
+ ]
151
+
152
+ FractionUnit = Literal["ratio", "percent"]
153
+ MeasurementUnit = Union[DurationUnit, InformationUnit, FractionUnit, str]
154
+
155
+ MeasurementValue = TypedDict(
156
+ "MeasurementValue",
157
+ {
158
+ "value": float,
159
+ "unit": NotRequired[Optional[MeasurementUnit]],
160
+ },
161
+ )
162
+
163
+ Event = TypedDict(
164
+ "Event",
165
+ {
166
+ "breadcrumbs": Annotated[
167
+ dict[Literal["values"], list[dict[str, Any]]]
168
+ ], # TODO: We can expand on this type
169
+ "check_in_id": str,
170
+ "contexts": dict[str, dict[str, object]],
171
+ "dist": str,
172
+ "duration": Optional[float],
173
+ "environment": Optional[str],
174
+ "errors": list[dict[str, Any]], # TODO: We can expand on this type
175
+ "event_id": str,
176
+ "exception": dict[
177
+ Literal["values"], list[dict[str, Any]]
178
+ ], # TODO: We can expand on this type
179
+ "extra": MutableMapping[str, object],
180
+ "fingerprint": list[str],
181
+ "level": LogLevelStr,
182
+ "logentry": Mapping[str, object],
183
+ "logger": str,
184
+ "measurements": dict[str, MeasurementValue],
185
+ "message": str,
186
+ "modules": dict[str, str],
187
+ "monitor_config": Mapping[str, object],
188
+ "monitor_slug": Optional[str],
189
+ "platform": Literal["python"],
190
+ "profile": object, # Should be sentry_sdk.profiler.Profile, but we can't import that here due to circular imports
191
+ "release": Optional[str],
192
+ "request": dict[str, object],
193
+ "sdk": Mapping[str, object],
194
+ "server_name": str,
195
+ "spans": Annotated[list[dict[str, object]]],
196
+ "stacktrace": dict[
197
+ str, object
198
+ ], # We access this key in the code, but I am unsure whether we ever set it
199
+ "start_timestamp": datetime,
200
+ "status": Optional[str],
201
+ "tags": MutableMapping[
202
+ str, str
203
+ ], # Tags must be less than 200 characters each
204
+ "threads": dict[
205
+ Literal["values"], list[dict[str, Any]]
206
+ ], # TODO: We can expand on this type
207
+ "timestamp": Optional[datetime], # Must be set before sending the event
208
+ "transaction": str,
209
+ "transaction_info": Mapping[str, Any], # TODO: We can expand on this type
210
+ "type": Literal["check_in", "transaction"],
211
+ "user": dict[str, object],
212
+ "_dropped_spans": int,
213
+ },
214
+ total=False,
215
+ )
216
+
217
+ ExcInfo = Union[
218
+ tuple[Type[BaseException], BaseException, Optional[TracebackType]],
219
+ tuple[None, None, None],
220
+ ]
221
+
222
+ # TODO: Make a proper type definition for this (PRs welcome!)
223
+ Hint = Dict[str, Any]
224
+
225
+ Log = TypedDict(
226
+ "Log",
227
+ {
228
+ "severity_text": str,
229
+ "severity_number": int,
230
+ "body": str,
231
+ "attributes": dict[str, str | bool | float | int],
232
+ "time_unix_nano": int,
233
+ "trace_id": Optional[str],
234
+ },
235
+ )
236
+
237
+ MetricType = Literal["counter", "gauge", "distribution"]
238
+
239
+ MetricAttributeValue = TypedDict(
240
+ "MetricAttributeValue",
241
+ {
242
+ "value": Union[str, bool, float, int],
243
+ "type": Literal["string", "boolean", "double", "integer"],
244
+ },
245
+ )
246
+
247
+ Metric = TypedDict(
248
+ "Metric",
249
+ {
250
+ "timestamp": float,
251
+ "trace_id": Optional[str],
252
+ "span_id": Optional[str],
253
+ "name": str,
254
+ "type": MetricType,
255
+ "value": float,
256
+ "unit": Optional[str],
257
+ "attributes": dict[str, str | bool | float | int],
258
+ },
259
+ )
260
+
261
+ MetricProcessor = Callable[[Metric, Hint], Optional[Metric]]
262
+
263
+ # TODO: Make a proper type definition for this (PRs welcome!)
264
+ Breadcrumb = Dict[str, Any]
265
+
266
+ # TODO: Make a proper type definition for this (PRs welcome!)
267
+ BreadcrumbHint = Dict[str, Any]
268
+
269
+ # TODO: Make a proper type definition for this (PRs welcome!)
270
+ SamplingContext = Dict[str, Any]
271
+
272
+ EventProcessor = Callable[[Event, Hint], Optional[Event]]
273
+ ErrorProcessor = Callable[[Event, ExcInfo], Optional[Event]]
274
+ BreadcrumbProcessor = Callable[[Breadcrumb, BreadcrumbHint], Optional[Breadcrumb]]
275
+ TransactionProcessor = Callable[[Event, Hint], Optional[Event]]
276
+ LogProcessor = Callable[[Log, Hint], Optional[Log]]
277
+
278
+ TracesSampler = Callable[[SamplingContext], Union[float, int, bool]]
279
+
280
+ # https://github.com/python/mypy/issues/5710
281
+ NotImplementedType = Any
282
+
283
+ EventDataCategory = Literal[
284
+ "default",
285
+ "error",
286
+ "crash",
287
+ "transaction",
288
+ "security",
289
+ "attachment",
290
+ "session",
291
+ "internal",
292
+ "profile",
293
+ "profile_chunk",
294
+ "monitor",
295
+ "span",
296
+ "log_item",
297
+ "trace_metric",
298
+ ]
299
+ SessionStatus = Literal["ok", "exited", "crashed", "abnormal"]
300
+
301
+ ContinuousProfilerMode = Literal["thread", "gevent", "unknown"]
302
+ ProfilerMode = Union[ContinuousProfilerMode, Literal["sleep"]]
303
+
304
+ MonitorConfigScheduleType = Literal["crontab", "interval"]
305
+ MonitorConfigScheduleUnit = Literal[
306
+ "year",
307
+ "month",
308
+ "week",
309
+ "day",
310
+ "hour",
311
+ "minute",
312
+ "second", # not supported in Sentry and will result in a warning
313
+ ]
314
+
315
+ MonitorConfigSchedule = TypedDict(
316
+ "MonitorConfigSchedule",
317
+ {
318
+ "type": MonitorConfigScheduleType,
319
+ "value": Union[int, str],
320
+ "unit": MonitorConfigScheduleUnit,
321
+ },
322
+ total=False,
323
+ )
324
+
325
+ MonitorConfig = TypedDict(
326
+ "MonitorConfig",
327
+ {
328
+ "schedule": MonitorConfigSchedule,
329
+ "timezone": str,
330
+ "checkin_margin": int,
331
+ "max_runtime": int,
332
+ "failure_issue_threshold": int,
333
+ "recovery_threshold": int,
334
+ },
335
+ total=False,
336
+ )
337
+
338
+ HttpStatusCodeRange = Union[int, Container[int]]
@@ -0,0 +1,98 @@
1
+ """
2
+ Copyright (c) 2007 by the Pallets team.
3
+
4
+ Some rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are
8
+ met:
9
+
10
+ * Redistributions of source code must retain the above copyright notice,
11
+ this list of conditions and the following disclaimer.
12
+
13
+ * Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in the
15
+ documentation and/or other materials provided with the distribution.
16
+
17
+ * Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
22
+ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
23
+ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
24
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25
+ COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
27
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28
+ USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
29
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
31
+ THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF
32
+ SUCH DAMAGE.
33
+ """
34
+
35
+ from typing import TYPE_CHECKING
36
+
37
+ if TYPE_CHECKING:
38
+ from typing import Dict
39
+ from typing import Iterator
40
+ from typing import Tuple
41
+
42
+
43
+ #
44
+ # `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
45
+ # https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361
46
+ #
47
+ # We need this function because Django does not give us a "pure" http header
48
+ # dict. So we might as well use it for all WSGI integrations.
49
+ #
50
+ def _get_headers(environ):
51
+ # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
52
+ """
53
+ Returns only proper HTTP headers.
54
+ """
55
+ for key, value in environ.items():
56
+ key = str(key)
57
+ if key.startswith("HTTP_") and key not in (
58
+ "HTTP_CONTENT_TYPE",
59
+ "HTTP_CONTENT_LENGTH",
60
+ ):
61
+ yield key[5:].replace("_", "-").title(), value
62
+ elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
63
+ yield key.replace("_", "-").title(), value
64
+
65
+
66
+ #
67
+ # `get_host` comes from `werkzeug.wsgi.get_host`
68
+ # https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145
69
+ #
70
+ def get_host(environ, use_x_forwarded_for=False):
71
+ # type: (Dict[str, str], bool) -> str
72
+ """
73
+ Return the host for the given WSGI environment.
74
+ """
75
+ if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ:
76
+ rv = environ["HTTP_X_FORWARDED_HOST"]
77
+ if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
78
+ rv = rv[:-3]
79
+ elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
80
+ rv = rv[:-4]
81
+ elif environ.get("HTTP_HOST"):
82
+ rv = environ["HTTP_HOST"]
83
+ if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
84
+ rv = rv[:-3]
85
+ elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
86
+ rv = rv[:-4]
87
+ elif environ.get("SERVER_NAME"):
88
+ rv = environ["SERVER_NAME"]
89
+ if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
90
+ ("https", "443"),
91
+ ("http", "80"),
92
+ ):
93
+ rv += ":" + environ["SERVER_PORT"]
94
+ else:
95
+ # In spite of the WSGI spec, SERVER_NAME might not be present.
96
+ rv = "unknown"
97
+
98
+ return rv
@@ -0,0 +1,7 @@
1
+ from .utils import (
2
+ set_data_normalized,
3
+ GEN_AI_MESSAGE_ROLE_MAPPING,
4
+ GEN_AI_MESSAGE_ROLE_REVERSE_MAPPING,
5
+ normalize_message_role,
6
+ normalize_message_roles,
7
+ ) # noqa: F401
@@ -0,0 +1,137 @@
1
+ import inspect
2
+ from functools import wraps
3
+
4
+ from sentry_sdk.consts import SPANDATA
5
+ import sentry_sdk.utils
6
+ from sentry_sdk import start_span
7
+ from sentry_sdk.tracing import Span
8
+ from sentry_sdk.utils import ContextVar
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from typing import Optional, Callable, Awaitable, Any, Union, TypeVar
14
+
15
+ F = TypeVar("F", bound=Union[Callable[..., Any], Callable[..., Awaitable[Any]]])
16
+
17
+ _ai_pipeline_name = ContextVar("ai_pipeline_name", default=None)
18
+
19
+
20
+ def set_ai_pipeline_name(name):
21
+ # type: (Optional[str]) -> None
22
+ _ai_pipeline_name.set(name)
23
+
24
+
25
+ def get_ai_pipeline_name():
26
+ # type: () -> Optional[str]
27
+ return _ai_pipeline_name.get()
28
+
29
+
30
+ def ai_track(description, **span_kwargs):
31
+ # type: (str, Any) -> Callable[[F], F]
32
+ def decorator(f):
33
+ # type: (F) -> F
34
+ def sync_wrapped(*args, **kwargs):
35
+ # type: (Any, Any) -> Any
36
+ curr_pipeline = _ai_pipeline_name.get()
37
+ op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline")
38
+
39
+ with start_span(name=description, op=op, **span_kwargs) as span:
40
+ for k, v in kwargs.pop("sentry_tags", {}).items():
41
+ span.set_tag(k, v)
42
+ for k, v in kwargs.pop("sentry_data", {}).items():
43
+ span.set_data(k, v)
44
+ if curr_pipeline:
45
+ span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline)
46
+ return f(*args, **kwargs)
47
+ else:
48
+ _ai_pipeline_name.set(description)
49
+ try:
50
+ res = f(*args, **kwargs)
51
+ except Exception as e:
52
+ event, hint = sentry_sdk.utils.event_from_exception(
53
+ e,
54
+ client_options=sentry_sdk.get_client().options,
55
+ mechanism={"type": "ai_monitoring", "handled": False},
56
+ )
57
+ sentry_sdk.capture_event(event, hint=hint)
58
+ raise e from None
59
+ finally:
60
+ _ai_pipeline_name.set(None)
61
+ return res
62
+
63
+ async def async_wrapped(*args, **kwargs):
64
+ # type: (Any, Any) -> Any
65
+ curr_pipeline = _ai_pipeline_name.get()
66
+ op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline")
67
+
68
+ with start_span(name=description, op=op, **span_kwargs) as span:
69
+ for k, v in kwargs.pop("sentry_tags", {}).items():
70
+ span.set_tag(k, v)
71
+ for k, v in kwargs.pop("sentry_data", {}).items():
72
+ span.set_data(k, v)
73
+ if curr_pipeline:
74
+ span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline)
75
+ return await f(*args, **kwargs)
76
+ else:
77
+ _ai_pipeline_name.set(description)
78
+ try:
79
+ res = await f(*args, **kwargs)
80
+ except Exception as e:
81
+ event, hint = sentry_sdk.utils.event_from_exception(
82
+ e,
83
+ client_options=sentry_sdk.get_client().options,
84
+ mechanism={"type": "ai_monitoring", "handled": False},
85
+ )
86
+ sentry_sdk.capture_event(event, hint=hint)
87
+ raise e from None
88
+ finally:
89
+ _ai_pipeline_name.set(None)
90
+ return res
91
+
92
+ if inspect.iscoroutinefunction(f):
93
+ return wraps(f)(async_wrapped) # type: ignore
94
+ else:
95
+ return wraps(f)(sync_wrapped) # type: ignore
96
+
97
+ return decorator
98
+
99
+
100
+ def record_token_usage(
101
+ span,
102
+ input_tokens=None,
103
+ input_tokens_cached=None,
104
+ output_tokens=None,
105
+ output_tokens_reasoning=None,
106
+ total_tokens=None,
107
+ ):
108
+ # type: (Span, Optional[int], Optional[int], Optional[int], Optional[int], Optional[int]) -> None
109
+
110
+ # TODO: move pipeline name elsewhere
111
+ ai_pipeline_name = get_ai_pipeline_name()
112
+ if ai_pipeline_name:
113
+ span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name)
114
+
115
+ if input_tokens is not None:
116
+ span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens)
117
+
118
+ if input_tokens_cached is not None:
119
+ span.set_data(
120
+ SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED,
121
+ input_tokens_cached,
122
+ )
123
+
124
+ if output_tokens is not None:
125
+ span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens)
126
+
127
+ if output_tokens_reasoning is not None:
128
+ span.set_data(
129
+ SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING,
130
+ output_tokens_reasoning,
131
+ )
132
+
133
+ if total_tokens is None and input_tokens is not None and output_tokens is not None:
134
+ total_tokens = input_tokens + output_tokens
135
+
136
+ if total_tokens is not None:
137
+ span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens)