From d322e772f0d4625da563699f1447e69b13ca0f3c Mon Sep 17 00:00:00 2001 From: "Jugal D. Bhatt" <55304795+jugaldb@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:12:36 +0530 Subject: [PATCH] Litellm add sentry scrubbing (#12210) * add sentry scrubbing * add new constants * remove_unused_import * sentry scrubbing test * added unit test --- litellm/constants.py | 26 ++++++++++++++ litellm/litellm_core_utils/litellm_logging.py | 11 ++++++ .../test_litellm_logging.py | 35 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/litellm/constants.py b/litellm/constants.py index 4ee28adff93..c883336274d 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -803,3 +803,29 @@ SPECIAL_LITELLM_AUTH_TOKEN = ["ui-token"] DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL = int( os.getenv("DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL", 60) ) + +# Sentry Scrubbing Configuration +SENTRY_DENYLIST = [ + # API Keys and Tokens + "api_key", "token", "key", "secret", "password", "auth", "credential", + "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AZURE_API_KEY", "COHERE_API_KEY", + "REPLICATE_API_KEY", "HUGGINGFACE_API_KEY", "TOGETHERAI_API_KEY", + "CLOUDFLARE_API_KEY", "BASETEN_KEY", "OPENROUTER_KEY", "DATAROBOT_API_TOKEN", + "FIREWORKS_API_KEY", "FIREWORKS_AI_API_KEY", "FIREWORKSAI_API_KEY", + # Database and Connection Strings + "database_url", "redis_url", "connection_string", + # Authentication and Security + "master_key", "LITELLM_MASTER_KEY", "auth_token", "jwt_token", "private_key", + "SLACK_WEBHOOK_URL", "webhook_url", "LANGFUSE_SECRET_KEY", + # Email Configuration + "SMTP_PASSWORD", "SMTP_USERNAME", "email_password", + # Cloud Provider Credentials + "aws_access_key", "aws_secret_key", "gcp_credentials", + "azure_credentials", "HCP_VAULT_TOKEN", "CIRCLE_OIDC_TOKEN", + # Proxy and Environment Settings + "proxy_url", "proxy_key", "environment_variables" +] +SENTRY_PII_DENYLIST = [ + "user_id", "email", "phone", "address", "ip_address", + "SMTP_SENDER_EMAIL", "TEST_EMAIL_ADDRESS" +] \ No newline at end of file diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 88c39a665aa..f7be8b1bb5c 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -44,6 +44,8 @@ from litellm.caching.caching_handler import LLMCachingHandler from litellm.constants import ( DEFAULT_MOCK_RESPONSE_COMPLETION_TOKEN_COUNT, DEFAULT_MOCK_RESPONSE_PROMPT_TOKEN_COUNT, + SENTRY_DENYLIST, + SENTRY_PII_DENYLIST, ) from litellm.cost_calculator import ( RealtimeAPITokenUsageProcessor, @@ -2949,6 +2951,10 @@ def set_callbacks(callback_list, function_id=None): # noqa: PLR0915 [sys.executable, "-m", "pip", "install", "sentry_sdk"] ) import sentry_sdk + from sentry_sdk.scrubber import EventScrubber + + + sentry_sdk_instance = sentry_sdk sentry_trace_rate = ( os.environ.get("SENTRY_API_TRACE_RATE") @@ -2966,6 +2972,11 @@ def set_callbacks(callback_list, function_id=None): # noqa: PLR0915 sample_rate=float( sentry_sample_rate if sentry_sample_rate else 1.0 ), + send_default_pii=False, # Prevent sending Personal Identifiable Information + event_scrubber=EventScrubber( + denylist=SENTRY_DENYLIST, + pii_denylist=SENTRY_PII_DENYLIST + ), ) capture_exception = sentry_sdk_instance.capture_exception add_breadcrumb = sentry_sdk_instance.add_breadcrumb diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index be51a1cce7a..d4512b16714 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -13,6 +13,7 @@ import time from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging from litellm.litellm_core_utils.litellm_logging import set_callbacks +from litellm.constants import SENTRY_DENYLIST, SENTRY_PII_DENYLIST @pytest.fixture @@ -309,3 +310,37 @@ def test_response_cost_calculator_with_response_cost_in_hidden_params(logging_ob assert response_cost is not None assert response_cost > 100 + + +def test_sentry_event_scrubber_initialization(monkeypatch): + # Step 1: Create a fake sentry_sdk.scrubber module + mock_event_scrubber_instance = MagicMock() + mock_event_scrubber_cls = MagicMock(return_value=mock_event_scrubber_instance) + + mock_scrubber_module = MagicMock() + mock_scrubber_module.EventScrubber = mock_event_scrubber_cls + + # Step 2: Create a fake sentry_sdk module and insert into sys.modules + mock_sentry_sdk = MagicMock() + mock_sentry_sdk.scrubber = mock_scrubber_module + mock_init = MagicMock() + mock_sentry_sdk.init = mock_init + + # Step 3: Inject both into sys.modules BEFORE import occurs + sys.modules["sentry_sdk"] = mock_sentry_sdk + sys.modules["sentry_sdk.scrubber"] = mock_scrubber_module + + # Step 4: Run the actual sentry setup code + set_callbacks(["sentry"]) + + # Step 5: Assert the EventScrubber was constructed correctly + mock_event_scrubber_cls.assert_called_once_with( + denylist=SENTRY_DENYLIST, + pii_denylist=SENTRY_PII_DENYLIST, + ) + + # Step 6: Assert the event_scrubber and PII args were passed + mock_init.assert_called_once() + call_args = mock_init.call_args[1] + assert call_args["event_scrubber"] == mock_event_scrubber_instance + assert call_args["send_default_pii"] is False \ No newline at end of file