Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .agents/skills/techdebt/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
---
name: techdebt
description: Analyze branch changes for technical debt, code duplication, and unnecessary complexity
description: >-
Review a code diff / branch / PR for technical debt — code duplication,
unnecessary complexity / over-engineering, and redundant or dead code. Use
whenever the user wants a tech-debt, cleanup, or refactor review, asks to check
a branch or PR for duplication / complexity / dead code before opening a PR, or
mentions "techdebt". Refactor-only: it reports issues and offers
behavior-preserving fixes; it never changes behavior.
user-invocable: true
context: fork
allowed-tools:
Expand Down
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

## Claude Code workflow

- Before creating a pull request, run `/techdebt` to check for technical debt, code duplication, and unnecessary complexity in the branch changes.
- Before creating a pull request, run the [Review Guidelines](../AGENTS.md#review-guidelines) checks over the branch changes.
- After running `/migrate-groovy-to-java`, run `/review-groovy-migration` on the migrated files before opening a PR.
- Always write new unit tests using JUnit 5 and Java. If the target test file is `.groovy`, run the `/migrate-groovy-to-java` skill on it first.
28 changes: 7 additions & 21 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ docs/ Developer documentation (see below)
| Testing guide (6 test types) | [docs/how_to_test.md](docs/how_to_test.md) |
| Working with Gradle | [docs/how_to_work_with_gradle.md](docs/how_to_work_with_gradle.md) |
| Bootstrap/premain constraints | [docs/bootstrap_design_guidelines.md](docs/bootstrap_design_guidelines.md) |
| Instrumentation/advice constraints | [docs/instrumentation_design_guidelines.md](docs/instrumentation_design_guidelines.md) |
| CI/CD workflows | [.github/workflows/README.md](.github/workflows/README.md) |

**When working on a topic above, read the linked file first** — they are the source of truth maintained by humans.
Expand All @@ -56,14 +57,12 @@ docs/ Developer documentation (see below)
## Code conventions

- **Formatting**: google-java-format enforced via Spotless. Run `./gradlew spotlessApply` before committing.
- **Static imports**: Prefer static imports over class-qualified calls for call-style helpers — JUnit `Assertions`, Mockito (`mock`, `when`, `verify`, `anyString`, `RETURNS_DEFAULTS`, ...), Hamcrest/AssertJ matchers, internal test DSLs, and similar. Same goes for production code: `Collections.emptyList()` is fine, but if you find yourself repeatedly writing `Foo.bar(...)` where `Foo` adds no information at the call site, static-import `bar`. Wildcard `import static x.*` is disallowed (enforced by IDE config in CONTRIBUTING.md).
- **Static imports**: Prefer static imports over class-qualified calls for call-style helpers, in both test (Assertions.assertEquals, Mockito.mock) and production code (Collections.emptyList). Wildcard imports disallowed — see CONTRIBUTING.md.

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.

Wildcard imports disallowed

Isn't this already part of the formatter? What reason is it here as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The formatter will detect use of wildcard imports, but it won't tell you to avoid continually writing Collections.emptyList() tens of times in the same file and instead prefer to static import Collections.emptyList and then use emptyList() everywhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So we can drop the bit about wildcard imports, but we still need the rest

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.

The formatter will detect use of wildcard imports, but it won't tell you to avoid continually writing Collections.emptyList() tens of times in the same file and instead prefer to static import Collections.emptyList and then use emptyList() everywhere.

Is there a benefit to doing this or is this just personal preference for the codebase?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Helps readability - we've found agents don't use static imports as much, and that can make the code harder to read - especially for APIs that were designed to be used with static imports like JUnit/Mockito

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.

Not blocking, but is there any path to doing these as static analysis as well? Said differently, is there a big downside to a static import for a single emptyList()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will look into that as a follow-up item

- **Instrumentation layout**: `dd-java-agent/instrumentation/{framework}/{framework}-{minVersion}/`
- **Instrumentation pattern**: Type matching → Method matching → Advice class (bytecode advice, not AOP)
- **Test frameworks**: Always use JUnit 5 for unit tests. Only use Groovy / Spock tests for instrumentation and smoke tests.
- **Forked tests**: Use `ForkedTest` suffix when tests need a separate JVM
- **Flaky tests**: Annotate with `@Flaky` — they are skipped in CI by default
- **Instrumentation one-shot methods**: Never extract the return values of `triggerClasses()`, `contextStore()`, `classLoaderMatcher()`, or `methodAdvice()` into static constants. These are called once by the framework — extracting to a constant adds constant-pool bloat with no benefit.
- **Scope lifecycle order**: Keep the scope open until all work that needs the current active span is done — decorator calls, async callback registration, etc. Required order: decorator calls and callback registration, then `scope.close()`, then `span.finish()`.

## PR conventions

Expand All @@ -74,23 +73,10 @@ docs/ Developer documentation (see below)

## Review Guidelines

Before marking a PR ready, run the applicable reviews below over the branch changes (the diff since the merge-base with `master`). These are **advisory, precision-first** checks — they surface high-severity issues early and are not merge gates. The linked guidelines are the tool-agnostic source of truth.
- **Technical debt**: run `/techdebt` over branch changes before marking a PR ready to catch code duplication, unnecessary complexity, and dead code (refactor-only, never changes behavior) — see [.agents/skills/techdebt/SKILL.md](.agents/skills/techdebt/SKILL.md).
- **Performance**: run `/perf-review` over branch changes before marking a PR ready (advisory, not a merge gate) — see [.agents/skills/perf-review/SKILL.md](.agents/skills/perf-review/SKILL.md).
Comment thread
mcculls marked this conversation as resolved.

### Performance Review
## Critical constraints

Follow the performance-review guidelines in [.agents/skills/perf-review/SKILL.md](.agents/skills/perf-review/SKILL.md) — the do-no-harm / assume-hot posture, the hot-path checks (universal + Java J1–J11 + ByteBuddy-Advice idioms), and the confidence/severity model, with the detailed rubric in its `references/`. Scope: hot-path allocation, unbounded memory, repeated work, escaping objects, native-boundary crossings, and JVM-specific pitfalls.

## Bootstrap constraints (critical)

Code running in the agent's `premain` phase must **not** use:
- `java.util.logging.*` — locks in log manager before app configures it
- `java.nio.file.*` — triggers premature provider initialization
- `javax.management.*` — causes class loading issues

See [docs/bootstrap_design_guidelines.md](docs/bootstrap_design_guidelines.md) for details and alternatives.

## Advice constraints (critical)

Advice methods (OnMethodEnter/OnMethodExit) are inlined into the instrumented class's bytecode.
Calling a static interface method there, such as Context.root() or Context.current(), can cause
a VerifyError at runtime — use Java8BytecodeBridge (rootContext, currentContext, etc.) instead.
- **Bootstrap**: never use `java.util.logging.*`, `java.nio.file.*`, or `javax.management.*` in `premain` code — see [docs/bootstrap_design_guidelines.md](docs/bootstrap_design_guidelines.md).
- **Advice**: never call a static interface method from advice — can cause a `VerifyError` — see [docs/instrumentation_design_guidelines.md](docs/instrumentation_design_guidelines.md).

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.

[optional suggestion] Is it possible to have this as static analysis, rather than prose? If so, we can spend the tokens on this up-front rather than having every agent need to read this and enforce it every time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The tricky part is identifying advice classes which must avoid static interface methods, vs helpers which don't need to (where doing that would just add fluff) - it's basically methods annotated with @Advice.OnMethodEnter / @Advice.OnMethodExit but we'd need to roll our own analyzer to filter on that ... or expand instrumentation-annotation-processor to also dig into the advice bytecode.

That's possible but that also would just add a guard - it's still helpful to flag the existence of Java8BytecodeBridge and guide the agent to do the right thing to begin with.

@bm1549 bm1549 Jul 20, 2026

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.

Depending on how prevalent it is through the codebase, the analyzer could either run everywhere and be suppressed in the locations where it's safe or we could make the finer-grained targeting you're describing

As for guarding vs guiding, you can do both but as the models evolve, guarding tends to be the piece that survives while the guiding bits can be deleted

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Basically I've had agents introduce static interface methods a lot during recent refactorings and it's been annoying to remind them not to do this - a guard would stop them from reaching a reviewable PR, but you'd still have that loop of a) initial refactor b) fails build check c) find alternative (Java8BytecodeBridge) d) fix code e) pass build check

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.

Yep that's correct, so having both would be the belt-and-suspenders so to say

@jordan-wong jordan-wong Jul 21, 2026

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.

I do agree with this, generally I imagine this as a funnel where we generally:

  • use skills as a catch-all for advice/ documentation, there will be items that only apply to specific situations where an LLM can figure out when theyre relevant
  • certain parts we can figure out where to encode as general static checks
  • we generally move from skill -> static checks as we refine the skills/ optimize

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree, will look into this as a follow-up item

10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ For IntelliJ IDEA, we suggest the following settings and plugin.
* To run test in a specific JDK use the `testJvm` property, e.g. `-PtestJvm=11`
* Install the [Google Java Format](https://plugins.jetbrains.com/plugin/8527-google-java-format) plugin

### Static imports

Prefer static imports over class-qualified calls for call-style helpers — JUnit `Assertions`, Mockito (`mock`,
`when`, `verify`, `anyString`, `RETURNS_DEFAULTS`, ...), Hamcrest/AssertJ matchers, internal test DSLs, and similar.

Same goes for production code: `Collections.emptyList()` is fine, but if you find yourself repeatedly writing
`Foo.bar(...)` where `Foo` adds no information at the call site, static-import `bar`.

Wildcard imports (`import x.*` or `import static x.*`) are disallowed — see the IntelliJ IDEA settings above.

### Troubleshooting

* Gradle fails with a "too many open files" error.
Expand Down
83 changes: 83 additions & 0 deletions docs/instrumentation_design_guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Instrumentation and Advice Design Guidelines

This document outlines design constraints and best practices when writing or refactoring instrumentation and advice code.
Following these guidelines will help avoid constant-pool bloat and subtle span/scope lifecycle bugs.

## Background

Instrumentation classes wire up type/method matchers and advice once, when the framework loads them. Advice methods
are inlined into the instrumented application's bytecode and run on every invocation of the matched method, so their
ordering directly affects when spans are visible to nested work.

## Constraints to Follow

### 1. Instrumentation one-shot methods

**Why to avoid extracting to constants:**

- `triggerClasses()`, `contextStore()`, `classLoaderMatcher()`, and `methodAdvice()` are called exactly once by the
framework when the instrumentation is registered
- Extracting their return values into static constants adds constant-pool bloat with no runtime benefit

**What to do instead:**

Return the values directly from the method; don't cache them in a field.

```java
// BAD - unnecessary static constant
private static final ElementMatcher.Junction<ClassLoader> CL_MATCHER = hasClassesNamed("foo.Bar");

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return CL_MATCHER;
}

// GOOD - construct directly in the one-shot method
@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("foo.Bar");
}
```

### 2. Scope lifecycle order

**Why to avoid closing the scope early:**

- Keeping the scope open until all work needing the current active span is done ensures decorator calls and async
callback registration see the span as active
- Closing the scope before that work runs can make nested code lose track of the current span

**What to do instead:**

Required order: decorator calls and callback registration, then `scope.close()`, then `span.finish()`.

```java
// GOOD
DECORATE.onOperation(span, request);
registerCallback(span);
scope.close();
span.finish();
```

### 3. Static interface methods in advice

**Why to avoid calling them directly:**

- Advice methods (`@Advice.OnMethodEnter`/`@Advice.OnMethodExit`) are inlined into the instrumented class's bytecode
- Calling a static interface method there, such as `Context.current()` or `Context.root()`, can cause a
`VerifyError` at runtime

**What to use instead:**

Use `Java8BytecodeBridge` (`currentContext()`, `rootContext()`, etc.) in place of the static interface method,
static-imported for readability.

```java
import static datadog.trace.bootstrap.instrumentation.api.Java8BytecodeBridge.currentContext;

// BAD - static interface method call, can cause VerifyError when inlined
Context ctx = Context.current();

// GOOD - use the bytecode bridge, static-imported
Context ctx = currentContext();
```
Loading