From a9d2d6702d771067bd8be7e783c271cc9bd4c4eb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 27 Oct 2026 11:30:44 +0100 Subject: [PATCH 1/3] feat(o11y): Make SDK 3.0 compatible --- devservices/commands/up.py | 63 ++++++++++++++++++++++---------------- devservices/main.py | 51 +++++++++++++++++++----------- pyproject.toml | 2 +- uv.lock | 8 ++--- 4 files changed, 74 insertions(+), 50 deletions(-) diff --git a/devservices/commands/up.py b/devservices/commands/up.py index 54bbf32..a11bab3 100644 --- a/devservices/commands/up.py +++ b/devservices/commands/up.py @@ -10,7 +10,7 @@ from sentry_sdk import capture_exception from sentry_sdk import logger as sentry_logger from sentry_sdk import set_context -from sentry_sdk import start_span +from sentry_sdk.traces import start_span from devservices.constants import CONFIG_FILE_NAME from devservices.constants import DEPENDENCY_CONFIG_VERSION @@ -133,7 +133,8 @@ def up(args: Namespace, existing_status: Status | None = None) -> None: ) as status: local_runtime_dependency_names = set() with start_span( - op="service.dependencies.check", name="Check local runtime dependencies" + name="Check local runtime dependencies", + attributes={"sentry.op": "service.dependencies.check"}, ) as span: services_with_local_runtime = state.get_services_by_runtime( ServiceRuntime.LOCAL @@ -149,13 +150,14 @@ def up(args: Namespace, existing_status: Status | None = None) -> None: status.warning( f"Skipping '{service_with_local_runtime}' as it is set to run locally" ) - span.set_data("service_name", service.name) - span.set_data("mode", mode) - span.set_data( + span.set_attribute("service_name", service.name) + span.set_attribute("mode", mode) + span.set_attribute( "local_runtime_dependency_count", len(local_runtime_dependency_names) ) - span.set_data( - "local_runtime_dependency_names", local_runtime_dependency_names + span.set_attribute( + "local_runtime_dependency_names", + sorted(local_runtime_dependency_names), ) context_name = f"local_runtime_dependencies.{service.name}" @@ -219,11 +221,12 @@ def up(args: Namespace, existing_status: Status | None = None) -> None: ) status.warning(f"Continuing with service '{service.name}'") with start_span( - op="service.containers.start", name="Start service containers" + name="Start service containers", + attributes={"sentry.op": "service.containers.start"}, ) as span: - span.set_data("service_name", service.name) - span.set_data("mode", mode) - span.set_data("exclude_local", exclude_local) + span.set_attribute("service_name", service.name) + span.set_attribute("mode", mode) + span.set_attribute("exclude_local", exclude_local) try: bring_up_docker_compose_services( service, @@ -238,10 +241,11 @@ def up(args: Namespace, existing_status: Status | None = None) -> None: status.failure(f"Failed to start {service.name}: {dce.stderr}") exit(1) with start_span( - op="service.supervisor.start", name="Start supervisor programs" + name="Start supervisor programs", + attributes={"sentry.op": "service.supervisor.start"}, ) as span: - span.set_data("service_name", service.name) - span.set_data("supervisor_programs", supervisor_programs) + span.set_attribute("service_name", service.name) + span.set_attribute("supervisor_programs", supervisor_programs) try: bring_up_supervisor_programs(service, supervisor_programs, status) except SupervisorError as se: @@ -257,14 +261,15 @@ def _install_service_dependencies( service: Service, mode: str, status: Status, offline: bool = False ) -> set[InstalledRemoteDependency]: with start_span( - op="service.dependencies.install", name="Install dependencies" + name="Install dependencies", + attributes={"sentry.op": "service.dependencies.install"}, ) as span: status.info( "Using cached dependencies" if offline else "Retrieving dependencies" ) - span.set_data("service_name", service.name) - span.set_data("mode", mode) - span.set_data("offline", offline) + span.set_attribute("service_name", service.name) + span.set_attribute("mode", mode) + span.set_attribute("offline", offline) update_mode = ( DependencyUpdateMode.OFFLINE if offline else DependencyUpdateMode.FORCE @@ -275,7 +280,7 @@ def _install_service_dependencies( update_mode, modes=[mode], ) - span.set_data("remote_dependency_count", len(remote_dependencies)) + span.set_attribute("remote_dependency_count", len(remote_dependencies)) return remote_dependencies except DependencyError as de: capture_exception(de) @@ -295,7 +300,8 @@ def _install_service_dependencies( def _create_devservices_network() -> None: with start_span( - op="service.network.create", name="Create devservices network" + name="Create devservices network", + attributes={"sentry.op": "service.network.create"}, ) as span: try: subprocess.run( @@ -304,17 +310,19 @@ def _create_devservices_network() -> None: stderr=subprocess.DEVNULL, check=True, ) - span.set_data("network_created", True) + span.set_attribute("network_created", True) except subprocess.CalledProcessError: # Network already exists, ignore the error - span.set_data("network_created", False) + span.set_attribute("network_created", False) def _pull_dependency_images( cmd: DockerComposeCommand, current_env: dict[str, str], status: Status ) -> None: - with start_span(op="service.images.pull", name="Pull dependency images") as span: - span.set_data("command", cmd.full_command) + with start_span( + name="Pull dependency images", attributes={"sentry.op": "service.images.pull"} + ) as span: + span.set_attribute("command", cmd.full_command) run_cmd(cmd.full_command, current_env, retries=4) for dependency in cmd.services: status.info(f"Pulled image for {dependency}") @@ -324,10 +332,11 @@ def _bring_up_dependency( cmd: DockerComposeCommand, current_env: dict[str, str], status: Status ) -> None: with start_span( - op="service.containers.up", name="Bring up dependency containers" + name="Bring up dependency containers", + attributes={"sentry.op": "service.containers.up"}, ) as span: - span.set_data("command", cmd.full_command) - span.set_data("services", cmd.services) + span.set_attribute("command", cmd.full_command) + span.set_attribute("services", cmd.services) for dependency in cmd.services: status.info(f"Starting {dependency}") run_cmd(cmd.full_command, current_env) diff --git a/devservices/main.py b/devservices/main.py index d7bbac7..e126188 100644 --- a/devservices/main.py +++ b/devservices/main.py @@ -7,16 +7,18 @@ import os import platform from importlib import metadata +from typing import Any +from sentry_sdk import VERSION as SENTRY_SDK_VERSION from sentry_sdk import capture_exception from sentry_sdk import flush from sentry_sdk import init from sentry_sdk import set_context from sentry_sdk import set_tag from sentry_sdk import set_user -from sentry_sdk import start_transaction from sentry_sdk.integrations.argv import ArgvIntegration from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk.traces import start_span from sentry_sdk.types import Event from sentry_sdk.types import Hint @@ -49,6 +51,8 @@ disable_sentry = os.environ.get("DEVSERVICES_DISABLE_SENTRY", default="0") == "1" logging.basicConfig(level=logging.INFO) current_version = metadata.version("devservices") +# devservices supports both sentry-sdk 2.x and 3.x +sentry_sdk_major_version = int(SENTRY_SDK_VERSION.split(".")[0]) error_trace_ids = set() @@ -64,25 +68,26 @@ def before_send_error(event: Event, hint: Hint) -> Event: return event -def before_send_transaction(event: Event, hint: Hint) -> Event: - """Manually sets the status of a transaction. - - This function is used as a before_send_transaction callback for Sentry to mark transaction status - as unknown if they don't correspond to errors we care about. - """ - if event["contexts"]["trace"]["trace_id"] not in error_trace_ids: - event["contexts"]["trace"]["status"] = "unknown" - return event - - if not disable_sentry: + # sentry-sdk 3.x renamed LoggingIntegration's sentry_logs_level option to level + # and captures Sentry logs by default, which 2.x only does with capture_sentry_logs + if sentry_sdk_major_version >= 3: + logging_integration_options: dict[str, Any] = {"level": logging.DEBUG} + else: + logging_integration_options = { + "sentry_logs_level": logging.DEBUG, + "capture_sentry_logs": True, + } + init( dsn="https://56470da7302c16e83141f62f88e46449@o1.ingest.us.sentry.io/4507946704961536", traces_sample_rate=1.0, - profiles_sample_rate=1.0, + trace_lifecycle="stream", + profile_session_sample_rate=1.0, + profile_lifecycle="trace", integrations=[ ArgvIntegration(), - LoggingIntegration(sentry_logs_level=logging.DEBUG), + LoggingIntegration(**logging_integration_options), ], # By default the Sentry SDK loads integrations for all relevant libraries in # the venv. When running devservices in the seer directory we end up loading @@ -91,10 +96,8 @@ def before_send_transaction(event: Event, hint: Hint) -> Event: # actually using those libraries. By disabling this behaviour we can reduce # startup time for devservices. auto_enabling_integrations=False, - enable_logs=True, environment=sentry_environment, before_send=before_send_error, - before_send_transaction=before_send_transaction, release=current_version, ) username = getpass.getuser() @@ -178,8 +181,20 @@ def main() -> None: logger.setLevel(logging.DEBUG) if args.command: - with start_transaction(op="command", name=args.command): - args.func(args) + exc: BaseException | None = None + with start_span(name=args.command, attributes={"sentry.op": "command"}) as span: + try: + args.func(args) + except BaseException as e: + exc = e + + if span.trace_id in error_trace_ids: + span.status = "error" + else: + span.status = "ok" + + if exc is not None: + raise exc else: parser.print_help() diff --git a/pyproject.toml b/pyproject.toml index 4b8d09a..34a6d7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.11" dependencies = [ "pyyaml", "sentry-devenv", - "sentry-sdk", + "sentry-sdk>=2.68.0,<4", "packaging", "supervisor>=4.3.0", ] diff --git a/uv.lock b/uv.lock index 888b8e0..598be45 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.11" [[package]] @@ -99,7 +99,7 @@ requires-dist = [ { name = "packaging" }, { name = "pyyaml" }, { name = "sentry-devenv" }, - { name = "sentry-sdk" }, + { name = "sentry-sdk", specifier = ">=2.68.0,<4" }, { name = "supervisor", specifier = ">=4.3.0" }, ] @@ -389,14 +389,14 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.54.0" +version = "2.73.0" source = { registry = "https://pypi.devinfra.sentry.io/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] wheels = [ - { url = "https://pypi.devinfra.sentry.io/wheels/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de" }, + { url = "https://pypi.devinfra.sentry.io/wheels/sentry_sdk-2.73.0-py3-none-any.whl", hash = "sha256:0bcfa6928ea5adbb89591858b514d27adac27c7a3cccc31b20ed10e0b09eda9e" }, ] [[package]] From 7375932248b18ec32334bf779d1709c6e50b3d0f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 27 Oct 2026 11:34:20 +0100 Subject: [PATCH 2/3] add test --- .github/workflows/test.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed8bdba..0a573ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,3 +40,23 @@ jobs: uses: codecov/test-results-action@1b5b448b98e58ba90d1a1a1d9fcb72ca2263be46 # v1.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} + + test-sentry-sdk-3: + runs-on: ubuntu-latest + env: + SENTRY_SDK: sentry-sdk>=3.0.0b2,<4 + steps: + - name: Configure git + run: | + git config --global user.email "test@example.com" + git config --global user.name "test" + - name: Checkout repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Set up uv + uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + - name: Install dependencies + run: uv sync --python 3.13.1 + - name: Type check + run: uv run --with "$SENTRY_SDK" mypy devservices tests + - name: Run tests + run: uv run --with "$SENTRY_SDK" pytest -n4 From 1b7ad2eccb6feda63b612bb9b9c6590b75876f53 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 27 Oct 2026 11:43:14 +0100 Subject: [PATCH 3/3] revert --- .github/workflows/test.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0a573ed..ed8bdba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,23 +40,3 @@ jobs: uses: codecov/test-results-action@1b5b448b98e58ba90d1a1a1d9fcb72ca2263be46 # v1.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} - - test-sentry-sdk-3: - runs-on: ubuntu-latest - env: - SENTRY_SDK: sentry-sdk>=3.0.0b2,<4 - steps: - - name: Configure git - run: | - git config --global user.email "test@example.com" - git config --global user.name "test" - - name: Checkout repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Set up uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 - - name: Install dependencies - run: uv sync --python 3.13.1 - - name: Type check - run: uv run --with "$SENTRY_SDK" mypy devservices tests - - name: Run tests - run: uv run --with "$SENTRY_SDK" pytest -n4