Repository: getsentry/sentry-python

Issue:
AttributeError: 'dict' object has no attribute 'f_back' in profiler thread
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.16.0

### Steps to Reproduce

Run sentry SDK with profiling on, Python 3.11

### Expected Result

The thread runs fine without crashing.

### Actual Result

The profiler thread (`<Thread(sentry.profiler.ThreadScheduler, started daemon 140454125577984)>`) fails this stack trace:

```
AttributeError: 'dict' object has no attribute 'f_back'
  File "threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
```

Unfortunately there is no more information. It looks like the full stack trace is not captured / lost, perhaps because this is an exception within a thread?

Full screenshot from Sentry UI:

<img width="1134" alt="Xnapper-2023-03-15-14 21 38" src="https://user-images.githubusercontent.com/857609/225338517-a1d7cfb3-20f6-45b1-a0e6-69cfa77d7e1e.png">

It's not clear from a quick glance at the source how a `dict` could be ending up in the profiler thread's queue.


Discussion:
Hey @adamchainz !

Thanks for reporting this. We will see if we can reproduce this!
Hey @adamchainz, are you able to share the following?
- What frameworks do you use to run your application?
- How do you run it?
- How does your `sentry_sdk.init` look like?
- Do you do any monkey patching in your application? Or is there some monkey patching happening from a package you use?
>     * What frameworks do you use to run your application?

Django

>     * How do you run it?

Gunicorn

>     * How does your `sentry_sdk.init` look like?

```python
sentry_sdk.init(
    dsn=os.environ["RAVEN_DSN"],
    integrations=[
        DjangoIntegration(),
    ],
    send_default_pii=True,
    release=GIT_HASH,
    environment=ENV,
    server_name=env("HOSTNAME"),
    request_bodies="medium",
    before_send=sentry_before_send,
    traces_sampler=traces_sampler,
    _experiments={
        "profiles_sample_rate": 1.0,
    },
)
```

`sentry_before_send` just drops `KeyboardInterrupt`

`traces_sampler` returns 0.0 for some high traffic paths that don't need profiling, like the heartbeat url.

>     * Do you do any monkey patching in your application? Or is there some monkey patching happening from a package you use?

It's hard to audit for monkey patching, but I've grepped. I don't see any monkey patching of things from `sentry_sdk` or `threading`. There’s one patch on requests [as per my blog post](https://adamj.eu/tech/2022/06/23/how-to-patch-requests-to-have-a-default-timeout/) but that doesn't seem relevant here.
Not sure if helpful, but we've had the same issue also using `gunicorn` but instead of Django we use Flask (with python 3.11).

Our init looks like:
```
sentry_sdk.init(
    integrations=[FlaskIntegration()],
    attach_stacktrace=True,
    traces_sample_rate=1.0,
    _experiments={
        "profiles_sample_rate": 1.0,
    },
    environment=Config.APP_ENV,
)
```

We went back to 1.15.0, which resolved the issues for us. 
> Not sure if helpful, but we've had the same issue also using `gunicorn` but instead of Django we use Flask (with python 3.11).
> 
> Our init looks like:
> 
> ```
> sentry_sdk.init(
>     integrations=[FlaskIntegration()],
>     attach_stacktrace=True,
>     traces_sample_rate=1.0,
>     _experiments={
>         "profiles_sample_rate": 1.0,
>     },
>     environment=Config.APP_ENV,
> )
> ```
> 
> We went back to 1.15.0, which resolved the issues for us.

Thanks for the additional info! That's helpful to know

@LiveNL @adamchainz One follow-up question for the two of you. Is this error preventing you from collecting profiles completely? Or is it happening occasionally and you are still able to collect some profiles?

Resolving patch:
diff --git a/sentry_sdk/profiler.py b/sentry_sdk/profiler.py
--- a/sentry_sdk/profiler.py
+++ b/sentry_sdk/profiler.py
@@ -26,6 +26,7 @@
 from sentry_sdk._compat import PY33, PY311
 from sentry_sdk._types import TYPE_CHECKING
 from sentry_sdk.utils import (
+    capture_internal_exception,
     filename_for_module,
     is_valid_sample_rate,
     logger,
@@ -252,8 +253,16 @@ def extract_stack(
     frames = deque(maxlen=max_stack_depth)  # type: Deque[FrameType]
 
     while frame is not None:
+        try:
+            f_back = frame.f_back
+        except AttributeError:
+            capture_internal_exception(sys.exc_info())
+            # For some reason, the frame we got isn't a `FrameType` and doesn't
+            # have a `f_back`. When this happens, we continue with any frames
+            # that we've managed to extract up to this point.
+            break
         frames.append(frame)
-        frame = frame.f_back
+        frame = f_back
 
     if prev_cache is None:
         stack = tuple(extract_frame(frame, cwd) for frame in frames)


Test patch: