Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2c219fd
add `di_enabled` to settings response
daniel-mohedano Jul 4, 2025
a911ae0
add FTR related metrics
daniel-mohedano Jul 4, 2025
d20159e
add FTR to execution settings
daniel-mohedano Jul 4, 2025
c4c84d6
add basic exception replay integration in agent mode
daniel-mohedano Jul 14, 2025
ae31670
feat: headless and agentless changes
daniel-mohedano Jul 22, 2025
4f2d6a9
Merge branch 'master' into daniel.mohedano/failed-test-replay
daniel-mohedano Jul 22, 2025
c7eeac8
fix: tests
daniel-mohedano Jul 23, 2025
14cf11c
fix: testng capabilities
daniel-mohedano Aug 4, 2025
461fb1f
feat: refactor agentless intakes
daniel-mohedano Aug 6, 2025
14b1054
Merge branch 'master' into daniel.mohedano/failed-test-replay
daniel-mohedano Aug 6, 2025
a87eff0
chore: update smoke test fixtures
daniel-mohedano Aug 6, 2025
19b4edf
test: add unit test for new Intake enum
daniel-mohedano Aug 6, 2025
9e81cc7
test: remove ftr from instrumentation tests (not used)
daniel-mohedano Aug 6, 2025
4e3deb4
test: introduce FTR smoke tests for headfull and headless modes
daniel-mohedano Aug 11, 2025
f931654
style: spotless and codenarc
daniel-mohedano Aug 11, 2025
eacde60
feat: add test event finished FTR telemetry
daniel-mohedano Aug 11, 2025
0228f71
feat: add `product` field to snapshots
daniel-mohedano Aug 11, 2025
88fd665
feat: implement SuiteEnd listener for sink flushing
daniel-mohedano Aug 11, 2025
ec9c54d
feat: introduce new config variables for debugger
daniel-mohedano Aug 12, 2025
d4d2432
chore: remove todo
daniel-mohedano Aug 19, 2025
ffda9b3
Merge branch 'master' into daniel.mohedano/failed-test-replay
daniel-mohedano Aug 19, 2025
49eaebf
feat: align FTR settings with JS' implementation
daniel-mohedano Aug 21, 2025
b4cef2c
feat: PR suggestions
daniel-mohedano Aug 22, 2025
d82a7fa
feat: implement debugger config bridge
daniel-mohedano Aug 27, 2025
ab8d8e9
fix: move check from policy to history for FTR
daniel-mohedano Aug 27, 2025
bd81769
fix: tests
daniel-mohedano Aug 27, 2025
c257390
chore: update codeowners
daniel-mohedano Aug 27, 2025
73bf81d
Merge branch 'master' into daniel.mohedano/failed-test-replay
daniel-mohedano Aug 27, 2025
3a89ca3
test: better unit tests
daniel-mohedano Sep 1, 2025
54dbb03
Merge branch 'master' into daniel.mohedano/failed-test-replay
daniel-mohedano Sep 1, 2025
a1f58f3
fix: add CI Intake back
daniel-mohedano Sep 1, 2025
719a934
style: spotless
daniel-mohedano Sep 1, 2025
b223a21
fix: tests
daniel-mohedano Sep 1, 2025
58a096b
fix: correct intake usage
daniel-mohedano Sep 1, 2025
920421a
fix: change info to debug log
daniel-mohedano Sep 3, 2025
2852d25
feat: move away from deferred queue in favor of merging updates
daniel-mohedano Sep 3, 2025
d80e531
fix: pr suggestions
daniel-mohedano Sep 4, 2025
34cf624
feat: refactor FTR logic into it's own exceptiondebugger implementation
daniel-mohedano Sep 4, 2025
887350f
fix: jacoco coverage
daniel-mohedano Sep 4, 2025
f1f1264
fix: remove snapshot product and async instrumentation config
daniel-mohedano Sep 8, 2025
8e145f4
fix: remove sync config test
daniel-mohedano Sep 8, 2025
bb06f75
Merge branch 'master' into daniel.mohedano/failed-test-replay
daniel-mohedano Sep 8, 2025
098b929
fix: add history on test start
daniel-mohedano Sep 8, 2025
a56d10f
fix: use synchronized instead of atomic refs in bridge
daniel-mohedano Sep 8, 2025
f32dcef
fix: abstract repeated smoke test logic
daniel-mohedano Sep 8, 2025
ea090f9
fix: synchronization on bridge
daniel-mohedano Sep 9, 2025
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
Prev Previous commit
Next Next commit
feat: refactor FTR logic into it's own exceptiondebugger implementation
  • Loading branch information
daniel-mohedano committed Sep 4, 2025
commit 34cf624cd9ded2d6c402490bd5329f2a613721b8
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import static datadog.trace.util.AgentThreadFactory.AGENT_THREAD_GROUP;

import com.datadog.debugger.codeorigin.DefaultCodeOriginRecorder;
import com.datadog.debugger.exception.AbstractExceptionDebugger;
import com.datadog.debugger.exception.DefaultExceptionDebugger;
import com.datadog.debugger.exception.FailedTestReplayExceptionDebugger;
import com.datadog.debugger.sink.DebuggerSink;
import com.datadog.debugger.sink.ProbeStatusSink;
import com.datadog.debugger.sink.SnapshotSink;
Expand Down Expand Up @@ -62,7 +64,7 @@ public class DebuggerAgent {
private static volatile ClassNameFilter classNameFilter;
private static volatile SymDBEnablement symDBEnablement;
private static volatile ConfigurationUpdater configurationUpdater;
private static volatile DefaultExceptionDebugger exceptionDebugger;
private static volatile AbstractExceptionDebugger exceptionDebugger;
private static final AtomicBoolean commonInitDone = new AtomicBoolean();
static final AtomicBoolean dynamicInstrumentationEnabled = new AtomicBoolean();
static final AtomicBoolean exceptionReplayEnabled = new AtomicBoolean();
Expand Down Expand Up @@ -211,7 +213,13 @@ public static void startExceptionReplay() {
Config config = Config.get();
commonInit(config);
initClassNameFilter();
exceptionDebugger = new DefaultExceptionDebugger(configurationUpdater, classNameFilter, config);
if (config.isCiVisibilityEnabled()) {
exceptionDebugger =
new FailedTestReplayExceptionDebugger(configurationUpdater, classNameFilter, config);
} else {
exceptionDebugger =
new DefaultExceptionDebugger(configurationUpdater, classNameFilter, config);
}
DebuggerContext.initExceptionDebugger(exceptionDebugger);
LOGGER.info("Started Exception Replay");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package com.datadog.debugger.exception;

import static com.datadog.debugger.agent.ConfigurationAcceptor.Source.EXCEPTION;
import static com.datadog.debugger.util.ExceptionHelper.createThrowableMapping;

import com.datadog.debugger.agent.ConfigurationUpdater;
import com.datadog.debugger.agent.DebuggerAgent;
import com.datadog.debugger.sink.Snapshot;
import com.datadog.debugger.util.ExceptionHelper;
import datadog.trace.bootstrap.debugger.DebuggerContext;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.util.AgentTaskScheduler;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Abstract implementation of {@link DebuggerContext.ExceptionDebugger} that uses {@link
* ExceptionProbeManager} to instrument the exception stacktrace and send snapshots.
*/
public abstract class AbstractExceptionDebugger implements DebuggerContext.ExceptionDebugger {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExceptionDebugger.class);
public static final String DD_DEBUG_ERROR_PREFIX = "_dd.debug.error.";
public static final String DD_DEBUG_ERROR_EXCEPTION_ID = DD_DEBUG_ERROR_PREFIX + "exception_id";
public static final String DD_DEBUG_ERROR_EXCEPTION_HASH =
DD_DEBUG_ERROR_PREFIX + "exception_hash";
public static final String SNAPSHOT_ID_TAG_FMT = DD_DEBUG_ERROR_PREFIX + "%d.snapshot_id";

private final ExceptionProbeManager exceptionProbeManager;
private final ConfigurationUpdater configurationUpdater;
private final DebuggerContext.ClassNameFilter classNameFiltering;
private final int maxCapturedFrames;
private final boolean applyConfigAsync;

AbstractExceptionDebugger(
ExceptionProbeManager exceptionProbeManager,
ConfigurationUpdater configurationUpdater,
DebuggerContext.ClassNameFilter classNameFiltering,
int maxCapturedFrames,
boolean applyConfigAsync) {
this.exceptionProbeManager = exceptionProbeManager;
this.configurationUpdater = configurationUpdater;
this.classNameFiltering = classNameFiltering;
this.maxCapturedFrames = maxCapturedFrames;
this.applyConfigAsync = applyConfigAsync;
}

protected abstract boolean shouldHandleException(Throwable t, AgentSpan span);

@Override
public void handleException(Throwable t, AgentSpan span) {
if (!shouldHandleException(t, span)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Skip handling error: {}", t.toString());
}
return;
}

String fingerprint = Fingerprinter.fingerprint(t, classNameFiltering);
if (fingerprint == null) {
LOGGER.debug("Unable to fingerprint exception", t);
return;
}
Deque<Throwable> chainedExceptions = new ArrayDeque<>();
Throwable innerMostException = ExceptionHelper.getInnerMostThrowable(t, chainedExceptions);
if (innerMostException == null) {
LOGGER.debug("Unable to find root cause of exception");
return;
}
List<Throwable> chainedExceptionsList = new ArrayList<>(chainedExceptions);
if (exceptionProbeManager.isAlreadyInstrumented(fingerprint)) {
ExceptionProbeManager.ThrowableState state =
exceptionProbeManager.getStateByThrowable(innerMostException);
if (state == null) {
LOGGER.debug("Unable to find state for throwable: {}", innerMostException.toString());
return;
}
processSnapshotsAndSetTags(
t, span, state, chainedExceptionsList, fingerprint, maxCapturedFrames);
exceptionProbeManager.updateLastCapture(fingerprint);
} else {
// climb up the exception chain to find the first exception that has instrumented frames
Throwable throwable;
int chainedExceptionIdx = 0;
while ((throwable = chainedExceptions.pollFirst()) != null) {
ExceptionProbeManager.CreationResult creationResult =
exceptionProbeManager.createProbesForException(
throwable.getStackTrace(), chainedExceptionIdx);
if (creationResult.probesCreated > 0) {
if (!applyConfigAsync) {
applyExceptionConfiguration(fingerprint);
} else {
AgentTaskScheduler.INSTANCE.execute(() -> applyExceptionConfiguration(fingerprint));
}
break;
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"No probe created, nativeFrames={}, thirdPartyFrames={} for exception: {}",
creationResult.nativeFrames,
creationResult.thirdPartyFrames,
ExceptionHelper.foldExceptionStackTrace(throwable));
}
}
chainedExceptionIdx++;
}
}
}

private void applyExceptionConfiguration(String fingerprint) {
configurationUpdater.accept(EXCEPTION, exceptionProbeManager.getProbes());
exceptionProbeManager.addFingerprint(fingerprint);
}

protected void addStackFrameTags(
AgentSpan span, Snapshot snapshot, int frameIndex, StackTraceElement stackFrame) {
String tagName = String.format(SNAPSHOT_ID_TAG_FMT, frameIndex);
span.setTag(tagName, snapshot.getId());
LOGGER.debug("add tag to span[{}]: {}: {}", span.getSpanId(), tagName, snapshot.getId());
}

private void processSnapshotsAndSetTags(
Throwable t,
AgentSpan span,
ExceptionProbeManager.ThrowableState state,
List<Throwable> chainedExceptions,
String fingerprint,
int maxCapturedFrames) {
if (span.getTag(DD_DEBUG_ERROR_EXCEPTION_ID) != null) {
LOGGER.debug("Clear previous frame tags");
// already set for this span, clear the frame tags
span.getTags()
.forEach(
(k, v) -> {
if (k.startsWith(DD_DEBUG_ERROR_PREFIX)) {
span.setTag(k, (String) null);
}
});
}
boolean snapshotAssigned = false;
List<Snapshot> snapshots = state.getSnapshots();
int maxSnapshotSize = Math.min(snapshots.size(), maxCapturedFrames);
for (int i = 0; i < maxSnapshotSize; i++) {
Snapshot snapshot = snapshots.get(i);
Throwable currentEx = chainedExceptions.get(snapshot.getChainedExceptionIdx());
int[] mapping = createThrowableMapping(currentEx, t);
StackTraceElement[] innerTrace = currentEx.getStackTrace();
int currentIdx = innerTrace.length - snapshot.getStack().size();
if (!sanityCheckSnapshotAssignment(snapshot, innerTrace, currentIdx)) {
continue;
}
int frameIndex = mapping[currentIdx];
if (frameIndex == -1) {
continue;
}

addStackFrameTags(span, snapshot, frameIndex, innerTrace[currentIdx]);

if (!state.isSnapshotSent()) {
DebuggerAgent.getSink().addSnapshot(snapshot);
}
snapshotAssigned = true;
}
if (snapshotAssigned) {
state.markAsSnapshotSent();
span.setTag(DD_DEBUG_ERROR_EXCEPTION_ID, state.getExceptionId());
LOGGER.debug(
"add tag to span[{}]: {}: {}",
span.getSpanId(),
DD_DEBUG_ERROR_EXCEPTION_ID,
state.getExceptionId());
span.setTag(Tags.ERROR_DEBUG_INFO_CAPTURED, true);
span.setTag(DD_DEBUG_ERROR_EXCEPTION_HASH, fingerprint);
}
}

private static boolean sanityCheckSnapshotAssignment(
Snapshot snapshot, StackTraceElement[] innerTrace, int currentIdx) {
String className = snapshot.getProbe().getLocation().getType();
String methodName = snapshot.getProbe().getLocation().getMethod();
if (!className.equals(innerTrace[currentIdx].getClassName())
|| !methodName.equals(innerTrace[currentIdx].getMethodName())) {
LOGGER.warn("issue when assigning snapshot to frame: {} {}", className, methodName);
return false;
}
return true;
}

public ExceptionProbeManager getExceptionProbeManager() {
return exceptionProbeManager;
}
}
Loading
Loading