Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-integrations-mcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10","3.13","3.14","3.14t","3.15","3.15t"]
python-version: ["3.10","3.11","3.13","3.14","3.14t","3.15","3.15t"]
os: [ubuntu-22.04]
env:
# 3.7 runs in the python:X.Y container; tell uv to use that system Python.
Expand Down
4 changes: 2 additions & 2 deletions scripts/aws/aws-attach-layer-to-lambda-function.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ echo "Getting ARN of newest Sentry lambda layer..."
LAYER_ARN=$(aws lambda list-layer-versions --layer-name SentryPythonServerlessSDK-local-dev --query "LayerVersions[0].LayerVersionArn" | tr -d '"')
echo "Done getting ARN of newest Sentry lambda layer $LAYER_ARN."

echo "Attaching Lamba layer to function $FUNCTION_NAME..."
echo "Attaching Lambda layer to function $FUNCTION_NAME..."
echo "Warning: This remove all other layers!"
aws lambda update-function-configuration \
--function-name "$FUNCTION_NAME" \
--layers "$LAYER_ARN" \
--no-cli-pager
echo "Done attaching Lamba layer to function '$FUNCTION_NAME'."
echo "Done attaching Lambda layer to function '$FUNCTION_NAME'."

echo "All done. Have a nice day!"
2 changes: 1 addition & 1 deletion scripts/build_aws_lambda_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def install_python_packages(self) -> None:
"pip",
"install",
"--no-cache-dir", # always access PyPI
"--no-deps", # the right depencencies have been installed in the call above
"--no-deps", # the right dependencies have been installed in the call above
"--quiet",
sentry_python_sdk,
"--target",
Expand Down
4 changes: 2 additions & 2 deletions scripts/find_raise_from_none.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ def main():
scan_file(module_path)

raise_from_none_count = {
file: len(occurences)
for file, occurences in RaiseFromNoneVisitor.line_numbers.items()
file: len(occurrences)
for file, occurrences in RaiseFromNoneVisitor.line_numbers.items()
}
if raise_from_none_count:
exc = Exception("Detected unexpected raise ... from None.")
Expand Down
42 changes: 14 additions & 28 deletions scripts/init_serverless_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
'sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler'
"""

import importlib
import os
import re
import sys
from typing import TYPE_CHECKING

import sentry_sdk
Expand All @@ -27,8 +26,6 @@


class AWSLambdaModuleLoader:
DIR_PATH_REGEX = r"^(.+)\/([^\/]+)$"

def __init__(self, sentry_initial_handler):
try:
module_path, self.handler_name = sentry_initial_handler.rsplit(".", 1)
Expand All @@ -41,35 +38,24 @@ def extract_and_load_lambda_function_module(self, module_path):
"""
Method that extracts and loads lambda function module from module_path
"""
py_version = sys.version_info

if re.match(self.DIR_PATH_REGEX, module_path):
# With a path like -> `scheduler/scheduler/event`
# `module_name` is `event`, and `module_file_path` is `scheduler/scheduler/event.py`
module_name = module_path.split(os.path.sep)[-1]
module_file_path = module_path + ".py"

# Supported python versions are 3.7, 3.8
if py_version >= (3, 7):
import importlib.util

spec = importlib.util.spec_from_file_location(
module_name, module_file_path
)
self.lambda_function_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.lambda_function_module)
else:
raise ValueError("Python version %s is not supported." % py_version)
else:
import importlib

self.lambda_function_module = importlib.import_module(module_path)
# the handler used to be loaded from a file path, so keep accepting
# `./dir/file` and absolute `<task root>/dir/file` by making them relative
# to the task root, which is on `sys.path`.
task_root = os.environ.get("LAMBDA_TASK_ROOT", os.getcwd())
module_path = os.path.relpath(os.path.join(task_root, module_path), task_root)

# like the Lambda runtime: `dir/file` is the module `dir.file`, which `sys.modules`
# keeps, so its code only runs on the first invocation.
# https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/fa2462d93d11da632aad8f8bfb07a3e060438150/awslambdaric/bootstrap.py#L53
self.lambda_function_module = importlib.import_module(
module_path.replace("/", ".")
)

def get_lambda_handler(self):
return getattr(self.lambda_function_module, self.handler_name)


def sentry_lambda_handler(event: "Any", context: "Any") -> None:
def sentry_lambda_handler(event: "Any", context: "Any") -> "Any":
"""
Handler function that invokes a lambda handler which path is defined in
environment variables as "SENTRY_INITIAL_HANDLER"
Expand Down
1 change: 1 addition & 0 deletions scripts/populate_tox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@
},
"redis": {
"package": "redis",
"include": "!=8.2.0b1",
"deps": {
"*": ["fakeredis!=1.7.4", "pytest<8.0.0", "pytest-asyncio"],
">=4.0,<5.0": ["fakeredis<2.31.0"],
Expand Down
351 changes: 233 additions & 118 deletions scripts/populate_tox/package_dependencies.jsonl

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions scripts/populate_tox/populate_tox.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def fetch_package_dependencies(
print(f" Resolving dependencies: {package}=={version} on Python {python_version}")

# Removing non-report output with -qqq may be brittle, but avoids file I/O.
# Currently -qqq supresses all non-report output that would break json.loads().
# Currently -qqq suppresses all non-report output that would break json.loads().
result = subprocess.run(
[
"uv",
Expand Down Expand Up @@ -1327,6 +1327,11 @@ def main() -> dict[str, list]:

write_tox_file(packages)

# Unused cache entries are only pruned on full runs. With --only/--skip,
# skipped integrations don't touch (most of) their cache entries, so pruning
# would wipe them.
prune_cache = not (only_update or skip_update)

# Sort the release cache file
releases = []
with open(RELEASES_CACHE_FILE) as releases_cache:
Expand All @@ -1335,7 +1340,8 @@ def main() -> dict[str, list]:
with open(RELEASES_CACHE_FILE, "w") as releases_cache:
for release in releases:
if (
CACHE[_normalize_name(release["info"]["name"])][
not prune_cache
or CACHE[_normalize_name(release["info"]["name"])][
release["info"]["version"]
]["_accessed"]
is True
Expand All @@ -1350,7 +1356,8 @@ def main() -> dict[str, list]:
with open(DEPENDENCIES_CACHE_FILE, "w") as releases_cache:
for release in releases:
if (
DEPENDENCIES_CACHE[release["python_version"]][
not prune_cache
or DEPENDENCIES_CACHE[release["python_version"]][
release["install_targets_hash"]
]["_accessed"]
is True
Expand Down
58 changes: 33 additions & 25 deletions scripts/populate_tox/releases.jsonl

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/split_tox_gh_actions/split_tox_gh_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
This way each group of frameworks defined in tox.ini will get its own GitHub actions configuration file
which allows them to be run in parallel in GitHub actions.

This will generate/update several configuration files, that need to be commited to Git afterwards.
This will generate/update several configuration files, that need to be committed to Git afterwards.
Whenever tox.ini is changed, this script needs to be run.

Usage:
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ class _Client(BaseClient):
the client options as keyword arguments and optionally the DSN as first
argument.

Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support)
Alias of :py:class:`sentry_sdk.Client`. (Was created for better IntelliSense support)
"""

def __init__(self, *args: "Any", **kwargs: "Any") -> None:
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/django/signals_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def _get_receiver_name(receiver: "Callable[..., Any]") -> str:
name = receiver.__name__
elif hasattr(
receiver, "func"
): # certain functions (like partials) dont have a name
): # certain functions (like partials) don't have a name
if hasattr(receiver, "func") and hasattr(receiver.func, "__name__"):
name = "partial(<function " + receiver.func.__name__ + ">)"

Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/huggingface_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any":
prompt = args[1]

if prompt is None:
# invalid call, dont instrument, let it return error
# invalid call, don't instrument, let it return error
return f(*args, **kwargs)

hf_client = args[0]
Expand Down
4 changes: 2 additions & 2 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ async def _sentry_exceptionmiddleware_call(
# Also add the user (that was eventually set by be Authentication middle
# that was called before this middleware). This is done because the authentication
# middleware sets the user in the scope and then (in the same function)
# calls this exception middelware. In case there is no exception (or no handler
# for the type of exception occuring) then the exception bubbles up and setting the
# calls this exception middleware. In case there is no exception (or no handler
# for the type of exception occurring) then the exception bubbles up and setting the
# user information into the sentry scope is done in auth middleware and the
# ASGI middleware will then send everything to Sentry and this is fine.
# But if there is an exception happening that the exception middleware here
Expand Down
4 changes: 2 additions & 2 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,7 @@ def strip_string(
def parse_version(version: str) -> "Optional[Tuple[int, ...]]":
"""
Parses a version string into a tuple of integers.
This uses the parsing loging from PEP 440:
This uses the parsing logic from PEP 440:
https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
"""
VERSION_PATTERN = r""" # noqa: N806
Expand Down Expand Up @@ -1463,7 +1463,7 @@ def sanitize_url(
def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
"""
Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
parameters will be sanitized to remove sensitive data. The autority (username and password)
parameters will be sanitized to remove sensitive data. The authority (username and password)
in the URL will always be removed.
"""
parsed_url = sanitize_url(
Expand Down
4 changes: 2 additions & 2 deletions tests/integrations/asgi/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ def test_get_ip():
ip = _get_ip(scope)
assert ip == "127.0.0.1"

# x-forwarded-for header overides the ip from client
# x-forwarded-for header overrides the ip from client
headers = [
(b"x-forwarded-for", b"8.8.8.8"),
]
Expand All @@ -624,7 +624,7 @@ def test_get_ip():
ip = _get_ip(scope)
assert ip == "8.8.8.8"

# x-real-for header overides the ip from client
# x-real-for header overrides the ip from client
headers = [
(b"x-real-ip", b"10.10.10.10"),
]
Expand Down
8 changes: 4 additions & 4 deletions tests/integrations/asyncio/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def test_sentry_task_factory_no_factory(MockTask, mock_get_running_loop): # noq
# Set the original task factory to None
mock_loop.get_task_factory.return_value = None

# Retieve sentry task factory (since it is an inner function within patch_asyncio)
# Retrieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)

# The call we are testing
Expand Down Expand Up @@ -278,7 +278,7 @@ def test_sentry_task_factory_with_factory(mock_get_running_loop):
orig_task_factory = mock_loop.get_task_factory.return_value
orig_task_factory._is_sentry_task_factory = False

# Retieve sentry task factory (since it is an inner function within patch_asyncio)
# Retrieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)

# The call we are testing
Expand Down Expand Up @@ -310,7 +310,7 @@ def test_sentry_task_factory_context_no_factory(
# Set the original task factory to None
mock_loop.get_task_factory.return_value = None

# Retieve sentry task factory (since it is an inner function within patch_asyncio)
# Retrieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)

# The call we are testing
Expand Down Expand Up @@ -343,7 +343,7 @@ def test_sentry_task_factory_context_with_factory(mock_get_running_loop):
orig_task_factory = mock_loop.get_task_factory.return_value
orig_task_factory._is_sentry_task_factory = False

# Retieve sentry task factory (since it is an inner function within patch_asyncio)
# Retrieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)

# The call we are testing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# counts the invocations since this module was loaded; gets reset on cold start or
# when the module is reloaded.
num_invocations = 0


def handler(event, context):
global num_invocations
num_invocations += 1
return {"num_invocations": num_invocations}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# counts the invocations since this module was loaded; gets reset on cold start or
# when the module is reloaded.
num_invocations = 0


def handler(event, context):
global num_invocations
num_invocations += 1
return {"num_invocations": num_invocations}
15 changes: 15 additions & 0 deletions tests/integrations/aws_lambda/test_aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ def test_init_error(lambda_client, test_environment):
assert segment_spans[0]["name"] == "InitError"


@pytest.mark.parametrize("function_name", ["PathHandler", "AbsolutePathHandler"])
def test_path_handler_loaded_once(lambda_client, test_environment, function_name):
num_invocations = [
json.loads(
lambda_client.invoke(FunctionName=function_name, Payload=json.dumps({}))[
"Payload"
].read()
)["num_invocations"]
for _ in range(2)
]
# Sentry layer should load the function's module once (on cold start or whenever
# the module is reloaded), not on every warm invocation.
assert num_invocations == [1, 2]


def test_timeout_error_scope_modified(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="TimeoutErrorScopeModified",
Expand Down
10 changes: 9 additions & 1 deletion tests/integrations/aws_lambda/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
"BasicOkSpanStreamingDataCollection": ("prod", "5"),
}

# functions whose handler is a path instead of `index.handler`.
INITIAL_HANDLERS = {
"PathHandler": "app/index.handler",
"AbsolutePathHandler": "/var/task/app/index.handler",
}

PYTHON_VERSION = f"python{sys.version_info.major}.{sys.version_info.minor}"

# Match the host CPU architecture so local runs on ARM machines (e.g. macOS)
Expand Down Expand Up @@ -124,7 +130,9 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
"Environment": {
"Variables": {
"SENTRY_DSN": dsn,
"SENTRY_INITIAL_HANDLER": "index.handler",
"SENTRY_INITIAL_HANDLER": INITIAL_HANDLERS.get(
lambda_dir, "index.handler"
),
"SENTRY_TRACES_SAMPLE_RATE": "1.0",
}
},
Expand Down
4 changes: 2 additions & 2 deletions tests/integrations/gcp/test_gcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ def cloud_function(functionhandler, event):

def test_error_has_new_trace_context_performance_disabled(run_cloud_function):
"""
Check if an 'trace' context is added to errros and transactions when performance monitoring is disabled.
Check if a 'trace' context is added to errors and transactions when performance monitoring is disabled.
"""
envelope_items, _, _ = run_cloud_function(
dedent(
Expand Down Expand Up @@ -399,7 +399,7 @@ def cloud_function(functionhandler, event):

def test_error_has_existing_trace_context_performance_disabled(run_cloud_function):
"""
Check if an 'trace' context is added to errros and transactions
Check if a 'trace' context is added to errors and transactions
from the incoming 'sentry-trace' header when performance monitoring is disabled.
"""
trace_id = "471a43a4192642f0b136d5159a501701"
Expand Down
2 changes: 1 addition & 1 deletion tests/integrations/grpc/compile_test_services.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ python -m grpc_tools.protoc \
--grpc_python_out=$TARGET_PATH/ \
$TARGET_PATH/protos/grpc_test_service.proto

echo Code generation successfull
echo Code generation successful
2 changes: 1 addition & 1 deletion tests/integrations/sanic/test_sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def new_test_client(self):
Sanic.test_client = property(new_test_client)

if SANIC_VERSION >= (20, 12) and SANIC_VERSION < (22, 6):
# Some builds (20.12.0 intruduced and 22.6.0 removed again) have a feature where the instance is stored in an internal class
# Some builds (20.12.0 introduced and 22.6.0 removed again) have a feature where the instance is stored in an internal class
# registry for later retrieval, and so add register=False to disable that
sanic_app = Sanic("Test", register=False)
else:
Expand Down
4 changes: 2 additions & 2 deletions tests/integrations/tornado/test_tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ def test_error_has_existing_trace_context_performance_enabled(
tornado_testcase, sentry_init, capture_items
):
"""
Check if an 'trace' context is added to events
Check if a 'trace' context is added to events
from the incoming 'sentry-trace' header when performance monitoring is enabled.
"""
sentry_init(
Expand Down Expand Up @@ -739,7 +739,7 @@ def test_error_has_existing_trace_context_performance_disabled(
tornado_testcase, sentry_init, capture_events
):
"""
Check if an 'trace' context is added to events
Check if a 'trace' context is added to events
from the incoming 'sentry-trace' header when performance monitoring is disabled.
"""
sentry_init(
Expand Down
Loading
Loading