fix: avoid using svc_src: m when service is among active integration defaults#17712
fix: avoid using svc_src: m when service is among active integration defaults#17712emmettbutler wants to merge 34 commits intoemmett.butler/unignore-svc-srcfrom
Conversation
Performance SLOsComparing candidate emmett.butler/sqlite3-snapshot-2 (aaa0415) with baseline emmett.butler/unignore-svc-src (d025b3e) 📈 Performance Regressions (1 suite)📈 telemetryaddmetric - 30/30✅ 1-count-metric-1-timesTime: ✅ 2.324µs (SLO: <20.000µs 📉 -88.4%) vs baseline: 📈 +11.7% Memory: ✅ 36.569MB (SLO: <38.000MB -3.8%) vs baseline: +5.5% ✅ 1-count-metrics-100-timesTime: ✅ 155.132µs (SLO: <220.000µs 📉 -29.5%) vs baseline: +3.0% Memory: ✅ 36.746MB (SLO: <38.000MB -3.3%) vs baseline: +6.4% ✅ 1-distribution-metric-1-timesTime: ✅ 2.431µs (SLO: <20.000µs 📉 -87.8%) vs baseline: +0.7% Memory: ✅ 36.589MB (SLO: <38.000MB -3.7%) vs baseline: +5.9% ✅ 1-distribution-metrics-100-timesTime: ✅ 168.558µs (SLO: <230.000µs 📉 -26.7%) vs baseline: +2.3% Memory: ✅ 36.608MB (SLO: <38.000MB -3.7%) vs baseline: +5.9% ✅ 1-gauge-metric-1-timesTime: ✅ 2.300µs (SLO: <20.000µs 📉 -88.5%) vs baseline: +1.2% Memory: ✅ 36.569MB (SLO: <38.000MB -3.8%) vs baseline: +6.1% ✅ 1-gauge-metrics-100-timesTime: ✅ 168.948µs (SLO: <190.000µs 📉 -11.1%) vs baseline: +3.2% Memory: ✅ 36.746MB (SLO: <38.000MB -3.3%) vs baseline: +6.2% ✅ 1-rate-metric-1-timesTime: ✅ 2.279µs (SLO: <20.000µs 📉 -88.6%) vs baseline: +3.5% Memory: ✅ 36.667MB (SLO: <38.000MB -3.5%) vs baseline: +6.3% ✅ 1-rate-metrics-100-timesTime: ✅ 169.244µs (SLO: <250.000µs 📉 -32.3%) vs baseline: +3.9% Memory: ✅ 36.471MB (SLO: <38.000MB -4.0%) vs baseline: +5.5% ✅ 100-count-metrics-100-timesTime: ✅ 15.682ms (SLO: <22.000ms 📉 -28.7%) vs baseline: +4.5% Memory: ✅ 36.451MB (SLO: <38.000MB -4.1%) vs baseline: +5.5% ✅ 100-distribution-metrics-100-timesTime: ✅ 1.763ms (SLO: <2.550ms 📉 -30.9%) vs baseline: +1.5% Memory: ✅ 36.569MB (SLO: <38.000MB -3.8%) vs baseline: +5.6% ✅ 100-gauge-metrics-100-timesTime: ✅ 1.731ms (SLO: <1.850ms -6.5%) vs baseline: +3.8% Memory: ✅ 36.510MB (SLO: <38.000MB -3.9%) vs baseline: +5.8% ✅ 100-rate-metrics-100-timesTime: ✅ 1.745ms (SLO: <2.550ms 📉 -31.5%) vs baseline: +4.2% Memory: ✅ 36.549MB (SLO: <38.000MB -3.8%) vs baseline: +5.9% ✅ flush-1-metricTime: ✅ 3.540µs (SLO: <20.000µs 📉 -82.3%) vs baseline: -1.6% Memory: ✅ 36.510MB (SLO: <38.000MB -3.9%) vs baseline: +4.6% ✅ flush-100-metricsTime: ✅ 173.881µs (SLO: <250.000µs 📉 -30.4%) vs baseline: -1.2% Memory: ✅ 36.530MB (SLO: <38.000MB -3.9%) vs baseline: +4.6% ✅ flush-1000-metricsTime: ✅ 2.187ms (SLO: <2.500ms 📉 -12.5%) vs baseline: +0.1% Memory: ✅ 37.336MB (SLO: <38.750MB -3.6%) vs baseline: +4.7% 🟡 Near SLO Breach (7 suites)🟡 djangosimple - 28/28✅ appsecTime: ✅ 20.136ms (SLO: <22.300ms -9.7%) vs baseline: +2.3% Memory: ✅ 71.600MB (SLO: <73.500MB -2.6%) vs baseline: +5.2% ✅ exception-replay-enabledTime: ✅ 1.386ms (SLO: <1.450ms -4.4%) vs baseline: +0.9% Memory: ✅ 69.570MB (SLO: <71.500MB -2.7%) vs baseline: +4.8% ✅ iastTime: ✅ 20.170ms (SLO: <22.250ms -9.3%) vs baseline: +2.2% Memory: ✅ 71.438MB (SLO: <75.000MB -4.7%) vs baseline: +5.0% ✅ profilerTime: ✅ 15.166ms (SLO: <16.550ms -8.4%) vs baseline: -0.2% Memory: ✅ 60.365MB (SLO: <61.000MB 🟡 -1.0%) vs baseline: +4.9% ✅ resource-renamingTime: ✅ 20.015ms (SLO: <21.750ms -8.0%) vs baseline: +2.3% Memory: ✅ 71.460MB (SLO: <73.500MB -2.8%) vs baseline: +4.9% ✅ span-code-originTime: ✅ 20.362ms (SLO: <28.200ms 📉 -27.8%) vs baseline: +2.2% Memory: ✅ 71.811MB (SLO: <75.000MB -4.3%) vs baseline: +5.4% ✅ tracerTime: ✅ 20.167ms (SLO: <21.750ms -7.3%) vs baseline: +2.0% Memory: ✅ 71.487MB (SLO: <75.000MB -4.7%) vs baseline: +5.1% ✅ tracer-and-profilerTime: ✅ 21.530ms (SLO: <23.500ms -8.4%) vs baseline: +2.3% Memory: ✅ 73.511MB (SLO: <75.000MB 🟡 -2.0%) vs baseline: +5.1% ✅ tracer-dont-create-db-spansTime: ✅ 20.226ms (SLO: <21.500ms -5.9%) vs baseline: +2.1% Memory: ✅ 71.526MB (SLO: <75.000MB -4.6%) vs baseline: +4.9% ✅ tracer-minimalTime: ✅ 18.022ms (SLO: <18.500ms -2.6%) vs baseline: +0.5% Memory: ✅ 71.465MB (SLO: <75.000MB -4.7%) vs baseline: +5.0% ✅ tracer-no-cachesTime: ✅ 18.991ms (SLO: <19.650ms -3.4%) vs baseline: ~same Memory: ✅ 71.467MB (SLO: <75.000MB -4.7%) vs baseline: +4.9% ✅ tracer-no-databasesTime: ✅ 21.060ms (SLO: <21.100ms 🟡 -0.2%) vs baseline: +1.6% Memory: ✅ 71.408MB (SLO: <75.000MB -4.8%) vs baseline: +4.9% ✅ tracer-no-middlewareTime: ✅ 19.906ms (SLO: <21.500ms -7.4%) vs baseline: +1.9% Memory: ✅ 71.529MB (SLO: <75.000MB -4.6%) vs baseline: +5.0% ✅ tracer-no-templatesTime: ✅ 19.952ms (SLO: <22.000ms -9.3%) vs baseline: +2.9% Memory: ✅ 71.500MB (SLO: <73.500MB -2.7%) vs baseline: +4.9% 🟡 errortrackingflasksqli - 6/6✅ errortracking-enabled-allTime: ✅ 2.232ms (SLO: <2.300ms -3.0%) vs baseline: +5.5% Memory: ✅ 58.275MB (SLO: <60.000MB -2.9%) vs baseline: +4.7% ✅ errortracking-enabled-userTime: ✅ 2.243ms (SLO: <2.250ms 🟡 -0.3%) vs baseline: +5.9% Memory: ✅ 58.393MB (SLO: <60.000MB -2.7%) vs baseline: +4.8% ✅ tracer-enabledTime: ✅ 2.234ms (SLO: <2.300ms -2.9%) vs baseline: +5.7% Memory: ✅ 58.471MB (SLO: <60.000MB -2.5%) vs baseline: +5.0% 🟡 flasksqli - 6/6✅ appsec-enabledTime: ✅ 2.231ms (SLO: <4.200ms 📉 -46.9%) vs baseline: +5.6% Memory: ✅ 58.570MB (SLO: <66.000MB 📉 -11.3%) vs baseline: +5.0% ✅ iast-enabledTime: ✅ 2.239ms (SLO: <2.800ms 📉 -20.0%) vs baseline: +5.5% Memory: ✅ 58.648MB (SLO: <62.500MB -6.2%) vs baseline: +5.0% ✅ tracer-enabledTime: ✅ 2.228ms (SLO: <2.250ms 🟡 -1.0%) vs baseline: +5.6% Memory: ✅ 58.629MB (SLO: <60.000MB -2.3%) vs baseline: +4.8% 🟡 otelspan - 22/22✅ add-eventTime: ✅ 41.540ms (SLO: <47.150ms 📉 -11.9%) vs baseline: +0.3% Memory: ✅ 41.469MB (SLO: <47.000MB 📉 -11.8%) vs baseline: +4.9% ✅ add-metricsTime: ✅ 235.230ms (SLO: <344.800ms 📉 -31.8%) vs baseline: ~same Memory: ✅ 45.526MB (SLO: <47.500MB -4.2%) vs baseline: +5.2% ✅ add-tagsTime: ✅ 264.456ms (SLO: <330.000ms 📉 -19.9%) vs baseline: +0.4% Memory: ✅ 45.192MB (SLO: <47.500MB -4.9%) vs baseline: +4.3% ✅ get-contextTime: ✅ 81.136ms (SLO: <92.350ms 📉 -12.1%) vs baseline: +0.7% Memory: ✅ 41.189MB (SLO: <46.500MB 📉 -11.4%) vs baseline: +4.9% ✅ is-recordingTime: ✅ 38.024ms (SLO: <44.500ms 📉 -14.6%) vs baseline: +0.4% Memory: ✅ 41.013MB (SLO: <47.500MB 📉 -13.7%) vs baseline: +5.5% ✅ record-exceptionTime: ✅ 62.689ms (SLO: <67.650ms -7.3%) vs baseline: -0.1% Memory: ✅ 41.810MB (SLO: <47.000MB 📉 -11.0%) vs baseline: +5.2% ✅ set-statusTime: ✅ 43.659ms (SLO: <50.400ms 📉 -13.4%) vs baseline: +0.8% Memory: ✅ 40.836MB (SLO: <47.000MB 📉 -13.1%) vs baseline: +4.9% ✅ startTime: ✅ 38.853ms (SLO: <44.500ms 📉 -12.7%) vs baseline: +4.8% Memory: ✅ 41.122MB (SLO: <47.000MB 📉 -12.5%) vs baseline: +5.6% ✅ start-finishTime: ✅ 90.351ms (SLO: <92.000ms 🟡 -1.8%) vs baseline: +0.6% Memory: ✅ 38.928MB (SLO: <46.500MB 📉 -16.3%) vs baseline: +5.5% ✅ start-finish-telemetryTime: ✅ 92.156ms (SLO: <93.000ms 🟡 -0.9%) vs baseline: +0.2% Memory: ✅ 38.791MB (SLO: <46.500MB 📉 -16.6%) vs baseline: +4.9% ✅ update-nameTime: ✅ 39.077ms (SLO: <45.150ms 📉 -13.5%) vs baseline: +1.0% Memory: ✅ 40.944MB (SLO: <47.000MB 📉 -12.9%) vs baseline: +5.0% 🟡 recursivecomputation - 8/8✅ deepTime: ✅ 311.880ms (SLO: <320.950ms -2.8%) vs baseline: +0.1% Memory: ✅ 37.356MB (SLO: <38.750MB -3.6%) vs baseline: +5.6% ✅ deep-profiledTime: ✅ 331.777ms (SLO: <359.150ms -7.6%) vs baseline: -1.2% Memory: ✅ 43.608MB (SLO: <46.000MB -5.2%) vs baseline: +5.7% ✅ mediumTime: ✅ 7.365ms (SLO: <7.450ms 🟡 -1.1%) vs baseline: ~same Memory: ✅ 36.313MB (SLO: <38.000MB -4.4%) vs baseline: +5.0% ✅ shallowTime: ✅ 1.041ms (SLO: <1.050ms 🟡 -0.8%) vs baseline: +2.6% Memory: ✅ 36.176MB (SLO: <38.000MB -4.8%) vs baseline: +5.0% 🟡 span - 26/26✅ add-eventTime: ✅ 20.547ms (SLO: <22.500ms -8.7%) vs baseline: +1.0% Memory: ✅ 38.653MB (SLO: <53.000MB 📉 -27.1%) vs baseline: +5.2% ✅ add-metricsTime: ✅ 90.801ms (SLO: <93.500ms -2.9%) vs baseline: +0.3% Memory: ✅ 42.861MB (SLO: <53.000MB 📉 -19.1%) vs baseline: +5.1% ✅ add-tagsTime: ✅ 136.959ms (SLO: <155.000ms 📉 -11.6%) vs baseline: +0.5% Memory: ✅ 42.661MB (SLO: <53.000MB 📉 -19.5%) vs baseline: +5.1% ✅ get-contextTime: ✅ 18.006ms (SLO: <20.500ms 📉 -12.2%) vs baseline: +2.1% Memory: ✅ 38.122MB (SLO: <53.000MB 📉 -28.1%) vs baseline: +5.5% ✅ is-recordingTime: ✅ 18.049ms (SLO: <20.500ms 📉 -12.0%) vs baseline: +1.5% Memory: ✅ 38.083MB (SLO: <53.000MB 📉 -28.1%) vs baseline: +5.5% ✅ record-exceptionTime: ✅ 41.921ms (SLO: <42.000ms 🟡 -0.2%) vs baseline: +0.3% Memory: ✅ 38.928MB (SLO: <53.000MB 📉 -26.6%) vs baseline: +4.8% ✅ set-statusTime: ✅ 19.779ms (SLO: <22.000ms 📉 -10.1%) vs baseline: +2.3% Memory: ✅ 38.299MB (SLO: <53.000MB 📉 -27.7%) vs baseline: +5.8% ✅ startTime: ✅ 19.002ms (SLO: <20.500ms -7.3%) vs baseline: +7.4% Memory: ✅ 38.221MB (SLO: <53.000MB 📉 -27.9%) vs baseline: +5.9% ✅ start-finishTime: ✅ 57.960ms (SLO: <58.500ms 🟡 -0.9%) vs baseline: -0.2% Memory: ✅ 36.156MB (SLO: <38.000MB -4.9%) vs baseline: +4.6% ✅ start-finish-telemetryTime: ✅ 59.312ms (SLO: <60.000ms 🟡 -1.1%) vs baseline: -0.3% Memory: ✅ 36.176MB (SLO: <38.000MB -4.8%) vs baseline: +4.9% ✅ start-finish-traceid128Time: ✅ 60.219ms (SLO: <62.000ms -2.9%) vs baseline: -0.7% Memory: ✅ 36.333MB (SLO: <38.000MB -4.4%) vs baseline: +5.3% ✅ start-traceid128Time: ✅ 17.943ms (SLO: <22.500ms 📉 -20.3%) vs baseline: +1.4% Memory: ✅ 38.103MB (SLO: <53.000MB 📉 -28.1%) vs baseline: +5.4% ✅ update-nameTime: ✅ 18.520ms (SLO: <22.000ms 📉 -15.8%) vs baseline: +1.5% Memory: ✅ 38.151MB (SLO: <53.000MB 📉 -28.0%) vs baseline: +4.7% 🟡 tracer - 6/6✅ largeTime: ✅ 32.948ms (SLO: <33.950ms -3.0%) vs baseline: +1.2% Memory: ✅ 37.336MB (SLO: <39.250MB -4.9%) vs baseline: +4.5% ✅ mediumTime: ✅ 3.185ms (SLO: <3.500ms -9.0%) vs baseline: +1.3% Memory: ✅ 36.117MB (SLO: <38.750MB -6.8%) vs baseline: +4.5% ✅ smallTime: ✅ 386.540µs (SLO: <390.000µs 🟡 -0.9%) vs baseline: +4.8% Memory: ✅ 36.215MB (SLO: <38.750MB -6.5%) vs baseline: +5.0%
|
Codeowners resolved as |
🎉 All green!❄️ No new flaky tests detected 🔗 Commit SHA: aaa0415 | Docs | Datadog PR Page | Give us feedback! |
# Description Remove Pin from valkey. Note that I removed a traced method that was just used by Pin. # Risks Tests were not modified so the risks should be low. Co-authored-by: louis.tricot <[email protected]>
#17675) ## Description This fixes a surprising race condition happening under AnyIO asynchronous testing where we could sometimes end up constructing a `GatheringFuture` without an active event loop. The assumption was that `gather` would always be called with an event loop existing, but that can apparently be not the case at exit with certain additional `asyncio`-related packages. Co-authored-by: thomas.kowalski <[email protected]>
## Description Fixes #17557. This PR moves `ConfigType` import from `ddtrace.llmobs._experiment.py` in the `_writer.py` to `ExperimentConfigType`, and hides it behind a `TYPE_CHECK` flag to avoid any potential circular imports at app runtime. ## Risks Minimal. The change only affects the form of two type annotations (object → forward-reference string). `TypedDict` accepts string annotations, and `ConfigType` is not referenced at runtime in `_writer.py`. ## Additional Notes Claude session: `a3853e6b-9ff8-4941-8644-e0728502235d` Resume: `claude --resume a3853e6b-9ff8-4941-8644-e0728502235d` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: yun.kim <[email protected]>
…17338) ## Description Adds `injectible_lines` (list of contiguous line ranges extractable from a code object) and `has_injectible_lines` (boolean convenience flag) to the `Scope` dataclass. Includes correctness tests for the range-building helper. Refs: [DEBUG-5381](https://datadoghq.atlassian.net/browse/DEBUG-5381) [DEBUG-5381]: https://datadoghq.atlassian.net/browse/DEBUG-5381?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ Co-authored-by: gabriele.tornetta <[email protected]>
… URL discrepancies (#17668) ## Description Adds telemetry metrics to track when git commit SHA or repository URL values differ across providers (user-supplied `DD_GIT_*` env vars, CI provider, local git client). This helps understand how common these discrepancies are in customer setups. Two new count metrics: - `git.commit_sha_match` — emitted every time git info is built, with tag `matched: "true"|"false"` - `git.commit_sha_discrepancy` — emitted for each mismatched pair, with tags `expected_provider`, `discrepant_provider`, and `type` (`commit_discrepancy` or `repository_discrepancy`) All 6 provider-pair × field combinations are checked explicitly, matching the cross-language reference implementation (Ruby: DataDog/datadog-ci-rb#360). ## Testing Unit tests covering: - All 6 check combinations (3 provider pairs × 2 fields) - Edge cases: empty strings, missing values, no telemetry instance - Telemetry method serialization (`test_telemetry.py`) ## Risks Low — telemetry-only change, no impact on tag values or test execution behavior. Co-authored-by: federico.mon <[email protected]>
## Description Targeted refactor of `ddtrace/internal/telemetry/dependency_tracker.py` and the writer's heartbeat path, focused on concurrency safety and removing redundant work in `collect_report`. No behavior change on the wire. **Concurrency fix (heartbeat):** `writer._report_heartbeat` previously called `tracker.get_all_dependencies()` and then serialized each `DependencyEntry` outside the tracker lock. The returned list was a snapshot, but entries themselves remained live — concurrent SCA hooks (`attach_metadata` / `register_cve`) mutate `DependencyEntry.metadata` and `ReachabilityMetadata.value["reached"]`. In particular, `json.dumps(self.value)` iterates the `reached` list, and a concurrent `append` is not safe. Now the heartbeat calls a new `DependencyTracker.snapshot_for_heartbeat()` that builds telemetry dicts while holding `self._lock`. **collect_report cleanup:** - Removed the thin `_update_imported` wrapper (single caller, pure pass-through). - Normalized new-dep names exactly once; the resulting set is reused both for sent-marking and for skipping just-reported deps in the re-report scan. The previous code normalized each new dep twice and also called `_normalize_dep_name(entry.name)` for every tracked entry on every flush. - Split `collect_report` into `_mark_sent(keys)` and `_collect_rereports(skip_keys)` helpers for clarity (SRP). Public API unchanged. - Dropped unused `get_all_dependencies()`. **Split out:** the `data.get_host_info` `@cached()` migration is tracked separately in #17669 to keep this PR focused on the tracker. **Context:** Findings from a pragmatic code review (L5) of the telemetry dependency tracker on an earlier branch. These are internal-only changes; nothing externally observable shifts. The three standalone helpers (`update_imported_dependencies`, `attach_reachability_metadata`, `register_cve_metadata`) are preserved untouched — they are called from benchmarks, tests, and monkey-patched by `tests/appsec/architectures/mini.py`. The deferred `tracer_config` imports are also preserved: `settings/_config.py` imports telemetry at module top, so a top-level import in the tracker would create a circular import. ## Performance Isolated A/B microbenchmark of `collect_report()`'s body — byte-for-byte reproduction of `main` vs. this branch, same mocked distribution lookups on both sides, 33 interleaved samples per case. Times are the best of the run; medians track the same direction. | Scenario (SCA enabled unless noted) | Before (main) | After (this PR) | Δ | |---|---:|---:|---:| | 2 000 tracked deps, 50 new imports | 2 695 µs | 1 981 µs | **+26.5 % faster** | | 5 000 tracked deps, 50 new imports | 6 567 µs | 4 752 µs | **+27.6 % faster** | | 10 000 tracked deps, 50 new imports | 13 075 µs | 9 539 µs | **+27.0 % faster** | | **Idle tick**, 5 000 deps, 0 new | 1 880 µs | 148 µs | **+92.1 % faster** | | **Idle tick**, 10 000 deps, 0 new | 3 790 µs | 298 µs | **+92.1 % faster** | | SCA **disabled**, 2 000 deps, 50 new | 1 767 µs | 1 783 µs | ~flat (−0.9 %) | **Where the speedup comes from:** the previous re-report scan called `_normalize_dep_name(entry.name)` (a regex substitution) on every tracked entry, every tick. The refactor iterates `imported.items()` and uses the already-normalized dict key directly, eliminating N regex calls per heartbeat. The idle-path win is large because with no new modules the only work done is that re-report scan. The SCA-disabled fast path was not touched and is statistically flat, as expected. ## Testing - `hatch run lint:fmt -- ...` — clean - `pytest tests/telemetry/test_dependency.py` — **69 passed**, including a new `TestSnapshotForHeartbeat` class: - Empty-tracker case returns `[]` - All entries serialized correctly; `metadata=None` entries omit the `"metadata"` key - Already-sent metadata is still included in the heartbeat payload - **Concurrency test**: a writer thread repeatedly calls `attach_metadata` while the main thread builds 200 snapshots. Without the lock, the writer races `json.dumps` iteration over `reached`; with the lock, the test is clean. - `pytest tests/telemetry/test_data.py` — 15 passed, 2 failures that are pre-existing and unrelated (subprocess-marked tests that assume docker paths in `conftest.py`; verified identical failure on clean `origin/main`). - `pytest tests/telemetry/test_writer.py` — same pre-existing subprocess-path failures on both clean `main` and this branch; no new regressions. ## Risks Low — internal refactor with no wire-format or public-API changes. - `snapshot_for_heartbeat()` holds the tracker lock during a list comprehension that calls `to_telemetry_dict(include_all_metadata=True)` per entry, which includes a `json.dumps` per `ReachabilityMetadata`. On an app with 10k+ tracked deps this is marginally longer than the previous lock-then-serialize-outside pattern, but in exchange we get correctness. Heartbeats run at most once per extended-interval (default 24h), so this is not hot. - `collect_report` helper split is a pure refactor; `_mark_sent` and `_collect_rereports` remain private and require the caller to hold `self._lock`. ## Additional Notes - Added `changelog/no-changelog` — the SCA-related telemetry (reachability metadata on `DependencyEntry`) is unreleased, and the rest of this PR is internal refactor. Co-authored-by: alberto.vara <[email protected]>
…ctives (#17677) ## Summary The APM_TRACING remote-config handler in `ddtrace/llmobs/_product.py` previously wrote `llmobs.enabled = None` into `_rc_value` on every poll that did not carry an `llmobs` section, then branched on `enabled_config.value()`. When LLMObs had been enabled programmatically via `LLMObs.enable()`, `_llmobs_enabled` had no `_env_value` or `_code_value` set, so `value()` fell through to the default (`False`), which fired the `elif not enabled_config.value() and LLMObs.enabled:` branch and silently called `LLMObs.disable()` on every RC poll. Services that enabled LLMObs via `DD_LLMOBS_ENABLED=true` were not affected — `_env_value` shielded `value()` from the `_rc_value = None` fallback. This is why the regression was hard to reproduce in simple tests and why the original test `test_rc_missing_llmobs_does_not_disable_enabled_llmobs` passes against the broken handler. ### Root cause `LLMObs.enable()` flipped `cls.enabled = True` but never wrote `ddtrace.config._llmobs_enabled`, so `_ConfigItem.value()` contradicted the effective state. The RC handler trusted `value()` and disabled LLMObs whenever the config appeared to say "off" — which it always did for programmatic enables. ### Fix Align `ddtrace.config._llmobs_enabled` with the effective state of LLMObs at every call site. `LLMObs.enable()` / `LLMObs.disable()` now mirror `cls.enabled` into `config._llmobs_enabled` via `Config.__setattr__` (→ `set_value(bool, "code")`), gated on the existing `_auto` parameter: - **User-initiated calls** (`_auto=False`): write `_code_value` so `value()` reflects the user's explicit choice. - **Auto-start / RC-initiated calls** (`_auto=True`): skip the write. The caller (the RC handler, or the env-var auto-start path) has already set the appropriate higher-precedence source (`remote_config` or `env_var`); stamping `"code"` on top would mask it when `_rc_value` later clears. `disable()` gains the same `_auto` parameter and the RC handler passes `_auto=True` when it disables. With the invariant correct at every call site, the RC handler reverts to the simple `value()`-based form — the pre-regression shape, plus `lib_config.get("llmobs") or {}` as a defense against `{"llmobs": null}` payloads, plus `_auto=True` on the `LLMObs.disable()` call. `set_value` is called unconditionally, so stale `_rc_value` clears naturally when a directive disappears from the merged `lib_config`. ## Test plan - [x] `test_enable_disable_keeps_global_config_llmobs_enabled_in_sync` (in `test_llmobs_service.py`) pins the invariant: `ddtrace.config._llmobs_enabled` reflects effective state after `LLMObs.enable()` and after `LLMObs.disable()`. - [x] `test_rc_missing_llmobs_does_not_disable_programmatically_enabled_llmobs` — the scenario that reproduces the 4.8.0rc bug. Simulates the post-`enable()` invariant (both `cls.enabled=True` and `config._llmobs_enabled=True`) and verifies that `{}`, `{"llmobs": {}}`, and `{"llmobs": {"ml_app_name": "x"}}` payloads do not call `LLMObs.disable()`. - [x] `test_rc_directive_removal_clears_rc_override` — a first payload sets `_rc_value` on both `_llmobs_enabled` and `_llmobs_ml_app`; a subsequent payload without an `llmobs` section clears both back to `None`. - [x] `test_rc_missing_llmobs_is_noop` now also covers `{"llmobs": null}` (pre-fix would have crashed on `None.get()`). - [x] Existing `test_rc_missing_llmobs_does_not_disable_enabled_llmobs` / `test_rc_enables_llmobs_and_sets_ml_app` / `test_rc_disables_llmobs` still exercise the env-var and happy paths. - [ ] CI green on the full llmobs test suite. ## Manual verification Reproduced and verified against a real staging Datadog Agent + staging RC service. ### Reproduction conditions 1. LLMObs enabled programmatically via `LLMObs.enable(ml_app=...)` (NOT via `DD_LLMOBS_ENABLED` env var). 2. `DD_ENV=staging` + `DD_SERVICE=<svc>` so the staging RC rule targets the process. 3. That RC rule ships a `lib_config` with no `llmobs.*` key (in staging it carries only `exception_replay_enabled: True`). 4. Tracer started via `ddtrace-run` so remote config is active and hits the local agent. ### Observed on unpatched `ddtrace==4.8.0rc4` ``` Disabling LLMObs via Remote Config: {} ``` …flipping LLMObs off for the remainder of the process (1/12 iterations shipped spans). ### Observed with this PR's fix applied - `Disabling LLMObs via Remote Config` — **0 occurrences** - `LLMObs.enabled=True` across **12/12** iterations - `Dispatching 1 payloads to product APM_TRACING` still fires; the handler correctly no-ops on the empty `llmobs` section - LLMObs spans flushed successfully to the staging intake throughout the run Co-authored-by: ZStriker19 <[email protected]> Co-authored-by: zach.groves <[email protected]>
Co-authored-by: zach.groves <[email protected]>
…tags (#16531) ## Description The v3 pytest plugin (`ddtrace.testing.internal.pytest`) does not call `_patch_all()` by default, so the Selenium integration was never applied — browser tests were missing `test.is_browser`, `test.browser.*` tags. This PR explicitly patches Selenium in `pytest_sessionstart` when the ddtrace trace filter is active (which is the default for the v3 plugin), ensuring browser test visibility tags are set even without `--ddtrace-patch-all`. ## Changes - **`ddtrace/testing/internal/pytest/plugin.py`**: Patch Selenium in `pytest_sessionstart` when `enable_ddtrace_trace_filter` is true but `enable_all_ddtrace_integrations` is false. - **`ddtrace/contrib/internal/selenium/patch.py`**: Added docstring explaining how the integration works with both v2 and v3 plugins. - **`tests/contrib/selenium/test_selenium_chrome.py`**: Added `test_selenium_v3_plugin_tags` to verify browser tags are set with the v3 plugin. Existing snapshot tests are pinned to the old plugin (`DD_PYTEST_USE_NEW_PLUGIN=false`) since they rely on agent traces. ## Testing - New test `test_selenium_v3_plugin_tags` uses `pytester` with a mocked `WebDriver` to verify `test.is_browser`, `test.browser.name`, and `test.browser.version` tags are correctly set. ## Risks None — the Selenium patch is wrapped in a try/except and only applies in the v3 plugin path. Co-authored-by: federico.mon <[email protected]>
## Summary Split out of #17667. Replace the `_host_info` module-level global + `None`-check in `ddtrace/internal/telemetry/data.py` with `@callonce`, the idiomatic no-arg memoization decorator in this codebase (already used by `ddtrace/internal/packages.py` and `ddtrace/internal/debug.py`). - Removes the unsynchronized `None`-check race. `@callonce` also has no explicit lock, but `get_host_info` is deterministic and idempotent, so a benign double-compute on first-call contention produces identical results with no torn state. - `get_host_info()` is still the public entry point and still returns the same dict shape. - Originally this PR used `@cached()` (i.e. `functools.lru_cache`), but `@cached()` requires at least one hashable argument and forced a dummy `_: tuple = ()` parameter plus a wrapper function. `@callonce` is purpose-built for zero-arg functions and can be applied directly to `get_host_info`, eliminating the indirection. ## Performance Microbenchmark on the warm path (A/B reproduction of the two implementations, 10 000 calls per sample, 11 samples): | Per-call (warm) | Before (global None-check) | After (`@callonce`) | Δ | |---|---:|---:|---:| | Best | ~20 ns | ~30-60 ns | negligible | `get_host_info()` is called at most once per telemetry heartbeat (~60 s), so any small constant-factor difference is not observable in practice. The motivation is code clarity and consistency with the rest of the codebase, not speed. ## Test plan - [x] `tests/telemetry/test_data.py::test_get_host_info` passes on Python 3.14 via `scripts/run-tests`. - [x] Existing `tests/telemetry/` suite passes (no behavior change at the function boundary — same dict contents, same call signature). - [x] No release note needed — internal refactor, `changelog/no-changelog` applied. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: alberto.vara <[email protected]>
## Summary - Adds `.omc/` to `.gitignore` so oh-my-claudecode local state (e.g. `project-memory.json`) is not tracked. ## Test plan - [x] `git check-ignore` confirms `.omc/` and its contents are ignored. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: alberto.vara <[email protected]>
[MLOB-6633] ## Description Extracted from #17235 to narrow review scope. This PR changes the default `model_name` and `model_provider` for LLM and embedding spans from `""` / `"custom"` to the shared `UNKNOWN_MODEL_NAME` / `UNKNOWN_MODEL_PROVIDER` constants (both `"unknown"`) consistently across every entry point: - Auto-instrumentation fallback in `_build_span_meta` (`ddtrace/llmobs/_llmobs.py`) - Manual instrumentation APIs `LLMObs.llm()` and `LLMObs.embedding()` - Decorator helper `_get_llmobs_span_options` used by `@llm` and `@embedding` Previously these sites independently defaulted to `""` or `"custom"`; unifying them prevents the inconsistency where the same "no provider specified" signal would surface as `"custom"` through the manual APIs but `"unknown"` through the auto-instrumentation path. ## Testing - `tests/llmobs/test_llmobs_service.py`: renamed `test_default_model_provider_set_to_custom` → `..._unknown` (same for embedding), updated `test_llm_span_no_model_sets_default` / `test_embedding_span_no_model_sets_default` / `test_ml_app_override` to assert the new default via the `UNKNOWN_MODEL_NAME` / `UNKNOWN_MODEL_PROVIDER` constants. - `tests/llmobs/test_llmobs.py`: renamed `test_model_provider_defaults_to_custom` → `..._unknown`, updated to use the constant. - `tests/llmobs/test_llmobs_decorators.py`: updated 8 assertion sites to use the constants. - `tests/llmobs/_utils.py`: helper uses the constants. - `tests/contrib/claude_agent_sdk/test_claude_agent_sdk_llmobs.py`: assertion fix for auto-instrumented default. ## Risks **Customer-visible behavior change.** Any monitor or dashboard filtering `model_provider:custom` against spans that relied on the default — whether from auto-instrumentation, manual `LLMObs.llm()` / `LLMObs.embedding()` calls, or the `@llm` / `@embedding` decorators — will stop matching once those spans start emitting `"unknown"`. The release note calls this out; users filtering explicitly by a non-default provider value are unaffected. Public API docstrings on `LLMObs.llm()` and `LLMObs.embedding()` are updated to document the new default, but the function signatures are unchanged (no caller code needs to change). ## Additional Notes Part of a 3-PR split of #17235. Order: 1. **This PR** — default model=unknown (S) 2. #17684 — trace/parent IDs as strings (M) 3. #17685 — initialize tags and span name at span start (L, stacked on #17684) Claude session: `da6fb254-912e-4d04-aec2-ed09cd1980ac` Resume: `claude --resume da6fb254-912e-4d04-aec2-ed09cd1980ac` [MLOB-6633]: https://datadoghq.atlassian.net/browse/MLOB-6633 Co-authored-by: yun.kim <[email protected]>
…llocator (#17664) ## Description This PR fixes a segmentation fault in the memory allocation profiler that occurs when a hook call races with `memalloc` start/stop operations. The issue arises from concurrent access to the saved allocator struct, which could be partially written while being read, resulting in`NULL` function pointers being dereferenced. The key indicator in that case is that `#1 0x0000000000000000` frame -- we are trying to execute a null function pointer. ```` Error UnixSignal: Process terminated with SEGV_MAPERR (SIGSEGV) #0 0x00007ff3c303a8d4 #1 0x0000000000000000 memalloc_alloc (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/profiling/collector/_memalloc.cpp:68) #2 0x00007ff39dcb3b20 memalloc_alloc (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/profiling/collector/_memalloc.cpp:68) #3 0x00007ff39dcb3b20 memalloc_malloc(void*, unsigned long) (/go/src/github.com/DataDog/apm-reliability/dd-trace-py/ddtrace/profiling/collector/_memalloc.cpp:80) #4 0x00007ff3c3087e1b PyUnicode_New #5 0x00007ff3c30889f4 #6 0x00007ff3c3170c84 #7 0x00007ff3c316b931 #8 0x00007ff3c31aaac8 #9 0x00007ff3c31033ac #10 0x00007ff3c310e2a6 PyObject_CallMethodObjArgs #11 0x00007ff3c310e46d #12 0x00007ff3c31a96c2 #13 0x00007ff3c3102fd7 PyObject_Vectorcall #14 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #15 0x00007ff3c323c094 #16 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #17 0x00007ff3c323c094 #18 0x00007ff3c30e997d PyObject_CallOneArg #19 0x00007ff3c306a480 _PyObject_GenericGetAttrWithDict #20 0x00007ff3c30c620d PyObject_GetAttr #21 0x00007ff3c32309e7 _PyEval_EvalFrameDefault #22 0x00007ff3c323c094 #23 0x00007ff3c312880e #24 0x00007ff3c30e917c _PyObject_MakeTpCall #25 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #26 0x00007ff3c323c094 #27 0x00007ff3c312880e #28 0x00007ff3c30e917c _PyObject_MakeTpCall #29 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #30 0x00007ff3c323c094 #31 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #32 0x00007ff3c323c094 #33 0x00007ff3c317d0fd #34 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #35 0x00007ff3c323c094 #36 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #37 0x00007ff3c323c094 #38 0x00007ff3c317d1b5 #39 0x00007ff3c3102fd7 PyObject_Vectorcall #40 0x00007ff3c3232f4a _PyEval_EvalFrameDefault #41 0x00007ff3c3240da5 #42 0x00007ff3c324112d #43 0x00007ff3c3233be1 _PyEval_EvalFrameDefault #44 0x00007ff3c323c094 #45 0x00007ff3c317d1b5 #46 0x00007ff3c3102fd7 PyObject_Vectorcall #47 0x00007ff3c3232f4a _PyEval_EvalFrameDefault #48 0x00007ff3c323c094 #49 0x00007ff3c31033ac #50 0x00007ff3c310358d PyObject_CallFunctionObjArgs #51 0x00007ff3bf7eb91d WraptBoundFunctionWrapper_call (/project/src/wrapt/_wrappers.c:3750) #52 0x00007ff3c3104055 _PyObject_Call #53 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #54 0x00007ff3c323c094 #55 0x00007ff3c317d23c #56 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #57 0x00007ff3c323c094 #58 0x00007ff3c310416f _PyObject_Call #59 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #60 0x00007ff3c3240da5 #61 0x00007ff3c324112d #62 0x00007ff3c3233be1 _PyEval_EvalFrameDefault #63 0x00007ff3c323c094 #64 0x00007ff3c317d1b5 #65 0x00007ff3c3102fd7 PyObject_Vectorcall #66 0x00007ff3c3232f4a _PyEval_EvalFrameDefault #67 0x00007ff3c323c094 #68 0x00007ff3c317d1b5 #69 0x00007ff3c310416f _PyObject_Call #70 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #71 0x00007ff3c323c094 #72 0x00007ff3c317d1b5 #73 0x00007ff3c310416f _PyObject_Call #74 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #75 0x00007ff3c323c094 #76 0x00007ff3c31033ac #77 0x00007ff3c310358d PyObject_CallFunctionObjArgs #78 0x00007ff3bf7eb91d WraptBoundFunctionWrapper_call (/project/src/wrapt/_wrappers.c:3750) #79 0x00007ff3c30e917c _PyObject_MakeTpCall #80 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #81 0x00007ff3c323c094 #82 0x00007ff3c317d518 #83 0x00007ff3c3155963 #84 0x00007ff3c315393d #85 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #86 0x00007ff3c323c094 #87 0x00007ff3c317d0fd #88 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #89 0x00007ff3c323c094 #90 0x00007ff3c317d0fd #91 0x00007ff3c317d518 #92 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #93 0x00007ff3c323c094 #94 0x00007ff3c30e9371 _PyObject_FastCallDictTstate #95 0x00007ff3c30e958d _PyObject_Call_Prepend #96 0x00007ff3c3109150 #97 0x00007ff3c3104055 _PyObject_Call #98 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #99 0x00007ff3c323c094 #100 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #101 0x00007ff3c30e958d _PyObject_Call_Prepend #102 0x00007ff3c3109150 #103 0x00007ff3c30e917c _PyObject_MakeTpCall #104 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #105 0x00007ff3c323c094 #106 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #107 0x00007ff3c30e958d _PyObject_Call_Prepend #108 0x00007ff3c3109150 #109 0x00007ff3c30e917c _PyObject_MakeTpCall #110 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #111 0x00007ff3c323c094 #112 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #113 0x00007ff3c30e958d _PyObject_Call_Prepend #114 0x00007ff3c3109150 #115 0x00007ff3c30e917c _PyObject_MakeTpCall #116 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #117 0x00007ff3c323c094 #118 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #119 0x00007ff3c30e958d _PyObject_Call_Prepend #120 0x00007ff3c3109150 #121 0x00007ff3c30e917c _PyObject_MakeTpCall #122 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #123 0x00007ff3c323c094 #124 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #125 0x00007ff3c30e958d _PyObject_Call_Prepend #126 0x00007ff3c3109150 #127 0x00007ff3c30e917c _PyObject_MakeTpCall #128 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #129 0x00007ff3c323c094 #130 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #131 0x00007ff3c30e958d _PyObject_Call_Prepend #132 0x00007ff3c3109150 #133 0x00007ff3c30e917c _PyObject_MakeTpCall #134 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #135 0x00007ff3c323c094 #136 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #137 0x00007ff3c30e958d _PyObject_Call_Prepend #138 0x00007ff3c3109150 #139 0x00007ff3c30e917c _PyObject_MakeTpCall #140 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #141 0x00007ff3c323c094 #142 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #143 0x00007ff3c30e958d _PyObject_Call_Prepend #144 0x00007ff3c3109150 #145 0x00007ff3c30e917c _PyObject_MakeTpCall #146 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #147 0x00007ff3c323c094 #148 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #149 0x00007ff3c30e958d _PyObject_Call_Prepend #150 0x00007ff3c3109150 #151 0x00007ff3c30e917c _PyObject_MakeTpCall #152 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #153 0x00007ff3c323c094 #154 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #155 0x00007ff3c30e958d _PyObject_Call_Prepend #156 0x00007ff3c3109150 #157 0x00007ff3c30e917c _PyObject_MakeTpCall #158 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #159 0x00007ff3c323c094 #160 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #161 0x00007ff3c30e958d _PyObject_Call_Prepend #162 0x00007ff3c3109150 #163 0x00007ff3c30e917c _PyObject_MakeTpCall #164 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #165 0x00007ff3c323c094 #166 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #167 0x00007ff3c30e958d _PyObject_Call_Prepend #168 0x00007ff3c3109150 #169 0x00007ff3c30e917c _PyObject_MakeTpCall #170 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #171 0x00007ff3c323c094 #172 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #173 0x00007ff3c30e958d _PyObject_Call_Prepend #174 0x00007ff3c3109150 #175 0x00007ff3c30e917c _PyObject_MakeTpCall #176 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #177 0x00007ff3c323c094 #178 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #179 0x00007ff3c30e958d _PyObject_Call_Prepend #180 0x00007ff3c3109150 #181 0x00007ff3c30e917c _PyObject_MakeTpCall #182 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #183 0x00007ff3c323c094 #184 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #185 0x00007ff3c30e958d _PyObject_Call_Prepend #186 0x00007ff3c3109150 #187 0x00007ff3c30e917c _PyObject_MakeTpCall #188 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #189 0x00007ff3c323c094 #190 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #191 0x00007ff3c323c094 #192 0x00007ff3c30e92f1 _PyObject_FastCallDictTstate #193 0x00007ff3c30e958d _PyObject_Call_Prepend #194 0x00007ff3c3109150 #195 0x00007ff3c30e917c _PyObject_MakeTpCall #196 0x00007ff3c32335a2 _PyEval_EvalFrameDefault #197 0x00007ff3c323c094 #198 0x00007ff3c317d0fd #199 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #200 0x00007ff3c323c094 #201 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #202 0x00007ff3c323c094 #203 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #204 0x00007ff3c323c094 #205 0x00007ff3c3233dd3 _PyEval_EvalFrameDefault #206 0x00007ff3c323c094 #207 0x00007ff3c317d23c #208 0x00007ff3c31a7ec5 #209 0x00007ff3c301ac77 #210 0x00007ff3c357c573 ```` The fix implements two key changes. 1. **Hook functions (`memalloc_alloc`, `memalloc_realloc`)**: Snapshot the allocator struct locally before use and guard indirect function calls with `NULL` checks. This prevents crashes if a partially-written struct is observed during a start/stop race. 2. **Start/stop operations (`memalloc_start`, `memalloc_stop`)**: Use local variables and single assignments when publishing the allocator struct to `global_memalloc_ctx.pymem_allocator_obj`. This ensures concurrent hook calls observe either the old or new struct, never a partially-written intermediate state. The real root cause is that `PyMem_GetAllocator` is not documented as atomic, and the struct could be read field-by-field while being written to concurrently. By using local copies and single assignments, we ensure atomicity at the C level and prevent observation of inconsistent state. Co-authored-by: thomas.kowalski <[email protected]>
## Description When the RC agent deletes a config entry it sends a payload with `content=None`. Previously, `APMTracingCallback` tracked active configs incrementally across poll cycles, but deletion payloads were silently skipped — the config was never removed from the internal map and stale settings persisted indefinitely. **Fix:** rebuild `_config_map` from scratch on every invocation. Because deletion payloads (`content=None`) are skipped during the rebuild, deleted configs are simply absent from the new map. The next `dispatch` reflects the updated (or empty) configuration automatically, with no extra bookkeeping. ## Testing Three new regression tests in `tests/internal/remoteconfig/test_remoteconfig.py`, all `@pytest.mark.subprocess`, passing locally on Python 3.14: - `test_apm_tracing_same_config_id_delete_is_not_skipped` — verifies via the live `dispatch → tracer_rc` chain that a deletion payload in the same batch as a content payload wins; asserts `ddtrace.config._tracing_enabled` is not overwritten by stale content. - `test_apm_tracing_rc_handlers_dispatched_from_products` — verifies the full `on() → APMTracingCallback → dispatch()` round-trip reaches downstream product handlers. - `test_apm_tracing_start_collects_product_apm_capabilities` — verifies `start()` aggregates `APMCapabilities` from all registered products into the RC registration. ## Risks Low. The behavioral change is limited to deletion payloads, which previously caused a silent no-op. The rebuild approach assumes the agent sends a full config set on every poll (the same assumption made by the previous cleanup loop). ## Additional Notes None. Co-authored-by: munir.abdinur <[email protected]>
…#17703) ## Description It can be nice to know some extra information about the artifacts that were built. Especially on generic S3 paths like `/main/` which are not tied directly to a commit sha or pipeline id. This PR introduces two changes: 1. Adds metadata about pipeline id, commit sha, timestamp, pacakge version, etc to `index.html` files generated. Both as text and html metadata. 2. Adds a `metadata.txt` file which contains the same information as `key=value` pairs ## Testing <!-- Describe your testing strategy or note what tests are included --> ## Risks <!-- Note any risks associated with this change, or "None" if no risks --> ## Additional Notes <!-- Any other information that would be helpful for reviewers --> Co-authored-by: brett.langdon <[email protected]>
## Description This PR fixes the following crash. ``` Error UnixSignal: Process terminated with SEGV_MAPERR (SIGSEGV) #0 0x00005a87adf31055 dictkeys_get_index (/usr/src/python/Objects/dictobject.c:344:13) #1 0x00005a87adf31055 unicodekeys_lookup_generic (/usr/src/python/Objects/dictobject.c:876:14) #2 0x00005a87adf31055 _Py_dict_lookup (/usr/src/python/Objects/dictobject.c:1056:18) #3 0x00005a87adf31055 PyDict_GetItemWithError (/usr/src/python/Objects/dictobject.c:1789:10) #4 0x00007b2e6fc1a8e0 __pyx_pw_7ddtrace_8internal_9telemetry_18metrics_namespaces_15MetricNamespace_5add_metric #5 0x00005a87ae00c955 _PyObject_VectorcallTstate (/usr/src/python/./Include/internal/pycore_call.h:92:11) #6 0x00005a87ae00c955 PyObject_Vectorcall (/usr/src/python/Objects/call.c:299:12) #7 0x00005a87ae05ee1d _PyEval_EvalFrameDefault (/usr/src/python/Python/ceval.c:4760:23) #8 0x00005a87ae05e060 _PyEval_EvalFrame (/usr/src/python/./Include/internal/pycore_ceval.h:73:16) #9 0x00005a87ae05e060 _PyEval_Vector (/usr/src/python/Python/ceval.c:6425:24) #10 0x00005a87ae00d587 PyObject_Call #11 0x00005a87ae061fd8 do_call_core (/usr/src/python/Python/ceval.c:7343:12) #12 0x00005a87ae061fd8 _PyEval_EvalFrameDefault (/usr/src/python/Python/ceval.c:5367:22) #13 0x00005a87ae05e060 _PyEval_EvalFrame (/usr/src/python/./Include/internal/pycore_ceval.h:73:16) #14 0x00005a87ae05e060 _PyEval_Vector (/usr/src/python/Python/ceval.c:6425:24) #15 0x00005a87ae00e0ba _PyObject_VectorcallTstate (/usr/src/python/./Include/internal/pycore_call.h:92:11) #16 0x00005a87ae00e0ba method_vectorcall (/usr/src/python/Objects/classobject.c:59:18) #17 0x00005a87ae00c955 _PyObject_VectorcallTstate (/usr/src/python/./Include/internal/pycore_call.h:92:11) #18 0x00005a87ae00c955 PyObject_Vectorcall (/usr/src/python/Objects/call.c:299:12) #19 0x00005a87ae05ee1d _PyEval_EvalFrameDefault (/usr/src/python/Python/ceval.c:4760:23) #20 0x00005a87ae05e060 _PyEval_EvalFrame (/usr/src/python/./Include/internal/pycore_ceval.h:73:16) #21 0x00005a87ae05e060 _PyEval_Vector (/usr/src/python/Python/ceval.c:6425:24) #22 0x00005a87ae00d02f _PyFunction_Vectorcall (/usr/src/python/Objects/call.c:393:16) #23 0x00005a87ae00d02f _PyObject_FastCallDictTstate (/usr/src/python/Objects/call.c:141:15) #24 0x00005a87ae00d02f _PyObject_Call_Prepend (/usr/src/python/Objects/call.c:482:24) #25 0x00005a87ae0bafd9 slot_tp_call (/usr/src/python/Objects/typeobject.c:7623:15) #26 0x00005a87ae00c4d9 _PyObject_MakeTpCall (/usr/src/python/Objects/call.c:214:18) #27 0x00005a87ae05ee1d _PyEval_EvalFrameDefault (/usr/src/python/Python/ceval.c:4760:23) #28 0x00005a87ae05e060 _PyEval_EvalFrame (/usr/src/python/./Include/internal/pycore_ceval.h:73:16) #29 0x00005a87ae05e060 _PyEval_Vector (/usr/src/python/Python/ceval.c:6425:24) #30 0x00005a87ae00d02f _PyFunction_Vectorcall (/usr/src/python/Objects/call.c:393:16) #31 0x00005a87ae00d02f _PyObject_FastCallDictTstate (/usr/src/python/Objects/call.c:141:15) #32 0x00005a87ae00d02f _PyObject_Call_Prepend (/usr/src/python/Objects/call.c:482:24) #33 0x00005a87ae041f7b slot_tp_init (/usr/src/python/Objects/typeobject.c:7854:15) #34 0x00005a87ae040731 type_call (/usr/src/python/Objects/typeobject.c:1103:19) #35 0x00005a87ae00c4d9 _PyObject_MakeTpCall (/usr/src/python/Objects/call.c:214:18) #36 0x00005a87ae05ee1d _PyEval_EvalFrameDefault (/usr/src/python/Python/ceval.c:4760:23) #37 0x00005a87ae05e060 _PyEval_EvalFrame (/usr/src/python/./Include/internal/pycore_ceval.h:73:16) #38 0x00005a87ae05e060 _PyEval_Vector (/usr/src/python/Python/ceval.c:6425:24) #39 0x00005a87ae00e134 _PyObject_VectorcallTstate (/usr/src/python/./Include/internal/pycore_call.h:92:11) #40 0x00005a87ae00e134 method_vectorcall (/usr/src/python/Objects/classobject.c:89:18) #41 0x00005a87ae00d587 PyObject_Call #42 0x00005a87ae061fd8 do_call_core (/usr/src/python/Python/ceval.c:7343:12) #43 0x00005a87ae061fd8 _PyEval_EvalFrameDefault (/usr/src/python/Python/ceval.c:5367:22) #44 0x00005a87ae05e060 _PyEval_EvalFrame (/usr/src/python/./Include/internal/pycore_ceval.h:73:16) #45 0x00005a87ae05e060 _PyEval_Vector (/usr/src/python/Python/ceval.c:6425:24) #46 0x00005a87ae00d587 PyObject_Call #47 0x00005a87ae061fd8 do_call_core (/usr/src/python/Python/ceval.c:7343:12) #48 0x00005a87ae061fd8 _PyEval_EvalFrameDefault (/usr/src/python/Python/ceval.c:5367:22) #49 0x00005a87ae05e060 _PyEval_EvalFrame (/usr/src/python/./Include/internal/pycore_ceval.h:73:16) #50 0x00005a87ae05e060 _PyEval_Vector (/usr/src/python/Python/ceval.c:6425:24) #51 0x00005a87ae00e04b _PyObject_VectorcallTstate (/usr/src/python/./Include/internal/pycore_call.h:92:11) #52 0x00005a87ae00e04b method_vectorcall (/usr/src/python/Objects/classobject.c:67:20) #53 0x00005a87ae00d587 PyObject_Call #54 0x00005a87ae10e9b3 thread_run (/usr/src/python/./Modules/_threadmodule.c:1124:21) #55 0x00005a87ae0f1098 pythread_wrapper (/usr/src/python/Python/thread_pthread.h:241:5) #56 0x00007b2e71c49609 start_thread #57 0x00007b2e71a14353 clone ``` Co-authored-by: brettlangdon <[email protected]>
## Description Allowing failure to keep release pipelines green ## Testing <!-- Describe your testing strategy or note what tests are included --> ## Risks <!-- Note any risks associated with this change, or "None" if no risks --> ## Additional Notes <!-- Any other information that would be helpful for reviewers --> Co-authored-by: gyuheon.oh <[email protected]>
) ## Description `PeriodicThread_join` (native, in `ddtrace/internal/_threads.cpp`) guarded its argument parsing with `args != NULL && kwargs != NULL`. CPython passes `kwargs == NULL` whenever the caller uses only positional arguments, so `t.join(0.1)` skipped parsing and `timeout` stayed `Py_None`, falling through to `self->_stopped->wait()` — an unbounded wait. Both `PeriodicService.join` (`ddtrace/internal/periodic.py:64`) and `Timer.join` (`ddtrace/internal/periodic.py:151`) forward to the underlying worker positionally, so every caller that did `service.join(timeout=...)` was silently waiting for the thread to exit on its own instead of honoring the requested timeout. The telemetry writer shutdown path (`self.join(timeout=2)`) is one user-visible example. The fix drops the spurious `kwargs != NULL` check: `PyArg_ParseTupleAndKeywords` already accepts `kwargs == NULL`. This bug has been in the file since `PeriodicThread_join` was first introduced; it was found by a randomized lifecycle stress test targeting the native `_threads.cpp` that I'm prototyping separately. ## Testing New regression test in `tests/internal/test_periodic.py`: `test_periodic_join_positional_timeout_is_honored` — asserts both `t.join(0.1)` and `t.join(timeout=0.1)` return within 1s against a long-interval thread. Before the fix, the positional form hangs forever; after the fix both forms return in ~0.1s. Quick standalone verification on a Linux workspace: Before: ``` kwarg join(timeout=0.1): elapsed=0.100s WATCHDOG: positional join hung past 2s — killing ``` After: ``` positional join(0.1): elapsed=0.100s keyword join(t=0.1): elapsed=0.100s ``` ## Risks Very low. Behavior change is scoped to `PeriodicThread.join(X)` called positionally — previously an unbounded wait, now bounded by `X`. Callers that were relying on the (undocumented) infinite-wait-when-passing-a-number behavior would be affected, but that matches no documented contract and all existing positional callsites in the repo already want the timeout honored (they are `service.join(timeout=...)` wrappers forwarding positionally). No API surface change. Release note included. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: erwan.viollet <[email protected]>
62fca60 to
b58ef83
Compare
This change fixes an issue reported via Support in which
svc_srcis set tomin cases whereservicematches the_default_serviceof an active integration config. In such cases, the intended behavior is that itsvc_srcis equal toservice.