diff --git a/dd-java-agent/instrumentation/guidewire-10.0/build.gradle b/dd-java-agent/instrumentation/guidewire-10.0/build.gradle
new file mode 100644
index 00000000000..b80aecd1420
--- /dev/null
+++ b/dd-java-agent/instrumentation/guidewire-10.0/build.gradle
@@ -0,0 +1,21 @@
+// Guidewire is proprietary and not published to any repository, so this module has no
+// compile dependency on it: the target classes are matched purely by name at runtime.
+muzzle {
+ pass {
+ coreJdk()
+ }
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+
+tasks.named("compileJava") {
+ configureCompiler(it, 8)
+}
+
+dependencies {
+ // Not required (the module self-activates in $Activate); kept so the test also exercises the
+ // default config where RunnableInstrumentation wraps run() too — the double activation is safe.
+ testImplementation project(':dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-1.8')
+ // @Trace on fixture methods, to materialize the child span whose parent we assert.
+ testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation')
+}
diff --git a/dd-java-agent/instrumentation/guidewire-10.0/src/main/java/datadog/trace/instrumentation/guidewire/WsiAsyncResponseInstrumentation.java b/dd-java-agent/instrumentation/guidewire-10.0/src/main/java/datadog/trace/instrumentation/guidewire/WsiAsyncResponseInstrumentation.java
new file mode 100644
index 00000000000..fa9ff044f1a
--- /dev/null
+++ b/dd-java-agent/instrumentation/guidewire-10.0/src/main/java/datadog/trace/instrumentation/guidewire/WsiAsyncResponseInstrumentation.java
@@ -0,0 +1,87 @@
+package datadog.trace.instrumentation.guidewire;
+
+import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.nameStartsWith;
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.endTaskScope;
+import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.startTaskScope;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import datadog.context.ContextScope;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.InstrumenterModule;
+import datadog.trace.bootstrap.InstrumentationContext;
+import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+/**
+ * Propagates trace context across the raw thread Guidewire's WSI layer spawns per outbound SOAP
+ * call ({@code AsyncResponseImpl$WebserviceInvocationThread}, seen at runtime as {@code
+ * "WSI-Invocation"}).
+ *
+ *
{@code java.lang.Thread} can't be instrumented (agent global-ignore + bootstrap), so we match
+ * the application-loaded worker subclass instead: capture the context in its {@code } (still
+ * on the parent thread) and re-activate it in {@code run()}. Both halves live here so it works even
+ * if the runnable/executor integration is off; if that also wraps {@code run()}, the double
+ * activation is safe because the continuation is consumed once.
+ */
+@AutoService(InstrumenterModule.class)
+public final class WsiAsyncResponseInstrumentation extends InstrumenterModule.ContextTracking
+ implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
+
+ private static final String ASYNC_RESPONSE = "gw.internal.xml.ws.AsyncResponseImpl";
+
+ public WsiAsyncResponseInstrumentation() {
+ super("guidewire");
+ }
+
+ @Override
+ public String hierarchyMarkerType() {
+ return ASYNC_RESPONSE;
+ }
+
+ @Override
+ public ElementMatcher hierarchyMatcher() {
+ // '$' matches only nested classes of AsyncResponseImpl, not top-level siblings.
+ return nameStartsWith(ASYNC_RESPONSE + "$").and(extendsClass(named("java.lang.Thread")));
+ }
+
+ @Override
+ public Map contextStore() {
+ return singletonMap(Runnable.class.getName(), State.class.getName());
+ }
+
+ @Override
+ public void methodAdvice(MethodTransformer transformer) {
+ transformer.applyAdvice(isConstructor(), getClass().getName() + "$Capture");
+ transformer.applyAdvice(
+ named("run").and(takesArguments(0)).and(isPublic()), getClass().getName() + "$Activate");
+ }
+
+ public static final class Capture {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void onConstruct(@Advice.This final Runnable thiz) {
+ capture(InstrumentationContext.get(Runnable.class, State.class), thiz);
+ }
+ }
+
+ public static final class Activate {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static ContextScope enter(@Advice.This final Runnable thiz) {
+ return startTaskScope(InstrumentationContext.get(Runnable.class, State.class), thiz);
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void exit(@Advice.Enter final ContextScope scope) {
+ endTaskScope(scope);
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/datadog/trace/instrumentation/guidewire/WsiAsyncResponseInstrumentationTest.java b/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/datadog/trace/instrumentation/guidewire/WsiAsyncResponseInstrumentationTest.java
new file mode 100644
index 00000000000..0e920b43ee1
--- /dev/null
+++ b/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/datadog/trace/instrumentation/guidewire/WsiAsyncResponseInstrumentationTest.java
@@ -0,0 +1,108 @@
+package datadog.trace.instrumentation.guidewire;
+
+import static datadog.trace.agent.test.assertions.SpanMatcher.span;
+import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME;
+import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
+
+import datadog.context.ContextScope;
+import datadog.trace.agent.test.AbstractInstrumentationTest;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import gw.internal.xml.ws.AsyncResponseImpl;
+import gw.internal.xml.ws.UnrelatedWorker;
+import org.junit.jupiter.api.Test;
+
+class WsiAsyncResponseInstrumentationTest extends AbstractInstrumentationTest {
+
+ @FunctionalInterface
+ interface Body {
+ void run() throws Exception;
+ }
+
+ private static void runUnderTrace(String operationName, Body body) throws Exception {
+ AgentSpan span = startSpan("guidewire-test", operationName);
+ try (ContextScope scope = activateSpan(span)) {
+ body.run();
+ } finally {
+ span.finish();
+ }
+ }
+
+ @Test
+ void namedWorkerPropagatesContext() throws Exception {
+ // Constructed and run while the caller's span is active; invoke() blocks until the worker ends.
+ runUnderTrace("parent", () -> new AsyncResponseImpl().invoke());
+
+ assertTraces(
+ trace(
+ SORT_BY_START_TIME,
+ span().root().operationName("parent"),
+ span().childOfPrevious().operationName("soap.call")));
+ }
+
+ @Test
+ void anonymousWorkerPropagatesContext() throws Exception {
+ runUnderTrace("parent", () -> AsyncResponseImpl.anonymous().invoke());
+
+ assertTraces(
+ trace(
+ SORT_BY_START_TIME,
+ span().root().operationName("parent"),
+ span().childOfPrevious().operationName("soap.call")));
+ }
+
+ @Test
+ void synchronousRunPropagatesContext() throws Exception {
+ // callTimeout <= 0 path: AsyncResponseImpl.run() calls _thread.run() on the caller thread.
+ runUnderTrace("parent", () -> new AsyncResponseImpl().invokeSync());
+
+ assertTraces(
+ trace(
+ SORT_BY_START_TIME,
+ span().root().operationName("parent"),
+ span().childOfPrevious().operationName("soap.call")));
+ }
+
+ @Test
+ void unrelatedThreadIsNotInstrumented() throws Exception {
+ // Same construction pattern, but a class the narrow matcher must ignore.
+ runUnderTrace(
+ "parent",
+ () -> {
+ UnrelatedWorker worker = new UnrelatedWorker();
+ worker.start();
+ worker.join();
+ });
+
+ // No propagation: the worker's span starts its own trace instead of joining "parent".
+ assertTraces(
+ trace(span().root().operationName("parent")),
+ trace(span().root().operationName("unrelated.work")));
+ }
+
+ @Test
+ void noContextLeakToSubsequentInvocation() throws Exception {
+ runUnderTrace("parent", () -> new AsyncResponseImpl().invoke());
+ // Second invocation runs with no active span: capture is a no-op, so soap.call is its own root.
+ new AsyncResponseImpl().invoke();
+
+ assertTraces(
+ trace(
+ SORT_BY_START_TIME,
+ span().root().operationName("parent"),
+ span().childOfPrevious().operationName("soap.call")),
+ trace(span().root().operationName("soap.call")));
+ }
+
+ @Test
+ void workerConstructedButNeverRunDoesNotCorruptLaterTraces() throws Exception {
+ // Constructed under an active span but never run: capture happens but is never activated.
+ // Guards that the stranded continuation does not mis-attribute a later, unrelated trace.
+ runUnderTrace("outer", () -> new AsyncResponseImpl());
+ runUnderTrace("independent", () -> {});
+
+ // 'independent' is a clean, standalone root regardless of the stranded continuation.
+ assertTraces(trace(span().root().operationName("independent")));
+ }
+}
diff --git a/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/gw/internal/xml/ws/AsyncResponseImpl.java b/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/gw/internal/xml/ws/AsyncResponseImpl.java
new file mode 100644
index 00000000000..b5ff52050f5
--- /dev/null
+++ b/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/gw/internal/xml/ws/AsyncResponseImpl.java
@@ -0,0 +1,59 @@
+package gw.internal.xml.ws;
+
+import datadog.trace.api.Trace;
+
+/**
+ * Test double for Guidewire's WSI worker; kept in package {@code gw.internal.xml.ws} so the matcher
+ * applies.
+ */
+public class AsyncResponseImpl {
+
+ private final Thread thread;
+
+ public AsyncResponseImpl() {
+ this.thread = new WebserviceInvocationThread();
+ }
+
+ // The boolean only distinguishes this overload from the no-arg constructor; its value is unused.
+ private AsyncResponseImpl(boolean anonymous) {
+ this.thread =
+ new Thread() {
+ @Override
+ public void run() {
+ soapCall();
+ }
+ };
+ }
+
+ public static AsyncResponseImpl anonymous() {
+ return new AsyncResponseImpl(true);
+ }
+
+ public void invoke() throws InterruptedException {
+ thread.start();
+ thread.join();
+ }
+
+ public void invokeSync() {
+ thread.run();
+ }
+
+ @Trace(operationName = "soap.call")
+ static void soapCall() {}
+
+ // Chained constructor: two frames make capture fire twice, testing the State CAS dedup.
+ public static final class WebserviceInvocationThread extends Thread {
+ public WebserviceInvocationThread() {
+ this("WSI-Invocation");
+ }
+
+ private WebserviceInvocationThread(String name) {
+ super(name);
+ }
+
+ @Override
+ public void run() {
+ soapCall();
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/gw/internal/xml/ws/UnrelatedWorker.java b/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/gw/internal/xml/ws/UnrelatedWorker.java
new file mode 100644
index 00000000000..d70037df86a
--- /dev/null
+++ b/dd-java-agent/instrumentation/guidewire-10.0/src/test/java/gw/internal/xml/ws/UnrelatedWorker.java
@@ -0,0 +1,17 @@
+package gw.internal.xml.ws;
+
+import datadog.trace.api.Trace;
+
+/**
+ * Negative control: a Thread subclass the matcher must ignore (not an {@code AsyncResponseImpl$…}).
+ */
+public class UnrelatedWorker extends Thread {
+
+ @Override
+ public void run() {
+ unrelatedWork();
+ }
+
+ @Trace(operationName = "unrelated.work")
+ static void unrelatedWork() {}
+}
diff --git a/metadata/agent-jar-checks.properties b/metadata/agent-jar-checks.properties
index 913a83697b1..5cf2ecc7264 100644
--- a/metadata/agent-jar-checks.properties
+++ b/metadata/agent-jar-checks.properties
@@ -61,6 +61,7 @@ expected.integrations = IastInstrumentation,\
grpc,\
gson,\
guava,\
+ guidewire,\
hazelcast,\
hazelcast_legacy,\
hibernate,\
diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json
index 2d3d77ec355..53326c7542d 100644
--- a/metadata/supported-configurations.json
+++ b/metadata/supported-configurations.json
@@ -6281,6 +6281,14 @@
"aliases": ["DD_TRACE_INTEGRATION_GUAVA_ENABLED", "DD_INTEGRATION_GUAVA_ENABLED"]
}
],
+ "DD_TRACE_GUIDEWIRE_ENABLED": [
+ {
+ "version": "A",
+ "type": "boolean",
+ "default": "true",
+ "aliases": ["DD_TRACE_INTEGRATION_GUIDEWIRE_ENABLED", "DD_INTEGRATION_GUIDEWIRE_ENABLED"]
+ }
+ ],
"DD_TRACE_HAZELCAST_ENABLED": [
{
"version": "A",
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c2ef7c17958..8d693456591 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -377,6 +377,7 @@ include(
":dd-java-agent:instrumentation:grpc-1.5",
":dd-java-agent:instrumentation:gson-1.6",
":dd-java-agent:instrumentation:guava-10.0",
+ ":dd-java-agent:instrumentation:guidewire-10.0",
":dd-java-agent:instrumentation:hazelcast:hazelcast-3.6",
":dd-java-agent:instrumentation:hazelcast:hazelcast-3.9",
":dd-java-agent:instrumentation:hazelcast:hazelcast-4.0",