Skip to content

chore(profiling): don't crash apps on non-working imports under Python 3.15 (PROF-15769) - #19724

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 14 commits into
mainfrom
vlad/ci-fix-smoke-tests-py315
Aug 21, 2026
Merged

chore(profiling): don't crash apps on non-working imports under Python 3.15 (PROF-15769)#19724
gh-worker-dd-mergequeue-cf854d[bot] merged 14 commits into
mainfrom
vlad/ci-fix-smoke-tests-py315

Conversation

@vlad-scherbich

@vlad-scherbich vlad-scherbich commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Description

Part of the Python 3.15 integration parity effort
(parent tracker: #17809).

Closes #19730

Wheel smoke tests fail on 3.15 because wrapping raises at import (pulled in via ModuleWatchdog / WrappingContext) and ddtrace.profiling.auto loads native extensions that are not built yet. This PR makes those imports succeed and degrades instead of crashing.

If this PR merges and nothing else lands:

  • ddtrace.internal.wrapping (asyncs, generators, context) and ddtrace.internal.bytecode_injection import successfully on 3.15. wrap(), wrap_bytecode(), wrap_async(), wrap_generator(), WrappingContext.wrap(), inject_hook(), and inject_hooks() still raise NotImplementedError until chore: wrapping context support for Python 3.15 #17849.
  • ddtrace.profiling uses the same is_available / failure_msg pattern as ddtrace.internal.datadog.profiling.{ddup,stack}. If Profiler cannot be imported (native extensions missing), is_available is False and constructing Profiler() raises ImportError.
  • ddtrace.profiling.auto logs a warning and returns when is_available is False, instead of crashing the process.

Checklist

  • Bumped upstream pin in riotfile.py to a version that supports Python 3.15 — N/A (not an integration bump)
  • Lifted max_version="3.13" / "3.14" cap on the affected venv(s) (if present) — N/A
  • Ran riot generate <suite-pattern> and committed the regenerated .riot/requirements/*.txt lockfiles — N/A
  • Ran the suite locally on 3.15 via scripts/run-tests <suite> — intended gate is 3.15 wheel smoke tests in CI
  • Updated supported_versions.json if integration min/max versions changed — N/A
  • Release note added under releasenotes/notes/

Testing

  • CI

Wheel smoke tests fail on 3.15 because wrapping raises at import and
profiling.auto loads native extensions that are not built yet. Defer
the wrapping error until wrap is actually used, and disable the
profiler when those extensions are missing.
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

No remaining files require a CODEOWNERS review.

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 17, 2026

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 3 circular imports that already exist on the base branch and have not been changed by this PR.

ddtrace.llmobs -> ddtrace.llmobs._evaluators -> ddtrace.llmobs._evaluators.format -> ddtrace.llmobs._experiment -> ddtrace.llmobs
ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector
ddtrace.appsec._asm_request_context -> ddtrace.appsec._iast._iast_request_context_base -> ddtrace.appsec._iast._iast_env -> ddtrace.appsec._iast.reporter -> ddtrace.appsec._exploit_prevention.stack_traces -> ddtrace.appsec._asm_request_context

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 17, 2026

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 250 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 250 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=135)
ddtrace.llmobs._integrations.mcp -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=133)
ddtrace.llmobs._integrations.bedrock -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=133)
ddtrace.profiling.collector.pytorch -×-> ddtrace.trace  (product:profiling -> product:tracing, score=133)
ddtrace.llmobs._integrations.google_adk -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=133)

To see all violations, download the layers-base.json and layers-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/layers.py compare layers-base.json layers-pr.json

@vlad-scherbich vlad-scherbich changed the title fix(profiling): don't crash on import under Python 3.15 fix(profiling): don't crash on import under Python 3.15 (PROF-15769) Aug 17, 2026
@vlad-scherbich vlad-scherbich added changelog/no-changelog A changelog entry is not required for this PR. Profiling Continous Profling labels Aug 17, 2026
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 39fc59c | Docs | View more details | Give us feedback!

@vlad-scherbich vlad-scherbich changed the title fix(profiling): don't crash on import under Python 3.15 (PROF-15769) fix(profiling): don't crash apps on non-working imports under Python 3.15 (PROF-15769) Aug 17, 2026
@vlad-scherbich vlad-scherbich changed the title fix(profiling): don't crash apps on non-working imports under Python 3.15 (PROF-15769) chore(profiling): don't crash apps on non-working imports under Python 3.15 (PROF-15769) Aug 17, 2026
@pr-commenter

pr-commenter Bot commented Aug 17, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-21 05:11:31

Comparing candidate commit 39fc59c in PR branch vlad/ci-fix-smoke-tests-py315 with baseline commit f559280 in branch main.

📊 Benchmarking dashboard

Found 0 performance improvements and 7 performance regressions! Performance is the same for 615 metrics, 10 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:httppropagationextract-empty_headers

  • 🟥 execution_time [+145.890ns; +175.667ns] or [+14.084%; +16.958%]

scenario:httppropagationinject-ids_only

  • 🟥 execution_time [+1.663µs; +1.834µs] or [+8.650%; +9.541%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+108.621µs; +119.984µs] or [+25.916%; +28.628%]

scenario:iastaspectssplit-rsplit_aspect

  • 🟥 execution_time [+18.268µs; +23.526µs] or [+12.521%; +16.124%]

scenario:span-start

  • 🟥 execution_time [+1.477ms; +1.646ms] or [+9.867%; +10.995%]

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

  • 🟥 execution_time [+378.466ns; +413.392ns] or [+14.285%; +15.603%]

scenario:tracer-small

  • 🟥 execution_time [+28.839µs; +31.782µs] or [+8.610%; +9.489%]

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:coreapiscenario-context_with_data_listeners

  • unstable execution_time [-733.465ns; +748.438ns] or [-6.712%; +6.849%]

scenario:coreapiscenario-core_dispatch_1_listener

  • unstable execution_time [-30.695ns; +35.561ns] or [-5.026%; +5.823%]

scenario:coreapiscenario-core_dispatch_50_listeners

  • unstable execution_time [-1635.607ns; +1659.507ns] or [-9.648%; +9.789%]

scenario:coreapiscenario-core_dispatch_exception_listeners

  • unstable execution_time [-1357.523ns; +1172.516ns] or [-10.195%; +8.806%]

scenario:coreapiscenario-core_dispatch_listeners

  • unstable execution_time [-324.110ns; +326.127ns] or [-8.854%; +8.909%]

scenario:coreapiscenario-core_dispatch_no_args_listeners

  • unstable execution_time [-253.919ns; +248.526ns] or [-8.765%; +8.579%]

scenario:coreapiscenario-core_dispatch_with_results_1_listener

  • unstable execution_time [-86.537ns; +59.105ns] or [-7.569%; +5.170%]

scenario:coreapiscenario-core_dispatch_with_results_50_listeners

  • unstable execution_time [-4042.383ns; +3892.470ns] or [-10.021%; +9.650%]

scenario:coreapiscenario-core_dispatch_with_results_listeners

  • unstable execution_time [-791.941ns; +766.743ns] or [-9.827%; +9.514%]

scenario:packagesupdateimporteddependencies-import_many_stdlib_cached

  • unstable execution_time [-58.145µs; +62.694µs] or [-9.084%; +9.795%]

@vlad-scherbich vlad-scherbich removed the Profiling Continous Profling label Aug 17, 2026
@vlad-scherbich
vlad-scherbich marked this pull request as ready for review August 18, 2026 17:54
@vlad-scherbich
vlad-scherbich requested review from a team as code owners August 18, 2026 17:54

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

Copy link
Copy Markdown

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: fe36aeced2

ℹ️ 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/wrapping/__init__.py Outdated
Comment thread ddtrace/profiling/__init__.py
Comment thread ddtrace/profiling/bootstrap/sitecustomize.py Outdated
@vlad-scherbich
vlad-scherbich marked this pull request as draft August 18, 2026 18:30
Skip LazyWrappingContext bytecode wrapping on 3.15 and execute the lazy
initializer body in module scope instead, so debugging product entry points
load without failing the smoke-test product check.

Document the profiling is_available/failure_msg contract in basic_usage.rst.
No release note: this is pre-release 3.15 degrade behavior only.

Copilot AI left a comment

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.

Pull request overview

This PR improves Python 3.15 “import-time degrade” behavior so dd-trace-py doesn’t crash when unsupported wrapping/bytecode injection paths are imported (but not used) and when profiling native extensions aren’t available yet (e.g., during wheel smoke tests).

Changes:

  • Make ddtrace.internal.wrapping.* and ddtrace.internal.bytecode_injection import successfully on unsupported Python versions, while still raising NotImplementedError when the unsupported functionality is actually invoked.
  • Add a profiling availability gate (is_available / failure_msg) to ddtrace.profiling, and make ddtrace.profiling.auto degrade with a warning instead of crashing when profiling isn’t available.
  • Add/extend tests to cover 3.15 import-time degrade behavior and profiling auto-import degradation.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/smoke_test.py Restores environment correctly and asserts product plugins didn’t fail during bootstrap/WAF smoke path.
tests/profiling/test_profiler.py Adds a subprocess test ensuring ddtrace.profiling.auto import does not crash when profiling is unavailable.
tests/internal/test_py315_import_degrade.py New tests ensuring wrapping/bytecode injection modules import on 3.15 while still raising when used.
ddtrace/profiling/bootstrap/sitecustomize.py Adds is_available gate to avoid crashing when native profiling extensions are missing.
ddtrace/profiling/__init__.py Introduces is_available/failure_msg pattern and a stub Profiler that raises ImportError on construction when unavailable.
ddtrace/internal/wrapping/generators.py Removes import-time crash on unsupported versions; raises only when wrapping is invoked.
ddtrace/internal/wrapping/context.py Removes import-time crash on unsupported versions; raises only when wrapping is invoked.
ddtrace/internal/wrapping/asyncs.py Removes import-time crash on unsupported versions; raises only when wrapping is invoked.
ddtrace/internal/wrapping/__init__.py Ensures wrap() / wrap_bytecode() raise at call-time rather than import-time on unsupported versions.
ddtrace/internal/compat.py Adds NEXT_PY_VERSION* constants to centralize “first unsupported CPython version” logic.
ddtrace/internal/bytecode_injection/__init__.py Removes import-time crash on unsupported versions; raises only when injection is invoked.
Suppressed comments (2)

ddtrace/internal/wrapping/init.py:312

  • This NotImplementedError message hard-codes NEXT_PY_VERSION (e.g. "3.15"), which is confusing on later unsupported versions (it will still claim "...: 3.15" on 3.16+). Reword to describe the unsupported range instead of a single version.
    """
    if PY >= NEXT_PY_VERSION_INFO:
        raise NotImplementedError("This version of CPython is not supported yet: %s" % (NEXT_PY_VERSION,))

ddtrace/internal/bytecode_injection/init.py:232

  • This NotImplementedError message hard-codes NEXT_PY_VERSION (e.g. "3.15"), which is confusing on later unsupported versions (it will still claim "...: 3.15" on 3.16+). Reword to describe the unsupported range instead of a single version.
    """
    if PY >= NEXT_PY_VERSION_INFO:
        raise NotImplementedError("This version of CPython is not supported yet: %s" % (NEXT_PY_VERSION,))
    abstract_code = Bytecode.from_code(f.__code__)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ddtrace/internal/wrapping/asyncs.py Outdated
Comment thread ddtrace/profiling/bootstrap/sitecustomize.py Outdated
Comment thread ddtrace/internal/wrapping/generators.py Outdated
Comment thread ddtrace/internal/wrapping/context.py Outdated
Comment thread ddtrace/internal/wrapping/__init__.py Outdated
Comment thread ddtrace/internal/bytecode_injection/__init__.py Outdated
Use a shared unsupported-version message that describes the threshold
range (Python 3.15 and later) instead of implying only one version is
blocked. Import ddtrace.profiling as a module in sitecustomize so
is_available/failure_msg reflect runtime monkeypatches in tests.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

ddtrace/profiling/init.py:24

  • The fallback Profiler() ImportError message always claims "native extensions are not built", but the guarded import can fail for other reasons too. Including failure_msg in the exception text makes the error actionable and avoids an inaccurate root-cause statement.
            raise ImportError(
                "ddtrace.profiling is not available on this Python version "
                "(native extensions are not built). "
                "Import ddtrace.profiling and check is_available/failure_msg for details."
            ) from _profiler_import_error

ddtrace/internal/module.py:772

  • _exec_lazy_init() executes the parsed function body as a synthetic module. If the lazy initializer function has a docstring, the first string literal statement will be treated as the module docstring when compiled/executed, unexpectedly overwriting module_globals['doc']. Strip a leading docstring expression from the function body before compiling (and fix missing locations).
    func_def = tree.body[0]
    if not isinstance(func_def, ast.FunctionDef):
        raise TypeError("lazy() expects a function definition")
    mod = ast.Module(body=func_def.body, type_ignores=[])
    exec(compile(mod, f.__code__.co_filename, "exec"), module_globals)

@vlad-scherbich
vlad-scherbich marked this pull request as ready for review August 19, 2026 14:57
@vlad-scherbich
vlad-scherbich requested a review from a team as a code owner August 19, 2026 14:57

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

Copy link
Copy Markdown

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: e638fae677

ℹ️ 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/module.py Outdated
Execute the compiled @lazy initializer and promote its locals to module
scope instead of reconstructing the body via inspect.getsource(), so lazy
modules work from sourceless or frozen distributions.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

ddtrace/internal/bytecode_injection/init.py:233

  • inject_hook raises NotImplementedError on Python >= NEXT_PY_VERSION_INFO, but eject_hook does not. On unsupported versions this can lead to incorrect behavior since the injected opcode pattern is not defined/parsed. Add the same version guard to eject_hook.
def inject_hook(f: FunctionType, hook: HookType, line: int, arg: Any) -> FunctionType:
    """Inject a hook into a function.

    The hook is injected at the given line number and called with the given
    argument. The latter is also used as an identifier for the hook. This should
    be kept in case the hook needs to be removed.
    """
    if PY >= NEXT_PY_VERSION_INFO:
        raise NotImplementedError(NEXT_PY_UNSUPPORTED_MSG)
    abstract_code = Bytecode.from_code(f.__code__)

ddtrace/internal/bytecode_injection/init.py:188

  • inject_hooks/inject_hook correctly raise NotImplementedError on Python >= NEXT_PY_VERSION_INFO. However eject_hooks has no version guard, so it can execute on unsupported versions even though INJECTION_ASSEMBLY/_INJECT_HOOK_OPCODES are not defined for them (they may be empty), which can lead to incorrect ejection behavior or unexpected errors. Add the same NotImplementedError guard to eject_hooks.

This issue also appears on line 223 of the same file.

        raise NotImplementedError(NEXT_PY_UNSUPPORTED_MSG)
    abstract_code = Bytecode.from_code(get_function_code(f))

    failed = []
    for hook, line, arg in hooks:

@emmettbutler emmettbutler left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is fantastic

Comment thread ddtrace/internal/wrapping/asyncs.py Outdated
_exec_lazy_init must always return its own trace function so it still
receives the return event when pytest or another tracer is already
installed via sys.settrace().
Do not forward trace events to an existing pytest/coverage tracer during
_exec_lazy_init; those tracers can reinstall themselves on "call" and
prevent our handler from seeing the return event. Capture line+return
snapshots with dict(frame.f_locals) for PEP 667.
@vlad-scherbich

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Aug 20, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-08-20 19:59:52 UTC ℹ️ Start processing command /merge


2026-08-20 19:59:55 UTC ❌ MergeQueue

PR already in the queue with status in_progress

@vlad-scherbich

Copy link
Copy Markdown
Contributor Author

/code blockers

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Aug 21, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-08-21 04:40:26 UTC ℹ️ Start processing command /code blockers


2026-08-21 04:40:27 UTC ℹ️ Devflow:

Checking merge blockers for #19724...


2026-08-21 04:40:43 UTC ℹ️ Devflow: /code blockers

No merge blockers detected.

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 69baea3 into main Aug 21, 2026
1309 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the vlad/ci-fix-smoke-tests-py315 branch August 21, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/no-changelog A changelog entry is not required for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[3.15] Import degrade: wrapping + profiling must not crash on cp315 (smoke/lib-injection unblock)

4 participants