Skip to content

refactor(core): deprecate raising_dispatch#17941

Open
avara1986 wants to merge 7 commits intomainfrom
avara1986/deprecate_rasing_dispatch
Open

refactor(core): deprecate raising_dispatch#17941
avara1986 wants to merge 7 commits intomainfrom
avara1986/deprecate_rasing_dispatch

Conversation

@avara1986
Copy link
Copy Markdown
Member

@avara1986 avara1986 commented May 7, 2026

Description

Deprecates and removes core.raising_dispatch in favor of an opt-in allow_raise=True flag on the existing core.dispatch API. Tightens the in-tree blocking-exception hierarchy and aligns LangChain non-stream wrappers to tag the LLM span on AI Guard block.

What changed

1. core.dispatch gains allow_raise=True

ddtrace/internal/core/event_hub.pydispatch() and dispatch_event() accept a keyword-only allow_raise: bool = False. When set, listener Exceptions propagate to the caller (first raiser wins; subsequent listeners are skipped). BaseException-derived exceptions (including DDBlockException) always propagate regardless of the flag — matching the existing default-deny semantics for blocks.

This collapses two near-duplicate dispatch entry points into one and lets call sites be explicit about whether listener exceptions should surface.

2. core.raising_dispatch removed

The symbol is removed from ddtrace.internal.core and event_hub.py. All in-tree call sites are migrated:

  • ddtrace/contrib/internal/langchain/patch.py — six before events (chat/llm × generate/agenerate/stream).
  • ddtrace/contrib/internal/openai/patch.py — four sites (sync before/after, async before/after).

The .sg rule and snapshot tests for core-raising-dispatch are removed since the symbol no longer exists.

3. New DDBlockException base class

ddtrace/internal/_exceptions.py introduces DDBlockException(BaseException) as the umbrella for any in-tree decision to abort the current operation (web-request blocking, AI Guard policy abort, future product blocks). BlockingException (ASM) and AIGuardAbortError (AI Guard) now derive from it.

This gives integrations a single, semantically-correct catch target for "the platform decided to block this call" — distinct from arbitrary user/library Exceptions. It still inherits from BaseException so a generic except Exception: cannot silently swallow a block decision.

4. LangChain non-stream wrappers: tag the LLM span on block

The four non-stream wrappers (traced_llm_generate, traced_llm_agenerate, traced_chat_model_generate, traced_chat_model_agenerate) now have an explicit except DDBlockException: arm that calls span.set_exc_info(*sys.exc_info()) before re-raising, in addition to the existing except Exception: arm.

Required because AIGuardAbortError derives from BaseException (via DDBlockException) and is therefore not caught by except Exception:. Without the explicit arm, blocked LangChain calls would emit an LLMObs span with no error info — leaving a hole between the AI Guard span (block decision) and the LLM span (no link back to the abort). Aligns LangChain's non-stream behavior with the contract from #17913 for openai (_patched_endpoint uses except BaseException as e: err = e; raise, then _traced_endpoint's finally calls set_exc_info).

5. OpenAIAIGuardAbortError: documented catchability asymmetry

The compound class inherits from both openai.UnprocessableEntityError (Exception-derived) and AIGuardAbortError (now BaseException-derived). Via MRO it remains Exception-derived — so except openai.APIError: blocks still work for users migrating from non-AI-Guard error handling. The asymmetry vs plain AIGuardAbortError is now spelled out in an AIDEV-NOTE on the class. Code that wants uniform block detection across providers should branch on isinstance(e, AIGuardAbortError).

Testing

  • tests/internal/test_context_events_api.py — new coverage for dispatch(..., allow_raise=True) semantics: Exceptions propagate when set, are swallowed when not, listeners short-circuit on first raise, BaseException-derived listener exceptions (DDBlockException subclasses) always propagate regardless of the flag, the dispatch_event variant behaves identically, and the new exception inheritance is asserted.
  • tests/appsec/ai_guard/langchain/test_langchain.py — two new tests pin the set_exc_info-on-block contract on the LLM span (sync chat.invoke + async chat.ainvoke). A regression that swaps the explicit except DDBlockException arm back to except Exception would surface immediately.
  • Local runs:
    • appsec::ai_guard_langchain venv 5484ca0 (Python 3.13) — passes.
    • appsec::ai_guard_openai venv 1224d93 (Python 3.12) — passes (this was the suite that initially exposed the missing openai-contrib migration).
  • hatch run lint:fmt clean on all modified files.

Risks

  • AIGuardAbortError inheritance change is user-visible. It now derives from DDBlockException(BaseException) instead of Exception. User code today doing try: model.invoke(...) except Exception: ... to handle aborts will silently stop catching them. This is intentional (the BaseException derivation is what prevents accidental swallowing of blocks) but it does break code that relied on the prior behavior. Spelled out in the release note's upgrade section. The OpenAI-compatible variant (OpenAIAIGuardAbortError) remains catchable by except Exception: via MRO for SDK compatibility.
  • core.raising_dispatch removal. Internal helper under ddtrace.internal.core — not part of the public API. All in-tree call sites are migrated in this PR. The release note's deprecations section documents the migration path for any out-of-tree consumer that imported it.
  • Trace shape change on blocked LangChain non-stream calls. The LLM span is now finished with error == 1 and error.type containing AIGuardAbortError. Downstream consumers that filter LLM spans by error == 0 will see blocked LangChain calls as errored — desired observability change per feat(ai_guard): add OpenAI SDK integration (streaming) #17913 but worth flagging.
  • No public API breakage on core.dispatch. allow_raise is keyword-only, defaults to False, preserves existing non-raising behavior for every existing caller.

Additional Notes

Release note: releasenotes/notes/aiguard-abort-baseexception-deprecate-raising-dispatch-3a5c7f61ee4d2e1b.yaml covers the AIGuardAbortError inheritance change (upgrade section) and the raising_dispatch removal (deprecations section).

@cit-pr-commenter-54b7da
Copy link
Copy Markdown

cit-pr-commenter-54b7da Bot commented May 7, 2026

Codeowners resolved as

ddtrace/contrib/internal/langchain/patch.py                             @DataDog/ml-observability
ddtrace/contrib/internal/openai/patch.py                                @DataDog/ml-observability

@datadog-prod-us1-3
Copy link
Copy Markdown

datadog-prod-us1-3 Bot commented May 7, 2026

Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: b05ce20 | Docs | Datadog PR Page | Give us feedback!

@pr-commenter
Copy link
Copy Markdown

pr-commenter Bot commented May 7, 2026

Benchmarks

Benchmark execution time: 2026-05-08 20:21:54

Comparing candidate commit b05ce20 in PR branch avara1986/deprecate_rasing_dispatch with baseline commit d1a7113 in branch main.

Found 0 performance improvements and 6 performance regressions! Performance is the same for 591 metrics, 4 unstable metrics.

scenario:iastaspects-index_aspect

  • 🟥 execution_time [+12.286µs; +17.158µs] or [+9.789%; +13.670%]

scenario:iastaspects-ljust_noaspect

  • 🟥 execution_time [+21.234µs; +28.373µs] or [+8.117%; +10.846%]

scenario:iastaspects-stringio_noaspect

  • 🟥 execution_time [+45.237µs; +51.374µs] or [+12.885%; +14.633%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+84.442µs; +91.825µs] or [+19.816%; +21.549%]

scenario:span-start

  • 🟥 execution_time [+1.293ms; +1.440ms] or [+8.521%; +9.495%]

scenario:telemetryaddmetric-1-count-metric-1-times

  • 🟥 execution_time [+189.189ns; +227.892ns] or [+9.033%; +10.881%]

@avara1986 avara1986 added the changelog/no-changelog A changelog entry is not required for this PR. label May 7, 2026
@avara1986 avara1986 force-pushed the avara1986/deprecate_rasing_dispatch branch from df478a5 to a96248a Compare May 8, 2026 09:23
@avara1986
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53f175fd3d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ddtrace/internal/core/__init__.py
@avara1986 avara1986 changed the title chore(core): depreacate raising_dispatch chore(core): deprecate raising_dispatch May 8, 2026
Replace ``core.raising_dispatch`` with a keyword-only ``allow_raise=True``
flag on ``core.dispatch`` (and ``dispatch_event``). Migrate all in-tree
call sites in the langchain and openai contribs.

Introduce ``DDBlockException(BaseException)`` as the umbrella class for
in-tree block decisions. ``BlockingException`` (ASM) and
``AIGuardAbortError`` now derive from it. ``AIGuardAbortError``'s base
moves from ``Exception`` to ``DDBlockException`` so a generic
``except Exception:`` no longer silently swallows a block — release note
spells out the upgrade impact for users.

Tag the LLM span on AI Guard block in the four LangChain non-stream
wrappers: an explicit ``except DDBlockException:`` arm calls
``span.set_exc_info`` before re-raising, since the existing
``except Exception:`` does not catch ``BaseException``-derived aborts.
Mirrors the contract from #17913 (openai ``_patched_endpoint``).

Document the catchability asymmetry on ``OpenAIAIGuardAbortError``: it
inherits from ``openai.UnprocessableEntityError`` (Exception-derived)
*and* ``AIGuardAbortError`` (BaseException-derived), so that subclass
remains catchable by ``except Exception:`` for OpenAI SDK compatibility,
while plain ``AIGuardAbortError`` is not.

Tests:
- ``tests/internal/test_context_events_api.py`` — coverage for
  ``dispatch(..., allow_raise=True)`` semantics, ``BaseException``
  propagation, and the new exception-class hierarchy.
- ``tests/appsec/ai_guard/langchain/test_langchain.py`` — pin the
  LLM-span ``set_exc_info`` contract on AI Guard block (sync + async).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@avara1986 avara1986 force-pushed the avara1986/deprecate_rasing_dispatch branch from 1cc763c to 8a5cb85 Compare May 8, 2026 10:12
@avara1986
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9eb78aa6a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ddtrace/contrib/internal/openai/patch.py Outdated
@avara1986 avara1986 marked this pull request as ready for review May 8, 2026 19:55
@avara1986 avara1986 requested review from a team as code owners May 8, 2026 19:55
@avara1986 avara1986 requested review from emmettbutler and wantsui May 8, 2026 19:55
@avara1986 avara1986 changed the title chore(core): deprecate raising_dispatch refactor(core): deprecate raising_dispatch May 8, 2026
@avara1986 avara1986 removed the changelog/no-changelog A changelog entry is not required for this PR. label May 8, 2026
completions = None
try:
core.raising_dispatch("langchain.llm.agenerate.before", (prompts,))
core.dispatch("langchain.llm.agenerate.before", (prompts,), allow_raise=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just to clarify we're just replacing raising_dispatch with dispatch and adding allow_raise=True to do the exact same thing (raise/propagate) as before?

completions = await func(*args, **kwargs)
core.dispatch("langchain.llm.agenerate.after", (prompts, completions))
except Exception:
except (DDBlockException, Exception):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are DDBlockExceptions new or covering existing functionality?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I saw the rest of the PR, AIGuard errors now derive from DDBlockException. Makes sense to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants