Repository: wandb/wandb

Issue:
[CLI]: W&B crashes Sentry integration
### Describe the bug

<!--- Description of the issue below  -->
Importing `wandb` module disables [Sentry](https://sentry.io/) integration.

<!--- A minimal code snippet between the quotes below  -->
```python 
import sentry_sdk
import os

sentry_sdk.init(
    os.environ.get("SENTRY_DSN", ""),
    environment="development",
)
import wandb  # <=== Comment this line or replace with any other import and everything works fine again
raise Exception()  # <=== This exception is no longer handled by Sentry
```


### Additional Files

_No response_

### Environment

WandB version: tested on 0.12.10 - 0.12.13

OS: Ubuntu 20.04

Python version: Python 3.9.6

Versions of relevant libraries: sentry_sdk==1.5.8


### Additional Context

In real scenario the import comes from unpickling a class, which imports `wandb`.


Discussion:
Hi @iamhatesz,
Thank you for reporting. I was able to replicate the issue but also found that if you `import wandb` before `import sentry_sdk` then both will function properly.

Let me know if this is an acceptable workaround or if you would like us to investigate this further.

Thank you,
Nate
@nate-wandb that's the workaround I've used as well. However, a good practice is that package import should have no side-effects, especially on other packages.
@iamhatesz I'll report this to our engineering team to look into and let you know if we implement a fix on this.

Thank you again for reporting. 

Resolving patch:
diff --git a/wandb/util.py b/wandb/util.py
--- a/wandb/util.py
+++ b/wandb/util.py
@@ -50,7 +50,6 @@
 
 import requests
 import sentry_sdk  # type: ignore
-from sentry_sdk import capture_exception, capture_message
 import shortuuid  # type: ignore
 import wandb
 from wandb.env import error_reporting_enabled, get_app_url, SENTRY_DSN
@@ -99,19 +98,23 @@ def get_platform_name() -> str:
 
 
 # TODO(sentry): This code needs to be moved, sentry shouldn't be initialized as a
-# side effect of loading a module.
+#  side effect of loading a module.
+sentry_client: Optional["sentry_sdk.client.Client"] = None
+sentry_hub: Optional["sentry_sdk.hub.Hub"] = None
+sentry_default_dsn = (
+    "https://a2f1d701163c42b097b9588e56b1c37e@o151352.ingest.sentry.io/5288891"
+)
 if error_reporting_enabled():
-    default_dsn = (
-        "https://a2f1d701163c42b097b9588e56b1c37e@o151352.ingest.sentry.io/5288891"
-    )
-    sentry_dsn = os.environ.get(SENTRY_DSN, default_dsn)
-    sentry_sdk.init(
+    sentry_dsn = os.environ.get(SENTRY_DSN, sentry_default_dsn)
+    sentry_client = sentry_sdk.Client(
         dsn=sentry_dsn,
-        release=wandb.__version__,
         default_integrations=False,
         environment=SENTRY_ENV,
+        release=wandb.__version__,
     )
 
+    sentry_hub = sentry_sdk.Hub(sentry_client)
+
 POW_10_BYTES = [
     ("B", 10**0),
     ("KB", 10**3),
@@ -135,7 +138,8 @@ def get_platform_name() -> str:
 
 def sentry_message(message: str) -> None:
     if error_reporting_enabled():
-        capture_message(message)
+        sentry_hub.capture_message(message)  # type: ignore
+    return None
 
 
 def sentry_exc(
@@ -153,11 +157,12 @@ def sentry_exc(
 ) -> None:
     if error_reporting_enabled():
         if isinstance(exc, str):
-            capture_exception(Exception(exc))
+            sentry_hub.capture_exception(Exception(exc))  # type: ignore
         else:
-            capture_exception(exc)
-        if delay:
-            time.sleep(2)
+            sentry_hub.capture_exception(exc)  # type: ignore
+    if delay:
+        time.sleep(2)
+    return None
 
 
 def sentry_reraise(exc: Any) -> None:
@@ -182,8 +187,8 @@ def sentry_set_scope(
     ] = None,
     process_context: Optional[str] = None,
 ) -> None:
-    # Using GLOBAL_HUB means these tags will persist between threads.
-    # Normally there is one hub per thread.
+    if not error_reporting_enabled():
+        return None
 
     # Tags come from two places: settings and args passed into this func.
     args = dict(locals())
@@ -206,7 +211,7 @@ def sentry_set_scope(
     def get(key: str) -> Any:
         return getattr(s, key, None)
 
-    with sentry_sdk.hub.GLOBAL_HUB.configure_scope() as scope:
+    with sentry_hub.configure_scope() as scope:  # type: ignore
         scope.set_tag("platform", get_platform_name())
 
         # apply settings tags


Test patch:
diff --git a/functional_tests/debug/04-sentry-tags.py b/functional_tests/debug/04-sentry-tags.py
--- a/functional_tests/debug/04-sentry-tags.py
+++ b/functional_tests/debug/04-sentry-tags.py
@@ -9,8 +9,12 @@
   platforms:
     - linux
     - mac
+var:
+  - num_sentry_events:
+      :fn:len: :wandb:sentry_events
 assert:
   - :wandb:runs_len: 1
+  - :num_sentry_events: 1
   - :wandb:sentry_events[0][level]: error
   - :wandb:sentry_events[0][exception][values][0][type]: FileNotFoundError
   - :wandb:sentry_events[0][tags][entity]: mock_server_entity
diff --git a/functional_tests/debug/05-sentry-third-party.yea b/functional_tests/debug/05-sentry-third-party.yea
new file mode 100644
--- /dev/null
+++ b/functional_tests/debug/05-sentry-third-party.yea
@@ -0,0 +1,25 @@
+id: 0.debug.05-sentry-third-party
+plugin:
+  - wandb
+tag:
+  platforms:
+    - linux
+    - mac
+command:
+  program: sentry-third-party.py
+depend:
+  requirements:
+    - sentry_sdk
+var:
+  - num_sentry_events:
+      :fn:len: :wandb:sentry_events
+assert:
+  - :wandb:runs_len: 1
+  - :num_sentry_events: 1
+  - :wandb:runs[0][summary][num_third_party_sentry_events]: 2
+  - :wandb:sentry_events[0][level]: error
+  - :wandb:sentry_events[0][platform]: python
+  - :wandb:sentry_events[0][exception][values][0][type]: FileNotFoundError
+  - :wandb:sentry_events[0][exception][values][0][stacktrace][frames][0][filename]: wandb/sdk/internal/internal_util.py
+  - :wandb:sentry_events[0][exception][values][0][stacktrace][frames][0][function]: run
+  - :yea:exit: 255
diff --git a/functional_tests/debug/06-sentry-third-party-no-wandb-error-reporting.yea b/functional_tests/debug/06-sentry-third-party-no-wandb-error-reporting.yea
new file mode 100644
--- /dev/null
+++ b/functional_tests/debug/06-sentry-third-party-no-wandb-error-reporting.yea
@@ -0,0 +1,22 @@
+id: 0.debug.06-sentry-third-party-no-wandb-error-reporting
+plugin:
+  - wandb
+tag:
+  platforms:
+    - linux
+    - mac
+command:
+  program: sentry-third-party.py
+depend:
+  requirements:
+    - sentry_sdk
+env:
+  - WANDB_ERROR_REPORTING: "false"
+var:
+  - num_sentry_events:
+      :fn:len: :wandb:sentry_events
+assert:
+  - :wandb:runs_len: 1
+  - :num_sentry_events: 0
+  - :wandb:runs[0][summary][num_third_party_sentry_events]: 2
+  - :yea:exit: 255
diff --git a/functional_tests/debug/sentry-third-party.py b/functional_tests/debug/sentry-third-party.py
new file mode 100644
--- /dev/null
+++ b/functional_tests/debug/sentry-third-party.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+import shutil
+from typing import Any, Dict
+
+import sentry_sdk
+
+
+def main():
+    third_party_sentry_events = []
+
+    # store events in a list instead of sending them to Sentry
+    def capture_event(event: Dict[str, Any]) -> None:
+        if "exception" in event:
+            third_party_sentry_events.append(event)
+
+    # this should not interfere with our Sentry integration
+    sentry_sdk.init(transport=capture_event)
+
+    import wandb
+    import wandb.util
+
+    # assert that importing wandb does not set Sentry's global hub/client
+    assert sentry_sdk.Hub.current.client.dsn != wandb.util.sentry_default_dsn
+    # but an internal Sentry client for wandb is created ok if WANDB_ERROR_REPORTING != False
+    if wandb.util.error_reporting_enabled():
+        assert isinstance(wandb.util.sentry_client, sentry_sdk.client.Client)
+
+    run = wandb.init()
+
+    # raise two exceptions and capture them with the third-party sentry client
+    for i in range(2):
+        try:
+            print(
+                f"Raising exception #{i + 1} to be captured by third-party sentry client"
+            )
+            raise ValueError("Catch me if you can!")
+        except ValueError:
+            sentry_sdk.capture_exception()
+
+    num_third_party_sentry_events = len(third_party_sentry_events)
+    run.log({"num_third_party_sentry_events": num_third_party_sentry_events})
+
+    # Triggers a FileNotFoundError from the internal process
+    # because the internal process reads/writes to the current run directory.
+    shutil.rmtree(run.dir)
+    try:
+        run.log({"data": 5})
+    except FileNotFoundError:
+        pass
+
+    # no new events should be captured
+    assert num_third_party_sentry_events == len(third_party_sentry_events)
+
+
+if __name__ == "__main__":
+    main()