Skip to content

Commit 5a8dcc6

Browse files
jordan-wongdevflow.devflow-routing-intake
andauthored
skill(apm-integrations): async instrumentation - add rules from review feedback (#11990)
skill(apm-integrations): library-native context-map pattern + wrap-placement guidance Adds two subsections to context-tracking.md driven by reactor-core-3.1 blind regen findings (dd-trace-java PR #11940): 1. **Library-native context maps — the *ContextBridge pattern** (RI-6). Prescribes preservation/regeneration of the Reactor-style context-bridge helper for libraries with a first-class Context concept (Reactor, Kotlin coroutines, JAX-RS, Vert.x). The subscriber-wrapping pattern alone is insufficient — regenerating a reactive module without the bridge silently breaks Spring WebFlux, Spring Kafka reactive, and related downstream libraries. 2. **Wrap placement and context-store lifecycle** (from @mcculls' review of PR #11940). Boundary-only wrapping, Subscriber-lifecycle stores, avoid double-wrapping. Prevents the "increased memory use" pattern the eval output demonstrated. Motivating incident: dd-trace-java PR #11940 eval dropped 5 of 7 master reactor-core-3.1 instrumentation classes including ReactorContextBridge, which caused spring-messaging-4.0's KafkaBatchListenerCoroutineTest to time out. Sources: - @mcculls on PR #11940 FluxInstrumentation.java:20 - docs/eval-research/cycles/2026-07-14-async-cycle-report.md RI-5, RI-6 - Master reference: dd-java-agent/instrumentation/reactor-core-3.1/ skill(apm-integrations): strengthen Rule #2 for regen — super(), package, classes, ordering Adds five prescriptions to instrumenter-module.md driven by async cycle iteration-1 findings (docs/eval-research/cycles/2026-07-14-async-cycle-report.md) plus @ygree review feedback on rxjava-3.0 PR #11939. 1. **super() alias strengthening (RI-1)** — concrete rxjava-3.0 example showing DD_TRACE_RXJAVA_3_ENABLED breakage when the version alias is dropped. Existing "preserve super() verbatim" rule was too abstract. 2. **Package layout preservation on regen (RI-4)** — concrete reactor-core-3.1 example showing graal-native-image cross-module reference breakage when the eval renamed reactor.core -> reactorcore. 3. **Enumerate all master *Instrumentation.java classes on regen (RI-5)** — the reactor-core-3.1 regen kept 2 of 7 master classes, silently dropping ReactorContextBridge and 4 others. Rule prescribes an explicit pre-generation enumeration + post-generation diff check. 4. **Preserve declarative-array ordering (NR-1)** — @ygree flagged "meaningless reshuffling" in helperClassNames(). Rule: preserve master's ordering unless a semantic change requires reordering. 5. **Hoist repeated Class.getName() in contextStore() (NR-2)** — @ygree flagged five inlined copies of Context.class.getName() as a regression from master's hoist pattern. Sources: - docs/eval-research/cycles/2026-07-14-async-cycle-report.md RI-1, RI-4, RI-5 - @ygree PR #11939 comments 3591691401, 3591695153 skill(apm-integrations): namespace-isolation fail block + dep version parity Adds three prescriptions to muzzle.md from async cycle iteration-1 findings and @mcculls review feedback. 1. **Namespace-isolation fail block for major-version siblings (RI-2).** rxjava-3.0 master has `muzzle { fail { name = "rxjava2-must-not-match" } }` to assert rxjava3 advice never matches rxjava2 coordinates. Eval dropped the block. Rule prescribes preservation for any module with a prior-major sibling module in the repo. 2. **compileOnly dep version parity on regen (RI-3).** rxjava-3.0 master uses reactive-streams 1.0.3; eval regenerated as 1.0.0, silently narrowing the tested API surface. Rule extends the existing testImplementation parity rule to compileOnly. 3. **Test-scope build.gradle dep preservation (NR-5, @mcculls PR #11940).** reactor-core-3.1 eval dropped 8 testImplementation and latestDepTest deps that back annotation-driven tests and cross-module interop tests. Rule prescribes superset-of-master semantics for test deps. skill(apm-integrations): latestDepTest source set + no banner comments in tests Adds two prescriptions to tests.md from async cycle iteration-1 findings and @ygree review feedback. 1. **latestDepTest source-set preservation (RI-7).** reactor-core-3.1 master has src/latestDepTest/groovy/ for version-sensitive tests. The eval collapsed all tests into src/test/java/, which caused Schedulers.elastic() (removed in Reactor 3.4+) to break :latestDepTest compilation across all JVM shards. Rule prescribes preserving the split when master has it, and using it for libraries with breaking API changes across recent minor versions. 2. **No banner comments in test files (NR-4).** @ygree flagged `// --------- Successful completion ---------` style banners in RxJava3ResultExtensionTest.java as "distracting and don't offer much value because their scope is unclear." Rule: omit or extract into separate test classes with focused Javadoc. skill(apm-integrations): no inline explanatory comments in Advice methods Adds NR-3 from @ygree review of PR #11939. Advice bodies are typically short; the wrapper/helper class is where reviewers look to understand semantics. Duplicating the explanation as a `//`-comment at the advice call site inflates the diff and drifts out of sync with the wrapper's Javadoc. Source: @ygree, PR #11939 comment on SingleInstrumentation.java:55. skill(apm-integrations): address Copilot review on #11990 — latestDepTest nuance, find vs ls glob, muzzle fail scope, ContextStore implementation accuracy skill(apm-integrations): address Codex review on #11990 — 5 P2 comments - context-tracking.md: narrow ContextStore key rule — Reactor uses Publisher/Subscriber; JAX-RS uses ContainerRequestContext; Vert.x uses its own Context; coroutines use Continuation/CoroutineContext. Do not force Reactor's key type onto libraries that don't expose Publisher / Subscriber. Also broaden lifecycle-boundary examples per library shape. - tests.md: distinguish latest-only APIs (belong in src/latestDepTest/) from removed-in-latest APIs (belong in src/test/, or use replacement API in latestDepTest). The Reactor Schedulers.elastic() removal is the removed-in-latest case, not the latest-only case. - muzzle.md #1 (fail-block scope): explicitly show same-coordinate cases (jedis/okhttp/jetty-server) alongside different-coordinate cases (rxjava, jms api). The bounded 'versions' range in the fail block is what asserts non-overlap for same-coordinate siblings. - muzzle.md #2 (test-dep preservation): extend the preservation list to cover testRuntimeOnly / latestDepTestRuntimeOnly / forkedTestRuntimeOnly. Runtime-only test deps do NOT trigger compile failures if dropped, so losing them silently removes cross-instrumentation coexistence coverage (e.g. rxjava-3.0's testRuntimeOnly on rxjava-2.0). - instrumenter-module.md: broaden the pre-regen source-file enumeration from `src/main/java` to every production source set: src/main/java17, src/main/java11, src/main/groovy, src/main/scala, src/main/kotlin. Kafka-clients-3.8 and jetty-server-12.0 keep classes under java17; a `find` limited to `src/main/java` misses them. All five findings are P2 severity per Codex classification; each corrects a case where the iter-2 rule was too narrow and could mislead a regen. skill(apm-integrations): condense regen-preservation content, wire new rules into SKILL.md Feedback from @mcculls on this PR and separately from a senior engineer: async is a hard area where AI output should be a starting point for human+AI iteration, not held to a "perfect on first try" bar. Most of this PR's new content was regen-specific "keep things stable when rewriting" directives, disproportionate for a skill whose SKILL.md is otherwise written entirely for the common case (writing a new integration). 8 of the 12 new sections fell into this category. Collapsed all 8 into a single "editing an existing module" note per file (3 files), each a few sentences: read the current file, preserve what's there unless you have a reason to change it, applies to super()/package/class-set/ordering/dependencies/test-source-sets alike. Kept exactly one concrete example (ReactorContextBridge — the highest-stakes case, since dropping it silently breaks sibling modules that reference the class by FQN) instead of one narrated failure story per rule. Net: ~365 lines added by the PR, ~200 removed here. The 4 sections that are genuine new async-instrumentation content (library-native context maps / *ContextBridge pattern, wrap-placement memory considerations, no-inline-advice-comments, no-banner-comments) are unchanged — those aren't regen-specific, they apply to any reactive library instrumentation, new or edited. Also addresses the Datadog Autotest P1 finding (2026-07-21): the new rules existed in reference files but SKILL.md's per-step pointers were generic ("read this file") rather than pointing at the specific new subsections. Added explicit pointers at Steps 4.1, 5, 7, and 9.2/9.3 so a code-gen LLM sees "read the ContextBridge section" / "add the namespace-isolation fail block" inline in its step-by-step flow instead of needing to discover them by skimming a 200+ line reference file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> heading fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> skill(apm-integrations): fix 4 factual errors from Codex review — verified against dd-trace-java source Codex flagged (2026-07-23) that the previous fix broadening the ContextBridge rule beyond Reactor invented plausible-sounding but factually wrong specifics for 3 libraries. Verified each claim against actual dd-trace-java modules before fixing: - JAX-RS: SKILL.md listed it as needing a ContextTracking bridge, but AbstractRequestContextInstrumentation extends InstrumenterModule.Tracing — it's span-owning code, not context-tracking. Removed the JAX-RS mention from SKILL.md and context-tracking.md entirely. - Kotlin coroutines: told agents to key a ContextStore by Continuation/CoroutineContext, but KotlinCoroutinesModule has no contextStore() at all — it uses ThreadContextElement (DatadogThreadContextElement implements ThreadContextElement<Context>). Corrected to point at the actual pattern. - Vert.x: told agents to key by io.vertx.core.Context, but that type is shared across many handler executions on the same Vert.x context — existing vertx-web instrumentation keys per-request state on RoutingContext instead. Keying by core Context would cause cross-request parentage. Corrected to point at RoutingContext. - Reactor wrap-placement: the "prefer subscriber over publisher key" guidance implied these were alternatives, but master's ReactorCoreModule.contextStore() keys BOTH Publisher (via HandoffContext, read before a subscriber exists) AND Subscriber (via Context, for the wrapping pattern) — they're complementary, not a choice. Reworded so the publisher-keyed store is called out as exempt from the "prefer bounded lifetime" optimization. Also fixed a latestDepTest gap Codex found: the prior wording said to put removed-in-latest-version tests in src/test/, but that's only safe when the module uses addTestSuite('latestDepTest') with separate sources. Modules using addTestSuiteForDir('latestDepTest', 'test') reuse src/test/ for both suites, so a removed-API test placed there still gets compiled against latestDepTestImplementation and fails the same way. Split the guidance by which build.gradle wiring the module uses. Root cause: the earlier fix (responding to a prior Codex comment about Reactor-only bias) generalized to other libraries from category-level reasoning ("JAX-RS surely uses a context map like Reactor does") rather than reading those libraries' actual dd-trace-java modules first. Fixed per the java-eval-research skill's canonical-first rule: don't publish a claim about library-specific behavior without checking the source. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Merge branch 'master' into feat/skill-async-iteration-2 Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 77f9ba5 commit 5a8dcc6

6 files changed

Lines changed: 170 additions & 8 deletions

File tree

.agents/skills/apm-integrations/SKILL.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ pattern before writing new code. Use it as a template.
6060

6161
Read [Context-Tracking Instrumentation](references/context-tracking.md) and decide whether the library needs `InstrumenterModule.Tracing` (I/O operations that create spans) or `InstrumenterModule.ContextTracking` (async-boundary bridging, no spans).
6262

63+
**If you picked `ContextTracking`:** the same file's "Library-native context maps" section tells you whether the library needs a `*ContextBridge`-style helper (Reactor is the canonical example) in addition to the subscriber-wrapping pattern — read it before moving to Step 5. Its "Wrap placement" section covers where to allocate wrappers and context-store entries to avoid per-operator overhead on hot reactive paths.
64+
6365
## Step 5 – Write the InstrumenterModule
6466

65-
**Read [InstrumenterModule Guidance](references/instrumenter-module.md).**
67+
**Read [InstrumenterModule Guidance](references/instrumenter-module.md).** If you're editing an existing module rather than writing a new one, its "Editing an existing module" note applies — read the current file first and change only what the task requires.
6668

6769
## Step 6 – Write the Decorator
6870

@@ -77,7 +79,7 @@ Read [Context-Tracking Instrumentation](references/context-tracking.md) and deci
7779

7880
## Step 7 – Write the Advice class (highest-risk step)
7981

80-
**Read [Writing the Advice Class](references/advice-class.md) — the highest-risk step.** Pay particular attention to: `@Advice.OnMethodEnter/Exit` annotations; `CallDepthThreadLocalMap` reentrancy guarding; span lifecycle order; and the "Must NOT do" list.
82+
**Read [Writing the Advice Class](references/advice-class.md) — the highest-risk step.** Pay particular attention to: `@Advice.OnMethodEnter/Exit` annotations; `CallDepthThreadLocalMap` reentrancy guarding; span lifecycle order; and the "Must NOT do" list. If the target wraps or delegates to another async client, its "Do not double-span async HTTP clients" section covers whether to open a second span or rely on context-propagation-only advice. If a returned `CompletableFuture`/`CompletionStage` needs a completion callback, see [Context-Tracking Instrumentation](references/context-tracking.md)'s "Preserving cancellation" section for the read-only-return + named-callback pattern — do not reassign the future with `future = future.whenComplete(...)`.
8183

8284
## Step 8 – Add SETTER/GETTER adapters (if applicable)
8385

@@ -95,10 +97,12 @@ Cover all mandatory test types:
9597

9698
### 2. Muzzle directives (mandatory)
9799

98-
**Read [Muzzle Directives](references/muzzle.md)** — it covers all three valid patterns and their `assertInverse` rules. Search adjacent module `build.gradle` files for `skipVersions` before declaring a new version-bounded module's muzzle directives.
100+
**Read [Muzzle Directives](references/muzzle.md)** — it covers all three valid patterns and their `assertInverse` rules. Search adjacent module `build.gradle` files for `skipVersions` before declaring a new version-bounded module's muzzle directives. **If a prior-major-version sibling module already exists in the repo** (e.g. you're writing `rxjava-3.0` and `rxjava-2.0` exists), add the "Namespace-isolation `fail` block" that section describes — it's not optional, it's how CI catches accidental cross-version advice matching.
99101

100102
### 3. Latest dependency test (mandatory)
101103

104+
If the library's API surface changes across minor versions (deprecated/removed methods, changed signatures), see [Writing Tests](references/tests.md)'s "Version-sensitive tests belong in a separate `latestDepTest` source set" section for which tests belong in `src/test/` vs `src/latestDepTest/`.
105+
102106
Use `latestDepTestImplementation` in `build.gradle` to pin the latest available version. Run with:
103107
```bash
104108
./gradlew :dd-java-agent:instrumentation:$framework:$framework-$version:latestDepTest

.agents/skills/apm-integrations/references/advice-class.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,36 @@ For route-only enrichers: no `AgentScope`, no `startSpan()`, no `decorator.after
152152
### Advice classes must not declare non-constant static fields
153153

154154
`*Advice.java` classes are inlined at instrumentation sites; non-constant static fields (fields that aren't `static final` primitives or string literals) get pulled into every instrumented callsite and violate muzzle's assumptions. Keep only `static final` constants — no logger references, no cached decorators, no state. If you need shared state, put it on a helper class registered via `helperClassNames()`, not on the advice.
155+
156+
### No inline explanatory comments in Advice methods
157+
158+
Do NOT add narrative `//`-comments inside `@Advice.OnMethodEnter` / `@Advice.OnMethodExit` methods to explain *why* wrapping, subscription capture, or span activation happens. If the target helper class (e.g. `TracingSingleObserver`, `TracingRunnable`) already has a class-level Javadoc documenting the intent, duplicating the explanation at the call site is treated as noise by reviewers.
159+
160+
```java
161+
// ❌ Redundant inline comment (TracingSingleObserver's Javadoc already explains this)
162+
@Advice.OnMethodEnter(suppress = Throwable.class)
163+
public static void enter(
164+
@Advice.Argument(value = 0, readOnly = false) SingleObserver<Object> observer) {
165+
AgentSpan parentSpan = activeSpan();
166+
if (parentSpan != null) {
167+
// wrap the observer so spans from its events treat the captured span as their parent
168+
observer = new TracingSingleObserver<>(observer, parentSpan);
169+
}
170+
}
171+
172+
// ✅ Wrapper's Javadoc carries the explanation
173+
@Advice.OnMethodEnter(suppress = Throwable.class)
174+
public static void enter(
175+
@Advice.Argument(value = 0, readOnly = false) SingleObserver<Object> observer) {
176+
AgentSpan parentSpan = activeSpan();
177+
if (parentSpan != null) {
178+
observer = new TracingSingleObserver<>(observer, parentSpan);
179+
}
180+
}
181+
```
182+
183+
Advice bodies are typically short (5–15 lines); the wrapper/helper class is where reviewers look to understand semantics. Inline `//`-comments in advice methods duplicate that documentation, inflate the diff, and drift out of sync with the wrapper's Javadoc.
184+
185+
**When an inline comment IS appropriate:** to explain a non-obvious constraint or hidden invariant the reader cannot recover from the wrapper's Javadoc (e.g. "must run before the delegate's own advice runs because …" — an ordering fact not visible from either class alone). These are rare; the default is no inline comment.
186+
187+
Source: @ygree review on PR #11939 (SingleInstrumentation.java).

.agents/skills/apm-integrations/references/context-tracking.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,40 @@ When a boundary type exposes multiple overloads of the subscribe / invoke method
5555

5656
**Reference:** dd-trace-java's `rxjava-2.0` module hooks the single-argument `subscribe(...)` method (matcher: `named("subscribe").and(takesArguments(1))`) with the argument typed as the base callback interface (e.g. `Observer` for `Observable`, `Subscriber` for `Flowable`). Read the module source to see the exact matcher — pattern-match on this rather than copying overload names.
5757

58+
## Library-native context maps — the `*ContextBridge` pattern
59+
60+
Some Reactive-Streams libraries expose their own request-scoped context map that users write to explicitly — Reactor's `reactor.util.context.Context` (immutable per-subscription map) is the canonical example; users pass a span via `.contextWrite(ctx -> ctx.put("dd.span", span))`.
61+
62+
When a library has a first-class context-map concept like this, the observer/subscriber wrapping pattern documented above is **not sufficient by itself** — it only handles the "capture the currently-active trace context at subscribe time" case. It does NOT handle the case where a user has placed a span in the library's own context map and expects that span to propagate through the operator chain.
63+
64+
**A context-tracking module for such a library MUST include a `*ContextBridge`-style helper** that:
65+
66+
1. Reads a well-known key (Datadog convention: `"dd.span"`) from the library-native context.
67+
2. Adapts the retrieved object (which implements `WithAgentSpan` or is an `AgentSpan`) into a `datadog.context.Context`.
68+
3. Stores that context in the toolkit-native `ContextStore`. For Reactor specifically, master keys **both** `Publisher` (via `HandoffContext`, for the explicit `dd.span` bridge — read by the blocking/subscribe advices before a subscriber may exist) **and** `Subscriber` (via `Context`, for the wrapping pattern) — these are complementary, not alternatives. Read the target library's own context-propagation surface before assuming Reactor's keying scheme transfers directly; a library with a different context/lifecycle shape (e.g. one that carries context via a thread-local element rather than a per-subscription map) needs its own keying, not a forced `Publisher`/`Subscriber` pair.
69+
4. Activates the stored context at the right lifecycle boundaries: on subscribe, on signal delivery, on blocking, and on internal subscriber handoff (e.g. Reactor's fused-operator path).
70+
71+
**Reference:** `dd-java-agent/instrumentation/reactor-core-3.1/` — master has `ReactorContextBridge.java` plus four supporting instrumentations that hook the specific lifecycle points: `BlockingPublisherInstrumentation` (for `.block()` / `.blockFirst()` / `.blockLast()` on the calling thread), `ContextWritingSubscriberInstrumentation` (for `.contextWrite(...)` subscribers at subscribe time), `CorePublisherInstrumentation` (for the base publisher interface handing off to downstream subscribers), and `OptimizableOperatorInstrumentation` (for Reactor's internal fused-operator optimization path). Regenerating this module without preserving these classes silently breaks downstream libraries — Spring WebFlux, Spring Kafka reactive `@KafkaListener suspend fun`, resilience4j-reactor, reactor-netty — that rely on the `dd.span` propagation path.
72+
73+
**How to detect the pattern in an unfamiliar library:** search the library's public API for a `Context` type with `put`/`get`/`hasKey` methods that user code can write to (as opposed to a Datadog-internal context). If such a type exists, the library has a native context map and needs a `*ContextBridge` helper. If the library instead carries context via a thread-local mechanism (e.g. Kotlin coroutines' `ThreadContextElement`) or a per-request object already used for span ownership elsewhere in the codebase (e.g. Vert.x web's `RoutingContext`), follow that library's existing pattern instead of introducing a `ContextStore`-backed bridge — read the sibling instrumentation module for that library first.
74+
75+
**Editing an existing reactive module:** account for every `*Instrumentation.java` class already present, including the `*ContextBridge` helper — don't drop it in favor of the subscriber-wrapping pattern alone. A dropped bridge silently breaks any sibling module that depends on it (see `instrumenter-module.md`).
76+
77+
## Wrap placement and context-store lifecycle — memory considerations
78+
79+
Context-tracking instrumentations allocate two kinds of long-lived state that add up on hot reactive paths:
80+
81+
- **Wrapper instances** — one `TracingObserver`/`TracingSubscriber`/etc. per subscribe call.
82+
- **Context-store entries** — one `ContextStore.put(subscriber, context)` per subscribe call, held until the subscription completes/cancels.
83+
84+
Place both at the narrowest scope that preserves correctness:
85+
86+
- **Wrap only at chain boundaries** — subscribe, `.contextWrite(...)`, `.block()` — not at every internal operator. Operator-level wrapping causes N-fold allocations for an N-operator chain and does not add propagation coverage that boundary wrapping doesn't already provide.
87+
- **Prefer subscriber-lifecycle context stores when the store's only purpose is the wrapping pattern** — the store should drop entries when the subscription completes or cancels. `ContextStore` implementations in dd-trace-java use field injection on the key instance when possible (the common fast path), or a weak-map fallback when field injection is unavailable. Prefer a key with a bounded lifetime — the subscriber (scoped to one subscription) — over the publisher (which may be shared and long-lived across many subscriptions), **unless the library-native context map requires a publisher-keyed store for its own reasons** (e.g. Reactor's `Publisher → HandoffContext` bridge is read before a subscriber exists — see "Library-native context maps" above; that store is not a candidate for this optimization).
88+
- **Avoid double-wrapping** — when a downstream operator already carries the context via the library-native context map (e.g., Reactor's `Context` flows through the whole operator chain by construction), do not add a per-operator wrap on top.
89+
90+
**Why this matters:** a 10-operator Reactor chain with per-operator wrapping allocates 10× the wrappers of a boundary-only implementation, and every allocation must be reclaimed when the subscription completes. In steady-state reactive services, that's the difference between context-tracking being invisible in profiling and being a measurable overhead.
91+
5892
## When NOT to write a context-tracking instrumentation
5993

6094
If the library DOES perform I/O — sends HTTP requests, runs DB queries, makes RPC calls, talks to a broker, reads/writes a cache — write a **span-creating instrumentation** (`InstrumenterModule.Tracing`) instead. Context-tracking is only for libraries that coordinate work; the moment there's actual I/O to observe, you want spans around it.

.agents/skills/apm-integrations/references/instrumenter-module.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,10 @@ Users can then set `DD_TRACE_OKHTTP_ENABLED=false` (group off) OR `DD_TRACE_OKHT
7979

8080
**New module you expect will soon have a sibling** — add the alias upfront and document why in the commit message. If no sibling appears, drop the alias in a follow-up.
8181

82-
**Existing module** (modifying, refactoring, or splitting): read the existing module's `super(...)` and copy it verbatim. Integration names are public config API — renaming one silently breaks customer `DD_TRACE_*_ENABLED` settings.
83-
84-
Before choosing, list the `dd-java-agent/instrumentation/$framework/` directory — the contents are the ground truth for whether siblings exist.
82+
**Editing an existing module:** read it first, then change only what the task requires. Copy `super(...)` verbatim — integration names are public config API, and silently changing the number of arguments or a string value breaks customer `DD_TRACE_*_ENABLED` settings. The same "read before touching, preserve unless you have a reason to change it" rule covers everything else on the class: overridden methods (`defaultEnabled()`, `helperClassNames()`, `contextStore()`, `orderPriority()`), the Java package, the set and order of production classes, and array/map entry ordering. Don't rename, reorder, drop, or restructure any of it as a side effect of regenerating the file — every one of those looks harmless in isolation but reads as an unexplained regression to a reviewer, and some (like a dropped `*ContextBridge` helper — see `context-tracking.md`) silently break sibling modules that reference the class by FQN.
8583

8684
Do NOT add version aliases to the decorator's `instrumentationNames()` — that method is for analytics keys only.
8785

88-
**When rewriting or refactoring an existing module, preserve every override the master version has.** Not just `super(...)` — also `defaultEnabled()`, `helperClassNames()`, `contextStore()`, `orderPriority()`, `muzzleDirective()`, and any other overridden method. Read the current file on master before writing; carry each override forward verbatim unless there's a documented reason to change it. Silent loss of `defaultEnabled() = false` (or similar opt-in flags) ships an integration with a different default than users expected.
89-
9086
### Do not create a helper class just for CallDepthThreadLocalMap when only one type is instrumented
9187

9288
When only one type is being instrumented, use `CallDepthThreadLocalMap` directly in the Advice class. A separate helper class that just wraps `CallDepthThreadLocalMap.incrementCallDepth` / `decrementCallDepth` adds indirection without value:

.agents/skills/apm-integrations/references/muzzle.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,34 @@ muzzle {
172172
**How to discover these**: when verify fails with a task name that has a doubled module name (e.g., `redis.clients-jedis-jedis-3.6.2`), check the existing production module for the same library at another version. If it has `skipVersions` entries, copy them. This is library-specific tribal knowledge that lives in the existing modules.
173173

174174
When in doubt, **search adjacent module build.gradle files for `skipVersions`** before declaring a new version-bounded module's muzzle directives.
175+
176+
## Namespace-isolation `fail` blocks for major-version siblings
177+
178+
When a library has multiple major versions that must never be instrumented by the same advice — whether published under different `group:module` coordinates (e.g., `io.reactivex.rxjava2:rxjava` vs `io.reactivex.rxjava3:rxjava`) or under the same coordinates at incompatible major versions (e.g., `org.springframework:spring-webflux` at major 5 vs 6) — assert namespace isolation with a `muzzle { fail { ... } }` block:
179+
180+
```groovy
181+
muzzle {
182+
pass {
183+
group = "io.reactivex.rxjava3"
184+
module = "rxjava"
185+
versions = "[3.0.0,)"
186+
}
187+
// Assert the rxjava3 advice never resolves against rxjava2 — the two namespaces
188+
// must not overlap. rxjava3 references io.reactivex.rxjava3.core.*, absent from
189+
// the rxjava2 artifact, so muzzle must fail to match it.
190+
fail {
191+
name = "rxjava2-must-not-match"
192+
group = "io.reactivex.rxjava2"
193+
module = "rxjava"
194+
versions = "[2.0.0,)"
195+
}
196+
}
197+
```
198+
199+
Muzzle would likely fail naturally on the sibling major anyway (the FQNs usually don't exist in that artifact), but the explicit assertion catches it at CI time with a specific error message instead of a generic muzzle mismatch. Add this block whenever a prior-major sibling module already exists in the repo — whether that sibling uses different Maven coordinates (`rxjava-2.0` vs `rxjava-3.0`, `javax-jms-*` vs `jakarta-jms-*`) or the same coordinates at an incompatible major (`okhttp-2.0` vs `okhttp-3.0`, `jedis-1.4`/`jedis-3.0`/`jedis-4.0`). **Editing an existing module that already has one: preserve it verbatim** — don't drop it as a side effect of regenerating the file.
200+
201+
## `compileOnly` and test-scope dependencies
202+
203+
`compileOnly` pins the API surface your advice compiles against; test scopes (`testImplementation`, `latestDepTestImplementation`, `forkedTestImplementation`, and their `*RuntimeOnly` counterparts) pin what your tests exercise. When writing a new module, choose these deliberately — the version constraint and dependency list are part of the instrumentation's contract, not incidental build config.
204+
205+
**Editing an existing module: preserve every dependency version and entry unless you have a reason to change it.** This includes runtime-only scopes, which don't show up as compile failures if dropped — e.g. `rxjava-3.0` declares `testRuntimeOnly project(':dd-java-agent:instrumentation:rxjava:rxjava-2.0')` to load the sibling instrumenter into the test JVM and verify the two don't cross-fire (the coexistence coverage that pairs with the namespace-isolation `fail` block above). It also includes cross-module `project(...)` test deps that back annotation-driven tests (`@WithSpan`, `@Traced`) and version-drift workaround deps (e.g. a `latestDepTestImplementation` pin needed only because a newer library version requires it). None of these produce a compile error when silently dropped — they just remove coverage.

0 commit comments

Comments
 (0)