# Migrate from 2.x to 3.x | Sentry for Python

##### Python SDK 3.0 pre-release

Version 3.0 of the Sentry Python SDK is currently in pre-release. If you feel like giving it a spin, check out [our most recent releases](https://pypi.org/project/sentry-sdk/#history). Your feedback at this stage is invaluable, so please let us know about your experience, whether positive or negative, [on GitHub](https://github.com/getsentry/sentry-python/discussions/3936) or [on Discord](https://discord.com/invite/Ww9hbqr): How did the migration go? Did you encounter any issues? Is everything working as expected?

This guide describes the common patterns involved in migrating to version `3.x` of the `sentry-python` SDK. For the full list of changes, check out the [detailed migration guide in the repository](https://github.com/getsentry/sentry-python/blob/potel-base/MIGRATION_GUIDE.md).

## [Python Version Support](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#python-version-support)

Sentry Python SDK `3.x` only supports Python 3.7 and higher. If you're on an older Python version, you'll need to stay on an older version of the SDK:

* Python 2.7-3.5: SDK `1.x`
* Python 3.6: SDK `2.x`

## [Configuration](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#configuration)

The `enable_tracing` option was removed. Use [`traces_sample_rate`](https://docs.sentry.io/platforms/python/configuration/options.md#traces_sample_rate) directly, or configure a [`traces_sampler`](https://docs.sentry.io/platforms/python/configuration/options.md#traces_sampler) for more fine-grained control over which spans should be sampled.

```python
  sentry_sdk.init(
-     enable_tracing=True,
+     traces_sample_rate=1.0,
  )
```

The deprecated `propagate_traces` option was removed. Use [`trace_propagation_targets`](https://docs.sentry.io/platforms/python/configuration/options.md#trace_propagation_targets) instead.

```python
  sentry_sdk.init(
      # don't propagate trace info downstream
-     propagate_traces=False,
+     trace_propagation_targets=[],
  )
```

Note that this only affects the global SDK option. The [`propagate_traces`](https://docs.sentry.io/platforms/python/integrations/celery.md#options) option of the Celery integration remains unchanged.

The `profiles_sample_rate` and `profiler_mode` options previously nested under `_experiments` have been removed. They're replaced by top-level options of the same name:

```python
  sentry_sdk.init(
-     _experiments={
-         "profiles_sample_rate": 1.0,
-         "profiler_mode": "thread",
-     },
+     profiles_sample_rate=1.0,
+     profiler_mode="thread",
  )
```

## [API Changes](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#api-changes)

`add_attachment()` is now a part of the top-level level API and should be imported and used directly from `sentry_sdk`.

```python
  import sentry_sdk

- scope = sentry_sdk.get_current_scope()
- scope.add_attachment(bytes=b"Hello World!", filename="attachment.txt")
+ sentry_sdk.add_attachment(bytes=b"Hello World!", filename="attachment.txt")
```

Using `sentry_sdk.add_attachment()` directly also makes sure the attachment is added to the correct scope internally.

### [Custom Tracing API](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#custom-tracing-api)

Tracing in the Sentry Python SDK `3.x` is powered by [OpenTelemetry](https://opentelemetry.io/) in the background, which also means we're moving away from the Sentry-specific concept of transactions and towards a span-only future. `sentry_sdk.start_transaction()` is now deprecated in favor of `sentry_sdk.start_span()`.

```python
- with sentry_sdk.start_transaction():
+ with sentry_sdk.start_span():
      ...
```

Any spans without a parent span will become transactions by default. If you want to avoid promoting a span without a parent to a transaction, you can pass the `only_if_parent=True` keyword argument to `sentry_sdk.start_span()`.

`sentry_sdk.start_transaction()` and `sentry_sdk.start_span()` no longer take the following arguments: `trace_id`, `baggage`, `span_id`, `parent_span_id`, `custom_sampling_context` (see below). Use `sentry_sdk.continue_trace()` for propagating trace data.

`sentry_sdk.continue_trace()` no longer returns a `Transaction` and is now a context manager. To continue a trace from headers or environment variables, start a new span inside `sentry_sdk.continue_trace()`:

```python
- transaction = sentry_sdk.continue_trace({...})
- with sentry_sdk.start_transaction(transaction=transaction):
-     ...
+ with sentry_sdk.continue_trace({...}):
+     with sentry_sdk.start_span():
+         ...
```

The functions `continue_from_headers`, `continue_from_environ` and `from_traceparent` have been removed. Use the `sentry_sdk.continue_trace()` context manager instead.

## [Span Data](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#span-data)

In OpenTelemetry, there is no concept of separate categories of data on a span: everything is simply a span attribute. This is a concept the Sentry SDK is also adopting. We deprecated `set_data()` and added a new span method called `set_attribute()`:

```python
  with sentry_sdk.start_span(...) as span:
-      span.set_data("my_attribute", "my_value")
+      span.set_attribute("my_attribute", "my_value")
```

You can also set attributes directly when creating the span. This has the advantage that these initial attributes will be accessible in the sampling context in your `traces_sampler`/`profiles_sampler` (see also the [Sampling section](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#sampling)).

```python
with sentry_sdk.start_span(attributes={"my_attribute": "my_value"}):
    ...
```

##### Span attribute type restrictions

There are important type restrictions to consider when setting attributes on a span via `span.set_attribute()` and `start_span(attributes={...})`. The keys must be non-empty strings and the values can only be several primitive types (excluding `None`) or a list of a single primitive type. See [the OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/common/#attribute) for details.

Note that since the SDK is now exclusively using span attributes, this restriction applies to other ways of setting data on a span as well like `span.set_data()`, `span.set_measurement()`, `span.set_context()`.

## [Sampling](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#sampling)

It's no longer possible to change the sampling decision of a span by setting `span.sampled` directly after the span has been created. Use either a custom `traces_sampler` (preferred) or the `sampled` argument to `start_span()` for determining whether a span should be sampled.

```python
with sentry_sdk.start_span(sampled=True) as span:
    ...
```

##### Sampling non-root spans

Both `traces_sampler` and the `sampled` argument will only influence whether root spans (transactions) are sampled. They can't be used for sampling child spans.

The `sampling_context` argument of `traces_sampler` and `profiles_sampler` has changed considerably for spans coming from our auto-instrumented integrations. As a consequence of using OpenTelemetry under the hood, spans can only carry specific, primitive types of data. This prevents us from making custom objects, for example, the `Request` object for several web frameworks, accessible on the span.

AIOHTTP sampling context changes

The AIOHTTP integration doesn't add the `aiohttp_request` object anymore. Instead, some of the individual properties of the request are accessible, if available, as follows:

| Request property  | Sampling context key(s)         |
| ----------------- | ------------------------------- |
| `path`            | `url.path`                      |
| `query_string`    | `url.query`                     |
| `method`          | `http.request.method`           |
| `host`            | `server.address`, `server.port` |
| `scheme`          | `url.scheme`                    |
| full URL          | `url.full`                      |
| `request.headers` | `http.request.header.{header}`  |

Celery sampling context changes

The Celery integration doesn't add the `celery_job` dictionary anymore. Instead, the individual keys are now available as:

| Dictionary keys        | Sampling context key        | Example                        |
| ---------------------- | --------------------------- | ------------------------------ |
| `celery_job["args"]`   | `celery.job.args.{index}`   | `celery.job.args.0`            |
| `celery_job["kwargs"]` | `celery.job.kwargs.{kwarg}` | `celery.job.kwargs.kwarg_name` |
| `celery_job["task"]`   | `celery.job.task`           |                                |

Tornado sampling context changes

The Tornado integration doesn't add the `tornado_request` object anymore. Instead, some of the individual properties of the request are accessible, if available, as follows:

| Request property  | Sampling context key(s)                             |
| ----------------- | --------------------------------------------------- |
| `path`            | `url.path`                                          |
| `query`           | `url.query`                                         |
| `protocol`        | `url.scheme`                                        |
| `method`          | `http.request.method`                               |
| `host`            | `server.address`, `server.port`                     |
| `version`         | `network.protocol.name`, `network.protocol.version` |
| full URL          | `url.full`                                          |
| `request.headers` | `http.request.header.{header}`                      |

WSGI sampling context changes

The WSGI integration doesn't add the `wsgi_environ` object anymore. Instead, the individual properties of the environment are accessible, if available, as follows:

| Env property      | Sampling context key(s)                           |
| ----------------- | ------------------------------------------------- |
| `PATH_INFO`       | `url.path`                                        |
| `QUERY_STRING`    | `url.query`                                       |
| `REQUEST_METHOD`  | `http.request.method`                             |
| `SERVER_NAME`     | `server.address`                                  |
| `SERVER_PORT`     | `server.port`                                     |
| `SERVER_PROTOCOL` | `server.protocol.name`, `server.protocol.version` |
| `wsgi.url_scheme` | `url.scheme`                                      |
| full URL          | `url.full`                                        |
| `HTTP_*`          | `http.request.header.{header}`                    |

ASGI sampling context changes

The ASGI integration doesn't add the `asgi_scope` object anymore. Instead, the individual properties of the scope, if available, are accessible as follows:

| Scope property | Sampling context key(s)         |
| -------------- | ------------------------------- |
| `type`         | `network.protocol.name`         |
| `scheme`       | `url.scheme`                    |
| `path`         | `url.path`                      |
| `query`        | `url.query`                     |
| `http_version` | `network.protocol.version`      |
| `method`       | `http.request.method`           |
| `server`       | `server.address`, `server.port` |
| `client`       | `client.address`, `client.port` |
| full URL       | `url.full`                      |
| `headers`      | `http.request.header.{header}`  |

RQ sampling context changes

The RQ integration doesn't add the `rq_job` object anymore. Instead, the individual properties of the job and the queue, if available, are accessible as follows:

| RQ property     | Sampling context key         | Example                |
| --------------- | ---------------------------- | ---------------------- |
| `rq_job.args`   | `rq.job.args.{index}`        | `rq.job.args.0`        |
| `rq_job.kwargs` | `rq.job.kwargs.{kwarg}`      | `rq.job.args.my_kwarg` |
| `rq_job.func`   | `rq.job.func`                |                        |
| `queue.name`    | `messaging.destination.name` |                        |
| `rq_job.id`     | `messaging.message.id`       |                        |

Note that `rq.job.args`, `rq.job.kwargs`, and `rq.job.func` are serialized and not the actual objects on the job.

AWS Lambda sampling context changes

The AWS Lambda integration doesn't add the `aws_event` and `aws_context` objects anymore. Instead, the following, if available, is accessible:

| AWS property                                | Sampling context key(s)         |
| ------------------------------------------- | ------------------------------- |
| `aws_event["httpMethod"]`                   | `http.request.method`           |
| `aws_event["queryStringParameters"]`        | `url.query`                     |
| `aws_event["path"]`                         | `url.path`                      |
| full URL                                    | `url.full`                      |
| `aws_event["headers"]["X-Forwarded-Proto"]` | `network.protocol.name`         |
| `aws_event["headers"]["Host"]`              | `server.address`                |
| `aws_context["function_name"]`              | `faas.name`                     |
| `aws_event["headers"]`                      | `http.request.headers.{header}` |

GCP sampling context changes

The GCP integration doesn't add the `gcp_env` and `gcp_event` keys anymore. Instead, the following, if available, is accessible:

| Old sampling context key          | New sampling context key       |
| --------------------------------- | ------------------------------ |
| `gcp_env["function_name"]`        | `faas.name`                    |
| `gcp_env["function_region"]`      | `faas.region`                  |
| `gcp_env["function_project"]`     | `gcp.function.project`         |
| `gcp_env["function_identity"]`    | `gcp.function.identity`        |
| `gcp_env["function_entry_point"]` | `gcp.function.entry_point`     |
| `gcp_event.method`                | `http.request.method`          |
| `gcp_event.query_string`          | `url.query`                    |
| `gcp_event.headers`               | `http.request.header.{header}` |

The ability to set `custom_sampling_context` on `start_transaction` was removed. If there is custom data that you want to have accessible in the `sampling_context` of a `traces_sampler` or `profiles_sampler`, set it on the span via the `attributes` argument, as all span attributes are now included in the `sampling_context` by default:

```python
- with start_transaction(custom_sampling_context={"custom_attribute": "custom_value"}):
+ with start_span(attributes={"custom_attribute": "custom_value"}) as span:
      # custom_attribute will now be accessible in the sampling context
      # of your traces_sampler/profiles_sampler
      ...
```

##### Span attribute type restrictions

As mentioned above, span attribute keys must be non-empty strings and values can only be several primitive types (excluding `None`) or a list of a single primitive type. See [the OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/common/#attribute) for details.

## [Errors](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#errors)

We've updated how we handle `ExceptionGroup`s. You will now get more data if `ExceptionGroup`s appear in chained exceptions. As an indirect consequence, you might notice a change in how issues are grouped in Sentry.

## [Integrations](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#integrations)

Additional integrations will now be activated automatically if the SDK detects the respective package is installed: Ariadne, ARQ, asyncpg, Chalice, clickhouse-driver, GQL, Graphene, huey, Loguru, PyMongo, Quart, Starlite, Strawberry. You can [opt-out of specific integrations with the `disabled_integrations` option](https://docs.sentry.io/platforms/python/integrations.md#disabling-integrations).

We no longer support Django older than 2.0, trytond older than 5.0, and Falcon older than 3.0.

### [Logging](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#logging)

The logging integration, which implements out-of-the-box support for the Python standard library `logging` framework, doesn't capture error logs as events anymore by default. The original behavior can still be achieved by providing a custom `event_level` to the `LoggingIntegration`:

```python
sentry_sdk.init(
    integrations=[
        # capture error, critical, exception logs
        # and send them to Sentry as errors
        LoggingIntegration(event_level="ERROR"),
    ],
)
```

### [Threading](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#threading)

The parameter `propagate_hub` has been removed from `ThreadingIntegration`. Use the new `propagate_scope` parameter instead. (If you had `ThreadingIntegration(propagate_hub=True)`, you can remove the parameter.)

### [clickhouse-driver](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#clickhouse-driver)

The query being executed is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`).

### [PyMongo](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#pymongo)

The PyMongo integration no longer sets tags automatically. The data is still accessible via span attributes.

The PyMongo integration doesn't set `operation_ids` anymore. The individual IDs (`operation_id`, `request_id`, `session_id`) are now accessible as separate span attributes.

### [Redis](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#redis)

In Redis pipeline spans, there is no `span["data"]["redis.commands"]` that contains a dictionary `{"count": 3, "first_ten": ["cmd1", "cmd2", ...]}`. Instead, there is `span["data"]["redis.commands.count"]` (containing `3`) and `span["data"]["redis.commands.first_ten"]` (containing `["cmd1", "cmd2", ...]`).

## [Measurements](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#measurements)

The `set_measurement()` API was removed. You can set custom attributes on the span instead with `set_attribute()`.

## [Sessions](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#sessions)

The `auto_session_tracking()` context manager was removed. Use `track_session()` instead.

## [Scope](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#scope)

Setting `Scope.user` directly is no longer supported. Use `Scope.set_user()` instead.

## [Metrics](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#metrics)

The `sentry_sdk.metrics` API doesn't exist anymore in SDK `3.x` as the [metrics beta has come to an end](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Coming-to-an-End). The associated experimental options `enable_metrics`, `before_emit_metric` and `metric_code_locations` have been removed as well.

## [Internals](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x/#internals)

There is no concept of a hub anymore and all APIs and attributes that were connected to hubs have been removed.
